325 lines
No EOL
16 KiB
Markdown
325 lines
No EOL
16 KiB
Markdown
# Retail `-u` USECODE Startup Override
|
|
|
|
## Question
|
|
|
|
If retail non-Japanese `CRUSADER.EXE` is started with `-u <arg>`, does that add extra usecode on top of the stock runtime, or does it replace the stock usecode root? And if it really is a replacement path, what parts of the game does that replacement actually control?
|
|
|
|
Active analysis target for the binary side was live `CRUSADER.EXE` in the Ghidra session.
|
|
|
|
## Short Answer
|
|
|
|
Current best answer:
|
|
|
|
- `-u` is a real retail startup override, not dead parser residue.
|
|
- In retail `CRUSADER.EXE`, it behaves like a **replacement** of the live usecode runtime root, not an additive overlay.
|
|
- The replacement root is then used by the normal usecode event/process/interpreter machinery.
|
|
|
|
So the practical meaning is:
|
|
|
|
> `-u <arg>` points startup at an alternate usecode/EUSECODE source, and subsequent scripted behavior runs against that replacement runtime instead of the stock one.
|
|
|
|
The strongest remaining open questions are now the precise Filespec path rules for `<arg>` and the still-unclosed live-NE stock bootstrap path that seeds the same runtime when `-u` is absent.
|
|
|
|
## What `<arg>` Currently Looks Like
|
|
|
|
The token-shape question is no longer completely open.
|
|
|
|
Current best retail read from the live `1420:0cdf` helper is:
|
|
|
|
- the parser still copies the raw argv token into `1478:065a`
|
|
- the helper does **not** pass that token straight to the loader as the final filename
|
|
- instead it loads a far pointer from `1478:06d6/06d8` to the mutable string at `1478:07a0`, which is `eusecode.flx`
|
|
- it forces the first byte of that filename template to `'e'`
|
|
- it then calls `Filespec_GetFullPath(0, s_usecode, "eusecode.flx", 0)`
|
|
- it probes that constructed full path with `File_Exists`
|
|
- if the path exists, it rebuilds the same full path again and passes that result into the runtime loader/constructor path
|
|
|
|
That makes the safest current interpretation:
|
|
|
|
- `<arg>` is a Filespec **path-like** argument that supplies the directory/resource-root component
|
|
- the filename component is still the fixed retail archive name `eusecode.flx`
|
|
|
|
So the current evidence argues **against** these stronger interpretations:
|
|
|
|
- arbitrary full filename override
|
|
- free-form alternate archive basename
|
|
- direct one-off script/body filename
|
|
|
|
If the user passes a literal filename as `<arg>`, the recovered helper shape suggests the game would still try to append the fixed filename template rather than use the supplied token as the final filename.
|
|
|
|
The remaining uncertainty is narrower now:
|
|
|
|
- whether the path component can be only a filesystem directory
|
|
- whether it can also be a loader alias/resource root that `Filespec_GetFullPath` understands
|
|
- whether relative, absolute, and CD-backed forms are all accepted equally
|
|
|
|
External engine code reinforces the fixed-filename part of this read. Both Pentagram and ScummVM build the default Crusader main usecode path as `usecode/` + language letter + `usecode.flx`, and for Remorse/Regret the language-usecode letter is `'e'`, which matches the retail helper's forced `eusecode.flx` template.
|
|
|
|
## Why This Looks Like Replacement, Not Addition
|
|
|
|
### 1. There is one live runtime root, not a list of roots
|
|
|
|
The key global pair is:
|
|
|
|
- `1478:6611/6613`
|
|
|
|
That far pointer is read directly by all of the main recovered usecode-side consumers, including:
|
|
|
|
- `Usecode_ItemCallEvent` at `1420:0e3a`
|
|
- `UsecodeProcess_CreateProcess` at `1420:0f20`
|
|
- `Interpreter_NextUsecodeOp` at `1418:332c`
|
|
- `Item_GetDamaged` at `10a0:277b`
|
|
|
|
No parallel second root pointer, linked list, or merge walk was recovered in this pass.
|
|
|
|
That by itself already makes additive-overlay behavior unlikely.
|
|
|
|
### 2. The `-u` startup helper overwrites that single root pointer
|
|
|
|
The retail parser case at `1048:0a46` copies the following argv token into `1478:065a`.
|
|
|
|
Startup later calls `startup_apply_u_override_if_present` at `1420:0cdf` from `1048:05d3`.
|
|
|
|
Inside `startup_apply_u_override_if_present`:
|
|
|
|
- if `1478:065a` is empty, it returns without changing anything
|
|
- if `1478:065a` is non-empty, it resolves/loads an alternate source and then writes the resulting far pointer to `1478:6611/6613`
|
|
- it sets `1478:6615 = 1`
|
|
- it rebuilds the cumulative slot-base words at `1478:8c7c..8c82`
|
|
|
|
The important behavior is the direct assignment:
|
|
|
|
- `1420:0d49` writes `DX -> 1478:6613`
|
|
- `1420:0d4d` writes `AX -> 1478:6611`
|
|
|
|
That is a swap of the live root pointer, not registration of a side table.
|
|
|
|
### 3. The helper rebuilds the slot-base state for the new root
|
|
|
|
After the root overwrite, `startup_apply_u_override_if_present` immediately recomputes the cumulative slot-base words:
|
|
|
|
- `1478:8c7c`
|
|
- `1478:8c7e`
|
|
- `1478:8c80`
|
|
- `1478:8c82`
|
|
|
|
Those are the same runtime bases later used by the slot-index/category mapping and owner-row lookups.
|
|
|
|
That makes the replacement behavior even stronger:
|
|
|
|
- the code is not merely keeping an alternate archive pointer around
|
|
- it is re-deriving the runtime indexing state that downstream usecode dispatch relies on
|
|
|
|
### 4. No merge step was recovered
|
|
|
|
This pass did **not** recover any logic that:
|
|
|
|
- appends records from the `-u` source onto the stock root
|
|
- patches individual classes/events into an existing live root
|
|
- or checks two roots in order during normal dispatch
|
|
|
|
Instead, the recovered shape is:
|
|
|
|
1. default startup path establishes the ordinary runtime state
|
|
2. `startup_apply_u_override_if_present` runs once during startup
|
|
3. if `-u` was supplied, it replaces the root pointer and rebuilds the slot bases
|
|
4. later gameplay/usecode consumers all read the same replacement root
|
|
|
|
The stock runtime may still remain allocated in memory if it was created earlier, but the current evidence says it is no longer the one normal dispatch uses. That is replacement semantics, not additive semantics.
|
|
|
|
## Best Current Read Of The Stock Path
|
|
|
|
The default non-`-u` bootstrap is only partially closed in the live NE database.
|
|
|
|
What is directly closed in live `CRUSADER.EXE`:
|
|
|
|
- `Init_Everything` calls `startup_apply_u_override_if_present` at `1048:05d3`
|
|
- `1478:6611/6613` starts as zero in the loaded image
|
|
- the only currently recovered **explicit** write to `1478:6611/6613` in the live NE session is the `-u` override helper itself
|
|
|
|
That means the stock bootstrap still has an unresolved step in the live NE target.
|
|
|
|
The best current cross-reference evidence for that missing default path comes from the verified raw-side VM note in [docs/raw-000a-000d.md](docs/raw-000a-000d.md):
|
|
|
|
- `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) builds the external path and creates the runtime
|
|
- `entity_vm_runtime_create` (`000d:4c99`) creates the runtime object
|
|
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) allocates and fills the owner/resource table used later by slot/context creation
|
|
- that same raw-side init path also seeds the cumulative category bases at `0x8c7c/0x8c7e/0x8c80/0x8c82` from the four counts at `0x6608..0x660e`
|
|
|
|
That raw-side bootstrap is important because it matches the same state that the live retail `-u` helper rebuilds after replacement:
|
|
|
|
- same four category counts at `1478:6608..660e`
|
|
- same cumulative bases at `1478:8c7c..8c82`
|
|
- same downstream owner-row and slot-index consumers already recovered in the live NE session
|
|
|
|
So the safest current combined read is:
|
|
|
|
- the normal game also has a stock runtime bootstrap in the same VM family
|
|
- the raw-side path is strongly evidenced as that bootstrap
|
|
- but the exact live-NE caller that seeds `1478:6611/6613` before gameplay without `-u` is still not directly closed
|
|
|
|
That distinction matters for documentation quality. We can now say the stock path is strongly cross-referenced, but we should not yet claim a fully recovered live-NE writer when the current explicit xrefs do not show it.
|
|
|
|
## Replacement Root Layout So Far
|
|
|
|
The direct constructor/loader pair behind the retail `-u` path is now materially clearer in the live NE session.
|
|
|
|
### 1. `entity_vm_runtime_create` at `1420:1499`
|
|
|
|
This function is the direct constructor called by `startup_apply_u_override_if_present` after the alternate `eusecode.flx` path is resolved.
|
|
|
|
Current best read:
|
|
|
|
- allocates `0x1319` bytes when no existing object is supplied
|
|
- clears the first `0x1300` bytes through `1420:1536`
|
|
- stores several tail defaults in the `0x1300..0x1314` range
|
|
- calls `entity_vm_runtime_owner_resource_create` with the resolved path
|
|
- stores the returned far pointer at `+0x1315/+0x1317`
|
|
|
|
The zeroed first `0x1300` bytes are especially suggestive:
|
|
|
|
- `1420:1575` later walks that area as `0x80` entries of stride `0x26`
|
|
- for each entry it frees two far-pointer pairs at offsets `+0x1e/+0x20` and `+0x22/+0x24`
|
|
|
|
So the safest current shape is:
|
|
|
|
- runtime object header/tail metadata at `0x1300..0x1318`
|
|
- leading table of `0x80` slot/runtime records occupying `0x0000..0x12ff`
|
|
|
|
### 2. `entity_vm_runtime_owner_resource_create` at `1430:0000`
|
|
|
|
This helper is now named in the live NE session because its behavior matches the already-verified raw-side owner/resource loader closely.
|
|
|
|
Current best read:
|
|
|
|
- allocates a compact `0x14`-byte helper object
|
|
- initializes it as a file-backed loader object
|
|
- opens the resolved external `eusecode.flx` path through its vtable-backed file path
|
|
- queries an entry count through vtable `+0x04`
|
|
- allocates a backing buffer and stores its far pointer at `+0x10/+0x12`
|
|
- materializes indexed owner/resource records through vtable `+0x0c`
|
|
|
|
That aligns well with the raw-side note that the owner/resource loader manages indexed file-set materialization rather than directly exposing simple class-name lookups.
|
|
|
|
### 3. Why This Matters For `-u`
|
|
|
|
This constructor/loader pair gives the override note a more concrete partial object model:
|
|
|
|
- `-u` is not just swapping in a raw file handle or archive pointer
|
|
- it is constructing a full VM runtime object
|
|
- that object owns both a slot/runtime table region and an attached owner/resource helper
|
|
- the attached helper is what materializes the indexed owner-loaded data later consumed by event/process/interpreter paths
|
|
|
|
That makes the replacement semantics stronger in practice. The override is swapping the live VM runtime object graph, not merely redirecting one filename global.
|
|
|
|
## What The Replacement Root Actually Controls
|
|
|
|
### 1. Item/event dispatch
|
|
|
|
`Usecode_ItemCallEvent` at `1420:0e3a` reads `1478:6611`, follows the live runtime tables, checks the owner-row capability bits, and creates the event/process context from that data.
|
|
|
|
That means the `-u` override reaches normal item-event lanes such as the surviving Crusader event families already documented elsewhere:
|
|
|
|
- `use`
|
|
- `look`
|
|
- `hit`
|
|
- `gotHit`
|
|
- `equip`
|
|
- `unequip`
|
|
- `schedule`
|
|
- `anim`
|
|
- and the wider event-slot family behind class-specific behavior
|
|
|
|
### 2. Usecode process creation
|
|
|
|
`UsecodeProcess_CreateProcess` at `1420:0f20` also reads `1478:6611` and builds process/context state from the owner tables hanging off the live root.
|
|
|
|
That means the override is not limited to one-shot event callbacks. It reaches process-backed scripted behavior too.
|
|
|
|
### 3. Interpreter bytecode stepping
|
|
|
|
`Interpreter_NextUsecodeOp` at `1418:332c` pushes `1478:6611/6613` into its runtime helper path.
|
|
|
|
That is the strongest scope clue in the whole pass. The replacement is not just changing metadata or an event name table. It is feeding the same runtime used when the interpreter advances usecode bytecode.
|
|
|
|
### 4. Gameplay-side scripted capability checks
|
|
|
|
`Item_GetDamaged` at `10a0:277b` also reads the same live root and tests owner-row bit `0x40` before falling through to later behavior.
|
|
|
|
So the replacement root can affect gameplay-side scripted capability checks as well, not only the hidden/debugger-facing VM side.
|
|
|
|
## Practical Meaning For Custom USECODE
|
|
|
|
If we can construct a compatible replacement archive/source for the `-u` path, the current evidence says it should be able to influence the same kinds of scripted behavior the stock USECODE runtime already controls.
|
|
|
|
Current safest statement:
|
|
|
|
- a custom `-u` source should be able to replace class/event bodies that the retail interpreter and process/event creators consume
|
|
- that makes it relevant to experimentation with event handlers, scripted item behavior, process-backed behaviors, and other VM-driven logic already present in Crusader's usecode system
|
|
|
|
Current important limit:
|
|
|
|
- this does **not** imply arbitrary native-code injection
|
|
- the replacement still has to fit the existing Crusader usecode VM/runtime contracts
|
|
- it is constrained by the interpreter, intrinsic tables, class layout, event table layout, and slot/body decoding the retail game already knows how to execute
|
|
|
|
So the safe model is:
|
|
|
|
> `-u` gives us a path to run **custom data for the existing usecode VM**, not a generic plug-in system for arbitrary executable code.
|
|
|
|
## What A Replacement Probably Needs To Contain
|
|
|
|
Because the recovered runtime uses a single root plus one rebuilt slot-base family, the safest current expectation is that the `-u` target is a **complete replacement source** that the loader can resolve into a full owner/class runtime.
|
|
|
|
This pass did **not** recover evidence for a sparse overlay format such as:
|
|
|
|
- `only one class`
|
|
- `only one event body`
|
|
- `patch this one slot onto the stock archive`
|
|
|
|
That does not prove such a sparse mode is impossible, but it means we should not assume it.
|
|
|
|
For current tooling planning, the defensive assumption should be:
|
|
|
|
- prepare a structurally complete replacement source that satisfies the loader's normal expectations
|
|
- do not assume the game will merge one partial class file into the stock runtime for us
|
|
|
|
## Best Current Usefulness For Decompilation Work
|
|
|
|
This path is interesting for the current usecode-decompilation lane for two reasons.
|
|
|
|
### 1. It is a real in-engine execution route
|
|
|
|
If we can identify the accepted `-u` source format and feed it a compatible replacement archive, we get a retail-supported way to make the original game execute alternate usecode data.
|
|
|
|
That is far more valuable than a purely offline parser/export loop.
|
|
|
|
### 2. It can become a runtime validation path for reconstructed bodies
|
|
|
|
The current parser/export work is building toward editable and eventually round-trippable usecode.
|
|
|
|
If `-u` accepts a full replacement source, then a future recompiler/exporter could use it to answer questions like:
|
|
|
|
- does a reconstructed class/event body still execute in the retail engine?
|
|
- which opcodes/intrinsic signatures are mandatory for a valid replacement archive?
|
|
- which behavior changes are script-level and which still depend on native code?
|
|
|
|
That makes `-u` potentially useful as a **runtime validation hook** for the ongoing decompilation toolchain.
|
|
|
|
## Current Open Questions
|
|
|
|
1. Which exact Filespec forms does `<arg>` accept in practice: relative directory, absolute directory, resource alias, CD-rooted path, or more than one?
|
|
2. Which live-NE startup or game-start function establishes the stock `1478:6611/6613` root when `-u` is not used?
|
|
3. What exact source/container format does the `-u` loader expect on disk or in resources beyond the filename family `eusecode.flx`, and how does that map onto the `0x14`-byte owner-resource helper plus the `0x1319`-byte runtime object?
|
|
4. Does the replacement need to be a full archive, or can the loader tolerate a narrower-but-still-valid subset?
|
|
5. Are there any startup-visible sanity checks that reject malformed but syntactically valid usecode containers?
|
|
6. What is the smallest safe experimental replacement that can prove the runtime path end-to-end without destabilizing unrelated startup systems?
|
|
|
|
## Recommended Next Steps
|
|
|
|
1. Close the remaining Filespec semantics for `s_usecode`: confirm which directory/alias forms `Filespec_GetFullPath` accepts in this call shape.
|
|
2. Identify the live-NE stock-runtime initialization site that seeds `1478:6611/6613` when `-u` is absent.
|
|
3. Continue walking `entity_vm_runtime_create` / `entity_vm_runtime_owner_resource_create` so more of the `0x26`-stride slot records and the owner/helper object fields are named concretely.
|
|
4. Compare the resolved runtime root against known EUSECODE/USECODE container structure to decide whether `-u` expects a full FLEX-style archive or another packaging layer.
|
|
5. Design the smallest conservative replacement experiment, ideally one structurally complete archive with one intentionally changed low-risk class/event body.
|
|
6. Tie that future experiment back to the current parser/export pipeline so reconstructed bodies can eventually be runtime-tested through `-u`. |