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