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