ScottyDoesKnow Posted September 17, 2022 Share Posted September 17, 2022 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 More sharing options...
DlinnyLag Posted September 17, 2022 Share Posted September 17, 2022 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.dllI'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.cppHeap_Allocate implementation is in the GameAPI.cpp Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 17, 2022 Author Share Posted September 17, 2022 (edited) 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.cppHeap_Allocate implementation is in the GameAPI.cppThat 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 September 17, 2022 by ScottyDoesKnow Link to comment Share on other sites More sharing options...
DlinnyLag Posted September 17, 2022 Share Posted September 17, 2022 (edited) 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 September 17, 2022 by DlinnyLag Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 17, 2022 Author Share Posted September 17, 2022 (edited) 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 September 17, 2022 by ScottyDoesKnow Link to comment Share on other sites More sharing options...
DlinnyLag Posted September 17, 2022 Share Posted September 17, 2022 equippedData->unk18 = equippedData->unk18 >> 32 << 32;Why not just assing to 0? %)And general comment - too deep nesting level. Fail fast Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 17, 2022 Author Share Posted September 17, 2022 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 More sharing options...
DlinnyLag Posted September 17, 2022 Share Posted September 17, 2022 (edited) 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 September 17, 2022 by DlinnyLag Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 17, 2022 Author Share Posted September 17, 2022 (edited) 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 September 18, 2022 by ScottyDoesKnow Link to comment Share on other sites More sharing options...
DlinnyLag Posted September 18, 2022 Share Posted September 18, 2022 (edited) 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->inventoryLockIt is ok if there is no any (not your) code in application with reversed order of locks. Deadlock is possible, otherwise. Edited September 18, 2022 by DlinnyLag Link to comment Share on other sites More sharing options...
Recommended Posts