diff --git a/Code/client/Games/Skyrim/Forms/TESFaction.h b/Code/client/Games/Skyrim/Forms/TESFaction.h index b54126435..6f5d3f777 100644 --- a/Code/client/Games/Skyrim/Forms/TESFaction.h +++ b/Code/client/Games/Skyrim/Forms/TESFaction.h @@ -2,8 +2,58 @@ #include #include +#include +#include + +struct TESObjectREFR; +struct BGSListForm; +struct BGSOutfit; +struct TESNPC; + +struct TESReactionForm : BaseFormComponent +{ + GameValueList reactions; + uint8_t groupFormType; + uint8_t pad19; + uint16_t pad1A; + uint32_t pad1C; +}; + +static_assert(sizeof(TESReactionForm) == 0x20); +static_assert(offsetof(TESReactionForm, reactions) == 0x08); +static_assert(offsetof(TESReactionForm, groupFormType) == 0x18); struct TESFaction : TESForm { + // Where the game sends the player's belongings when they are arrested by this faction. + struct CrimeData + { + TESObjectREFR* jailMarker; // JAIL + TESObjectREFR* waitMarker; // WAIT + TESObjectREFR* stolenGoodsContainer; // STOL: stolen items taken on arrest + TESObjectREFR* playerInventoryContainer; // PLCN: the rest of the player's inventory, returned on release + BGSListForm* crimeGroup; // CRGR + BGSOutfit* jailOutfit; // JOUT + uint8_t crimeValues[0x14]; // CRVA + uint32_t pad44; + }; + TESFullName fullname; + TESReactionForm reactionForm; + creation::BSTHashMap* crimeGoldMap; + uint32_t factionFlags; // DATA + uint32_t pad5C; + CrimeData crimeData; + // Remaining vendor data, ranks, crime counts, and timestamps are not accessed here. + uint8_t padA8[0x100 - 0xA8]; }; + +static_assert(sizeof(TESFaction::CrimeData) == 0x48); +static_assert(sizeof(TESFaction) == 0x100); +static_assert(offsetof(TESFaction, fullname) == 0x20); +static_assert(offsetof(TESFaction, reactionForm) == 0x30); +static_assert(offsetof(TESFaction, crimeGoldMap) == 0x50); +static_assert(offsetof(TESFaction, factionFlags) == 0x58); +static_assert(offsetof(TESFaction, crimeData) == 0x60); +static_assert(offsetof(TESFaction, crimeData.stolenGoodsContainer) == 0x70); +static_assert(offsetof(TESFaction, crimeData.playerInventoryContainer) == 0x78); diff --git a/Code/client/Games/TES.h b/Code/client/Games/TES.h index 5ee1804ea..8c19d3a5c 100644 --- a/Code/client/Games/TES.h +++ b/Code/client/Games/TES.h @@ -5,6 +5,7 @@ struct TESObjectCELL; struct TESWorldSpace; struct NiPoint3; struct TESForm; +struct TESFaction; struct Actor; struct ImageSpaceModifierInstance; @@ -109,12 +110,17 @@ struct ModManager Mod* GetByName(const char* acpName) const noexcept; TESObjectCELL* GetCellFromCoordinates(int32_t aX, int32_t aY, TESWorldSpace* aWorldSpace, bool aSpawnCell) noexcept; - uint8_t pad0[0x748]; + // Form arrays start at 0x10 and are indexed by FormType, 0x18 bytes each. + uint8_t pad0[0x118]; + GameArray factions; + uint8_t pad130[0x748 - 0x130]; GameArray quests; uint8_t pad760[0xD60 - 0x760]; GameList mods; }; +static_assert(offsetof(ModManager, factions) == 0x118); +static_assert(offsetof(ModManager, quests) == 0x748); static_assert(offsetof(ModManager, mods) == 0xD60); struct Setting diff --git a/Code/client/Services/Generic/ObjectService.cpp b/Code/client/Services/Generic/ObjectService.cpp index 188dde477..b2140e2ed 100644 --- a/Code/client/Services/Generic/ObjectService.cpp +++ b/Code/client/Services/Generic/ObjectService.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include @@ -60,11 +62,41 @@ bool IsPlayerHome(const TESObjectCELL* pCell) noexcept return false; } -bool ShouldSyncObject(const TESObjectREFR* apObject) noexcept +// Find each loaded faction's containers for a jailed player's belongings and stolen items. +// Do not sync them: each player's items must stay separate (#700). +// Only compare the container pointers; never read through them. +Set GetPlayerStashContainers() noexcept +{ + Set containers{}; + + ModManager* pModManager = ModManager::Get(); + if (!pModManager) + return containers; + + for (const TESFaction* pFaction : pModManager->factions) + { + if (!pFaction) + continue; + + if (pFaction->crimeData.playerInventoryContainer) + containers.insert(pFaction->crimeData.playerInventoryContainer); + + if (pFaction->crimeData.stolenGoodsContainer) + containers.insert(pFaction->crimeData.stolenGoodsContainer); + } + + return containers; +} + +bool ShouldSyncObject(const TESObjectREFR* apObject, const Set& acPlayerStashContainers) noexcept { if (!apObject) return false; + if (acPlayerStashContainers.contains(apObject)) + return false; + + // Quest chests that take the player's whole inventory without going through faction crime data. switch (apObject->formID) { case 0x39CF1: // Don't sync the chest in the "Diplomatic Immunity" quest @@ -117,9 +149,11 @@ void ObjectService::OnCellChange(const CellChangeEvent& acEvent) noexcept AssignObjectsRequest request{}; + const Set playerStashContainers = GetPlayerStashContainers(); + for (TESObjectREFR* pObject : objects) { - if (!ShouldSyncObject(pObject)) + if (!ShouldSyncObject(pObject, playerStashContainers)) { spdlog::warn("Excluding sync for {:X}", pObject->formID); continue;