Jump to content

[Solved] F4SE: unresolved external symbol


ScottyDoesKnow

Recommended Posts

As soon as I add the line:

GetEventDispatcher<TESDeathEvent>()->AddEventSink(TESDeathEventHandler::GetSingleton());

I get a bunch of unresolved external symbol errors:

error LNK2019: unresolved external symbol "void * __cdecl Heap_Allocate(unsigned __int64)" (?Heap_Allocate@@YAPEAX_K@Z) referenced in function "public: bool __cdecl tArray<class BSTEventSink<struct TESDeathEvent> *,10,10>::Grow(unsigned long)" (?Grow@?$tArray@PEAV?$BSTEventSink@UTESDeathEvent@@@@$09$09@@QEAA_NK@Z)    main.obj

error LNK2019: unresolved external symbol "void __cdecl Heap_Free(void *)" (?Heap_Free@@YAXPEAX@Z) referenced in function "public: bool __cdecl tArray<class BSTEventSink<struct TESDeathEvent> *,10,10>::Grow(unsigned long)" (?Grow@?$tArray@PEAV?$BSTEventSink@UTESDeathEvent@@@@$09$09@@QEAA_NK@Z)    main.obj

error LNK2019: unresolved external symbol "public: void __cdecl SimpleLock::Lock(unsigned long)" (?Lock@SimpleLock@@QEAAXK@Z) referenced in function "public: __cdecl SimpleLocker::SimpleLocker(class SimpleLock *)" (??0SimpleLocker@@QEAA@PEAVSimpleLock@@@Z)    main.obj

error LNK2019: unresolved external symbol "public: void __cdecl SimpleLock::Release(void)" (?Release@SimpleLock@@QEAAXXZ) referenced in function "public: __cdecl SimpleLocker::~SimpleLocker(void)" (??1SimpleLocker@@QEAA@XZ)    main.obj

error LNK1120: 4 unresolved externals    MyFirstPlugin.dll

I've traced it down to the calls, like Heap_Allocate is in GameAPI.h/cpp. While tracing it, each file seems to include the correct header for the next reference. Anyone have an idea what's going on?

 

Edit: In case anyone finds this in the future, I found a better way than including the .cpp files: you just need to compile f4se as a static library. I've also uploaded my working project for people to take a look at.

Link to comment
Share on other sites

As soon as I add the line:

GetEventDispatcher<TESDeathEvent>()->AddEventSink(TESDeathEventHandler::GetSingleton());

I get a bunch of unresolved external symbol errors:

error LNK2019: unresolved external symbol "void * __cdecl Heap_Allocate(unsigned __int64)" (?Heap_Allocate@@YAPEAX_K@Z) referenced in function "public: bool __cdecl tArray<class BSTEventSink<struct TESDeathEvent> *,10,10>::Grow(unsigned long)" (?Grow@?$tArray@PEAV?$BSTEventSink@UTESDeathEvent@@@@$09$09@@QEAA_NK@Z)    main.obj

error LNK2019: unresolved external symbol "void __cdecl Heap_Free(void *)" (?Heap_Free@@YAXPEAX@Z) referenced in function "public: bool __cdecl tArray<class BSTEventSink<struct TESDeathEvent> *,10,10>::Grow(unsigned long)" (?Grow@?$tArray@PEAV?$BSTEventSink@UTESDeathEvent@@@@$09$09@@QEAA_NK@Z)    main.obj

error LNK2019: unresolved external symbol "public: void __cdecl SimpleLock::Lock(unsigned long)" (?Lock@SimpleLock@@QEAAXK@Z) referenced in function "public: __cdecl SimpleLocker::SimpleLocker(class SimpleLock *)" (??0SimpleLocker@@QEAA@PEAVSimpleLock@@@Z)    main.obj

error LNK2019: unresolved external symbol "public: void __cdecl SimpleLock::Release(void)" (?Release@SimpleLock@@QEAAXXZ) referenced in function "public: __cdecl SimpleLocker::~SimpleLocker(void)" (??1SimpleLocker@@QEAA@XZ)    main.obj

error LNK1120: 4 unresolved externals    MyFirstPlugin.dll

I've traced it down to the calls, like Heap_Allocate is in GameAPI.h/cpp. While tracing it, each file seems to include the correct header for the next reference. Anyone have an idea what's going on?

You have to include appropriate F4SE .cpp files to you project. Including .h file is not enough to compile binary file if compiler/linker doesn't have access to body of functions that just declared (but not implemented) in a header file.

 

SimpleLock implementation is in the GameTypes.cpp

Heap_Allocate implementation is in the GameAPI.cpp

Link to comment
Share on other sites

You have to include appropriate F4SE .cpp files to you project. Including .h file is not enough to compile binary file if compiler/linker doesn't have access to body of functions that just declared (but not implemented) in a header file.

 

 

SimpleLock implementation is in the GameTypes.cpp

Heap_Allocate implementation is in the GameAPI.cpp

That did it. Never would've found that, the cpp files don't even pop up in Intellisense. Thanks!

 

Huge shot in the dark, but you wouldn't know how to find how much ammo is in an Actor's gun when they died, would you? Right now I'm just checking random unknown values to see if any are in the right range.

Edited by ScottyDoesKnow
Link to comment
Share on other sites

 

Huge shot in the dark, but you wouldn't know how to find how much ammo is in an Actor's gun when they died, would you? Right now I'm just checking random unknown values to see if any are in the right range.

 

 

I guess this is the value that you want. Not sure what is an analogue in the original F4SE implementation

Edited by DlinnyLag
Link to comment
Share on other sites

 

I guess this is the value that you want. Not sure what is an analogue in the original F4SE implementation

 

Thanks for the suggestion, I did manage to find it as the lower 32 bits of EquippedWeaponData.unk18. I'm assuming it's 32 bits due to your link, so it did help.

 

So I'm able to find the ammo amount and I'm also able to remove it from the gun, finally I just need to add the removed ammo to the actor's inventory. Here's what I tried first which doesn't work (I know the for loop is awful, I was just testing things out).

// This ammo adding stuff doesn't work
/*auto ammo = equippedData->ammo;

for (int i = 0; i < 10; ++i) {
    BGSInventoryItem inventoryAmmo;
    inventoryAmmo.form = ammo;

    auto inventoryItems = actor->inventoryList->items;
    inventoryItems.Insert(inventoryItems.count, inventoryAmmo);
}*/

Do you know the correct way to add items? Preferably with an amount. Thanks again!

 

Edit: I also found Push rather than Insert, which didn't make a difference. The better option also is probably to find the ammo that's already in their inventory (which I think they should always have for their used weapon?) and add to the amount.

 

Edit2: Found it! It was in BGSInventoryItem.stack.count

 

If you wouldn't mind, could you take a look at the finished code and see if anything horribly wrong jumps out at you? I mostly code in C# so this has been a lot of trial and error.

#include "common/IDebugLog.h"  // IDebugLog
#include "f4se_common/f4se_version.h"  // RUNTIME_VERSION
#include "f4se/GameEvents.h" // TESDeathEvent
#include "f4se/GameObjects.h" // TESAmmo
#include "f4se/GameReferences.h" // Actor
#include "f4se/GameRTTI.h" // DYNAMIC_CAST
#include "f4se/PluginAPI.h"  // F4SEInterface, PluginInfo
#include "f4se/PluginManager.h"

#include <ShlObj.h>  // CSIDL_MYDOCUMENTS

#include "version.h"  // VERSION_VERSTRING, VERSION_MAJOR

#include "f4se/GameAPI.cpp" // Heap_Allocate
#include "f4se/GameRTTI.cpp" // DYNAMIC_CAST
#include "f4se/GameTypes.cpp" // SimpleLock

boolean onInitRan = false;

// 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;
        }

        auto middleProcess = actor->middleProcess;

        if (middleProcess) {
            auto unk08 = middleProcess->unk08;

            if (unk08) {
                auto equipDataArray = unk08->equipData;

                for (UInt64 i = 0; i < equipDataArray.count; ++i) {
                    Actor::MiddleProcess::Data08::EquipData equipData;
                    if (equipDataArray.GetNthItem(i, equipData)) {
                        auto equippedData = equipData.equippedData;

                        if (equippedData) {
                            auto ammoCount = equippedData->unk18 & UINT32_MAX;

                            // Sometimes this gets called twice, we'll stop it here on subsequent calls
                            if (ammoCount > 0) {
                                auto ammo = equippedData->ammo;
                                auto inventoryItems = actor->inventoryList->items;

                                for (int i = 0; i < inventoryItems.count; ++i) {
                                    BGSInventoryItem item;
                                    if (inventoryItems.GetNthItem(i, item)) {
                                        if (item.form == ammo) {
                                            // Add to count
                                            item.stack->count += ammoCount;

                                            // Clear ammo
                                            equippedData->unk18 = equippedData->unk18 >> 32 << 32;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        return kEvent_Continue;
    }

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

void OnInit(F4SEMessagingInterface::Message* msg) {
    if (onInitRan) {
        return;
    }
    else if (msg->type != F4SEMessagingInterface::kMessage_GameDataReady) { // https://github.com/Neanka/f4se/blob/master/f4se/f4seee/main.cpp
        _MESSAGE("[MESSAGE] OnInit not GameDataReady.");
        return;
    }
    else {
        onInitRan = true;
    }

    _MESSAGE("[MESSAGE] OnInit started.");

    GetEventDispatcher<TESDeathEvent>()->AddEventSink(TESDeathEventHandler::GetSingleton());

    _MESSAGE("[MESSAGE] OnInit finished.");
}

extern "C" {
    bool F4SEPlugin_Query(const F4SEInterface* a_f4se, PluginInfo* a_info)
    {
        gLog.OpenRelative(CSIDL_MYDOCUMENTS, "\\My Games\\Fallout4\\F4SE\\AmmoRemover.log");
        gLog.SetPrintLevel(IDebugLog::kLevel_DebugMessage);
        gLog.SetLogLevel(IDebugLog::kLevel_DebugMessage);

        _MESSAGE("[MESSAGE] v%s", MYFP_VERSION_VERSTRING);

        a_info->infoVersion = PluginInfo::kInfoVersion;
        a_info->name = "AmmoRemover";
        a_info->version = MYFP_VERSION_MAJOR;

        if (a_f4se->isEditor) {
            _FATALERROR("[FATAL ERROR] Loaded in editor, marking as incompatible!\n");
            return false;
        } else if (a_f4se->runtimeVersion != RUNTIME_VERSION_1_10_163) {
            _FATALERROR("[FATAL ERROR] Unsupported runtime version %08X!\n", a_f4se->runtimeVersion);
            return false;
        }

        return true;
    }

    bool F4SEPlugin_Load(const F4SEInterface* a_f4se)
    {
        _MESSAGE("[MESSAGE] Load started.");

        PluginHandle handle = a_f4se->GetPluginHandle();
        F4SEMessagingInterface* messagingInterface = (F4SEMessagingInterface*)a_f4se->QueryInterface(kInterface_Messaging); // https://github.com/Neanka/f4se/blob/master/f4se/f4seee/main.cpp
        if (!messagingInterface->RegisterListener(handle, "F4SE", OnInit)) {
            return false;
        }

        _MESSAGE("[MESSAGE] Load finished.");

        return true;
    }
};

 

Edited by ScottyDoesKnow
Link to comment
Share on other sites

 

 

 

equippedData->unk18 = equippedData->unk18 >> 32 << 32;

Why not just assing to 0? %)

And general comment - too deep nesting level. Fail fast

 

This is clearing out the lower 32 bits because the upper 32 bits have something else in them. The actual value of unk18 is a massive number.

 

And here's a less nested version:

    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;
        }

        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return kEvent_Continue;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return kEvent_Continue;
        }

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

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

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

            auto ammoCount = equippedData->unk18 & UINT32_MAX;
            if (ammoCount == 0) {
                // Sometimes this method gets called twice, this will skip it on subsequent calls
                continue;
            }

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

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

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

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

                    // Clear ammo
                    equippedData->unk18 = equippedData->unk18 >> 32 << 32;

                    // Stop searching
                    return kEvent_Continue;
                }
            }
        }

        return kEvent_Continue;
    }

 

Link to comment
Share on other sites

 

And here's a less nested version:

    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;
        }

        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return kEvent_Continue;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return kEvent_Continue;
        }

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

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

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

            auto ammoCount = equippedData->unk18 & UINT32_MAX;
            if (ammoCount == 0) {
                // Sometimes this method gets called twice, this will skip it on subsequent calls
                continue;
            }

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

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

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

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

                    // Clear ammo
                    equippedData->unk18 = equippedData->unk18 >> 32 << 32;

                    // Stop searching
                    return kEvent_Continue;
                }
            }
        }

        return kEvent_Continue;
    }

 

Algorithm notes:

1. if no ammo found in the inventory then outer loop continues to execute. Perfomance issue.

2. if no ammo found in the inventory then weapon will not be unloaded. Not sure, but seems like a problem.

 

Potential crash:

Iterating of equipped items should be done in critical section - lock (declared above tArray<EquipData> equipData in MiddleProcess) should be locked, while you iterate array of equipped items.

Same for inventory items, appropriate lock (BGSInventoryList::inventoryLock) should be used.

 

In my mod, I used actor->equipData to iterate equipped items. It has array with fixed size - ActorEquipData::kMaxSlots.

Maybe for you purposes approach with actor->middleProcess usage is more suitable, so I would not insist to use actor->equipData

 

 

Style notes:

 

1. Zeroing lower 4 bytes in 8-bytes variable (set ammo to 0):

It is easier to read if it is written like:

equippedData->unk18 &= ~0xFFFFFFFF;

See an example

 

 

2. UINT32_MAX usage:

See more correct option

 

Notes above can be avoided - just alter declaration of EquippedWeaponData class and declare necessary field explicitly with necessary type. You can fork F4SE and alter it as you need %)

Edited by DlinnyLag
Link to comment
Share on other sites

 

Algoriithm notes:

1. if no ammo found in the inventory then outer loop continues to execute. Perfomance issue.

2. if no ammo found in the inventory then weapon will not be unloaded. Not sure, but seems like a problem.

 

Potential crash:

Iterating of equipped items should be done in critical section - lock (declared above tArray<EquipData> equipData in MiddleProcess) should be locked, while you iterate array of equipped items.

Same for inventory items, appropriate lock (BGSInventoryList::inventoryLock) should be used.

 

In my mod, I used actor->equipData to iterate equipped items. It has array with fixed size - ActorEquipData::kMaxSlots.

Maybe for you purposes approach with actor->middleProcess usage is more suitable, so I would not insist to use actor->equipData

 

 

Style notes:

 

1. Zeroing lower 4 bytes in 8-bytes variable (set ammo to 0):

It is easier to read if it is written like:

equippedData->unk18 &= ~0xFFFFFFFF;

See an example

 

 

2. UINT32_MAX usage:

See more correct option

 

Notes above can be avoided - just alter declaration of EquippedWeaponData class and declare necessary field explicitly with necessary type. You can fork F4SE and alter it as you need %)

 

1. I believe the outer loop will only ever have one item in it. But if it doesn't, the second item could be looking for a different type of ammo.

2. This is on purpose, I'd rather it just not execute in that case than deal with adding new items to the inventory.

 

For the locks, I think the SimpleLock is fine as it should release when SimpleLocker destructs. This is how it's being used elsewhere. For the BSReadWriteLock, I just had to lock and unlock. This is also how it's being used elsewhere, but it worries me that I don't have any sort of try/finally structure for it. I've tried to make sure I have as many checks as possible in there so it won't error.

 

Edit: Updated the code again. The &= ~0xFFFFFFFF was completely clearing the value, not just the lower bits. I think because it might instantiate that as a uint32 so the inverse is just zero.

 

Edit2: And forking F4SE seems a bit much to me, I think it's fine doing a couple bitwise operations.

    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;
        }

        auto middleProcess = actor->middleProcess;
        if (!middleProcess) {
            return kEvent_Continue;
        }
        
        auto unk08 = middleProcess->unk08;
        if (!unk08) {
            return kEvent_Continue;
        }

        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 kEvent_Continue; // 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;
            }

            auto inventoryList = actor->inventoryList;
            if (!inventoryList) {
                return kEvent_Continue;
            }

            inventoryList->inventoryLock.LockForWrite();

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

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

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

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

                    // Stop searching
                    break;
                }
            }

            inventoryList->inventoryLock.UnlockWrite();
        }

        return kEvent_Continue;
    }
Edited by ScottyDoesKnow
Link to comment
Share on other sites

 

1. I believe the outer loop will only ever have one item in it. But if it doesn't, the second item could be looking for a different type of ammo.

 

2. This is on purpose, I'd rather it just not execute in that case than deal with adding new items to the inventory.

 

For the locks, I think the SimpleLock is fine as it should release when SimpleLocker destructs. This is how it's being used elsewhere. For the BSReadWriteLock, I just had to lock and unlock. This is also how it's being used elsewhere, but it worries me that I don't have any sort of try/finally structure for it. I've tried to make sure I have as many checks as possible in there so it won't error.

 

Edit: Updated the code again. The &= ~0xFFFFFFFF was completely clearing the value, not just the lower bits. I think because it might instantiate that as a uint32 so the inverse is just zero.

 

Edit2: And forking F4SE seems a bit much to me, I think it's fine doing a couple bitwise operations.

            inventoryList->inventoryLock.LockForWrite();

 

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.

Edited by DlinnyLag
Link to comment
Share on other sites

  • Recently Browsing   0 members

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