I've encountered a really odd bug (apparently not a bug)

My brother is beta testing my latest module at the moment and encountered a bug. When looking at it I couldn’t understand the bug at all, so I did what kevL_s has time and again told me to do. I altered the scripts and did a lot of debugging by using SendMessageToPC. What I found had me really baffled. So here’s the situation (I hope you can understand my explanation):

We are at a tavern. When entering the tavern there’s a speakscript firing when entering a trigger just after you’ve got in. In this trigger I set a local int on the player character to 1. At another trigger a bit further in there’s another conversation trigger forcing all but one of the companions to leave the party and wait where they are. You then go upstairs with one of the companions, and after you get back there’s yet another trigger starting a conversation where the rest of the companions rejoin.
Here’s the thing: If the local int is 1 the first conversation should not be triggered again if you go through that trigger, which it does now. However, when you leave the tavern, I have a script on the OnExit of the area where this same local int is set to 0. Thus, if you reenter the tavern a new (or the same, depending on circumstances) conversation should start again when entering the first trigger, because the local int is 0.

Ok, now what I noticed when debugging was this: When speaking with the companions and they rejoin the party, this local int is set to 0 about 4 times (there are 5 companions in total in the party). Why on earth would it do that??! This local int should only be set to 0 when exiting the tavern. I’m using a script that looks like this for the rejoining of the companions. My only guess it must be some hidden code in this that sets this local int to 0…or something. It doesn’t make any sense. Or maybe it does…somehow?
Why I suspect this script is because it is run 4 times, one for each of the companions who rejoins the party, and I get a message ingame 4 times that the local int is set to 0 when this happens.

/******************************************************************
ga_roster_companion

Adds a Companion NPC and sets their XP equal to the PC's if they
are below. This script merges four scripts together so that you
don't have to setup all four script calls in a conversation.

This script also has a convention: the RosterName is the same as
the creature's TAG, resref, etc. They should all be named
the same.
*******************************************************************/
#include "ginc_param_const"
#include "ginc_debug"
#include "ginc_misc"


void main(string sCompanionTag)
{
//FROM: ga_roster_add_object
//Adds a NPC to the global roster of NPCs available to be added to
//a player's party. Roster Name is a 10-character name used to
//reference that NPC in other Roster related functions.
//The NPC will be left in the game world, but will now exist
//in the roster as well.

object oCompanion = GetObjectByTag(sCompanionTag);
int bResult = AddRosterMemberByCharacter(sCompanionTag, oCompanion);

//FROM: ga_roster_selectable
SetIsRosterMemberSelectable(sCompanionTag, 1);

//FROM: ga_roster_party_add
object oPC = GetFirstPC();
AddRosterMemberToParty(sCompanionTag, oPC);


int nXP = GetPCAverageXP();
SetXP(oCompanion, nXP);
ForceRest(oCompanion);
}
1 Like

How do you force the companions to leave the party? I guess AddRosterMember will get the latest template available, but in the present case it doesn’t have the variable set to 1.

I’ve got rid of quite a few companions in taverns so the remaining one and the PC could jump into bed. You can make them walk off during the first conversation then when it stops jump them into a small room with a blacked out roof that has a locked plot door so they’re still in the party but stuck and can’t interfere. Then when you leave the area they will “magically” rejoin with nothing changed. Or you can open the door and let them out from another conversation eg. “where have they gone etc.”

If I was going to do it your way I’d global_int the condition, that way it wont matter who it’s on as it’s a module thing.

3 Likes

Well, I don’t think this is the problem since I debugged it. The local int is set and all that, but somehow it is set to 0 (it should be impossible) when using the script I’ve posted above.

How do I make the companions leave the party.? I use this script (and other variations of it) for that (one of kevL_s’ scripts and it works great):

const string sDEST_PC      		= "inn_pc";                 // waypoint tag of the PC's destination
const string sDEST_LOREEN 		= "inn_loreen";   	// waypoint tag of the Companions' destination
const string sDEST_CELESTINE    = "inn_celo";		// waypoint tag of the Companions' destination
const string sDEST_TAIK 		= "inn_taik";	// waypoint tag of the Companions' destination
const string sDEST_ANTO			= "inn_anto";	// waypoint tag of the Companions' destination
const string sDEST_ELVERA		= "inn_elvera";	// waypoint tag of the Companions' destination




// script variables
object oPC;
object oParty;
string sRoster;

string sRosterListCurrent;

void transition(string sDest);

//
void main()
{
    oPC = GetPCSpeaker();
	

	
    oParty = GetFirstFactionMember(oPC, FALSE);
    while (GetIsObjectValid(oParty))
    {
        sRoster = GetRosterNameFromObject(oParty);

     
        if (sRoster == "celestine")
        {
        
            transition(sDEST_CELESTINE);
            oParty = GetFirstFactionMember(oPC, FALSE);
			
        }
        else if (sRoster == "taik")
        {
            transition(sDEST_TAIK);
            oParty = GetFirstFactionMember(oPC, FALSE);
      	}
		
		 else if (sRoster == "anto")
        {
            transition(sDEST_ANTO);
            oParty = GetFirstFactionMember(oPC, FALSE);
      	}
		
		else if (sRoster == "loreen")
        {
            transition(sDEST_LOREEN);
            oParty = GetFirstFactionMember(oPC, FALSE);
      	}
		
        else
            oParty = GetNextFactionMember(oPC, FALSE);
    }

    if (sRosterListCurrent != "")
        SetLocalString(oPC, "CurrentRosterList", sRosterListCurrent);
	else
        DeleteLocalString(oPC, "CurrentRosterList");

    //SetLocalInt(oPC, "gotocamp", GetLocalInt(oPC, "gotocamp") + 1);

    AssignCommand(oPC, ClearAllActions());
    AssignCommand(oPC, JumpToObject(GetWaypointByTag(sDEST_PC)));
	
	object oCelestine = GetNearestObjectByTag("celestine");
	object oTaik = GetNearestObjectByTag("taik");
	object oLoreen = GetNearestObjectByTag("loreen");
	object oElvera = GetNearestObjectByTag("elvera");
	object oAnto = GetNearestObjectByTag("anto");
	
	if(GetIsObjectValid(oCelestine) && (!GetFactionEqual (oPC,oCelestine)))
	{
	AssignCommand(oCelestine, ActionInteractObject(GetObjectByTag("chairy8")));
	}
	

	if(GetIsObjectValid(oTaik) && (!GetFactionEqual (oPC,oTaik)))
	{
	AssignCommand(oTaik, ActionInteractObject(GetObjectByTag("chairy9")));
	}
	

	if(GetIsObjectValid(oLoreen) && (!GetFactionEqual (oPC,oLoreen)))
	{
	AssignCommand(oLoreen, ActionInteractObject(GetObjectByTag("chairy6")));
	}
	
	if(GetIsObjectValid(oAnto) && (!GetFactionEqual (oPC,oAnto)))
	{
	AssignCommand(oAnto, ActionInteractObject(GetObjectByTag("chairy10")));
	}
	
	if(GetIsObjectValid(oElvera) && (!GetFactionEqual (oPC,oElvera)))
	{
	AssignCommand(oElvera, ActionInteractObject(GetObjectByTag("chairy7")));
	}
	
}

//
void transition(string sDest)
{
    sRosterListCurrent += sRoster + ";";

    RemoveRosterMemberFromParty(sRoster, oPC, FALSE);

    AssignCommand(oParty, ClearAllActions());
    AssignCommand(oParty, JumpToObject(GetWaypointByTag(sDest)));
}

But I don’t think this “companions leave” script has anything to do with this (and they are still in the same indoor area, just in another room), since when I watch the debug messages, the one time it gets weird is when they are added again with the script in the former post. Nowhere in that (the one in the first post) script I can see that the game should go to the OnExit script of the area and run the script that I have put there, which has the local int set to 0. It doesn’t make sense.

Since I can’t make sense of this, I think I have a go-around solution. I will, after I’ve added the companions again, set the local int in the next node to 1. It’s a roundabout way of doing it, but if it works, what the heck, I say I’ll go for it. I’ll of course use a debug for that too to see that it really runs as it should before leaving it in the game permanently.

For those interested, the first trigger where I put the local int looks like this (it’s a speaktrigger with a modified script looking as follows:

// gtr_speak_node
/*
	Speak Trigger OnEnter handler to initiate a conversation.
	For custom conditions, modify <CUSTOM CONDITION>, save as a new script and attach to trigger's OnEnter.
	Suggested name: XX_trsn_xxx
	
	Starts a conversation based on variables set on trigger.
	The following variables will be examined on the trigger.  First 2 paramaters are required. Order is not important
	NPC_Tag 		- Tag of creature to make comment.  Must be in this area.  Blank defaults to the entering PC.
	Conversation 	- Conversation file to play.  
	Node			- Node of the conversation to speak.  Node indexes are set using gc_node() in the dialog conditionals.
					  Do not define a node 0, as this is reserved.
	Run				- Set to following:
						2 - Target Jumps to PC
						1 - Target runs to PC
						0 - (default) Target Walks to PC 
					   -1 - Target Stays Put (and TalkNow=1 by default)
	TalkNow			- Set to following:
						0 - (default) use default based on Run mode.
						1 - talk immediately, regardless of distance 
						2 - only talk when close (default except when Run = -1)
	CutsceneBars	- Set to following:
						0 - (default) no cutscene bars 
						1 - show cutscene bars
	OnceOnly		- Controls whether or not NPC's Node index is cleared after it's first use.  Set to following:
						0 - (default) the node index is not reset by gc_node
						1 - gc_node will reset the node index to 0 so that it only recognizes it once
	MultiUse		- Controls whether the Speak Trigger fires only once or on every enter
						0 - (default) Only triggers once
						1 - Triggers every time.
	CombatCutsceneSetup - Controls whether to Plot party, Hide hostiles, hold AI for duration of conversation.
						0 - (default) Normal speak trigger
						1 - Use CombatCutsceneSetup() and QueueCombatCutsceneCleanUp()

	
*/
// ChazM 6/25/05 - split functions into include
// BMA-OEI 3/6/06 - formated script
// ChazM 5/22/07 - Added comments

#include "ginc_trigger"
#include "ginc_group"

void main ()
{
    object oPC = GetEnteringObject();
	object oTaik = GetObjectByTag("taik");
	object oCelestine = GetObjectByTag("celestine");
	
	object oPC1 = GetFirstPC();
	SendMessageToPC(oPC1,"GetFirstPC works"); 
		
	if(GetLocalInt(oPC1,"InnOut")) 
	{
	SendMessageToPC(oPC1,"InnOut local int is already set to 1, therefore no trigger should run.");
	return;
	}
	
	//if(!GetIsGroupValid("taik_group"))
	//{
	
	//SendMessageToPC(GetFirstPC(FALSE),"Taik group doesn't exist");
	
	//ResetGroup("taik_group");
	//}

	//if (StandardSpeakTriggerConditions(oPC) == FALSE)
	//{
	//	return;
	//}
	
	
	if (!GetFactionEqual(oPC,oCelestine))
	{
			SendMessageToPC(oPC1,"Celestine is not part of the party");
			return;
	}

	SetLocalInt(oPC1,"InnOut",1);
	SendMessageToPC(oPC1,"Local Int InnOut set to 1 on Player");
		
	SetCutsceneMode(oPC);
	SendMessageToPC(oPC1,"Cutscene mode initiated");
	
	DoSpeakTrigger(oPC);
	SendMessageToPC(oPC1,"Speaktrigger run");
}

And the OnExit script of the area looks like this:

void main()
{

object oPC = GetFirstPC();

object oCelestine = GetObjectByTag("celestine");

	if (!GetFactionEqual(oPC,oCelestine))
	{
		return;
	}

	SetLocalInt(oPC,"InnOut",0);
	SendMessageToPC(oPC,"You have exited the inn");

}

It was the message “You have exited the inn” that I got when using the script in my first post here, where I let my companions rejoin the party.

Doing it in my roundabout way worked like a charm! I still don’t understand how the weird error occurs, but at least now I have a solid solution for it.

By the way, I’m SOOO grateful to @Lance_Botelle for helping me convert my module to campaign!! This would have been a nightmare to figure out if I’d had to do it the way I did it before, when I couldn’t just reload a savegame and try a new variation of a script or dialogue.

1 Like

Good that it’s working now, maybe it’s better to always go higher with local ints, the conversation conditions can block anything that doesn’t exist so a 2 is as good as a 0.

Also in the conversations you can add and remove companions by using ga_roster_party_remove and ga_roster_party_add, you just fill in the tags and there’s no need to script anything at all, it’s a lot quicker and easier. I think what you’ve posted at the beginning is probably about three times bigger than any script I’ve ever used !

I’m pretty sure the area’s OnExit will fire if you remove party members by despawn. That’s probably what was causing the LocalInt to reset to zero. Additionally, adding companions into your party that are already in the area will fire the area’s OnExit AND OnEnter.

3 Likes

Thanks @travus! If that is indeed the case then this behaviour is totally logical. Follow up question: Why does the area’s OnEnter and OnExit fire when adding companions? Is there a reason for this?

Yes, I am aware. If you were to do it the way they want to in the tutorial for NWN2 toolset (you can find that somewhere on the internet, I know) you need three scripts in a conversation just to add a companion, if I remember correctly. The script I’m using is from a guy who had merged those three scripts into one. Don’t remember where I found it since it was about 2-3 years ago. Since I thought that sounded very convenient, I’ve used that script in all my modules (well, perhaps not my second module since I used Colorsfade’s companion system on that one, but that was a bit buggy, which is why I’ve abandonded that for my third module), and it has always worked well. In this module it’s the first time ever that I used an OnExit script on an area, so that’s probably why I’ve never encountered it before.

There’s apparently a lot of “hidden stuff” in NWN2 that I’m not yet fully aware of. Lance found another such thing that I didn’t know with regards to scripting behaviours when it comes to companions, and if you control them. It was when I needed a game over when a certain companion died. Turned out that if I did my own script, naming it the way I pleased, it wouldn’t run on my companion if that companion was controlled, since some hard coded stuff said to use another script instead. Therefore you had to use that script name and do your own version of that instead, for it to work.

I’m really grateful to all you in the community who has deep knowledge of all of this, like travus, kevL_s and Lance Botelle to name but a few.

1 Like

I honestly don’t know, but since destroying creatures also fires the area’s OnExit, I can only guess that the engine destroys the instance that was there immediately before creating a new instance that will be used as the actual companion. The creation of the new companion then fires the area’s OnEnter.

3 Likes

It is useful for running code on any companions or henchmen that may be spawned and may require some treatment as they are added to the party.

I suppose it is also useful to have in case the builder wants to alter anything regarding said companions if and when the player has them leave or enter the party again.

Lance.

1 Like