Jump to content

[Solved] F4SE: unresolved external symbol


ScottyDoesKnow

Recommended Posts

 

 

You can use BSReadLocker/BSWriteLocker.

 

Potential deadlock:

You have nested critical sections in code. First - unk08->lock, second - inventoryList->inventoryLock

It is ok if there is no any (not your) code in application with reversed order of locks. Deadlock is possible, otherwise.

 

Good call on the deadlock issue. So here's the new version with the critical sections contained in their own methods. Changes the flow slightly, but again I think there should only ever be one equippedData so it's no big deal. And the failure state if there are more than one I'm fine with. I assume I only need to lock to iterate the list of equipment and not to set a value on it at the end (where I clear the ammo) or the flow will have to get a bit messier.

// https://github.com/powerof3/SKSEPlugins/blob/master/po3_FEC/main.cpp
class TESDeathEventHandler : public BSTEventSink<TESDeathEvent> {
public:
    static TESDeathEventHandler* GetSingleton() {
        static TESDeathEventHandler singleton;
        return &singleton;
    }
    
    virtual EventResult ReceiveEvent(TESDeathEvent* evn, void* dispatcher) override {
        if (!evn || !evn->source) {
            return kEvent_Continue;
        }
        
        Actor* actor = DYNAMIC_CAST(evn->source, TESObjectREFR, Actor);
        if (!actor) {
            return kEvent_Continue;
        }

        AmmoData ammoData;
        if (!FindAmmo(actor, ammoData)) {
            return kEvent_Continue;
        }

        RemoveAmmo(actor, ammoData);

        return kEvent_Continue;
    }

// https://stackoverflow.com/a/1008289
// Using C++ 98, so no delete
private:
    TESDeathEventHandler() {}
    TESDeathEventHandler(TESDeathEventHandler const&);
    void operator=(TESDeathEventHandler const&);

    struct AmmoData
    {
        EquippedWeaponData    * equippedData;
        UInt64                ammoCount;
        TESAmmo                * ammo;
    };

    bool FindAmmo(Actor* actor, AmmoData& ammoData) {
        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return false;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return false;
        }

        SimpleLocker locker(&unk08->lock);

        auto equipDataArray = unk08->equipData;
        for (UInt32 i = 0; i < equipDataArray.count; ++i) {

            Actor::MiddleProcess::Data08::EquipData equipData;
            if (!equipDataArray.GetNthItem(i, equipData)) {
                return false; // If one fails, all the rest will fail
            }

            auto equippedData = equipData.equippedData;
            if (!equippedData) {
                continue;
            }

            auto ammoCount = equippedData->unk18 & 0x00000000FFFFFFFF; // Ammo count is lower 32 bits
            if (ammoCount == 0) {
                continue; // Sometimes this method gets called twice, this will skip it on subsequent calls
            }

            auto ammo = equippedData->ammo;
            if (!ammo) {
                continue;
            }

            ammoData.equippedData = equippedData;
            ammoData.ammoCount = ammoCount;
            ammoData.ammo = ammo;

            // Stop searching
            return true;
        }

        return false;
    }

    bool RemoveAmmo(Actor* actor, AmmoData ammoData) {
        auto inventoryList = actor->inventoryList;
        if (!inventoryList) {
            return false;
        }

        BSWriteLocker locker(&inventoryList->inventoryLock);

        auto inventoryItems = inventoryList->items;
        for (UInt32 j = 0; j < inventoryItems.count; ++j) {

            BGSInventoryItem item;
            if (!inventoryItems.GetNthItem(j, item)) {
                return false; // If one fails, all the rest will fail
            }

            if (item.form == ammoData.ammo) {
                // Add to count
                item.stack->count += ammoData.ammoCount;

                // Clear ammo
                ammoData.equippedData->unk18 &= 0xFFFFFFFF00000000; // Clear lower 32 bits

                // Stop searching
                return true;
            }
        }

        return false;
    }
};

 

Link to comment
Share on other sites

1. I'm personnaly would prefer to avoid inventory scanning if amount of ammo is 0 (as it was done in first versions)

2. Altering of equppedData state outside of appropriate critical section seems dangerous for me. Actually, I guess the probability of changes in equipped items when actor dies is much higher than ammo changes in inventory, so it is more important making changes in equipped items withing critical section than making changes in inventory in critical section. But I might be wrong. If I would implement such algorithm, I would unload weapon in FindAmmo function (well, it should be renamed in this case)

Edited by DlinnyLag
Link to comment
Share on other sites

1. I'm personnaly would prefer to avoid inventory scanning if amount of ammo is 0 (as it was done in first versions)

2. Altering of equppedData state outside of appropriate critical section seems dangerous for me. Actually, I guess the probability of changes in equipped items when actor dies is much higher than ammo changes in inventory, so it is more important making changes in equipped items withing critical section than making changes in inventory in critical section. But I might be wrong. If I would implement such algorithm, I would unload weapon in FindAmmo function (well, it should be renamed in this case)

1. If ammoCount is 0, it will continue and eventually return false which returns before searching the inventory.

2. I agree. There's a risk of losing the ammo if there isn't a stack in the inventory, but I think it should always be there. I could go back after and put it back if I can't find it in the inventory but that seems like overkill.

 

Here's the (hopefully finished) code:

// https://github.com/powerof3/SKSEPlugins/blob/master/po3_FEC/main.cpp
class TESDeathEventHandler : public BSTEventSink<TESDeathEvent> {
public:
    static TESDeathEventHandler* GetSingleton() {
        static TESDeathEventHandler singleton;
        return &singleton;
    }
    
    virtual EventResult ReceiveEvent(TESDeathEvent* evn, void* dispatcher) override {
        if (!evn || !evn->source) {
            return kEvent_Continue;
        }
        
        Actor* actor = DYNAMIC_CAST(evn->source, TESObjectREFR, Actor);
        if (!actor) {
            return kEvent_Continue;
        }

        AmmoData ammoData;
        if (!RemoveAmmo(actor, ammoData)) {
            return kEvent_Continue;
        }

        AddAmmo(actor, ammoData);

        return kEvent_Continue;
    }

// https://stackoverflow.com/a/1008289
// Using C++ 98, so no delete
private:
    TESDeathEventHandler() {}
    TESDeathEventHandler(TESDeathEventHandler const&);
    void operator=(TESDeathEventHandler const&);

    struct AmmoData
    {
        TESAmmo    * ammo;
        UInt64    ammoCount;
    };

    bool RemoveAmmo(Actor* actor, AmmoData& ammoData) {
        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return false;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return false;
        }

        SimpleLocker locker(&unk08->lock);

        auto equipDataArray = unk08->equipData;
        for (UInt32 i = 0; i < equipDataArray.count; ++i) {

            Actor::MiddleProcess::Data08::EquipData equipData;
            if (!equipDataArray.GetNthItem(i, equipData)) {
                return false; // If one fails, all the rest will fail
            }

            auto equippedData = equipData.equippedData;
            if (!equippedData) {
                continue;
            }

            auto ammoCount = equippedData->unk18 & 0x00000000FFFFFFFF; // Ammo count is lower 32 bits
            if (ammoCount == 0) {
                continue; // Sometimes this method gets called twice, this will skip it on subsequent calls
            }

            auto ammo = equippedData->ammo;
            if (!ammo) {
                continue;
            }

            // Clear ammo
            equippedData->unk18 &= 0xFFFFFFFF00000000; // Clear lower 32 bits

            // Store data
            ammoData.ammo = ammo;
            ammoData.ammoCount = ammoCount;

            // Stop searching
            return true;
        }

        return false;
    }

    bool AddAmmo(Actor* actor, AmmoData ammoData) {
        auto inventoryList = actor->inventoryList;
        if (!inventoryList) {
            return false;
        }

        BSWriteLocker locker(&inventoryList->inventoryLock);

        auto inventoryItems = inventoryList->items;
        for (UInt32 j = 0; j < inventoryItems.count; ++j) {

            BGSInventoryItem item;
            if (!inventoryItems.GetNthItem(j, item)) {
                return false; // If one fails, all the rest will fail
            }

            if (item.form == ammoData.ammo) {
                // Add to count
                item.stack->count += ammoData.ammoCount;

                // Stop searching
                return true;
            }
        }

        return false;
    }
};

 

Link to comment
Share on other sites

I guess it is acceptable that no ammo will appear in inventory if inventory hasn't ammo at the moment of actor death.

So, code seems ok to me.

I can't leave well enough alone, so I handled this case too. Tested it by forcing AddAmmo to return false and load/unload seems to be working fine. Now I think it's finally ready for release. Couldn't have done it without you!

// https://github.com/powerof3/SKSEPlugins/blob/master/po3_FEC/main.cpp
class TESDeathEventHandler : public BSTEventSink<TESDeathEvent> {
public:
    static TESDeathEventHandler* GetSingleton() {
        static TESDeathEventHandler singleton;
        return &singleton;
    }
    
    virtual EventResult ReceiveEvent(TESDeathEvent* evn, void* dispatcher) override {
        if (!evn || !evn->source) {
            return kEvent_Continue;
        }
        
        Actor* actor = DYNAMIC_CAST(evn->source, TESObjectREFR, Actor);
        if (!actor) {
            return kEvent_Continue;
        }

        AmmoData ammoData;
        if (!LoadUnloadAmmo(actor, ammoData, false)) {
            return kEvent_Continue;
        }

        if (!AddAmmo(actor, ammoData)) {
            LoadUnloadAmmo(actor, ammoData, true);
        }

        return kEvent_Continue;
    }

// https://stackoverflow.com/a/1008289
// Using C++ 98, so no delete
private:
    TESDeathEventHandler() {}
    TESDeathEventHandler(TESDeathEventHandler const&);
    void operator=(TESDeathEventHandler const&);

    struct AmmoData
    {
        TESAmmo    * ammo;
        UInt64    ammoCount;
    };

    bool LoadUnloadAmmo(Actor* actor, AmmoData& ammoData, bool load) {
        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return false;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return false;
        }

        SimpleLocker locker(&unk08->lock);

        auto equipDataArray = unk08->equipData;
        for (UInt32 i = 0; i < equipDataArray.count; ++i) {

            Actor::MiddleProcess::Data08::EquipData equipData;
            if (!equipDataArray.GetNthItem(i, equipData)) {
                return false; // If one fails, all the rest will fail
            }

            auto equippedData = equipData.equippedData;
            if (!equippedData) {
                continue;
            }

            auto ammo = equippedData->ammo;
            if (!ammo) {
                continue;
            }

            auto ammoCount = equippedData->unk18 & 0x00000000FFFFFFFF; // Ammo count is lower 32 bits

            if (load) {
                if (ammoCount != 0) {
                    continue; // Don't load ammo if ammo isn't empty
                }

                if (ammo != ammoData.ammo) {
                    continue;
                }

                // Load ammo
                equippedData->unk18 += ammoData.ammoCount;
            }
            else {
                if (ammoCount == 0) {
                    continue; // Sometimes this method gets called twice, this will skip it on subsequent calls
                }

                // Unload ammo
                equippedData->unk18 &= 0xFFFFFFFF00000000; // Clear lower 32 bits

                // Store data
                ammoData.ammo = ammo;
                ammoData.ammoCount = ammoCount;
            }

            // Stop searching
            return true;
        }

        return false;
    }

    bool AddAmmo(Actor* actor, AmmoData ammoData) {
        auto inventoryList = actor->inventoryList;
        if (!inventoryList) {
            return false;
        }

        BSWriteLocker locker(&inventoryList->inventoryLock);

        auto inventoryItems = inventoryList->items;
        for (UInt32 j = 0; j < inventoryItems.count; ++j) {

            BGSInventoryItem item;
            if (!inventoryItems.GetNthItem(j, item)) {
                return false; // If one fails, all the rest will fail
            }

            if (item.form == ammoData.ammo) {
                // Add to count
                item.stack->count += ammoData.ammoCount;

                // Stop searching
                return true;
            }
        }

        return false;
    }
};

 

Link to comment
Share on other sites

I guess it is acceptable that no ammo will appear in inventory if inventory hasn't ammo at the moment of actor death.

So, code seems ok to me.

One more question if you're still around. It doesn't work with fusion cores so I added the following code. The "find" is in case a sorting mod renames them. See any problems with it? Or a better way to do it?

std::string ammoName = ammo->GetFullName();
if (ammoName.find("Fusion Core") != std::string::npos) {
    continue; // Ignore weapons with fusion cores
}

 

Link to comment
Share on other sites

 

One more question if you're still around. It doesn't work with fusion cores so I added the following code. The "find" is in case a sorting mod renames them. See any problems with it? Or a better way to do it?

std::string ammoName = ammo->GetFullName();
if (ammoName.find("Fusion Core") != std::string::npos) {
    continue; // Ignore weapons with fusion cores
}

Why not to use FormId of Fusion Core? It is pretty known

Link to comment
Share on other sites

 

Why not to use FormId of Fusion Core? It is pretty known

 

That works a lot better, didn't notice formID because it was buried so deep in the inheritance.

private:
    static const UInt32 FUSION_CORE_FORM_ID = 0x00075FE4;

    TESDeathEventHandler() {}
    TESDeathEventHandler(TESDeathEventHandler const&);
    void operator=(TESDeathEventHandler const&);

    struct AmmoData
    {
        TESAmmo    * ammo;
        UInt64    ammoCount;
    };

    bool LoadUnloadAmmo(Actor* actor, AmmoData& ammoData, bool load) {
        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return false;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return false;
        }

        SimpleLocker locker(&unk08->lock);

        auto equipDataArray = unk08->equipData;
        for (UInt32 i = 0; i < equipDataArray.count; ++i) {

            Actor::MiddleProcess::Data08::EquipData equipData;
            if (!equipDataArray.GetNthItem(i, equipData)) {
                return false; // If one fails, all the rest will fail
            }

            auto equippedData = equipData.equippedData;
            if (!equippedData) {
                continue;
            }

            auto ammo = equippedData->ammo;
            if (!ammo) {
                continue;
            }

            auto ammoCount = equippedData->unk18 & 0x00000000FFFFFFFF; // Ammo count is lower 32 bits

            if (load) {
                if (ammoCount != 0) {
                    continue; // Don't load ammo if ammo isn't empty
                }

                if (ammo != ammoData.ammo) {
                    continue;
                }

                // Load ammo
                equippedData->unk18 += ammoData.ammoCount;
            }
            else {
                if (ammoCount == 0) {
                    continue; // Sometimes this method gets called twice, this will skip it on subsequent calls
                }

                if (ammo->formID == FUSION_CORE_FORM_ID) {
                    continue; // Ignore weapons with fusion cores
                }

 

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...