Crusader_Decomp/docs/npc-action-process-class-layout.md

112 lines
6.3 KiB
Markdown
Raw Permalink Normal View History

2026-04-12 14:45:08 +02:00
# 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
## Recommended Next Pass
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