6.3 KiB
NPC Action Process Family Layout
Purpose
This note captures the current safest class-lift state for the bounded seg033 NPC AI process family in live CRUSADER.EXE.
The goal is not to force a complete inheritance or process-layout model yet. The current goal is narrower:
- preserve the now-verified class ownership in Ghidra
- record which methods are strong enough to live under class owners today
- keep the remaining uncertainty explicit so later vtable and datatype work can resume without rediscovery
Current Live Class Owners
The following class owners now exist live in CRUSADER.EXE under the Remorse namespace:
Remorse::NPCActionProcessRemorse::StandProcessRemorse::PaceProcessRemorse::SurrenderProcessRemorse::GuardProcessRemorse::LoiterProcess
This is an owner-first lift only. No full instance struct, base-process overlay, or vtable datatype has been forced yet.
Landed Method Batch
Base family
| Live address | Current name | Why it is safe |
|---|---|---|
1100:0000 |
Remorse::NPCActionProcess::Create |
Shared family create entry already named and bounded in the local process lane. |
1100:1084 |
Remorse::NPCActionProcess::RunNoop |
Base run body is an intentional no-op and does not need wider semantics yet. |
1100:1089 |
Remorse::NPCActionProcess::Destroy |
Shared slot-1 destroy body is grounded by live g_*ProcessFnPtr ownership across the NPC action family. |
1100:0fe3 |
Remorse::NPCActionProcess::VtableSlot10Noop |
Shared slot-10 no-op body is structurally bounded even though final slot semantics remain open. |
Derived families
| Live address | Current name | Why it is safe |
|---|---|---|
1100:02ed |
Remorse::StandProcess::CreateProcess |
Direct family create wrapper in the same bounded seg033 lane. |
1100:0359 |
Remorse::StandProcess::Run |
Direct owned run body for the stand policy. |
1100:1036 |
Remorse::StandProcess::Destroy |
Slot-1 destroy entry grounded by live process-function-table ownership. |
1100:0383 |
Remorse::SurrenderProcess::CreateProcess |
Direct family create wrapper in the same bounded seg033 lane. |
1100:04ab |
Remorse::SurrenderProcess::Run |
Direct owned run body for surrender behavior. |
1100:0437 |
Remorse::SurrenderProcess::Destroy |
Clear family destroy path; resets the surrender vtable root, clears the local NPC flag bit, and destroys two embedded dispatch-entry children. |
1100:0693 |
Remorse::PaceProcess::CreateProcess |
Direct family create wrapper in the same bounded seg033 lane. |
1100:0708 |
Remorse::PaceProcess::Run |
Direct owned run body for the pace policy. |
1100:0fe8 |
Remorse::PaceProcess::Destroy |
Slot-1 destroy entry grounded by live process-function-table ownership. |
1100:0984 |
Remorse::GuardProcess::CreateProcess |
Direct family create wrapper in the same bounded seg033 lane. |
1100:0a0e |
Remorse::GuardProcess::Run |
Direct owned run body for the guard policy. |
1100:0f95 |
Remorse::GuardProcess::Destroy |
Slot-1 destroy entry grounded by live process-function-table ownership. |
1100:0afb |
Remorse::LoiterProcess::CreateProcess |
Direct family create wrapper in the same bounded seg033 lane. |
1100:0bfa |
Remorse::LoiterProcess::Run |
Direct owned run body for the loiter policy. |
1100:0d3e |
Remorse::LoiterProcess::VtableSlot10DispatchByShapeIfAlive |
Loiter-only slot-10 override is behaviorally distinct enough to separate from the shared base slot-10 no-op. |
1100:0f47 |
Remorse::LoiterProcess::Destroy |
Slot-1 destroy entry grounded by live process-function-table ownership. |
Shared Helper That Still Stays Free
1100:0913 remains NPC_DoRandomIdleAnimTwiceIfNotBusy instead of being forced under one class owner.
Current evidence supports a helper role more strongly than a specific class-method role:
- the live caller map still shows only
Remorse::GuardProcess::RunandRemorse::LoiterProcess::Run - it gates on
NPC_IsBusy - it chooses between two idle-animation paths and dispatches
NPC_DoAnimtwice
That makes it strong enough to name and comment, but still weak for ownership under exactly one class.
Ghidra Documentation Landed In-Session
The live database now also carries short decompiler comments on the main ownership and evidence anchors:
NPCActionProcess::CreateNPCActionProcess::DestroySurrenderProcess::CreateProcessSurrenderProcess::DestroyGuardProcess::CreateProcessGuardProcess::RunLoiterProcess::CreateProcessLoiterProcess::RunLoiterProcess::VtableSlot10DispatchByShapeIfAliveNPC_DoRandomIdleAnimTwiceIfNotBusy
Those comments preserve the current rationale without pretending that the datatype and inheritance story is already closed.
Current Safest Structural Read
The family now reads as:
- one bounded
NPCActionProcessbase with a shared create entry, shared destroy entry, and at least one shared no-op virtual slot - multiple policy-specific derived families (
Stand,Pace,Surrender,Guard,Loiter) - one shared guard/loiter-side idle helper that still belongs outside the current class owners
That is enough for owner-first navigation in Ghidra, but not yet enough for a full ABI-clean C++ inheritance model.
Things Still Open
The main remaining gaps are now narrower than basic object identity:
- what slot
10and the adjacent slot11mean semantically across the broader process family - whether the current
NPCActionProcess::Createshould later split into a thinner base initializer plus a more explicit allocator/factory wrapper - whether the shared slot-1 destroy body can support a first provisional base-process overlay without destabilizing other process families
- whether
StandProcessandPaceProcesshave any equally strong family-local helper methods in the same seg033 window that should move next
Recommended Next Pass
The next pass on this family should stay conservative:
- recover the process-function-table roots for the seg033 family explicitly enough to document slot order
- inspect local caller/callee structure around
StandProcess::RunandPaceProcess::Runfor family-local helpers comparable to the guard/loiter idle helper - only then decide whether a provisional
/Remorse/NPCActionProcessdatatype is safe, or whether the family should remain owner-only for now