Crusader_Decomp/docs/npc-action-process-class-layout.md
2026-04-12 14:45:08 +02:00

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::NPCActionProcess
  • Remorse::StandProcess
  • Remorse::PaceProcess
  • Remorse::SurrenderProcess
  • Remorse::GuardProcess
  • Remorse::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::Run and Remorse::LoiterProcess::Run
  • it gates on NPC_IsBusy
  • it chooses between two idle-animation paths and dispatches NPC_DoAnim twice

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::Create
  • NPCActionProcess::Destroy
  • SurrenderProcess::CreateProcess
  • SurrenderProcess::Destroy
  • GuardProcess::CreateProcess
  • GuardProcess::Run
  • LoiterProcess::CreateProcess
  • LoiterProcess::Run
  • LoiterProcess::VtableSlot10DispatchByShapeIfAlive
  • NPC_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:

  1. one bounded NPCActionProcess base with a shared create entry, shared destroy entry, and at least one shared no-op virtual slot
  2. multiple policy-specific derived families (Stand, Pace, Surrender, Guard, Loiter)
  3. 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 10 and the adjacent slot 11 mean semantically across the broader process family
  • whether the current NPCActionProcess::Create should 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 StandProcess and PaceProcess have any equally strong family-local helper methods in the same seg033 window that should move next

The next pass on this family should stay conservative:

  1. recover the process-function-table roots for the seg033 family explicitly enough to document slot order
  2. inspect local caller/callee structure around StandProcess::Run and PaceProcess::Run for family-local helpers comparable to the guard/loiter idle helper
  3. only then decide whether a provisional /Remorse/NPCActionProcess datatype is safe, or whether the family should remain owner-only for now