Mechanism/pattern for global beforeAction()/afterAction()?

Is there any mechanism in TADS3/adv3 to globally register a object to be notified for arbitrary/all beforeAction() and/or afterAction() checks?

Action provides a way to register an object for that action: addBeforeAfterObj(). And any Thing that happens to be in the same scope as the action will automagically have its beforeAction() and afterAction() methods called at the appropriate time.

But I want something like a global singleton that gets pinged by every beforeAction() and afterAction() globally so it can decide if some condition(s) have been met. Specifically I want to be able to do this at a moment in processing where the called method(s) will have access to gActor, gAction, and so on.

This is to implement a scene controller thing kinda like I7’s scenes. There’s already a T3 contrib module that does something like this (Eric Eve’s scenes.t), but it uses PromptDaemon for polling every turn, and that happens before command evaluation, so gActor (and everything else) isn’t set when it does its checking. PromptDaemon is derived from Event, and all Event-based mechanisms seem to have the same problem.

Replying with a kludgy mess that illustrates what I’m trying to do.

This example defines a simplistic Scene class and a sceneController that handles the instances. Each Scene is triggered by matching three properties defined on the instance: actor to match who’s doing the action; target to match the direct object of the action; and action to match the action itself. So in the example

        target = Pebble
        action = ExamineAction

…will cause the Scene containing the above to match whenever anyone attempts to examine a pebble, thus:

>x pebble
The pebble will be examined.  A small, round pebble.  The pebble has been
examined.

The source:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

modify Action
        construct() {
                inherited();
                self.addBeforeAfterObj(sceneController);
        }
;

sceneController: PreinitObject
        // List of all scenes, built at runtime
        sceneList = []

        execute() {
                initSceneList();
        }

        // Create the scene list
        initSceneList() {
                local obj;

                obj = firstObj(Scene);
                while(obj) {
                        sceneList += obj;
                        obj = nextObj(obj, Scene);
                }
        }

        // Get a list of all the scenes that match the given actor, object,
        // and action
        matchSceneList(actor, obj, action) {
                local r;

                r = [];
                sceneList.forEach(function(o) {
                        if(!o.matchActionTuple(actor, obj, action))
                                return;
                        r += o;
                });

                return(r);
        }
        beforeAction() {
                local l = matchSceneList(gActor, gDobj, gAction);
                l.forEach(function(o) {
                        o.beforeAction();
                });
        }
        afterAction() {
                local l = matchSceneList(gActor, gDobj, gAction);
                l.forEach(function(o) {
                        o.afterAction();
                });
        }
;

class Scene: object
        // Actor, object, and action conditions for scene
        actor = nil
        target = nil
        action = nil

        // See if the passed actor, object, and action match our conditions
        matchActionTuple(actor, obj, action) {
                return(o.matchActor(actor) && o.matchTarget(obj)
                        && o.matchAction(action));
        }

        // Utility method
        _matchBit(v, cls) {
                local r;

                // No match criteria defined, always match
                if(!cls) return(true);

                // No argument, always fail
                if(!v) return(nil);

                // cls is a list, return true if we match any
                if(cls.ofKind(List)) {
                        r = nil;
                        cls.forEach(function(o) {
                                if(!v.ofKind(o)) return;
                                r = true;
                        });
                        return(r);
                }
                // If the cls isn't a list, it must be a class
                return(v.ofKind(cls));
        }
        matchActor(v) { return(_matchBit(v, actor)); }
        matchTarget(v) { return(_matchBit(v, target)); }
        matchAction(v) { return(_matchBit(v, action)); }

        beforeAction() {}
        afterAction() {}
;

beforeExamineScene: Scene
        target = Pebble
        action = ExamineAction

        beforeAction() {
                "The pebble will be examined. ";
        }
;
afterExamineScene: Scene
        target = Pebble
        action = ExamineAction

        afterAction() {
                "The pebble has been examined. ";
        }
;

class Pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble. "
;

startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ pebble: Pebble;

me:     Actor
        location = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

The thing that makes me unhappy with this is that it relies on modifying the Action base class’ constructor:

modify Action
        construct() {
                inherited();
                self.addBeforeAfterObj(sceneController);
        }
;

This seems like it’s probably a pretty brittle “solution”: I don’t know how many actions overwrite the constructor themselves, for example.

So I’d be delighted if anyone knows of a better solution to this kind of thing. Just adding the example to better illustrate what I’m trying to do.

There’s beforeActionMain() in Action. You could modify Action so that function calls into your singleton. gActor and gAction have been set by the time it’s invoked. (Likewise, there’s afterActionMain().)

I guess that’s probably safer than overwriting the constructor.

It’s…odd…that TADS3/adv3 don’t provide a mechanism for doing this more cleanly. Action already has the idea that objects will want to subscribe to notifications via addBeforeAfterObj(), there’s just no way (as near as I can tell) to do that for all actions (short of modifying Action itself).

By comparison, it’s reasonably trivial to write a PreinitObject that iterates through all of the objects using firstObj() and nextObj(), for example. But unless I missed it, there’s no similar approach that works for all the Actions.

Part of the issue here is that TADS doesn’t have a clean notion of static methods or functions. I could imagine Action in C# or Java having a static method that’s called at certain times to indicate “this instance of Action is doing X…”

(I have seen some code which seems to use a method statically, but I’ve never quite grokked what was going on there. Something I need to learn more about.)

I suppose another possibility is to add a static Vector property to Action (as described a quarter of the way down here) that is invoked by each Action, much as the instance-based one is invoked already. This is more involved, but also more extensible.

(The adv3Lite library has an experimental extension called Signals which permits a subscriber to listen for events from an object. But I don’t believe it has a notion of a subscriber receiving events for all objects of a particular type.)

Generally, TADS seems to deal with these problems in ad hoc ways, using some combination of PreinitObject and modify / replace tied into a singleton “manager” instance.

Since most games have a single Actor, or a severely constrained list of them, I suspect that’s where authors hook in to intercept actions. (I know that’s what I do when I need it.) If I was writing a game with a fair number of Actors, I would probably code a subclass and dispatch the intercepted Actions from there.