props subsystem
[mudsync.git] / mudsync / gameobj.scm
1 ;;; Mudsync --- Live hackable MUD
2 ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
3 ;;;
4 ;;; This file is part of Mudsync.
5 ;;;
6 ;;; Mudsync is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or
9 ;;; (at your option) any later version.
10 ;;;
11 ;;; Mudsync is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 ;;; General Public License for more details.
15 ;;;
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with Mudsync.  If not, see <http://www.gnu.org/licenses/>.
18
19 ;;; Game actor
20 ;;; ==========
21
22 (define-module (mudsync gameobj)
23   #:use-module (mudsync command)
24   #:use-module (mudsync utils)
25   #:use-module (8sync actors)
26   #:use-module (8sync agenda)
27   #:use-module (8sync rmeta-slot)
28   #:use-module (srfi srfi-1)
29   #:use-module (ice-9 control)
30   #:use-module (ice-9 format)
31   #:use-module (ice-9 match)
32   #:use-module (oop goops)
33   #:export (<gameobj>
34
35             gameobj-loc
36             gameobj-gm
37
38             gameobj-act-init
39             gameobj-set-loc!
40             gameobj-occupants
41             gameobj-self-destruct
42
43             slot-ref-maybe-runcheck
44             val-or-run
45
46             build-props
47             dyn-ref
48
49             ;; Some of the more common commands
50             cmd-take cmd-drop
51             cmd-take-from-no-op cmd-put-in-no-op))
52
53 ;;; Gameobj
54 ;;; =======
55
56 (define build-props build-rmeta-slot)
57
58 ;;; *all* game components that talk to players should somehow
59 ;;; derive from this class.
60 ;;; And all of them need a GM!
61
62 (define-class <gameobj> (<actor>)
63   ;; location id
64   (loc #:init-value #f
65        #:getter gameobj-loc)
66   
67   ;; Uses a hash table like a set (values ignored)
68   (occupants #:init-thunk make-hash-table)
69
70   ;; game master id
71   (gm #:init-keyword #:gm
72       #:getter gameobj-gm)
73
74   ;; a name to be known by
75   (name #:init-keyword #:name
76         #:init-value #f)
77   (goes-by #:init-keyword #:goes-by
78            #:init-value #f)
79
80   (desc #:init-value #f
81         #:init-keyword #:desc)
82
83   ;; @@: Maybe commands should be renamed to verbs, I dunno
84   ;; Commands we can handle
85   (commands #:allocation #:each-subclass
86             #:init-thunk (build-commands
87                           ("take" ((direct-command cmd-take)
88                                    (prep-indir-command cmd-take-from
89                                                        '("from" "out of"))))
90                           ("put" ((prep-indir-command cmd-put-in
91                                                       '("in" "inside" "on"))))))
92
93   ;; Commands we can handle by being something's container
94   ;;   dominant version (goes before everything)
95   (container-dom-commands #:allocation #:each-subclass
96                           #:init-thunk (build-commands))
97   ;;   subordinate version (goes after everything)
98   (container-sub-commands #:allocation #:each-subclass
99                           #:init-thunk (build-commands))
100
101   ;; Commands we can handle by being contained by something else
102   (contained-commands #:allocation #:each-subclass
103                       #:init-thunk 
104                       (build-commands
105                        ("drop" ((direct-command cmd-drop #:obvious? #f)))))
106
107   ;; The extremely squishy concept of "props"... properties!
108   ;; These are flags, etc etc of various types.  This is a hashq table.
109   ;; These have upsides and downsides, but the big upside is that you can
110   ;; query a "prop" of a prospective gameobj without knowing what type of
111   ;; gameobj that is, and not fear some kind of breakage.
112   ;;
113   ;; props by default only have a 'get-prop read-only action handler;
114   ;; any coordination of setting a prop between actors must be
115   ;; added to that actor, to keep things from getting out of control.
116   (props #:init-thunk make-hash-table
117          #:init-keyword #:props)
118   ;; gameobjs may inherit an initial list of these via the
119   ;; initial-props slot, which must always have its
120   ;; #:allocation #:each-subclass and use (build-props) for the
121   ;; #:init-thunk.
122   ;; The vanilla gameobj has no props, on purpose.
123   (initial-props #:allocation #:each-subclass
124                  #:init-thunk (build-props '()))
125
126   ;; Most objects are generally visible by default
127   (invisible? #:init-value #f
128               #:init-keyword #:invisible?)
129   ;; TODO: Fold this into a procedure in invisible? similar
130   ;;   to take-me? and etc
131   (visible-to-player?
132    #:init-value (wrap-apply gameobj-visible-to-player?))
133
134   ;; Can be a boolean or a procedure accepting
135   ;; (gameobj whos-acting #:key from)
136   (take-me? #:init-value #f
137             #:init-keyword #:take-me?)
138   ;; Can be a boolean or a procedure accepting
139   ;; (gameobj whos-acting where)
140   (drop-me? #:init-value #t
141             #:init-keyword #:drop-me?)
142
143   ;; TODO: Remove this and use actor-alive? instead.
144   ;; Set this on self-destruct
145   ;; (checked by some "long running" game routines)
146   (destructed #:init-value #f)
147
148   (actions #:allocation #:each-subclass
149            ;;; Actions supported by all gameobj
150            #:init-thunk
151            (build-actions
152             (init gameobj-act-init)
153             ;; Commands for co-occupants
154             (get-commands gameobj-get-commands)
155             ;; Commands for participants in a room
156             (get-container-dom-commands gameobj-get-container-dom-commands)
157             (get-container-sub-commands gameobj-get-container-sub-commands)
158             ;; Commands for inventory items, etc (occupants of the gameobj commanding)
159             (get-contained-commands gameobj-get-contained-commands)
160
161             (get-occupants gameobj-get-occupants)
162             (add-occupant! gameobj-add-occupant!)
163             (remove-occupant! gameobj-remove-occupant!)
164             (get-loc gameobj-act-get-loc)
165             (set-loc! gameobj-act-set-loc!)
166             (get-name gameobj-get-name)
167             (set-name! gameobj-act-set-name!)
168             (get-desc gameobj-get-desc)
169             (get-prop gameobj-act-get-prop)
170             (goes-by gameobj-act-goes-by)
171             (visible-name gameobj-visible-name)
172             (self-destruct gameobj-act-self-destruct)
173             (tell gameobj-tell-no-op)
174             (assist-replace gameobj-act-assist-replace)
175             (ok-to-drop-here? (lambda (gameobj message . _)
176                                 (<-reply message #t))) ; ok to drop by default
177             (ok-to-be-taken-from? gameobj-ok-to-be-taken-from)
178             (ok-to-be-put-in? gameobj-ok-to-be-put-in)
179
180             ;; Common commands
181             (cmd-take cmd-take)
182             (cmd-drop cmd-drop)
183             (cmd-take-from cmd-take-from-no-op)
184             (cmd-put-in cmd-put-in-no-op))))
185
186
187 ;;; gameobj message handlers
188 ;;; ========================
189
190 ;; Kind of a useful utility, maybe?
191 (define (simple-slot-getter slot)
192   (lambda (actor message)
193     (<-reply message (slot-ref actor slot))))
194
195 (define (gameobj-replace-step-occupants actor occupants)
196   ;; Snarf all the occupants!
197   (display "replacing occupant\n")
198   (when occupants
199     (for-each
200      (lambda (occupant)
201        (<-wait occupant 'set-loc!
202                #:loc (actor-id actor)))
203      occupants)))
204
205 (define gameobj-replace-steps*
206   (list gameobj-replace-step-occupants))
207
208 (define (run-replacement actor replaces replace-steps)
209   (when replaces
210     (mbody-receive (_ #:key occupants)
211         (<-wait replaces 'assist-replace)
212       (for-each
213        (lambda (replace-step)
214          (replace-step actor occupants))
215        replace-steps))))
216
217 (define (gameobj-setup-props gameobj)
218   (define class (class-of gameobj))
219   (define props (slot-ref gameobj 'props))
220   (maybe-build-rmeta-slot-cache! class 'initial-props
221                                  eq? hashq-set! hashq-ref)
222   ;; Kind of a kludge... we read through the rmeta-slot-cache
223   ;; and use that to build up the table
224   (hash-for-each
225    (lambda (key value)
226      (when (not (hashq-ref props key value)) ; don't override init'ed instance values
227        (hashq-set! props key value)))
228    (rmeta-slot-cache (class-slot-ref class 'initial-props))))
229
230 ;; TODO: Use the *init* action?
231 ;;   We could also use a generic method if they didn't have
232 ;;   what I'm pretty sure is O(n) dispatch in GOOPS...
233 (define* (gameobj-act-init actor message #:key replace)
234   "Your most basic game object init procedure."
235   (gameobj-setup-props actor)
236   (run-replacement actor replace gameobj-replace-steps*))
237
238 (define* (gameobj-get-prop gameobj key #:optional dflt)
239   (hashq-ref (slot-ref gameobj 'props) key dflt))
240
241 (define* (gameobj-set-prop! gameobj key val)
242   (hashq-set! (slot-ref gameobj 'props) key val))
243
244 (define* (gameobj-act-get-prop actor message key #:optional dflt)
245   (gameobj-get-prop actor key dflt))
246
247 (define (gameobj-goes-by gameobj)
248   "Find the name we go by.  Defaults to #:name if nothing else provided."
249   (cond ((slot-ref gameobj 'goes-by) =>
250          identity)
251         ((slot-ref gameobj 'name) =>
252          (lambda (name)
253            (list name)))
254         (else '())))
255
256 (define (gameobj-act-goes-by actor message)
257   "Reply to a message requesting what we go by."
258   (<-reply message (gameobj-goes-by actor)))
259
260 (define (val-or-run val-or-proc)
261   "Evaluate if a procedure, or just return otherwise"
262   (if (procedure? val-or-proc)
263       (val-or-proc)
264       val-or-proc))
265
266 (define (get-candidate-commands actor rmeta-sym verb)
267   (class-rmeta-ref (class-of actor) rmeta-sym verb
268                    #:dflt '()))
269
270 (define* (gameobj-get-commands actor message #:key verb)
271   "Get commands a co-occupant of the room might execute for VERB"
272   (define candidate-commands
273     (get-candidate-commands actor 'commands verb))
274   (<-reply message
275            #:commands candidate-commands
276            #:goes-by (gameobj-goes-by actor)))
277
278 (define* (gameobj-get-container-dom-commands actor message #:key verb)
279   "Get (dominant) commands as the container / room of message's sender"
280   (define candidate-commands
281     (get-candidate-commands actor 'container-dom-commands verb))
282   (<-reply message #:commands candidate-commands))
283
284 (define* (gameobj-get-container-sub-commands actor message #:key verb)
285   "Get (subordinate) commands as the container / room of message's sender"
286   (define candidate-commands
287     (get-candidate-commands actor 'container-sub-commands verb))
288   (<-reply message #:commands candidate-commands))
289
290 (define* (gameobj-get-contained-commands actor message #:key verb)
291   "Get commands as being contained (eg inventory) of commanding gameobj"
292   (define candidate-commands
293     (get-candidate-commands actor 'contained-commands verb))
294   (<-reply message
295            #:commands candidate-commands
296            #:goes-by (gameobj-goes-by actor)))
297
298 (define* (gameobj-add-occupant! actor message #:key who)
299   "Add an actor to our list of present occupants"
300   (hash-set! (slot-ref actor 'occupants)
301              who #t))
302
303 (define* (gameobj-remove-occupant! actor message #:key who)
304   "Remove an occupant from the room."
305   (hash-remove! (slot-ref actor 'occupants) who))
306
307 (define* (gameobj-occupants gameobj #:key exclude)
308   (hash-fold
309    (lambda (occupant _ prev)
310      (define exclude-it?
311        (match exclude
312          ;; Empty list and #f are non-exclusion
313          (() #f)
314          (#f #f)
315          ;; A list of addresses... since our address object is (annoyingly)
316          ;; currently a simple cons cell...
317          ((exclude-1 ... exclude-rest)
318           (member occupant exclude))
319          ;; Must be an individual address!
320          (_ (equal? occupant exclude))))
321      (if exclude-it?
322          prev
323          (cons occupant prev)))
324    '()
325    (slot-ref gameobj 'occupants)))
326
327 (define* (gameobj-get-occupants actor message #:key exclude)
328   "Get all present occupants of the room."
329   (define occupants
330     (gameobj-occupants actor #:exclude exclude))
331   (<-reply message occupants))
332
333 (define (gameobj-act-get-loc actor message)
334   (<-reply message (slot-ref actor 'loc)))
335
336 (define (gameobj-set-loc! gameobj loc)
337   "Set the location of this object."
338   (define old-loc (gameobj-loc gameobj))
339   (format #t "DEBUG: Location set to ~s for ~s\n"
340           loc (actor-id-actor gameobj))
341
342   (when (not (equal? old-loc loc))
343     (slot-set! gameobj 'loc loc)
344     ;; Change registation of where we currently are
345     (if old-loc
346         (<-wait old-loc 'remove-occupant! #:who (actor-id gameobj)))
347     (if loc
348         (<-wait loc 'add-occupant! #:who (actor-id gameobj)))))
349
350 ;; @@: Should it really be #:id ?  Maybe #:loc-id or #:loc?
351 (define* (gameobj-act-set-loc! actor message #:key loc)
352   "Action routine to set the location."
353   (gameobj-set-loc! actor loc))
354
355 (define (slot-ref-maybe-runcheck gameobj slot whos-asking . other-args)
356   "Do a slot-ref on gameobj, evaluating it including ourselves
357 and whos-asking, and see if we should just return it or run it."
358   (match (slot-ref gameobj slot)
359     ((? procedure? slot-val-proc)
360      (apply slot-val-proc gameobj whos-asking other-args))
361     (anything-else anything-else)))
362
363 (define gameobj-get-name (simple-slot-getter 'name))
364
365 (define* (gameobj-act-set-name! actor message val)
366   (slot-set! actor 'name val))
367
368 (define* (gameobj-desc actor #:key whos-looking)
369   (match (slot-ref actor 'desc)
370     ((? procedure? desc-proc)
371      (desc-proc actor whos-looking))
372     (desc desc)))
373
374 (define* (gameobj-get-desc actor message #:key whos-looking)
375   "This is the action equivalent of the gameobj-desc getter"
376   (<-reply message (gameobj-desc actor #:whos-looking whos-looking)))
377
378 (define (gameobj-visible-to-player? gameobj whos-looking)
379   "Check to see whether we're visible to the player or not.
380 By default, this is whether or not the generally-visible flag is set."
381   (not (slot-ref gameobj 'invisible?)))
382
383 (define* (gameobj-visible-name actor message #:key whos-looking)
384   ;; Are we visible?
385   (define we-are-visible
386     ((slot-ref actor 'visible-to-player?) actor whos-looking))
387
388   (define name-to-return
389     (if we-are-visible
390         ;; Return our name
391         (match (slot-ref actor 'name)
392           ((? procedure? name-proc)
393            (name-proc actor whos-looking))
394           ((? string? name)
395            name)
396           (#f #f))
397         #f))
398   (<-reply message #:text name-to-return))
399
400 (define (gameobj-self-destruct gameobj)
401   "General gameobj self destruction routine"
402   ;; Unregister from being in any particular room
403   (gameobj-set-loc! gameobj #f)
404   (slot-set! gameobj 'destructed #t)
405   ;; Boom!
406   (self-destruct gameobj))
407
408 (define* (gameobj-act-self-destruct gameobj message #:key why)
409   "Action routine for self destruction"
410   (gameobj-self-destruct gameobj))
411
412 ;; Unless an actor has a tell message, we just ignore it
413 (define gameobj-tell-no-op
414   (const 'no-op))
415
416 (define (gameobj-replace-data-occupants gameobj)
417   "The general purpose list of replacement data"
418   (list #:occupants (hash-map->list (lambda (occupant _) occupant)
419                                     (slot-ref gameobj 'occupants))))
420
421 (define (gameobj-replace-data* gameobj)
422   ;; For now, just call gameobj-replace-data-occupants.
423   ;; But there may be more in the future!
424   (gameobj-replace-data-occupants gameobj))
425
426 ;; So sad that objects must assist in their replacement ;_;
427 ;; But that's life in a live hacked game!
428 (define (gameobj-act-assist-replace gameobj message)
429   "Vanilla method for assisting in self-replacement for live hacking"
430   (apply <-reply message
431          (gameobj-replace-data* gameobj)))
432
433 (define (gameobj-ok-to-be-taken-from gameobj message whos-acting)
434   (call-with-values (lambda ()
435                       (slot-ref-maybe-runcheck gameobj 'take-me?
436                                                whos-acting #:from #t))
437     ;; This allows this to reply with #:why-not if appropriate
438     (lambda args
439       (apply <-reply message args))))
440
441 (define (gameobj-ok-to-be-put-in gameobj message whos-acting where)
442   (call-with-values (lambda ()
443                       (slot-ref-maybe-runcheck gameobj 'drop-me?
444                                                whos-acting where))
445     ;; This allows this to reply with #:why-not if appropriate
446     (lambda args
447       (apply <-reply message args))))
448
449 \f
450 ;;; Utilities every gameobj has
451 ;;; ---------------------------
452
453 (define (dyn-ref gameobj special-symbol)
454   "Dynamically look up a special object from the gm"
455   (match special-symbol
456     ;; if it's a symbol, look it up dynamically
457     ((? symbol? _)
458      (mbody-val (<-wait (slot-ref gameobj 'gm) 'lookup-special
459                         #:symbol special-symbol)))
460     ;; if it's false, return nothing
461     (#f #f)
462     ;; otherwise it's probably an address, return it as-is
463     (_ special-symbol)))
464
465
466 \f
467 ;;; Basic actions
468 ;;; -------------
469
470 (define* (cmd-take gameobj message
471                    #:key direct-obj
472                    (player (message-from message)))
473   (define player-name
474     (mbody-val (<-wait player 'get-name)))
475   (define player-loc
476     (mbody-val (<-wait player 'get-loc)))
477   (define our-name (slot-ref gameobj 'name))
478   (define self-should-take
479     (slot-ref-maybe-runcheck gameobj 'take-me? player))
480   ;; @@: Is there any reason to allow the room to object in the way
481   ;;   that there is for dropping?  It doesn't seem like it.
482   (call-with-values (lambda ()
483                       (slot-ref-maybe-runcheck gameobj 'take-me? player))
484     (lambda* (self-should-take #:key (why-not
485                                       `("It doesn't seem like you can take "
486                                         ,our-name ".")))
487       (if self-should-take
488           ;; Set the location to whoever's picking us up
489           (begin
490             (gameobj-set-loc! gameobj player)
491             (<- player 'tell
492                 #:text (format #f "You pick up ~a.\n"
493                                our-name))
494             (<- player-loc 'tell-room
495                 #:text (format #f "~a picks up ~a.\n"
496                                player-name
497                                our-name)
498                 #:exclude player))
499           (<- player 'tell #:text why-not)))))
500
501 (define* (cmd-drop gameobj message
502                    #:key direct-obj
503                    (player (message-from message)))
504   (define player-name
505     (mbody-val (<-wait player 'get-name)))
506   (define player-loc
507     (mbody-val (<-wait player 'get-loc)))
508   (define our-name (slot-ref gameobj 'name))
509   (define should-drop
510     (slot-ref-maybe-runcheck gameobj 'drop-me? player))
511   (define (room-objection-to-drop)
512     (mbody-receive (_ drop-ok? #:key why-not) ; does the room object to dropping?
513         (<-wait player-loc 'ok-to-drop-here? player (actor-id gameobj))
514       (and (not drop-ok?)
515            ;; Either give the specified reason, or give a boilerplate one
516            (or why-not
517                `("You'd love to drop " ,our-name
518                  " but for some reason it doesn't seem like you can"
519                  " do that here.")))))
520   (cond
521    ((not player-loc)
522     (<- player 'tell
523         #:text `("It doesn't seem like you can drop " ,our-name 
524                 " here, because you don't seem to be anywhere?!?")))
525    ;; TODO: Let ourselves supply a reason why not.
526    ((not should-drop)
527     (<- player 'tell
528         #:text (format #f "It doesn't seem like you can drop ~a.\n"
529                        our-name)))
530    ((room-objection-to-drop)
531     (<- player 'tell
532         #:text room-objection-to-drop))
533    (else
534     (gameobj-set-loc! gameobj player-loc)
535     ;; TODO: Allow more flavortext here.
536     (<- player 'tell
537         #:text (format #f "You drop ~a.\n"
538                        our-name))
539     (<- player-loc 'tell-room
540         #:text (format #f "~a drops ~a.\n"
541                        player-name
542                        our-name)
543         #:exclude player))))
544
545 (define* (cmd-take-from-no-op gameobj message
546                               #:key direct-obj indir-obj preposition
547                               (player (message-from message)))
548   (<- player 'tell
549       #:text `("It doesn't seem like you can take anything "
550                ,preposition " "
551                ,(slot-ref gameobj 'name) ".")))
552
553 (define* (cmd-put-in-no-op gameobj message
554                            #:key direct-obj indir-obj preposition
555                            (player (message-from message)))
556   (<- player 'tell
557       #:text `("It doesn't seem like you can put anything "
558                ,preposition " "
559                ,(slot-ref gameobj 'name) ".")))