actor state desc overwritten by followAgenda desc (adv3Lite)

When Harry and Mike meet up with Felipe, Felipe is in the felipeInCourtyard actor state, which has the following specialDesc

    specialDesc="Felipe stood off to the side of the door, a bit wobbly and
        with a somewhat blank look on his face. <.p>"

A followAgendaItem is defined for Felipe with these isReady conditions…

    isReady = (harry.location == hernandezCourtyard && 
              mike.location == hernandezCourtyard && 
              felipe.location == hernandezCourtyard &&
              gRevealed('filepeRealityCheck'))

When the agenda item becomse active, the following specialDesc is defined…

    specialDesc="Felipe stood sullenly in the middle of the courtyard waiting
                for Harry and Mike to follow.<.p>";

When I run the game code without adding this item to Felipe’s agenda, the actor state specialDesc is used, which is good.

But when I add the item to Felipe’s agenda, the follow item specialDesc is used even though the isReady flag evaluates to nil (all actors are in the required location, but gRevealed(‘filepeRealityCheck’) is nil).

Felipe should not be waiting for them to follow him until after felipeRealityCheck evaluates to true (which it never does in this test bed, but it does in the game code, after Harry has a conversation with Felipe).

Here’s the code in a testbed environment…

#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

versionInfo: GameID
    IFID = '445C38A3-AD1B-4729-957A-F584600DE5C1'
    name = 'test'
    byline = 'by Jerry Ford'
    htmlByline = 'by <a href="mailto:jerry.o.ford@gmail.com">
                  Jerry Ford</a>'
    version = '1'
    authorEmail = 'Jerry Ford <jerry.o.ford@gmail.com>'
    desc = 'Testing actor state conflict with follow agenda item'
    htmlDesc = 'Testing actor state conflict with follow agenda item.'

;

gameMain: GameMainDef
    /* the initial player character is 'harry' */
    initialPlayerChar = harry
    paraBrksBtwnSubcontents = nil
    usePastTense = true
;

// harry, main character
harry: Actor 'Harry;;man self' @roomA
    "Harry. <.p>"
    globalParamName = 'harry'
    isHim = true
    isInitState = true
    person = 3   
    proper = true

    actorBeforeAction()
    {
        felipe.addToAgenda(followFelipe);
    }
;
// mike character (tattoo parlor counterman)
mike: Actor 'Mike;tattoo artist clerk barrel-chested barrel chested;man counterman' @roomA
    "Mike wore an orange tank top that barely covered a barrel
    chest; thick, muscular biceps emerged from the tank top's thin shoulder
    straps. Inked artwork covered all visible skin from the jawline and lower ears
    downward.
    <.p> "
        
    globalParamName = 'mike'
    isHim = true
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3

    isFollowing = true
;

felipe: Actor 'Felipe;guard henchman' @hernandezCourtyard
    "Felipe was a short, stocky Mexican, with close cropped dark hair, a thin
    moustache, and puffy cheeks. 
    <.p> "
        
    globalParamName = 'felipe'
    isHim = true
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3
;

// in courtyard
+ felipeInCourtyard: ActorState
    name = 'felipeInCourtyard'
    specialDesc="Felipe stood off to the side of the door, a bit wobbly and
        with a somewhat blank look on his face. <.p>"
    
    isInitState = true;
;

// ********** FollowAgenda items **********************************************

// leading mike and harry across the courtyard
+ followFelipe: FollowAgendaItem
    isReady = (harry.location == hernandezCourtyard && 
              mike.location == hernandezCourtyard && 
              felipe.location == hernandezCourtyard &&
              gRevealed('filepeRealityCheck'))
    connectorList = [hernandezFrontHall]
    
    specialDesc()
    {
        if(isDone != true)
        {
            switch(getActor.getOutermostRoom)
            {
            case hernandezCourtyard:
                "Felipe stood sullenly in the middle of the courtyard waiting
                for Harry and Mike to follow.<.p>";
                break;
            }
        }
        else
            inherited;
    }
    arrivingDesc()
    {
        "Felipe motioned Harry to go south into the study, then he started up the stairs,
        Mike following close behind him. After they reached the top and 
        disappeared from view, Harry heard an upstairs door open and
        close. <.p>";
    }
    sayDeparting(hernandezFrontHall)
    {
        "Harry followed Felipe up the stairs to the east into the house. <.p>";
    }
;



roomA: Room 'Room A'
    "Now in Room A."
    
    east = hernandezCourtyard
;
hernandezCourtyard: Room 'Hernandez Courtyard'
    "Now in Hernandez Courtyard"
    
    west = roomA
    east = hernandezFrontHall
;
hernandezFrontHall: Room 'Hernandez Front Hall'
    "Now in Hernandez Front Hall"
    
    west = hernandezCourtyard
;

I haven’t had the chance to test this yet, but I think the problem is caused by the resetItem() method of the FollowAgendaItem class at around line 5717 in actor.t. The following statement in this method should be removed:

getActor.followAgendaItem = self;

The mistake arose because I’d forgotten that resetItem() was called by addToAgenda(); it’s causing the actor to think it has an active FollowAgendaItem even if the FollowAgendaItem isn’t ready yet. I’ll need to do a quick test to make sure that removing this statement cures the problem without breaking anything else, but in the meantime you can try commenting out the offending statement.

In any case I’m glad you caught this one before I issue the next release!

Incidentally, I appreciate that what you posted was testbed code and not necessarily what you’ve actually used in your game, but the following piece of code looks potentially problematic to me:

harry: Actor 'Harry;;man self' @roomA
    "Harry. <.p>"
    globalParamName = 'harry'
    isHim = true
    isInitState = true
    person = 3   
    proper = true

    actorBeforeAction()
    {
        felipe.addToAgenda(followFelipe);
    }
;

The potential problem I can see is that actorBeforeAction() will be executed on each turn, with the result that multiple copies of the followFelipe FollowAgendaItem will be added to felipe’s agendaList, with somewhat unpredictable consequences.

Also, by the way, defining isInitState on an Actor (as you’ve done here) is meaningless, since this property is only relevant to ActorStates; proper = true is redundant, since the library will deduce this from the fact that ‘Harry’ in the vocab property starts with a capital H, and you can get rid of the need to define isHim = true by adding ‘him’ to the end of the vocab property:

harry: Actor 'Harry;;man self; him' @roomA
    "Harry. <.p>"
    globalParamName = 'harry'
    
    person = 3   
    actorBeforeAction()
    {
        if(!felipe.agendaList.indexOf(followFelipe))
            felipe.addToAgenda(followFelipe);
    }
;

I’ve now been able to test my proposed fix, and so far as I can see it works okay, so that’s the change I’ll make to the library for the next release.

In order to test it I had to make a couple more tweaks to your testbed code, which now reads:

#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

versionInfo: GameID
    IFID = '445C38A3-AD1B-4729-957A-F584600DE5C1'
    name = 'test'
    byline = 'by Jerry Ford'
    htmlByline = 'by <a href="mailto:jerry.o.ford@gmail.com">
                  Jerry Ford</a>'
    version = '1'
    authorEmail = 'Jerry Ford <jerry.o.ford@gmail.com>'
    desc = 'Testing actor state conflict with follow agenda item'
    htmlDesc = 'Testing actor state conflict with follow agenda item.'

;

gameMain: GameMainDef
    /* the initial player character is 'harry' */
    initialPlayerChar = harry
    paraBrksBtwnSubcontents = nil
    usePastTense = true
    
    showIntro()
    {
        mike.startFollowing();   
    }
;

// harry, main character
harry: Actor 'Harry;;man self; him' @roomA
    "Harry. <.p>"
    globalParamName = 'harry'
   
    person = 3   
    actorBeforeAction()
    {
        if(felipe.agendaList == nil || !felipe.agendaList.indexOf(followFelipe))
            felipe.addToAgenda(followFelipe);
    }
;

// mike character (tattoo parlor counterman)
mike: Actor 'Mike;tattoo artist clerk barrel-chested barrel chested;man counterman' @roomA
    "Mike wore an orange tank top that barely covered a barrel
    chest; thick, muscular biceps emerged from the tank top's thin shoulder
    straps. Inked artwork covered all visible skin from the jawline and lower ears
    downward.
    <.p> "
       
    globalParamName = 'mike'
    isHim = true
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3   
;

felipe: Actor 'Felipe;guard henchman' @hernandezCourtyard
    "Felipe was a short, stocky Mexican, with close cropped dark hair, a thin
    moustache, and puffy cheeks.
    <.p> "
       
    globalParamName = 'felipe'
    isHim = true
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3
;

// in courtyard
+ felipeInCourtyard: ActorState
    name = 'felipeInCourtyard'
    specialDesc="Felipe stood off to the side of the door, a bit wobbly and
        with a somewhat blank look on his face. <.p>"
   
    isInitState = true;
;

// ********** FollowAgenda items **********************************************

// leading mike and harry across the courtyard
+ followFelipe: FollowAgendaItem
    isReady = (harry.location == hernandezCourtyard &&
              mike.location == hernandezCourtyard &&
              felipe.location == hernandezCourtyard &&
              gRevealed('filepeRealityCheck'))
    connectorList = [hernandezFrontHall]
   
    specialDesc()
    {
        if(isDone != true)
        {
            switch(getActor.getOutermostRoom)
            {
            case hernandezCourtyard:
                "Felipe stood sullenly in the middle of the courtyard waiting
                for Harry and Mike to follow.<.p>";
                break;
            }
        }
        else
            inherited;
    }
    arrivingDesc()
    {
        "Felipe motioned Harry to go south into the study, then he started up the stairs,
        Mike following close behind him. After they reached the top and
        disappeared from view, Harry heard an upstairs door open and
        close. <.p>";
    }
    sayDeparting(hernandezFrontHall)
    {
        "Harry followed Felipe up the stairs to the east into the house. <.p>";
    }
;



roomA: Room 'Room A'
    "Now in Room A."
   
    east = hernandezCourtyard
;
hernandezCourtyard: Room 'Hernandez Courtyard'
    "Now in Hernandez Courtyard"
   
    west = roomA
    east = hernandezFrontHall
;
hernandezFrontHall: Room 'Hernandez Front Hall'
    "Now in Hernandez Front Hall"
   
    west = hernandezCourtyard
;

Doer 'jump'
    execAction(c)
    {
        "<q>Follow me,</q> said Felipe. <.reveal filepeRealityCheck> ";
    }
    where = hernandezCourtyard
;    

The two additional changes here are:

(1) Removing isFollowing = true from the definition of the Mike actor (since this does absolutely nothing) and calling mike.startFollowing() from gameMain.showIntro() instead so that Mike actually will follow Harry as presumably intended.

(2) Adding a Doer that can be used to trigger the FollowAgendaItem (when a JUMP command is issued in the courtyard) so that it’s possible to check that the FollowAgendaItem still works properly.

You may want to use the revised code above if you want to check that my suggested fix works.

Okay, I’ve implemented your fixes in my test bed environment and all is well. Thanks.

Thanks also for your clean-up advice on my character definitions.

jerry

Well, there’s still a bit of a glitch in the transcript.

The fix does work, the followAgendaItem special desc text does not get used before its time, and it does get used after the item is added to the agenda list, so that solves my original problem.

But it takes two look commands to see the correct text.

In the following transcript excerpt, felipeRealityCheck becomes true when a crestfallen Felipe realizes he’s been had…

Here’s the code that makes it so…

        {: "<q>For now,</q> Mike said, <q>get us inside the house. We\'ll park
        Harry, here, in the study, and I\'ll go upstairs to talk to Cesar. Yes,
        that\'s right, he will talk to me. He is upstairs, isn\'t he, Felipe? 
        It\'s siesta time, isn\'t that right?</q> \b
        <q>Si,</q> said a crestfallen Felipe. <q>Siesta time. Very well,
        se&ntilde;or, follow me.</q> <.reveal
        felipeRealityCheck><.p>" }

A look immediately after shows the ActorState special desc text. It requires a second look immediately after to show the followAgendaItem special desc text…

The followAgendaItem gets added to Felipe’s agenda in actorBeforeAction() on the harry object…

    actorBeforeAction()
    {
        if(harry.location == hernandezCourtyard && 
           felipe.location == hernandezCourtyard && 
           gRevealed('felipeRealityCheck'))
        {
            if(felipe.agendaList == nil || !felipe.agendaList.indexOf(followFelipe))
                felipe.addToAgenda(followFelipe);
        }

Shouldn’t the actorBeforeAction() code get used prior to display of the transcript?

Jerry

I’ve installed the 0.92 library and still have a FollowAgendaItem descriptive text issue.

In the test bed code that you revised to place the <.reveal felipeRealityCheck> in Doer ‘jump’ code, the specialDesc test defined in the FollowAgendaItem item is used.

But if the reveal is in the desc text for the initial room, the FollowAgendaItem specialDesc text is never used, only the ActorState specialDesc text.

Under the current conditions, I have to force Harry to do some useless action (> jump) prior to entering the courtyard in order to get the FollowAgendaItem to show the correct description of Felipe. My expectation is that once the reveal has been made and the actors are all in place as listed in the follow item’s isReady property then it should work correctly.

But it doesn’t.

The test bed code sets the flag in the Room A description…

roomA: Room 'Room A'
    "Now in Room A. <.reveal felipeRealityCheck>"
    
    east = hernandezCourtyard
;

But when Harry enters the courtyard, Felipe’s description is from the ActorState

The follow item does work…

It’s just the specialDesc text that doesn’t.

I’ve put breakpoints in the debugger, and the felipe.agendaList is getting updated correctly when Harry moves east out of Room A…

    actorBeforeAction()
    {
        if(felipe.agendaList == nil || !felipe.agendaList.indexOf(followFelipe))
            felipe.addToAgenda(followFelipe);
    }

It just doesn’t trigger the use of the specialDesc text.

Here’s the current state of the test bed code, with both your Doer ‘jump’ code and my actorBeforeAction() in place…

#charset "us-ascii"

#include <tads.h>
#include "advlite.h"

versionInfo: GameID
    IFID = '445C38A3-AD1B-4729-957A-F584600DE5C1'
    name = 'test'
    byline = 'by Jerry Ford'
    htmlByline = 'by <a href="mailto:jerry.o.ford@gmail.com">
                  Jerry Ford</a>'
    version = '1'
    authorEmail = 'Jerry Ford <jerry.o.ford@gmail.com>'
    desc = 'Testing actor state conflict with follow agenda item using 0.92 adv3lite library'
    htmlDesc = 'Testing actor state conflict with follow agenda item using 0.92 adv3lite library.'

;

gameMain: GameMainDef
    /* the initial player character is 'harry' */
    initialPlayerChar = harry
    paraBrksBtwnSubcontents = nil
    usePastTense = true
    
    showIntro()
    {
        mike.startFollowing();
    }
;

// harry, main character
harry: Actor 'Harry;;man self;him' @roomA
    "Harry. <.p>"
    globalParamName = 'harry'
    person = 3   

    actorBeforeAction()
    {
        if(felipe.agendaList == nil || !felipe.agendaList.indexOf(followFelipe))
            felipe.addToAgenda(followFelipe);
    }
;
// mike character (tattoo parlor counterman)
mike: Actor 'Mike;tattoo artist clerk barrel-chested barrel chested;man
    counterman;him' @roomA
    "Mike wore an orange tank top that barely covered a barrel
    chest; thick, muscular biceps emerged from the tank top's thin shoulder
    straps. Inked artwork covered all visible skin from the jawline and lower ears
    downward.
    <.p> "
        
    globalParamName = 'mike'
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3
;

felipe: Actor 'Felipe;guard henchman;;him' @hernandezCourtyard
    "Felipe was a short, stocky Mexican, with close cropped dark hair, a thin
    moustache, and puffy cheeks. 
    <.p> "
        
    globalParamName = 'felipe'
    person = 3   
    bulkCapacity = 5000
    attentionSpan = 3
;

// in courtyard
+ felipeInCourtyard: ActorState
    name = 'felipeInCourtyard'
    specialDesc="Felipe stood off to the side of the door, a bit wobbly and
        with a somewhat blank look on his face. <.p>"
    
    isInitState = true;
;

// ********** FollowAgenda items **********************************************

// leading mike and harry across the courtyard
+ followFelipe: FollowAgendaItem
    isReady = (harry.location == hernandezCourtyard && 
              mike.location == hernandezCourtyard && 
              felipe.location == hernandezCourtyard &&
              gRevealed('felipeRealityCheck'))
    connectorList = [hernandezFrontHall]
    
    specialDesc()
    {
        if(isDone != true)
        {
            switch(getActor.getOutermostRoom)
            {
            case hernandezCourtyard:
                "Felipe stood sullenly in the middle of the courtyard waiting
                for Harry and Mike to follow.<.p>";
                break;
            }
        }
        else
            inherited;
    }
    arrivingDesc()
    {
        "Felipe motioned Harry to go south into the study, then he started up the stairs,
        Mike following close behind him. After they reached the top and 
        disappeared from view, Harry heard an upstairs door open and
        close. <.p>";
    }
    sayDeparting(hernandezFrontHall)
    {
        "Harry followed Felipe up the stairs to the east into the house. <.p>";
    }
;



roomA: Room 'Room A'
    "Now in Room A. <.reveal felipeRealityCheck>"
    
    east = hernandezCourtyard
;
hernandezCourtyard: Room 'Hernandez Courtyard'
    "Now in Hernandez Courtyard"
    
    west = roomA
    east = hernandezFrontHall
;
hernandezFrontHall: Room 'Hernandez Front Hall'
    "Now in Hernandez Front Hall"
    
    west = hernandezCourtyard
;

Doer 'jump'
    execAction(c)
    {
        "<q>Follow me,</q> said Felipe. <.reveal felipeRealityCheck>";
    }
;

The two cases are slightly different here. I’ll take them in the reverse order.

You can see what’s happening in your second case if you change the description of the courtyard thus:

hernandezCourtyard: Room 'Hernandez Courtyard'
    "Now in Hernandez Courtyard. Mike location = <<mike.location.name>>. "
   
    west = roomA
    east = hernandezFrontHall
;

Now when you try to move Harry into the courtyard you’ll see:

Room A
Now in Room A. 

Mike was there. 

>e

Hernandez Courtyard
Now in Hernandez Courtyard. Mike location = room a. 

Felipe stood off to the side of the door, a bit wobbly and with a somewhat blank look on his face. 

Mike followed behind Harry. 

This makes is clearer that Mike doesn’t follow Harry until after the rest of the room description has been displayed, so at the time that Felipe’s specialDesc is displayed, Mike isn’t yet in the courtyard, so the isReady condition of the FollowAgendaItem isn’t yet true, since it’s defined thus:

 isReady = (harry.location == hernandezCourtyard &&
              mike.location == hernandezCourtyard &&
              felipe.location == hernandezCourtyard &&
              gRevealed('felipeRealityCheck'))

In other words, I don’t believe the library is acting incorrectly here, it’s just that your isReady condition becomes true a bit later than you expected.

EDIT: So what you actually need to test for here is not where Mike is currently, but whether he’s following Harry:

isReady = (harry.location == hernandezCourtyard &&
              mike.fDaemon != nil &&
              felipe.location == hernandezCourtyard &&
              gRevealed('felipeRealityCheck'))

/EDIT

To go on to your earlier case, it looks to me as this is an unfortunate side-effect of everything working as it was designed to work.
The ‘felipeRealityCheck’ tag is revealed in response to a conversational command. This means that the FollowAgendaItem won’t be executed until the end of the following term (if an actor converses on one turn his agenda won’t be executed on that turn; this is deliberate in order to avoid any actor doing too much on any one turn, and, in particular, going off and doing his own thing in the middle of a conversation). It’s only when the FollowAgendaItem is executed that it registers itself with its actor for the purposes of using its specialDesc. Again, this is deliberate in order to prevent a FollowAgendaItem’s specialDesc being used before it’s ready.

But, although everything is actually working as it’s designed to here, I can see that the end result in this particular case is likely to appear counterintuitive to game authors, so instead of suggesting a workaround that authors would have to remember to use, it’s probably better to tweak the library so they don’t have to.

The tweak I suggest is amending actor.specialDesc (at line 81 in actor.t) to read as follows:

 specialDesc()
    {
        
        local followItem;
        
        /* 
         *   Check whether we have a FollowAgendaItem that's ready to be used,
         *   and if so, register it as our current followAgendaItem.
         */
        if(agendaList != nil &&
           (followItem = agendaList.valWhich({x: x.ofKind(FollowAgendaItem) &&
                                            x.isReady && !x.isDone})) != nil)
            followAgendaItem = followItem;
        
        /* If we have a current followAgendaItem, use its specialDesc */
        if(followAgendaItem != nil)
            followAgendaItem.showSpecialDesc();
        
        /* 
         *   Otherwise use our current ActorState's specialDesc if we have one
         *   or our our actorSpecialDesc if not.
         */
        else
            curState != nil ? curState.specialDesc : actorSpecialDesc;
    }

You can test this in your test-bed code by removing the reveal tag from the description of RoomA and then adding the following after the felipe object:


+ DefaultAnyTopic
    "<q>Follow me,</q> said Felipe. <.reveal felipeRealityCheck>"
;

With this, I get the following transcript:

Room A
Now in Room A. 

Mike was there. 

>e
Hernandez Courtyard
Now in Hernandez Courtyard. Mike location = room a. 

Felipe stood off to the side of the door, a bit wobbly and with a somewhat blank look on his face. 

Mike followed behind Harry. 

>ask felipe about foo
“Follow me,” said Felipe. 

>l
Hernandez Courtyard
Now in Hernandez Courtyard. Mike location = hernandez courtyard. 

Mike was there. 

Felipe stood sullenly in the middle of the courtyard waiting for Harry and Mike to follow.

Perhaps you’d like to try that and confirm whether it solves the problem in your actual game. You’ll find, incidentally, that the tweak will also alleviate the problem with your version of the test-bed game. As thing stands, you’d have to enter two LOOK commands before the FollowAgendaItem’s specialDesc is used for felipe; with the tweak in place you’d only have to type one.

EDIT: With the tweak in place and the change of isReady condition on the FollowAgendaItem so you’re testing for whether Mike’s following rather than where he is, you don’t have to enter a LOOK command at all; you’d then see the FollowAgendaItem’s specialDesc immediately on entering the courtyard (that is, if you’ve previously revealed the felipeRealityCheck tag).

Success!

I added your new code to actor.t and modified the isReady condition and the follow mechanism now appears to be working, both in the test bed and in the game.

The FollowAgendaItem specialDesc text gets displayed correctly without my having to implement any spurious actions just the get things initialized.

In the game, there are two places where an NPC invites Harry to follow (different actors, different conditions) and in both places the fix seems to be working.

Thanks.

Jerry