Beamdog / nwn-issues

Neverwinter Nights: Enhanced Edition Technical Bug Tracker
http://nwn.beamdog.com
31 stars 1 forks source link

GetNearestObjectByTag bug #52

Closed BhaalM closed 3 years ago

BhaalM commented 4 years ago

To Reproduce

Specifics

If needed, describe the bug

GetNearestObjectByTag(string sTag, object oTarget=OBJECT_SELF, int nNth=1) "randomly" returns OBJECT_INVALID even if an object with the tag sTag exists near oTarget.

When I say randomly I mean per server reset: After resetting the server, if GetNearestObjectByTag fails it will always returns OBJECT_INVALID until reset. If it works well it will always returns the correct object.

I think this bug was reported by Shadooow a long time ago in the old Redmine bug tracker. There is also a fix from Shadooow. The following one is based on his fix, but I modified it a bit:

object GetNearestObjectByTag_FIX(string sTag, object oTarget=OBJECT_SELF, int nNth=1)
{
    object o = GetNearestObjectByTag(sTag, oTarget, nNth);
    if(!GetIsObjectValid(o) && nNth==1)
    {
        float fDist = 1000000.0;
        object oArea = GetArea(oTarget);
        object oRes = OBJECT_INVALID;
        int nTh = 1;
        o = GetObjectByTag(sTag);
        while(GetIsObjectValid(o))
        {
            if(GetArea(o) == oArea && o!=OBJECT_SELF)
            {
                float fAux = GetDistanceBetween(o, oTarget);
                if(fAux<fDist)
                {
                    fDist=fAux;
                    oRes = o;
                }
            }
            o = GetObjectByTag(sTag,nTh++);
        }
        if(fDist<1000000.0)
        {
            WriteTimestampedLogEntry("ERROR: GetNearestObjectByTag("+sTag+") returned OBJECT_INVALID, workaround returned "+ObjectToString(oRes));   
            return oRes;
        }
    }
    return o;
}

Every now and then my log is filled with messages like:

ERROR: GetNearestObjectByTag(ad_carav_noinus_store) returned OBJECT_INVALID, workaround returned 367b

(Obviously with different tags)

BhaalM commented 4 years ago

I've been talking with Shadooow on discord, here is what he remembers from the bug he posted in the old redmine bug tracker (All credits to him):

Clockwerkd commented 3 years ago

I'm experiencing the same issue with GetNearestObjectByTag() and wanted to share what I have found through testing in hopes it will contribute to finding a solution.

I have been able to repro the issue consistently with the following scenario:

  1. Create an area.
  2. Create 2 non-overlapping generic triggers of approximately 2 x 2 in size within the area.
  3. At the center of the first trigger, place an invisible object set to PLOT, non-static, and give it the tag "I_SIC_BLADE_LOCATION".
  4. At the center of the second trigger, place a waypoint and give it the same tag as the invisible object.

image

  1. The waypoint and the invisible object will both be used to obtain a location for a visual effect and as an object to apply damage to the entering PC. We'll define these objects using GetNearestObjectByTag("I_SIC_BLADE_LOCATION").
  2. Place the following script in the OnEnter event for each trigger:
int IXP_GetIsAssociate(object oCreature);
void DropChance(object oPC);
void SlipForward(object oPC);
void SlipBack(object oPC);
void BladeDamage(object oPC);

void main()
{
    object oPC = GetEnteringObject();
    object oArea = GetArea(oPC);
    object oTrigger = GetNearestObjectByTag("I_SIC_BLADE_LOCATION", oPC, 1);
    location lTrigger = GetLocation(oTrigger);
    int nActivated = GetLocalInt(OBJECT_SELF, "ACTIVATED");

    // Exit out of script if a DM.
    if (GetIsDM(oPC))
        return;

    // Entering object must be a PC or an associate
    if (!GetIsPC(oPC) & !IXP_GetIsAssociate(oPC))
        return;

    // Ghosts should not be counted.
    if (GetLocalInt(oPC, "X2_L_IS_INCORPOREAL") == 1)
        return;

    // First check to see if the mechanism had been shut off.
    if (GetLocalInt(oArea, "BLADEOFF"))
        return;

    if (!nActivated)
        SetLocalInt(OBJECT_SELF, "ACTIVATED", 1);

    if (!nActivated)
    {
        effect eBlades = EffectVisualEffect(VFX_FNF_SWINGING_BLADE);

        ApplyEffectAtLocation(DURATION_TYPE_TEMPORARY, eBlades, lTrigger, 5.0f);
        DelayCommand(5.0, SetLocalInt(OBJECT_SELF, "ACTIVATED", 2));
        DelayCommand(15.0f, DeleteLocalInt(OBJECT_SELF, "ACTIVATED"));
    }

    if (nActivated != 2)
    {
        object oVictim = GetFirstInPersistentObject(OBJECT_SELF, OBJECT_TYPE_CREATURE);

        while (GetIsObjectValid(oVictim) && !GetLocalInt(oVictim, "X2_L_IS_INCORPOREAL"))
        {
            if (GetLocalInt(oVictim, "HITBYBLADES") != 1)
            {
                SetLocalInt(oVictim, "HITBYBLADES", 1);
                DelayCommand(0.5f, AssignCommand(oTrigger, BladeDamage(oVictim)));
            }

            oVictim = GetNextInPersistentObject(OBJECT_SELF, OBJECT_TYPE_CREATURE);
        }

    }
}

// Generates a location appropriate for an item that has dropped to the ground
// from an NPC's hand. Taken and modified from hc_inc_npccorpse (HCR 3.02 - Sunjammer et all)
//  - oPC:          the player character
//  - nSlot:        one of the INVENTORY_SLOT_*HAND constants
location GetLocationForDropped(object oPC, int nSlot)
{
    vector vBody = GetPosition(oPC);

    // get the offsets appropriate to the hand the item is falling from
    float fFacing = (nSlot == INVENTORY_SLOT_LEFTHAND) ? 45.0f : -45.0f;
    float fWeapon = (nSlot == INVENTORY_SLOT_LEFTHAND) ? -20.0f : 20.0f;

    // add a random element to facings and direction, values are arbitray
    fFacing += GetFacing(oPC) + IntToFloat(d20());
    fWeapon += GetFacing(oPC) - IntToFloat(d20(2));
    float fDistance = 0.5f + (IntToFloat(d10())/10);

    // get co-ordinates of a point fDistance meters away at fFacing degrees
    float fX = vBody.x + cos(fFacing) * fDistance;
    float fY = vBody.y + sin(fFacing) * fDistance;

    // return the location
    return Location(GetArea(oPC), Vector(fX, fY, vBody.z), fWeapon);
}

// Determines if oCreature is an associate.
int IXP_GetIsAssociate(object oCreature)
{
    if (GetAssociateType(oCreature) == ASSOCIATE_TYPE_NONE) return FALSE;

    return TRUE;
}

// DropChance() runs a check to see if the character drops the objects in his hands.
void DropChance(object oPC)
{
    int nChance = 50; // <-- Set percentage chance the character will drop an item here.

    // First check to see if the character has something in his right hand.
    if (GetItemInSlot(INVENTORY_SLOT_RIGHTHAND, oPC) != OBJECT_INVALID)
    {
        // Roll for righthand first.
        int nRoll = d100(1);

        if (nRoll <= nChance)
        {
            int nSlot = INVENTORY_SLOT_RIGHTHAND;
            object oItem = GetItemInSlot(nSlot, oPC);
            CopyObject(oItem, GetLocationForDropped(oPC, nSlot));
            DestroyObject(oItem);
        }
    }

    // Check the character's left hand.
    if (GetItemInSlot(INVENTORY_SLOT_LEFTHAND, oPC) != OBJECT_INVALID)
    {
        // Roll for lefthand.
        int nRoll = d100(1);

        if (nRoll <= nChance)
        {
            int nSlot = INVENTORY_SLOT_LEFTHAND;
            object oItem = GetItemInSlot(nSlot, oPC);
            CopyObject(oItem, GetLocationForDropped(oPC, nSlot));
            DestroyObject(oItem);
        }
    }
}

// SlipForward causes all actions to be cleared, forces the character to fall forward,
// and checks to see if the character drops any items in his hands.
void SlipForward(object oPC)
{
    AssignCommand(oPC, ClearAllActions());
    AssignCommand(oPC, ActionPlayAnimation(ANIMATION_LOOPING_DEAD_FRONT, 1.0f, 4.2f));
    DropChance(oPC);

    // Disable the character's action cue so he cannot interrupt the animation.
    DelayCommand(0.2, SetCommandable(FALSE, oPC));

    // Enable the character's action cue after the animation finishes.
    DelayCommand(6.4, SetCommandable(TRUE, oPC));
}

// SlipBack causes all actions to be cleared, forces the character to fall backward,
// and checks to see if the character drops any items in his hands.
void SlipBack(object oPC)
{
    AssignCommand(oPC, ClearAllActions());
    AssignCommand(oPC, ActionPlayAnimation(ANIMATION_LOOPING_DEAD_BACK, 1.0f, 4.2f));
    DropChance(oPC);

    // Disable the character's action cue so he cannot interrupt the animation.
    DelayCommand(0.2, SetCommandable(FALSE, oPC));

    // Enable the character's action cue after the animation finishes.
    DelayCommand(6.4, SetCommandable(TRUE, oPC));
}

void BladeDamage(object oPC)
{
    int nSaveDC = 20;
    int nSave = ReflexSave(oPC, 20, SAVING_THROW_TYPE_NONE);

    int nDamage = d6(7)+5;
    if (nSave == 1)
        nDamage = nDamage/2;

    int nVoice;
    switch (d3(1))
    {
        case 1: nVoice = VOICE_CHAT_PAIN1; break;
        case 2: nVoice = VOICE_CHAT_PAIN2; break;
        case 3: nVoice = VOICE_CHAT_PAIN3; break;
    }

    effect eDamage = EffectDamage(nDamage, DAMAGE_TYPE_SLASHING);
    effect eVis = EffectVisualEffect(VFX_COM_CHUNK_RED_SMALL);

    ApplyEffectToObject(DURATION_TYPE_INSTANT, eDamage, oPC);
    ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oPC);
    PlayVoiceChat(nVoice, oPC);

    switch (d2(1))
    {
        case 1:
            SlipForward(oPC); break;

        case 2:
            SlipBack(oPC); break;
    }

    DelayCommand(6.7f, DeleteLocalInt(oPC, "HITBYBLADES"));
}
  1. Set the module starting location within this area, save, and test the module.
  2. With your PC, enter the trigger with the invisible placeable first. The script should generate the swinging blade animation, and apply damage to the PC.
  3. Enter the second trigger with the waypoint. The script should generate the swinging blade animation, but will not apply damage. This is expected as the damage routine is sent as an AssignCommand command to the nearest object with the tag "I_SIC_BLADE_LOCATION" which is the waypoint. Because it is a waypoint, the routine isn't executed.
  4. Now, re-enter the 1st trigger with the invisible object. The visual effect will execute, but damage will no longer be applied.

If you were to enter the first trigger, then re-enter it without entering the second trigger, the script and damage are applied as expected. It is only when the damage command is passed to the waypoint that first trigger stops applying damage.

If you were to remove the waypoint and replace it with another invisible object with the same tag, the script will execute as expected consistently regardless of which trigger you enter.

BhaalM commented 3 years ago

Based on Shad000w repro module I made a very simple one.

1-Enter the module and activate the lever. Don't move!! 2-Wait

bug.zip

The Lever script is:

void GetMyDoor()
{
    int n = GetLocalInt(OBJECT_SELF,"ATTEMPTS")+1;
    SetLocalInt(OBJECT_SELF,"ATTEMPTS",n);

    object oTarget = GetNearestObjectByTag("DVERE");  //Tag of the door
    if(oTarget == OBJECT_INVALID)
    {
        SendMessageToPC(GetFirstPC(),"GetNearestObjectByTag didn't find door! number of loops: "+IntToString(n));
    }
    DelayCommand(0.1,GetMyDoor());
}

void main()
{
    SendMessageToPC(GetFirstPC(), "Activated");
    GetMyDoor();
}
BhaalM commented 3 years ago

Daaz created a tweak for NWNX. Seems to work. https://github.com/nwnxee/unified/pull/1381

BhaalM commented 3 years ago

For your information: the previous tweak also solve the GetFirst/GetNextObjectInShape bug with moving AoEs (the function returns OBJECT_INVALID even if there is clearly a creature in range)

Repro module for GetFirst/GetNextObjectInShape bug;

1-Click on the lever (don't move) 2-Wait

Maybe related with #66

bug.zip