Strange behaviour of removed henchman when PC rests

At one point I remove the PC’s henchman (using RemoveHenchman) and store them backstage, because they may or may not return. You know.

And then, the first (I think) time the PC rests after that, the henchman appears next to them and immediately starts walking towards the edge of the area, where they vanish. As you can probably all imagine, that’s a very, very unwelcome development, for a lot of story reasons.

I use this OnRest script:

void main()
{
    int iRestEvent = GetLastRestEventType();
    object oRestingPC = GetLastPCRested();
    int iAllowedToRest = GetLocalInt(oRestingPC, "ALLOWED_TO_REST");    // Set by a trigger
    //Check to see if the PC is starting to rest
    if (iRestEvent == REST_EVENTTYPE_REST_STARTED)
     {
        if (iAllowedToRest == 0)
            {
            AssignCommand(oRestingPC, ClearAllActions());
            FloatingTextStringOnCreature("This spot is unsuitable for resting.", oRestingPC, FALSE);
            }

        else if (iAllowedToRest == 1)
         {
         ActionRest();
         int iTime = GetTimeHour();
         SetTime(iTime+4, 0, 0, 0);
         }
     }
}

which I can’t remember how I came up with, but it was probably with some help from you guys.

This code doesn’t affect any NPCs directly, so the problem has to be located elsewhere. Probably triggered by creature’s scripts or some heartbeat (module’s or area’s).

Commence script runtime forensics:

  1. When did it start happening? Did you install some script system?
  2. Do you need to rest within a heartbeat of disbanding the hench or it doesn’t matter?
  3. Is there a delay between rest init and hench appearing?
  4. Are other henchmen also affected?
  5. Does it matter at what time you rest?
  6. Does it happen when you comment out the SetTime line?
  7. Does it happen when you comment out the ActionRest line?
  8. Does it happen when you park the hench in another area?
  9. Does it happen when you remove all/some scripts from the hench?

Based on this, I’d speculate that it might be caused by some “go home at night” code that kicks in when you advance the clock while the hench is no longer bound by the follower AI logic and acts like a common commoner.

1 Like

The module has no heartbeat. Only one area has a heartbeat, and that one only affects tagged creatures, none of whom are henchmen.

Details of the area hb, just in case

It’s probably unbelieveably clunky and inelegant, but it’s the only way I knew how to do it, so I did.
// (Ghosts vanish during day, appear during night. Changed from dawn/dusk because resting timeskip messed it up.)

void main()
{
object oPC = GetFirstPC();
if (!(GetArea(oPC) == OBJECT_SELF)) return;
if((GetLocalInt(oPC, "NW_JOURNAL_ENTRYcntr_ghost") > 1))return;

object oGhost1 = GetObjectByTag("cntr_ruin_ghost1");
object oGhost2 = GetObjectByTag("cntr_ruin_ghost2");
object oGhost3 = GetObjectByTag("cntr_ruin_ghost3");
object oFlame = GetObjectByTag("cntr_ruin_ghostflame");
effect eVis = EffectVisualEffect(VFX_FNF_SMOKE_PUFF);


    // check if it is dusk, use a variable to make sure this fires only once per dusk
    if(GetLocalInt(GetArea(OBJECT_SELF), "daytime") == 1 && GetIsNight())
    {
        SetLocalInt(GetArea(OBJECT_SELF), "daytime", 0);
        AssignCommand(oGhost1, ClearAllActions());
        AssignCommand(oGhost1, ActionJumpToObject(GetWaypointByTag("WP_cntr_ruin_night1")));
        ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eVis, GetLocation(GetWaypointByTag("WP_cntr_ruin_night1")));
        AssignCommand(oGhost2, ClearAllActions());
        AssignCommand(oGhost2, ActionJumpToObject(GetWaypointByTag("WP_cntr_ruin_night2")));
        ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eVis, GetLocation(GetWaypointByTag("WP_cntr_ruin_night2")));
        AssignCommand(oGhost3, ClearAllActions());
        AssignCommand(oGhost3, ActionJumpToObject(GetWaypointByTag("WP_cntr_ruin_night3")));
        ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eVis, GetLocation(GetWaypointByTag("WP_cntr_ruin_night3")));
        AssignCommand(oFlame, PlayAnimation(ANIMATION_PLACEABLE_ACTIVATE));
     }

     if(GetLocalInt(GetArea(OBJECT_SELF), "daytime") == 0 && GetIsDay())
     {
        SetLocalInt(GetArea(OBJECT_SELF), "daytime", 1);
        AssignCommand(oGhost1, ClearAllActions());
        AssignCommand(oGhost1, ActionJumpToObject(GetWaypointByTag("WP_cntr_ruin_day")));
        AssignCommand(oGhost2, ClearAllActions());
        AssignCommand(oGhost2, ActionJumpToObject(GetWaypointByTag("WP_cntr_ruin_day")));
        AssignCommand(oGhost3, ClearAllActions());
        AssignCommand(oGhost3, ActionJumpToObject(GetWaypointByTag("WP_cntr_ruin_day")));
        AssignCommand(oFlame, PlayAnimation(ANIMATION_PLACEABLE_DEACTIVATE));
     }

}

I’m using Carcerian’s Ultima NPC Schedules, that one works by putting a specific script in an NPC’s hb, the henchmen don’t use it.
I’ve noticed that behaviour in all henchmen who have been with the PC and then removed - two minor shmucks from escort quests and one Important Person. I got around it by simply destroying the randos when you’re done with them, but I can’t do it to the Important Person.
It may be quite some time since removing the henchman and it still happens. Also happens if you got a different henchman in the meantime. Happens when the removed henchman is jumped offstage.

I’ve just tested with the SetTime commented out and it didn’t happen, but I’ll run a few more tests, since I seem to recall it didn’t happen every single time (which is why I’ve been ignoring it for now and concentrating on other aspects of the module).
I didn’t know you could remove scripts from a creature, that sounds useful. How does that work?

(btw, put the code between ``` and ``` to enable the syntax highlighting.)

The SetTime test appears to confirm it to be triggered by the specific clock change, with high probability of that NPC schedule system being the cause. Check if it happens when day changes to night. Also when it happens naturally.

I meant simply removing them in the toolset to check which one might be related the the unwanted behavior. But if you want to debug AI in runtime like that, you can add a sentinel variable with an early return:

if(GetLocalInt(OBJECT_SELF, "no_hb")) return; // hb switch

It doesn’t remove the script per se, but disables its associated logic. EE introduced function which can actually swap scripts, but the above works.

I would focus on the hench’s heartbeat - is it a standard script or does it have some modifications that may be related to this?

1 Like

Not sure this will fix the problem, but I see two things that might cause engine issues:

  • ActionRest() is redundant (also, since this is a module script, the outcome of telling the module to rest might be undefined?)
  • It’s normal to advance the game clock in REST_EVENTTYPE_REST_FINISHED - not sure what the engine will make of changing the time at the outset

At risk of stating the obvious, ensure your rest script makes no reference to henchmen (lots of them do).

After issuing RemoveHenchman() it may help to have the henchman clear all actions.

2 Likes

I removed the ActionRest (wouldn’t have thought of it as redundant before you said it :smiley: ) but kept the time advancement at the start, mostly because I didn’t know how to make it work otherwise and I need and want to keep it. No ex-henchmen hauntings so far, but will keep observing. Not ready to call it a solved problem for now, possibly due to innate chickenitude.
Thank you very much for your time!

The module can’t rest, so yeah, that action doesn’t do anything.

I got intrigued and while unorthodox, changing time at
REST_EVENTTYPE_REST_STARTED doesn’t seem to cause any trouble since at that moment the rest hasn’t really started yet.

Click here for more - might have some answers.

However when you advance the clock during the rest, i.e.:

// bad code - don't do this
case REST_EVENTTYPE_REST_STARTED:
{
    DelayCommand(20.0, SendMessageToPC(oPC, "20 seconds?"));
    DelayCommand(3.0, SetTime(GetTimeHour() + 1, 0, 0, 0));
}

The rest will conclude in those 3 seconds (REST_EVENTTYPE_REST_FINISHED) and that message will appear. Lex says SetTime() affects DelayCommand() and it indeed seems to be the case (which makes sense). If the henchman had some delayed commands lingering around, they may fire immediately while PC is resting.

This also means one can’t keep updating the clock during the rest, but a singleplayer workaround is possible to - say - cancel the rest halfway through but spend 2/4 hours, so no free healing. It seems like a smart idea, but I haven’t seen any modules employing this.

Good to know.

From an immersion point of view, to me it makes more sense to advance the clock at the end of rest, otherwise the time objective is achieved even if rest is cancelled.

True, but the clock advancement could also be seen as a penalty, i.e. “pick your resting spot well or the shop will be closed if you wait too long”. Doing it at the start prevents the player from cheating by cancelling right before the end.

A bit off-topic, but the partial time increment I wrote above seems to strike a good balance: time moves on only by hours matching the actual rest fraction (i.e. cancel at 50% of the progress bar = wait 2 h instead of 4 h at full bar). Immersion-wise, its a good alternative to the usual “tell the campfire how many hours you want to wait”. And it’s just one script.

Welp, the problem persists, just got haunted. The henchmen use default scripts, only the hen_conv had a small modification.

Just to be sure: which default scripts you’re using? There are some possible variants of the pattern. Are you also positive those default scripts aren’t trumped by scripts from some hak?

Is this the system you’re using? (@Carcerian)

Ultima NPC Scheduled Activities | The Neverwinter Vault

If so, edit _actions_inc, change

const int DEBUG_SCHED = 0;

to (line 27)

const int DEBUG_SCHED = 1;

and recompile _schedule to debug that system. Observe the log.

Some more debug modes:

  • put SendMessageToPC() at start of every critical hench script to see what happens with them and when
  • change tag of the offenders in case they share it with some scheduled commoners (the include file references some hardcoded creature tags)
  • don’t advance time during rest, but do it in a manually triggered script, just to finally ensure that resting itself isn’t the problem here