-
Posts
98 -
Joined
-
Last visited
Everything posted by ScottyDoesKnow
-
It's been quite a while, but I think I found the more correct way than including .cpp files (which maybe you were getting at, I see you mentioned the linker). You just need to add the f4se_<version>.lib in the project's properties under Linker -> Input -> Additional Dependencies. Just let me know if you already knew this and there's some reason not to do it.
-
Far Harbor: White LOD
ScottyDoesKnow replied to Zorkaz's topic in Fallout 4's Creation Kit and Modders
Ya, I was thinking that this might be a false positive, since it's on so many nifs. Since it's in like 28% of all nifs, you could check some of the working nifs around a broken one and see if the working ones also have the C:\ paths, which would imply it's not actually the problem. Could also be worth downgrading or finding a previous DLCCoast - Main.ba2 to see if they're in the old stuff. It seems weird that it's in so many of them but other people don't seem to be seeing major issues. -
Far Harbor: White LOD
ScottyDoesKnow replied to Zorkaz's topic in Fallout 4's Creation Kit and Modders
Ah, thought you just meant the Materials folder was missing. Yup, grep shows 10357 broken nifs. -
Far Harbor: White LOD
ScottyDoesKnow replied to Zorkaz's topic in Fallout 4's Creation Kit and Modders
.ba2 files have folders inside them which I believe are just appended to the Data location for path references like these. Example: The ones you're looking for should be in DLCCoast - Main.ba2 -
Thanks a ton! It works and it's fast enough that if I kill an enemy point blank I never see the ammo in the inventory. Just had to use SInt32 and make my source non-const for the static_cast. const auto handle = PapyrusVM::GetHandleFromObject(static_cast<void*>(source), TESObjectREFR::kTypeID); bool silent = true; TESObjectREFR* target = nullptr; SendPapyrusEvent4<TESForm*, SInt32, bool, TESObjectREFR*>(handle, "ObjectReference", "RemoveItem", item.form, item.stack->count, silent, target);
-
You're awesome! Was having trouble finding anyone who knew about this stuff. I haven't used the CommonLib so I'm not aware of how to call Unk_6D since it doesn't take parameters. The Papyrus option should be fine, I assume the delay is on coding terms and not real-time terms? Basically this runs when an NPC dies and removes ammo from its inventory, so as long as it's gone by the time the player tries to loot it that should be fine. I can't test it immediately because I'm a bit busy and hitting some compile error where I'll have to include something else, but does this look generally correct? source is a TESObjectREFR* (event->source), the void* should be a different type of cast but that's what GetHandleFromObject wants, and target is null because I just want it gone: const auto handle = PapyrusVM::GetHandleFromObject((void*)source, TESObjectREFR::kTypeID); TESForm* form = item.form; int count = item.stack->count; bool silent = true; TESObjectREFR* target = nullptr; SendPapyrusEvent4<TESForm*, int, bool, TESObjectREFR*>(handle, "ObjectReference", "RemoveItem", form, count, silent, target);
-
Getting game crashes when I try to do this, here's the relevant method: bool TESDeathEventHandler::SetAmmo(const Actor* actor, const AmmoStack ammoStack) { const auto inventory = actor->inventoryList; if (!inventory) return false; BSWriteLocker locker(&inventory->inventoryLock); auto items = inventory->items; for (UInt32 i = 0; i < items.count; ++i) { BGSInventoryItem item; if (!items.GetNthItem(i, item)) return false; // If one fails, all the rest will fail if (item.form == ammoStack.ammo) { const SInt32 wepAmmo = ammoStack.count * wepAmmoPercent / 100; const SInt32 invAmmo = item.stack->count * invAmmoPercent / 100; const SInt32 total = wepAmmo + invAmmo; if (total > 0) item.stack->count = total; else items.Remove(i); return true; } } return false; } As far as I can tell, I'm doing everything correctly. The remove seems to work fine and the method returns, as well as the handler method calling it. But then Fallout crashes with "Exception thrown at 0x00007FF7AAA96329 in Fallout4.exe: 0xC0000005: Access violation reading location 0x0000000000000025." The "thrown at" and "reading location" change each time. I also tried just setting the stack count to 0, but that results in a bugged item rather than removing it. Anyone know what's wrong? Or a better way to debug the crashes? I attached the debugger and stepped through, but it fails outside my code and the call stack is all just random addresses in hex.
-
Not technically an example project (it's just my mod) but I made sure to structure it to make it as easy as possible to get started: https://github.com/ScottyDoesKnow/AmmoRemover Hopefully this helps someone out, I had a hell of a time updating my mod since I definitely did not set it up correctly when I first made it.
-
- 4
-
-
-
Thanks for the info. I'm glad I didn't figure it out because the flag check is a much cleaner solution in the end, I later found some weird weapons with that keyword. Now the code checks for the "Can't Drop" flag rather than looking at specific weapons or ammo. It also checks for fusion cores by finding the charge value rather than by editor ID to make it more compatible with mods. Here's (hopefully the final) code if you feel like checking it out. I think I managed to do the upgraded checks pretty cleanly. // 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; } private: static const UInt64 LOWER_32_MASK = 0x00000000FFFFFFFF; static const UInt64 UPPER_32_MASK = 0xFFFFFFFF00000000; static const size_t CANT_DROP_FLAG_POS = 2; static const int AMMO_CHARGE_INDEX = 1; // https://stackoverflow.com/a/1008289 // Using C++ 98, so no delete 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 item = equipData.item; if (!item) { continue; } if (std::bitset<sizeof(UInt32*CHAR_BIT)>(item->flags).test(CANT_DROP_FLAG_POS)) { continue; // Ignore weapons marked as Can't Drop } auto equippedData = equipData.equippedData; if (!equippedData) { continue; } auto ammo = equippedData->ammo; if (!ammo) { continue; } auto unk160 = ammo->unk160; if (unk160 && sizeof(unk160) > AMMO_CHARGE_INDEX) { auto ammoCharge = unk160[AMMO_CHARGE_INDEX] & LOWER_32_MASK; if (ammoCharge != 0) { continue; // Ignore ammo with charge } } auto ammoCount = equippedData->unk18 & LOWER_32_MASK; // 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 &= UPPER_32_MASK; // 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; } };
-
If you're still around, I've found a new issue. My code is triggering on weird ammo like what bloatflies fire. And apparently it finds an ammo stack to put it in too. Now this doesn't seem to matter for things like bloatflies, but I'd still rather not run the code. Currently I added checks for weird ammo types like bloatfly larva, mirelurk king sonic wave, etc... which works fine but is obviously not ideal. But that's not the real issue. The real issue is I tried killing a sentry bot and it took the 5.56 ammo out of its gun. So I need some way to check if a gun is lootable. I've found a keyword (ObjectTypeWeapon) which seems to be on all the normal guns but not the weird ones, but I can't figure out how to check for keywords in F4SE. Do you know if it's possible? Currently I'm checking for sentry bot guns and turret guns, which again isn't ideal. At this point it would be easier just to whitelist all the standard guns, but that would mean it doesn't work with mods. So being able to check for keywords seems like the best solution if it's possible, but I'm open to any other ideas as well. Edit: there is a "Can't Drop" flag. I'm checking out TESForm->flags now. Edit2: That did it!
-
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 }
-
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 }
-
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; } };
-
Side-Quest Recover-Weapons Legendary
ScottyDoesKnow replied to Frank0815's topic in Fallout 4's Mod Ideas
No I just meant it uses the same list that legendaries do to decide which weapon you get. I don't think you'll ever get a legendary from it. I've been playing but never got a note. -
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; } };
-
Side-Quest Recover-Weapons Legendary
ScottyDoesKnow replied to Frank0815's topic in Fallout 4's Mod Ideas
Ah, that's too bad. I guess it just switches it to the probability list for legendary weapons and doesn't actually make it legendary. With that not working I'm all out of ideas unfortunately. -
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; } };
-
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; }
-
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; }
-
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; } };