(TADS 3) can you dynamically add an ActorState to an actor?

I’m having trouble trying to get some ConvNode logic to work in this cardgame library where what I’m trying to do is basically this:

When an NPC actor decides to start a game, if that NPC is designated to be the dealer, then that NPC will be given a new instance of the BlackjackDealing actor state (I make a new instance of it at runtime), and that instance will be added to the actor in question (moved into its inventory, as if it had been defined with a “+” operator under the actor’s definition in the source code).

But it doesn’t work. Specifically the ConvNodes inside that ActorState don’t work.

I suspect it’s because ActorStates do not function correctly if you use moveInto() to add them to the actor’s inventory at runtime. They had to be there already at compile time because of some initialization code run at startup.

What I’m asking is this: Is the above statement true? If so I have to design things differently.

Why I am thinking this might be the case is because of this code in the adv3 library file: /usr/local/share/frobtads/tads3/lib/adv3/actor.t, line 501 :

    /* preinitialize */
    execute()
    {
        /* add every ConvNode object to our master table */
        forEachInstance(ConvNode,
                        { obj: obj.getActor().convNodeTab[obj.name] = obj });

        /* 
         *   set up the prompt daemon that makes automatic topic inventory
         *   suggestions when appropriate 
         */
        new PromptDaemon(self, &topicInventoryDaemon);
    }

This is run inside of conversationManager which is a kind of PreinitObject, meaning this is a bit of code that will run prior to the first turn of the game. Note the use of “getActor()” in the above call. calling a ConvNode’s getActor() method will abort with a nil reference error if the ActorState that the ConvNode is part of isn’t inside of an actor at the time. (Which is how I found out about this bit of code. At first it was crashing because my BlackjackDealing ActorState was defined without using the “class” keyword so there was an instance of it at preInit time and this code was trying to operate on that instance. I got rid of that error by making it a class, but when I think about the logic in use here, it seems like getting rid of the runtime error by doing that still won’t make the logic actually work if this all important initialization step was skipped.)

The implication I am getting from this is that the library expects all ConvNode objects to already be located inside ActorStates that are in turn already located inside an actor at compile time.

Therefore this sort of logic doesn’t work, I think:

// During the game, when a blackjack minigame begins, run this code:
//
// assume BlackjackDealing is a subclass of ActorState that contains one or more ConvNodes inside it that drive how the dealer will react to statements like "ask <<name>> for card".
// assume act is a variable referencing an instance of Actor who is to become the dealer of this game.
local dealerNode = new BlackjackDealing;
dealerNode.moveInto(actor);
act.setCurState(dealerNode);
// I have just "given" the BlackjackDealing ActorState to the act actor, but the ConvNodes inside that state don't work.

Am I correct? If so the implication is that all actors must contain hardcoded at compile time inside of them all the ActorStates that they might switch to during the course of the game.

Yeah, adv3 kind of expects that NPCs will have all of their behaviors present at compile time. You can work around this by identifying and duplicating the relevant PreInit logic – there’s nothing in either PreInit or Init that you can’t do during the game.

Those phases are just efficiency optimizations - PreInit is for stuff that needs the basic world model and objects in place, so that things like an object’s location are properly set. The changes made here get saved into the virtual machine image that becomes the .t3 file you distribute.

Init is for stuff that changes on a per-game or per-session basis, like randomizing values or setting runtime behavior based on the features supported by the player’s interpreter.

In this case you could override the moveInto method on ConvNode to also do the part where it adds itself to the destination’s convNodeTab lookup table.

Yeah it’s even messier than that now that I’ve been working on it for a few hours of experimenting.

You can’t even define a class of actor to subclass from and put the behavior in that class definition.

i.e. you can’t make:

class Dealer : Object
;
+ dealingState: ActorState
// stuff detailing the behavior of a dealer
;
++ dealingConvNode: ConvNode
// stuff detailing the speech of a dealer reacting to "ask dealer for card" commands or "tell dealer hit me", etc.
;

janet: Actor, Dealer
// stuff
;

bill: Actor, Dealer
// stuff
;

In other words, not only does it require that the behavior be there at compile time, but it also requires that the behavior be mentioned in cut-n-paste style directly in each instance of actor that will have it, rather than in a class all such actors can inherit from. This is because of the “+” location syntax. Using the syntax above there will exist only a single instance of the ActorState which is inside the class ,instead of two instances of the ActorState - one inside janet and one inside bill. What I’m trying to find is a way to say “each time some object is defined to be of class Dealer, that means it also also gets one dealingState which is located inside it, and one dealingConvNode which is inside that.” I tried doing this by nesting the declaration of dealingState inside Dealer (and then explicitly setting its location to the lexicalParent), and doing the same with dealingConvNode, but that also breaks.

It seems as if the entire conversation system is dependant on the assumption that there will never be a case where it would be useful to give the same behavior to more than one NPC actor. Every example I can find shows it being used in a one-off sort of way just to apply to one actor only.

I suspect that if I start delving into the guts of adv3 to : (A) understand exactly what it’s doing so I don’t break any assumptions it’s making, and (B) override everywhere where the behavior depends on non-dynamic application of state, that I will be carving out more work for myself than simply bypassing the conversation system entirely and just writing my own handlers for speech using plain dobjFor and iobFor rules.

You can instantiate a new copy of the dealer-to-be’s ActorState and convNode class, either at runtime (if they don’t start out able to deal, in a makeDealer method or something) or during Init (by adding similar code to the actor’s initializeThing method).

ActorStates and convNodes do need to be per-Actor, or at least you would have lots of code to rework if you want to change that assumption.

You’re right that out of the box, adv3 is better at doing one interactive NPC with lots of custom coding. The use case of NPC classes, where similar behaviors apply to a number of distinct game world characters, is not addressed.

Why try to add the ActorState while the game is running? You can park it there in your code from the beginning of the game, and then activate it when it’s needed using, if memory serves, someActor.setCurState(dealerState).

The problem was that there’s more than one actor using the same state code, and I’m trying to avoid implementing that via cut-n-paste. I want to say “THIS actor has a blackjackPlaying actor state, and so does THAT one, and that other one” without repeating all the blackjack playing code three times.

Normally it’s trivially easy to get more than one object that behaves similarly by just subclassing them from a common base class, but it’s not that trivial when the object in question is an Actor and the behavior you’re trying to inherit is driven by ActorStates and ConvNodes. One reason for this is that in TADS3, an inner class inside an outer class doesn’t really re-instance itself with each instance of the outer class, as I found out the hard way. Instead each instance of the outer class references the SAME instance of the inner class, which makes it a bit harder to create a generic class for all blackjack-playing actors to inherit from when I need them to inherit more than just the actor subclass, but the entire linkage of ActorStates and ConvNodes that are inside the actor subclass. This is because a crucial aspect of their relationship is that ConvNodes must have their location properties pointing to parent ActorStates which must have their location properties pointing to a parent Actor, and you can’t really get that type of relationship prior to runtime via just simply subclassing in TADS3. You need to run some code to make that linkage exist, and that code has to be run BEFORE the PreinitObject classes do their work because it is in the Preinit step that those location linkages are “walked” by the adv3 library. If they’re not there, adv3 will crash before turn 1 start, and they can’t be there yet prior to running the code in the engine because of the subclassing of inner classes problem mentioned above.

The only working solution seems to be to dynamically create instances of the ActorStates and ConvNodes underneath the Actor subclass each time a new instance of the Actor subclass is created. And that means finding a place to run code prior to Preinit.

I’m coming to a solution that involves using the construct() method, and the fact that all object instances are “run” once at compile time before the game image is saved. Thus I can get the compiler to run the construct() methods just before saving the initial game state in the t3 file and have it build up the location relationships I want then. This seems to be working but I want to test it out first before I post it as a solution. One feature of this is that each time you have code inside a property definition, like so:

myObj : Thing
  someProp = 3 * x + 2
;

(So someProp is not a fixed value, but an expression that needs to be run) the compiler, which contains a lot of the TADS3 code execution engine in it too, actually executes that statement once before storing the compiled image in the t3 file.

So I can make the location connections like so:

class MyDealerActor : Actor
  // other unrelated properties not shown.
  innerState = new MyState(self)
;
class MyState : ActorState
  // other unrelated properties not shown.
  innerConvNode = new MyConvNode(self)

  construct(containingObj)  // I found out this is unnecessary as ActorState already has a constructor that does exactly this.
  { inherited(containingObj);
    self.moveInto(containingObj);
  }
;
class MyConvNode: ConvNode
 // other unrelated properties not shown.

  construct(containingObj)  // But unlike ActorState, for some reason ConvNode does not have a constructor like this already, so this bit actually is neccessary.
  { inherited();
    self.moveInto(containingObj);
  }
;
// etc.

Thus the compiler (not the game runner) runs the initialization code for each instance of MyDealerActor (the ‘new’ operator in the property definition) and recurses through all these classes running all these new operators ONCE during compile time. This seems to be working as it causes the location relationships to get built at compile time despite them looking like dynamic code that runs at runtime.

I would think you could use a factory style approach to instantiate a given set of children and their relationships and add them properly to the actor. I’ve been thinking of doing something like that for complex furniture. So, instead of each time creating a dresser with 3 drawers and surface etc, you create something that correctly sets up each of the child components for you…yeah, I’m that lazy…and I hate dressers that just have “open drawer”:slight_smile: I haven’t tried implementing this, so I can’t say with 100% certainty that it would work, but I suspect that it will. And, I think the same idea would work for you. It would take a little work to make happen, though.

Yeah it also helps fix another problem I ran into, which is that the difference between running with debug enabled versus disabled not only makes a difference in whether the initializations happen at runtime or compile time, but it also can change the ORDER in which they happen too. With debug off, all initializations happen before the game begins. With debug on, the initialization of a property tended to wait until the first place the code tried to use the property, so they tended to occur in a different order relative to each other.

Try forcing preinit in debug builds. This is done with the “-pre” option (you can also add it to the makefile.)