Companion Inter-Area Transition Issue

When you have a door transition setup to connect to another door in the same area, companions are NOT being sent through the door transition when the PC uses it.

I’ve reported this to Service Desk, but I’m assuming this is related to the Transition Bug from a few months back. Or is this something new?

Fixed it by modifying NW_G0_Transition:

////////////////////////////////////////////////////////////
// OnClick/OnAreaTransitionClick
// NW_G0_Transition.nss
// Copyright (c) 2001 Bioware Corp.
////////////////////////////////////////////////////////////
// Created By: Sydney Tang
// Created On: 2001-10-26
// Description: This is the default script that is called
//              if no OnClick script is specified for an
//              Area Transition Trigger or
//              if no OnAreaTransitionClick script is
//              specified for a Door that has a LinkedTo
//              Destination Type other than None.
////////////////////////////////////////////////////////////
//:: Modified By: Deva Winblood
//:: Modified On: Apr 12th, 2008
//:: Added Support for Keeping mounts out of no mount areas
//::////////////////////////////////////////////////////////


#include "x3_inc_horse"
#include "x0_inc_henai"
#include "x0_i0_transport"


void main()
{
    object oClicker=GetClickingObject();
    object oTarget=GetTransitionTarget(OBJECT_SELF);
    location lPreJump=HORSE_SupportGetMountLocation(oClicker,oClicker,0.0); // location before jump
    int bAnim=GetLocalInt(OBJECT_SELF,"bDismountFast"); // community requested fast dismount for transitions if variable is not set (use X3_G0_Transition for animated)
    int nN=1;
    object oOb;
    object oAreaHere=GetArea(oClicker);
    object oAreaTarget=GetArea(oTarget);
    object oHitch;
    int bDelayedJump=FALSE;
    int bNoMounts=FALSE;
    float fX3_MOUNT_MULTIPLE=GetLocalFloat(GetArea(oClicker),"fX3_MOUNT_MULTIPLE");
    float fX3_DISMOUNT_MULTIPLE=GetLocalFloat(GetArea(oClicker),"fX3_DISMOUNT_MULTIPLE");
    if (GetLocalFloat(oClicker,"fX3_MOUNT_MULTIPLE")>fX3_MOUNT_MULTIPLE) fX3_MOUNT_MULTIPLE=GetLocalFloat(oClicker,"fX3_MOUNT_MULTIPLE");
    if (fX3_MOUNT_MULTIPLE<=0.0) fX3_MOUNT_MULTIPLE=1.0;
    if (GetLocalFloat(oClicker,"fX3_DISMOUNT_MULTIPLE")>0.0) fX3_DISMOUNT_MULTIPLE=GetLocalFloat(oClicker,"fX3_DISMOUNT_MULTIPLE");
    if (fX3_DISMOUNT_MULTIPLE>0.0) fX3_MOUNT_MULTIPLE=fX3_DISMOUNT_MULTIPLE; // use dismount multiple instead of mount multiple
    float fDelay=0.1*fX3_MOUNT_MULTIPLE;
    //SendMessageToPC(oClicker,"nw_g0_transition");
    if (!GetLocalInt(oAreaTarget,"X3_MOUNT_OK_EXCEPTION"))
    { // check for global restrictions
        if (GetLocalInt(GetModule(),"X3_MOUNTS_EXTERNAL_ONLY")&&GetIsAreaInterior(oAreaTarget)) bNoMounts=TRUE;
        else if (GetLocalInt(GetModule(),"X3_MOUNTS_NO_UNDERGROUND")&&!GetIsAreaAboveGround(oAreaTarget)) bNoMounts=TRUE;
    } // check for global restrictions
    if (GetLocalInt(oAreaTarget,"X3_NO_MOUNTING")||GetLocalInt(oAreaTarget,"X3_NO_HORSES")||bNoMounts)
    { // make sure all transitioning are not mounted
       //SendMessageToPC(oClicker,"nw_g0_transition:No Mounting");
        if (HorseGetIsMounted(oClicker))
        { // dismount clicker
            bDelayedJump=TRUE;
            AssignCommand(oClicker,HORSE_SupportDismountWrapper(bAnim,TRUE));
            fDelay=fDelay+0.2*fX3_MOUNT_MULTIPLE;
        } // dismount clicker
        oOb=GetAssociate(ASSOCIATE_TYPE_HENCHMAN,oClicker,nN);
        while(GetIsObjectValid(oOb))
        { // check each associate to see if mounted
            if (HorseGetIsMounted(oOb))
            { // dismount associate
                bDelayedJump=TRUE;
                DelayCommand(fDelay,AssignCommand(oOb,HORSE_SupportDismountWrapper(bAnim,TRUE)));
                fDelay=fDelay+0.2*fX3_MOUNT_MULTIPLE;
            } // dismount associate
            nN++;
            oOb=GetAssociate(ASSOCIATE_TYPE_HENCHMAN,oClicker,nN);
        } // check each associate to see if mounted
        if (fDelay>0.1) SendMessageToPCByStrRef(oClicker,111989);
        if (bDelayedJump)
        { // some of the party has/have been mounted, so delay the time to hitch
            fDelay=fDelay+2.0*fX3_MOUNT_MULTIPLE; // non-animated dismount lasts 1.0+1.0=2.0 by default, so wait at least that!
            if (bAnim) fDelay=fDelay+2.8*fX3_MOUNT_MULTIPLE; // animated dismount lasts (X3_ACTION_DELAY+HORSE_DISMOUNT_DURATION+1.0)*fX3_MOUNT_MULTIPLE=4.8 by default, so wait at least that!
        } // some of the party has/have been mounted, so delay the time to hitch
    } // make sure all transitioning are not mounted
    if (GetLocalInt(oAreaTarget,"X3_NO_HORSES")||bNoMounts)
    { // make sure no horses/mounts follow the clicker to this area
        //SendMessageToPC(oClicker,"nw_g0_transition:No Horses");
        bDelayedJump=TRUE;
        oHitch=GetNearestObjectByTag("X3_HITCHING_POST",oClicker);
        DelayCommand(fDelay,HorseHitchHorses(oHitch,oClicker,lPreJump));
        if (bAnim) fDelay=fDelay+1.8*fX3_MOUNT_MULTIPLE;
        //fDelay=fDelay+0.5*fX3_MOUNT_MULTIPLE; // delays jump to transition, makes you stay longer before jump
    } // make sure no horses/mounts follow the clicker to this area

    //SendMessageToPC(oClicker,"nw_g0_transition:Jump  fDelay="+FloatToString(fDelay));
    SetAreaTransitionBMP(AREA_TRANSITION_RANDOM);

    //if (GetArea(oTarget)!=GetArea(oClicker)) DelayCommand(fDelay,AssignCommand(oClicker,ForceJump(oClicker,oTarget,5.0)));
    //else { DelayCommand(fDelay,AssignCommand(oClicker,ForceJump(oClicker,oTarget,5.0))); }
    if (bDelayedJump)
    { // delayed jump
        DelayCommand(fDelay,AssignCommand(oClicker,ClearAllActions()));
        //DelayCommand(fDelay+0.05*fX3_MOUNT_MULTIPLE,AssignCommand(oClicker,ActionWait(X3_ACTION_DELAY/2*fX3_MOUNT_MULTIPLE)));
        DelayCommand(fDelay+0.1*fX3_MOUNT_MULTIPLE,AssignCommand(oClicker,TransportToLocation(oClicker, GetLocation(oTarget))));
    } // delayed jump
    else
    { // quick jump
        AssignCommand(oClicker,TransportToLocation(oClicker, GetLocation(oTarget)));
    } // quick jump
    DelayCommand(fDelay+4.0*fX3_MOUNT_MULTIPLE,HorseMoveAssociates(oClicker));
}

This issue - or is it a feature? - has been around from the earliest Bioware days.

My guess is that Bioware had an internal design standard that forbade transitions within an area. At any rate, they never worked.

My Travel Builder package has a version of nw_g0_transition which fixes this for all manner of followers. It can be used stand-alone without the other baggage.

Not sure I’d call this a feature, especially the way I’ve always preferred to implement secret doors. Although I’ve always coded them to use the XP1 transport script functions. so I never caught this before as I never use true internal transitions. TBH, I’m not sure why I did it this time.

I forgot about your travel pack - I’ll have to look at that again. Be nice to not have all this horse baggage for a small module that takes place 90% indoors.

You can easily avoid the horse overhead. All you need is to make henchies JumpToObject with you. I find that it’s sometimes easier to reinvent the wheel than wrestle with BWs scripts, especially when their purpose is to serve the campaign rather than be generic.

Here’s the code:

// makes all associates transition with their master
// by NWShacker, 01.2020
// use this script as OnAreaTransitionClick event handler
void main()
{
    object oPC = GetEnteringObject();

    if(!GetIsPC(oPC)) { return; }

    object oTarget = GetTransitionTarget(OBJECT_SELF);

    if(!GetIsObjectValid(oTarget)) { return; }

    AssignCommand(oPC, ClearAllActions());
    AssignCommand(oPC, JumpToObject(oTarget));

    object oHench;
    int iHenchType;
    int iHenchNumber;

    for(iHenchType = 1; iHenchType <= 5; iHenchType++)
    // 1 = ASSOCIATE_TYPE_HENCHMAN
    // 5 = ASSOCIATE_TYPE_DOMINATED
    {
        iHenchNumber = 1;
        while(TRUE)
        {
            oHench = GetAssociate(iHenchType, oPC, iHenchNumber++);
            if(!GetIsObjectValid(oHench)) { break; }
            // "I can't leave without my buddy oHench":
            // here check if hench has a condition that
            // prevents it from jumping (petrify, etc)
            // or if they are too far away, busy...
            AssignCommand(oHench, ClearAllActions());
            AssignCommand(oHench, JumpToObject(oTarget));
        }
    }
}

I guess @Proleric code does something similar…

BTW, functions from x0_i0_transport apparently transfer only first henchmen of given type. The “all” variants refer to “all PCs”, not henchies.

@NWShacker - nice little block of code there. I was able to fix the TransportToLocation function in x0_i0_transport with it. TY.

Community Patch also fixes this but builder or player needs to toggle it via PC Widget tool (or scripting) because as always, fixes like this are subject of heavy criticism of my project.

You lost me. Why would you use a PC Widget tool to be able to walk through a transition and take your companions with you? Simply placing @NWShacker’s looping code in the TransportToLocation function solves the issue of companions not coming through through a door transition whose destination is in the same area.

Because as with similar fixes. If I do that, automatically for everyone who installs my unofficial patch, builders I won’t mention will criticise that telling it will break their modules and it is not a bug anyway but Bioware’s intent.

Therefore, because I wanted to fix it, I had to do it this way - it is not automatically enabled but player/builder has to switch it on.

OK, I got you. I have never used the player widget. I’m assuming it just allows the player to toggle CPP module switches that are also located in the module OnLoad event?

Yes some of them. Yes builder of the module can set these switches himself either manually in module properties - variables or via scripting. However, in case of single player it will only work if the player has CPP installed unless you put the scripts handling that functionality into your module (which means you need to know which scipts handle it and for that there are no informations so it would be easier to put into it all of them - if you really wanted to use CPP for singleplayer and enforce that all players has the same experience).

For scripters, I’m just wondering if the built-in library “X0_I0_TRANSPORT” (i.e. #include "x0_io_transport") wouldn’t be of use as a simple alternative to coding it themselves (especially the 2 TransportTo* functions). From x0_io_transport.nss -

//:://////////////////////////////////////////////////
//:: X0_I0_TRANSPORT
//:: Copyright (c) 2002 Floodgate Entertainment
//:://////////////////////////////////////////////////
/*

Functions for making creatures travel/transport to new locations.

 */
//:://////////////////////////////////////////////////
//:: Created By: Naomi Novik
//:: Created On: 09/12/2002
//:://////////////////////////////////////////////////

/**********************************************************************
 * CONSTANTS
 **********************************************************************/


/**********************************************************************
 * FUNCTION PROTOTYPES
 **********************************************************************/


// Target goes to specified destination object intelligently.
// If location is in same area, walk (or run) there.
// If location is in different area, walk (or run) to
//     most appropriate door, area transition, or waypoint, 
//     then jump.
// If either of these fail, jump after fDelay seconds.
void TravelToObject(object oDest, object oTarget=OBJECT_SELF, int bRun=FALSE, float fDelay=10.0);

// Target goes to specified location intelligently. See
// TravelToObject for description.
void TravelToLocation(location lDest, object oTarget=OBJECT_SELF, int bRun=FALSE, float fDelay=10.0);

// Find nearest exit to target (either door or waypoint).
object GetNearestExit(object oTarget=OBJECT_SELF);

// Find best exit based on desired target area
object GetBestExit(object oTarget=OBJECT_SELF, object oTargetArea=OBJECT_INVALID);

// Transport a player and his/her associates to a waypoint.
// This does NOT transport the rest of the player's party,
// only their henchman, summoned, dominated, etc.
void TransportToWaypoint(object oPC, object oWaypoint);

// Transport a player and his/her associates to a location.
// This does NOT transport the rest of the player's party,
// only their henchman, summoned, dominated, etc.
void TransportToLocation(object oPC, location oLoc);

// Transport an entire party to a waypoint
void TransportAllToWaypoint(object oPC, object oWay);

// Transport an entire party to a location
void TransportAllToLocation(object oPC, location lLoc);

TR

Yeah, that’s what I did. I just modified the root transport function TransportToLocation (which is called by all the other transport functions) to loop through the PC’s associates and grab any multiple associate types such as henchmen - as suggested by @NWShacker.

void TransportToLocation(object oPC, location oLoc)
{
    // Jump the PC
    AssignCommand(oPC, ClearAllActions());
    AssignCommand(oPC, JumpToLocation(oLoc));

    // Not a PC, so has no associates
    if (!GetIsPC(oPC))
        return;

    object oHench;
    int iHenchType;
    int iHenchNumber;

    for(iHenchType = 1; iHenchType <= 5; iHenchType++)
    // 1 = ASSOCIATE_TYPE_HENCHMAN
    // 5 = ASSOCIATE_TYPE_DOMINATED
    {
        iHenchNumber = 1;
        while(TRUE)
        {
            oHench = GetAssociate(iHenchType, oPC, iHenchNumber++);
            if(!GetIsObjectValid(oHench)) { break; }
            // "I can't leave without my buddy oHench":
            // here check if hench has a condition that
            // prevents it from jumping (petrify, etc)
            // or if they are too far away, busy...
            AssignCommand(oHench, ClearAllActions());
            AssignCommand(oHench, JumpToLocation(oLoc));
        }
    }
}

I’d never trust any pre-HOTU BW function without looking at the source code first. I’d never trust any HOTU BW function either, but at least they are expected to consider the “latest” features of the engine (i.e. multiple henchmen).

@Pstemarie - I like how you used my function verbatim, with Superfly comments included. There could also be a paranoia check whether the target is valid to prevent the then-unnecessary action queue purge.

Oh and that for loop should really look like this:

for(iHenchType = ASSOCIATE_TYPE_HENCHMAN; iHenchType <= ASSOCIATE_TYPE_DOMINATED; iHenchType++)

I just wanted to save some space in the example. But since we can assume that for the foreseeable future those constants aren’t going to change, direct use of int literals like that should be safe (but naturally makes code harder to understand).

For the most general use, the script needs to be recursive, to jump the henchman’s associates (which can include horses and summoned creatures IIRC).

Modifying nw_g0_transition saves time, because it’s the default for new doors. The horse stuff in there is all original Bioware code.