Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Code/client/Games/Skyrim/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ GamePtr<Actor> Actor::Create(TESNPC* apBaseForm) noexcept
pActor->SetLevelMod(4);
pActor->MarkChanged(0x40000000);
pActor->SetParentCell(pCell);
pActor->SetBaseForm(apBaseForm);
pActor->SetObjectReference(apBaseForm);

auto position = pPlayer->position;
auto rotation = pPlayer->rotation;
Expand Down
7 changes: 7 additions & 0 deletions Code/client/Games/Skyrim/Components/TESActorBaseData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ TESActorBase* HookCreateTemplateActorBase(TESActorBase* apOriginalBase, TESActor
return pResult;
}

TESActorBase* TESActorBaseData::CreateTemplateActorBase(TESActorBase* apOriginalBase, TESActorBase* apTemplateBase) noexcept
{
using TCreateTemplateActorBase = decltype(TESActorBaseData::CreateTemplateActorBase);
POINTER_SKYRIMSE(TCreateTemplateActorBase, s_CreateTemplateActorBase, 14375);
return s_CreateTemplateActorBase.Get()(apOriginalBase, apTemplateBase);
}

static TiltedPhoques::Initializer s_actorBaseDataInitHooks(
[]()
{
Expand Down
2 changes: 2 additions & 0 deletions Code/client/Games/Skyrim/Components/TESActorBaseData.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ struct TESActorBaseData : BaseFormComponent
actorBaseFlags &= ~BaseFlags::IS_ESSENTIAL;
}

static TESActorBase* CreateTemplateActorBase(TESActorBase* apOriginalBase, TESActorBase* apTemplateBase) noexcept;

GameArray<FactionRank> factions;
};

Expand Down
50 changes: 50 additions & 0 deletions Code/client/Games/Skyrim/Forms/TESFaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,58 @@

#include <Forms/TESForm.h>
#include <Components/TESFullName.h>
#include <BSCore/BSTHashMap.h>
#include <Games/Primitives.h>

struct TESObjectREFR;
struct BGSListForm;
struct BGSOutfit;
struct TESNPC;

struct TESReactionForm : BaseFormComponent
{
GameValueList<void*> 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<const TESNPC*, uint32_t>* 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);
17 changes: 17 additions & 0 deletions Code/client/Games/Skyrim/Misc/GarbageCollector.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <TiltedOnlinePCH.h>

#include <Misc/GarbageCollector.h>

GarbageCollector* GarbageCollector::Get() noexcept
{
POINTER_SKYRIMSE(GarbageCollector*, s_singleton, 400329);
return *s_singleton.Get();
}

void GarbageCollector::Add(TESBoundObject* apObject) noexcept
{
// The base-object overload used by Actor::RecalcLeveledActor (among other functions);
TP_THIS_FUNCTION(TAdd, void, GarbageCollector, TESBoundObject*);
POINTER_SKYRIMSE(TAdd, s_add, 36460);
TiltedPhoques::ThisCall(s_add, this, apObject);
}
11 changes: 11 additions & 0 deletions Code/client/Games/Skyrim/Misc/GarbageCollector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

struct TESBoundObject;

struct GarbageCollector
{
static GarbageCollector* Get() noexcept;

// Uses the engine's immediate/deferred base-object deletion policy.
void Add(TESBoundObject* apObject) noexcept;
};
22 changes: 7 additions & 15 deletions Code/client/Games/Skyrim/TESObjectREFR.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ void TESObjectREFR::SetRotation(float aX, float aY, float aZ) noexcept
TiltedPhoques::ThisCall(RealRotateZ, this, aZ);
}

void TESObjectREFR::SetLeveledCreature(TESActorBase* apOriginalBase, TESActorBase* apTemplateA) noexcept
{
TP_THIS_FUNCTION(TSetLeveledCreature, void, TESObjectREFR, TESActorBase*, TESActorBase*);
POINTER_SKYRIMSE(TSetLeveledCreature, s_SetLeveledCreature, 20231);
TiltedPhoques::ThisCall(s_SetLeveledCreature, this, apOriginalBase, apTemplateA);
}

using TiltedPhoques::Serialization;

void TESObjectREFR::SaveAnimationVariables(AnimationVariables& aVariables) const noexcept
Expand Down Expand Up @@ -1128,21 +1135,6 @@ void TP_MAKE_THISCALL(HookLockChange, TESObjectREFR)
World::Get().GetRunner().Trigger(LockChangeEvent(apThis->formID, false, 0));
}

// Kept for reference: Actor::GetLeveledPick reads the engine's ExtraLeveledCreature directly.
// Called by Actor::RecalcLeveledActor (37323) and TESActorBaseData::CalcTemplateForRef (14374) in the engine.
void TP_MAKE_THISCALL(HookSetLeveledCreature, TESObjectREFR, TESActorBase* apOriginalBase, TESActorBase* apTemplateBase)
{
TiltedPhoques::ThisCall(RealSetLeveledCreature, apThis, apOriginalBase, apTemplateBase);

const uint32_t cOriginalBaseId = apOriginalBase ? apOriginalBase->formID : 0;
// ExtraDataList::SetLeveledCreature stores this pointer directly in templateBase.
const uint32_t cTemplateBaseId = apTemplateBase ? apTemplateBase->formID : 0;

TESForm* pResult = apThis->baseForm;
spdlog::debug(
"SetLeveledCreature: ref {:X}, original base {:X}, template base {:X}, current base {:X}", apThis->formID, cOriginalBaseId, cTemplateBaseId, pResult ? pResult->formID : 0);
}

static TiltedPhoques::Initializer s_objectReferencesHooks(
[]()
{
Expand Down
4 changes: 3 additions & 1 deletion Code/client/Games/Skyrim/TESObjectREFR.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
struct AnimationVariables;
struct TESWorldSpace;
struct TESBoundObject;
struct TESActorBase;
struct TESContainer;

enum class ITEM_REMOVE_REASON
Expand Down Expand Up @@ -136,7 +137,7 @@ struct TESObjectREFR : TESForm
virtual void sub_81();
virtual void sub_82();
virtual void sub_83();
virtual void SetBaseForm(TESBoundObject* apForm); // "void SetObjectReference(...)"?
virtual void SetObjectReference(TESBoundObject* apObject);
virtual void sub_85();
virtual void sub_86();
virtual void sub_87();
Expand Down Expand Up @@ -164,6 +165,7 @@ struct TESObjectREFR : TESForm
virtual void sub_9B();

void SetRotation(float aX, float aY, float aZ) noexcept;
void SetLeveledCreature(TESActorBase* apOriginalBase, TESActorBase* apTemplateA) noexcept;

BSPointerHandle<TESObjectREFR> GetHandle() const noexcept;
uint32_t GetCellId() const noexcept;
Expand Down
8 changes: 7 additions & 1 deletion Code/client/Games/TES.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct TESObjectCELL;
struct TESWorldSpace;
struct NiPoint3;
struct TESForm;
struct TESFaction;
struct Actor;
struct ImageSpaceModifierInstance;

Expand Down Expand Up @@ -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<TESFaction*> factions;
uint8_t pad130[0x748 - 0x130];
GameArray<TESQuest*> quests;
uint8_t pad760[0xD60 - 0x760];
GameList<Mod> mods;
};

static_assert(offsetof(ModManager, factions) == 0x118);
static_assert(offsetof(ModManager, quests) == 0x748);
static_assert(offsetof(ModManager, mods) == 0xD60);

struct Setting
Expand Down
33 changes: 17 additions & 16 deletions Code/client/Services/Generic/CharacterService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1556,19 +1556,12 @@ void CharacterService::ApplyLeveledNpcPick(Actor* apActor, const GameId& acPickI
if (!pBase)
return;

if (!pBase->IsTemporary())
{
// Conforming a shell is exactly what resolution would have done; any
// other static base is an already conformed actor.
if (!LeveledNpcSystem::IsUnresolvedLeveledShell(pBase))
if (!LeveledNpcSystem::GetOriginalBase(apActor))
{
spdlog::info("Leveled pick {:x}:{:x} received for actor {:X} whose base is not a leveled temp, skipping", acPickId.ModId, acPickId.BaseId, apActor->formID);
spdlog::warn("Leveled pick {:x}:{:x} received for actor {:X} without an original leveled base, keeping local base", acPickId.ModId, acPickId.BaseId, apActor->formID);
return;
}

spdlog::debug("Actor {:X} still carries unresolved shell base {:X}, conforming to owner's pick", apActor->formID, pBase->formID);
}

const uint32_t cPickId = World::Get().GetModSystem().GetGameId(acPickId);
if (cPickId == 0)
{
Expand All @@ -1586,7 +1579,8 @@ void CharacterService::ApplyLeveledNpcPick(Actor* apActor, const GameId& acPickI
const TESNPC* pLocalPick = apActor->GetLeveledPick();
const uint32_t localPickId = pLocalPick ? pLocalPick->formID : 0;

if (localPickId == cPickId)
// Even a pick matching the current base must supersede pending work.
if (pBase->IsTemporary() && localPickId == cPickId && m_pendingLeveledConforms.find(apActor->formID) == m_pendingLeveledConforms.end())
{
spdlog::info("Leveled actor {:X} already matches owner's pick {:X}", apActor->formID, cPickId);
return;
Expand Down Expand Up @@ -1648,9 +1642,9 @@ void CharacterService::ProcessLeveledConforms() noexcept
continue;
}

if (pActor->baseForm == pPick)
if (pActor->baseForm && pActor->baseForm->IsTemporary() && pActor->GetLeveledPick() == pPick)
{
spdlog::info("Completed leveled NPC reconciliation for actor {:X}, base: {:X}", it->first, cPickFormId);
spdlog::info("Completed leveled NPC reconciliation for actor {:X}, base: {:X}, pick: {:X}", it->first, pActor->baseForm->formID, cPickFormId);
stage = ReconciliationStage::None;
it = m_pendingLeveledConforms.erase(it);
continue;
Expand All @@ -1670,21 +1664,28 @@ void CharacterService::ProcessLeveledConforms() noexcept
continue;
}

// Disable and 3D teardown have completed; rebuild from the pick.
pActor->baseForm = pPick;
if (!LeveledNpcSystem::ApplyPick(pActor, pPick))
{
spdlog::warn("Could not rebuild leveled actor {:X} from its original base and pick {:X}, keeping local base", it->first, cPickFormId);
pActor->EnableImpl();
stage = ReconciliationStage::None;
it = m_pendingLeveledConforms.erase(it);
continue;
}

// Recompute the graph descriptor after changing picks; stale variable indices can cause out-of-bounds writes.
pActor->GetExtension()->GraphDescriptorHash = 0;

// Enable can return before the rebuilt 3D is available to discovery.
stage = ReconciliationStage::WaitingFor3D;
spdlog::info("Re-enabled conformed leveled actor {:X}, base: {:X}, waiting for 3D", it->first, cPickFormId);
pActor->EnableImpl();
spdlog::info("Re-enabled conformed leveled actor {:X}, base: {:X}, pick: {:X}, waiting for 3D",
it->first, pActor->baseForm->formID, cPickFormId);
++it;
continue;
}

if (!pActor->loadedState && !LeveledNpcSystem::IsUnresolvedLeveledShell(Cast<TESNPC>(pActor->baseForm)))
if (!pActor->loadedState && !LeveledNpcSystem::IsLeveledNpcBase(Cast<TESNPC>(pActor->baseForm)))
{
// Wait for distant actors to load 3D; newer picks replace pending work and disconnects clear it.
// Unresolved shells bypass this wait because they need a pick before they can load a model.
Expand Down
38 changes: 36 additions & 2 deletions Code/client/Services/Generic/ObjectService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include <Forms/TESObjectCELL.h>
#include <Forms/TESWorldSpace.h>
#include <Forms/BGSEncounterZone.h>
#include <Forms/TESFaction.h>
#include <Games/TES.h>

#include <inttypes.h>

Expand Down Expand Up @@ -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<const TESObjectREFR*> GetPlayerStashContainers() noexcept
{
Set<const TESObjectREFR*> 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<const TESObjectREFR*>& 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
Expand Down Expand Up @@ -117,9 +149,11 @@ void ObjectService::OnCellChange(const CellChangeEvent& acEvent) noexcept

AssignObjectsRequest request{};

const Set<const TESObjectREFR*> playerStashContainers = GetPlayerStashContainers();

for (TESObjectREFR* pObject : objects)
{
if (!ShouldSyncObject(pObject))
if (!ShouldSyncObject(pObject, playerStashContainers))
{
spdlog::warn("Excluding sync for {:X}", pObject->formID);
continue;
Expand Down
Loading