Add segment coverage ledger and mid-project plan for Crusader decompilation

- Created `crusader_segment_coverage_ledger.csv` to track segment coverage status, types, and known functions.
- Introduced `plan-mid.md` as a mid-project tracker outlining progress, objectives, and implementation priorities for the decompilation effort.
- Added scripts in `pyghidra_plans` to assist with instruction window dumping and reference inspection for the object at `0x4588`.
- Implemented functionality to scan for instruction uses of specific targets related to the decompilation project.
This commit is contained in:
MaddoScientisto 2026-03-21 16:19:46 +01:00
commit 519af09912
42 changed files with 2444 additions and 3 deletions

View file

@ -1757,7 +1757,7 @@ Current structural read of the cache globals:
- Current verified behavior for `runtime_cache_reset_sequence`:
- Calls `0008:7bfe`.
- Calls `game_mode_init(*(0x27c4))`.
- Executes one still-unresolved follow-up callsite at `0004:25a4`.
- Calls an import-resolved follow-up site at `0004:25a4`, now annotated as `NE IMPORT -> ASYLUM.24`.
- Then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers, and resumes the wider runtime reset flow.
- Known caller so far: `0004:262d` inside the tiny wrapper at `0004:2620`, which sets byte `+0x40` on the object at `0x6828` before invoking the reset sequence.
- `0004:eb1f` had also been truncated. It has now been repaired to the full body `0004:eb1f-eb9b` and renamed `entity_dispatch_entry_ctor_0f3a_with_cache_reset`.
@ -1765,10 +1765,104 @@ Current structural read of the cache globals:
- Allocates/initializes an entity dispatch entry when needed.
- Stamps entry type `0x0f3a`.
- Stores its two word payload fields from the incoming args.
- Runs local setup through `0004:ebf4`.
- When `tracked_entity_bucket_system_enabled` is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through `0009:b1c3`.
- Runs local setup through the embedded helper at `0004:ebf4`, which dispatches `entity_dispatch_reset_all(*0x7e22, 0x00f0)` and, when the local flag plus global `0x0ee1` allow it, allocates a type `0x0f5e` dispatch entry and passes it to `entity_pair_sync_b`.
- When `tracked_entity_bucket_system_enabled` is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through `0009:b1c3` in phase `0`.
- The sibling at `0004:eb9c` remains separate and valid; it builds the same `0x0f3a` entry type without the extra cache-reset tail, so the repaired `0004:eb1f` boundary stops cleanly at `0004:eb9b`.
### Follow-up: new local helper classification around the repaired seg004 path
- `0004:ea00` is now a real function object named `entity_dispatch_entry_alloc_type_0f5e` with body `0004:ea00-0004:ea46`.
- Verified behavior for `entity_dispatch_entry_alloc_type_0f5e`:
- Reuses the incoming FAR pointer when non-null; otherwise allocates `0x33` bytes through `mem_alloc_far`.
- Initializes the entry through `entity_dispatch_entry_init`.
- Stamps the entry type word at `+0x00` to `0x0f5e` before returning it.
- `0009:b1c3` remains conservatively unnamed, but it is now annotated as a phase-selected finalize helper:
- Both known call sites pass only phase bytes `0` or `1`.
- It forwards that byte twice to the object rooted at `0x4588` through vtable slot `+0x08`.
- It then sweeps the table rooted at `0x8724` up to count `0x879c`, calling `FUN_0009_a961` on each entry.
- That evidence is strong enough for comments, but not yet enough to promote `0009:b1c3` or `0x4588` to a more specific subsystem name.
### Follow-up: seg082 allocator cluster (`0009:a229`, `0009:af87`, `0009:b06b`, `0009:b1c3`)
- `0009:a229` is now verified as the public size-only wrapper around the seg082 allocator path.
- Caller evidence:
- `saveslot_table_clear` requests `0x2800` bytes through `0009:a229`, stores the returned FAR pointer at `0x2ba3/0x2ba5`, then zeroes the result in `0x400`-byte chunks.
- The wrapper lazily initializes the allocator on first use through `0009:bcb9`, then calls `0009:b06b(size, default_tag, 0xff)`.
- `0009:bcb9` is now annotated as the one-time lazy initializer for this path.
- It parses an optional `-x` tuning value from the PSP command line, clamps the derived percentage into `0x14..0x50`, then seeds local seg082 helpers before setting init flag `0x4096 = 1`.
- Table structure around `0x8724` is tighter now:
- `0x8724` is an array of `0x0c`-byte allocator heads.
- `0x879c` is the active head count / table limit.
- The per-node size/value encoding used under each head is manipulated through `0009:c628` and `0009:c6ae`, which read/write a packed 32-bit quantity split across `word + byte + byte` fields.
- `0009:af87` is now annotated as the free-space probe for the same cluster.
- It walks the node chain rooted at `0x8724`.
- For each node, it accumulates `node_size - 9` into a running total and tracks the largest single free block.
- Known callers include `cache_init` and the seg013 path at `0004:833b`, both of which use it to size subsequent allocation work.
- `0009:b06b` has been traced further as the internal sweep allocator for this cluster.
- It validates the requested size, reserves a temporary work token through `0009:e15f`, and scans the `0x8724` table in `0x0c`-byte entries via the local helper at `0009:a336`.
- On a successful fit, it commits the result through `0009:e2b4`, clears failure flag `0x4098`, and returns the allocated FAR pointer.
- When a pass does not find a fit, it interleaves up to two finalize phases through `0009:b1c3(phase)` before the final retry, then releases the work token through `0009:e1f6`.
- The formerly missing callee at `0009:a336` has now been recovered in-place as `allocator_head_try_alloc_block` with body `0009:a336-0009:a5d0`.
- It is the per-head first-fit allocator helper used by `0009:b06b` while sweeping the `0x8724` head table.
- It normalizes the requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds the local `0x0a` node header overhead, and enforces a minimum allocation size of `0x10` bytes.
- It walks the node chain for one allocator head until it finds a free span large enough.
- On success it unlinks the chosen free node, either consumes it whole or splits off a remainder node when at least `0x10` bytes remain, stores the owner/tag word, and returns `payload_ptr + 0x0a`.
- On failure for that head it returns `0`, which matches the calling pattern in `0009:b06b`.
- Boundary follow-up from the same read-only scan:
- The adjacent missing body at `0009:a5d1` has now also been recovered in-place as `allocator_head_free_block` with body `0009:a5d1-0009:a960`.
- It is the per-head free helper paired with `allocator_head_try_alloc_block`.
- It rebuilds the node header from a payload pointer (`payload - 0x0a`), validates the owner/tag word against the expected caller-supplied tag, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible.
- Its earlier branch targets `0009:a7a1` and `0009:a7b8` are now confirmed to be internal labels, not separate function entries.
- `0009:a961` is now better constrained as the per-head finalize sweep used by `0009:b1c3`.
- It walks one `0x8724` head's node chain, skips odd-tagged spans, coalesces or rewrites eligible spans, and updates head/back-pointer links when deferred space needs to be merged back into the chain.
- This strengthens the current interpretation that `0009:b1c3` is a phase-selected allocator-finalize pass rather than a cache-specific public API.
- `0009:b224` is now named `allocator_free_block_by_ptr`.
- Current verified behavior: converts the payload pointer back through the local header helpers, scans the `0x8724` head table for the owning range, dispatches to `allocator_head_free_block`, and aborts if no owning head is found.
- Known wrappers `0009:a24f` and `0009:a27a` are now clearly small checked entry points into this free-by-pointer path.
- With both recovered bodies in place, the seg082 cluster now has a verified alloc/free pair at the per-head level:
- `allocator_head_try_alloc_block` (`0009:a336`)
- `allocator_head_free_block` (`0009:a5d1`)
- `allocator_free_block_by_ptr` (`0009:b224`)
- `0009:b1c3` now has a corrected single-byte phase parameter in Ghidra; the object at `0x4588` is still left unnamed because its subsystem role is not yet strong enough.
- This narrows the remaining ambiguity around `0009:b1c3`: the unresolved part is now the role of the object at `0x4588`, not the local allocator mechanics around `0x8724`. `ASYLUM.24` is still not identified from the current evidence.
### Follow-up: `0x4588` object-role evidence (Priority 1 start)
- A direct instruction scan found real uses of `0x4588` / `0x458a` even though normal static reference APIs were not materializing them.
- Current verified behavior from those uses:
- `entity_conditional_render_dispatch` (`0009:9216`) calls through the runtime-installed object at `0x4588` via vtable slot `+0x0c` when the entity flags allow the alternate path and `param_2 == 0`.
- `000a:4a56` is a one-shot teardown/reset path for the same object: it checks a local once-flag at `0x4595`, clears `0x4588` when non-null, optionally performs a vtable `+0x0c` callback when `0x4590 != 0x458c`, then calls vtable slot `+0x04` followed by `FUN_0009_0d30()`.
- A read-only data probe of `0x4588` in the current database returned all zero bytes, so the object pointer is null-initialized statically and likely installed later at runtime.
- Conservative conclusion:
- The `0x4588` object now looks like a runtime-installed callback / dispatch object that participates in conditional render or presentation-side flow and has an explicit teardown path.
- That is enough for comments and ledger progress, but still not enough to safely rename `0009:b1c3` or the global itself to a concrete subsystem name.
### Follow-up: `0x4588` install/clear windows from the no-function hit list
- A read-only PyGhidra instruction-window pass against an unlocked project clone confirmed that the planned no-function hit list is real code, not aligned data.
- New verified lifecycle evidence:
- `000a:4932` and `000a:4936` store the same incoming dword into `0x4590` and `0x458c`, then `000a:493e` stores the incoming FAR object pointer into `0x4588`.
- `0004:5b8c` and `0004:5bbf` both clear `0x4588` immediately before the fatal/reporting-style seg091 call through `000a:454d`.
- `0004:5ea7` and `0004:6430` both clear `0x4588` and then immediately run the one-shot teardown path `000a:4a56(1)`.
- `000a:b9e5`, `000a:ba66`, `000d:9d5e`, and `000d:a3b7` all push a two-word value pair followed by the `0x4588` FAR pointer and call the object's vtable slot `+0x0c`.
- `entity_conditional_render_dispatch` remains the only named caller found so far for the same slot, but it passes a single literal `0x0101` argument instead of a two-word pair.
- Conservative conclusion after the window pass:
- `0x4588` is definitely a nullable runtime-installed FAR object with explicit install, clear, callback, and teardown transitions.
- The unresolved part is now its concrete subsystem identity, not whether the object lifecycle is real.
- The best next cheap win is no longer broad instruction searching; it is caller-side recovery around the still-unbounded `000a:b9e5` / `000a:ba66` and `000d:9d5e` / `000d:a3b7` windows.
### Follow-up: `ASYLUM.24` vs nearby `ASYLUM` ordinals
- `ASYLUM.24` remains unresolved by name, but its call pattern is now narrower.
- In `runtime_cache_reset_sequence` (`0004:2592`), it is a parameterless import call placed after `game_mode_init(*(0x27c4))` and before `cache_reset_runtime_state` plus the tracked-handle/cache-side reset tail.
- That makes it look like a module-level reset/init hook rather than a per-object method.
- Nearby `ASYLUM` ordinals in the seg011 caller at `0004:6f15` show a different pattern:
- `ASYLUM.36` returns an object-like handle that is used immediately through indirect vtable calls.
- `ASYLUM.37` is then called with explicit arguments against that object flow.
- Current conservative conclusion:
- `ASYLUM.24` is probably from the same external module family, but it does not currently match the object-construction / object-method calling pattern observed for `ASYLUM.36` and `ASYLUM.37`.
- Keep the import unresolved by name until another caller or string anchor narrows the exact module role.
### Repair note: overlapping bad function body
- Recovery of `cache_init` required a conservative boundary repair: a stray function object `FUN_000a_eee3` had incorrectly claimed body range `000a:6710-000a:fe79`, blocking creation of the real `cache_init` body.