ScottyDoesKnow Posted September 18, 2022 Author Share Posted September 18, 2022 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. 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 More sharing options...
DlinnyLag Posted September 18, 2022 Share Posted September 18, 2022 (edited) 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 September 18, 2022 by DlinnyLag Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 19, 2022 Author Share Posted September 19, 2022 I'm away on a work trip for the week so I don't know how much time I'll have to work on this, but I'll definitely get back to you. Thanks again for all the help. Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 23, 2022 Author Share Posted September 23, 2022 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 More sharing options...
DlinnyLag Posted September 23, 2022 Share Posted September 23, 2022 (edited) 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. Edited September 23, 2022 by DlinnyLag Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 23, 2022 Author Share Posted September 23, 2022 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 More sharing options...
ScottyDoesKnow Posted September 24, 2022 Author Share Posted September 24, 2022 https://www.nexusmods.com/fallout4/mods/64426 Link to comment Share on other sites More sharing options...
ScottyDoesKnow Posted September 24, 2022 Author Share Posted September 24, 2022 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 More sharing options...
DlinnyLag Posted September 24, 2022 Share Posted September 24, 2022 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 More sharing options...
ScottyDoesKnow Posted September 24, 2022 Author Share Posted September 24, 2022 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 More sharing options...
Recommended Posts