22 KiB
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:
-uis 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/06d8to the mutable string at1478:07a0, which iseusecode.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_GetFullPathunderstands - whether relative, absolute, and CD-backed forms are all accepted equally
What is now materially tighter from the live Filespec_GetFullPath and File_Exists implementations:
- the path is interpreted with DOS file APIs, not Windows host-path APIs
- the builder uses the literal separator string
"\\", not"/" - the parser-side
s_usecodebuffer is only0x1ebytes long, so the raw-utoken is truncated to at most29visible characters plus the terminator
That makes the safest current syntax guidance:
- prefer DOS-style backslashes, not forward slashes
- prefer short path tokens
- assume the path must exist inside the game's DOS-visible filesystem view
If the game is running under DOSBox or a similar DOS environment, C:\MADDOCODE refers to the guest DOS C: drive, not automatically to the Windows host C: drive.
One more practical constraint is now strong enough to call out explicitly:
- the
-uexistence probe uses an old DOS find-first style path, so8.3-safe directory names are the safest override roots
For the current GOG install used in this investigation, Windows reports the short alias:
MADDOCODE->MADDOC~1
So the highest-probability next token for this specific setup is:
-u MADDOC~1
or the explicit relative form:
-u .\MADDOC~1
Why -u FLICTEST.FLX Fails
The live helper shape is now tight enough to explain the failed FLICTEST attempts directly.
What retail CRUSADER.EXE does at 1420:0cf0..0d33 is:
- read the copied argv token from
1478:065a - load the fixed filename template through
1478:06d6/06d8 -> 1478:07a0 - use that template as
eusecode.flx - call
Filespec_GetFullPath(0, <copied token>, "eusecode.flx", 0)
The raw data bytes at 1478:079a confirm the fixed template directly:
65 75 73 65 63 6f 64 65 2e 66 6c 78 00- ASCII =
eusecode.flx\0
So these example invocations do not mean load this exact archive file:
-u USECODE/FLICTEST.FLX-u FLICTEST.FLX
They instead mean, in the current safest static interpretation:
- use
USECODE/FLICTEST.FLXas thepathcomponent and still append the fixed filenameeusecode.flx - or use
FLICTEST.FLXas thepathcomponent and still append the fixed filenameeusecode.flx
That makes both attempts wrong for a direct single-file override. The game is not being told to open your FLICTEST.FLX archive as the final filename at all.
Safest Current Experiment Shape
The current safest static-model experiment is therefore:
- create a directory that will act as the override root
- place a structurally valid replacement archive in that directory
- name that archive
EUSECODE.FLX - pass the directory path to
-u, not the archive filename
Examples that now fit the recovered helper better are:
-u USECODE-u .\\USECODE-u \USECODE-u C:\USECODE-u MOD_UC
with the important condition that the target directory must contain EUSECODE.FLX, not FLICTEST.FLX.
The safest current path interpretations for those examples are:
-u MADDOCODE-> triesMADDOCODE\EUSECODE.FLXrelative to the game's current DOS working directory-u .\MADDOCODE-> tries.\MADDOCODE\EUSECODE.FLX-u \MADDOCODE-> tries\MADDOCODE\EUSECODE.FLXon the current DOS drive-u C:\MADDOCODE-> triesC:\MADDOCODE\EUSECODE.FLXon DOS driveC:
The current safest negative guidance is:
- do not use forward slashes like
USECODE/FLICTEST.FLX - do not pass the archive filename itself as the
-utoken - do not assume a long absolute host path will survive the
0x1e-byte parser buffer intact - do not assume a directory name longer than classic
8.3DOS spelling will be accepted exactly as typed
Expected Visible Output
There is currently no evidence that retail -u prints a success or failure banner before the game loads.
What the live code shows:
- the
-uparser case at1048:0a46only copies the token into1478:065a - unlike
-debug,-warp,-skill,-mapoff,-egg, and-demo, that parser case does not callConsolePrintf startup_apply_u_override_if_presentalso does not callConsolePrintf- if
File_Existssays the constructed path is missing, the helper just returns and startup continues
So the safest current user-facing expectation is:
- no dedicated
-uconsole line on success - no dedicated
-uconsole line on file-not-found failure - if the override path is wrong, the game most likely falls back silently to stock usecode
That means no message appeared before the game loaded is currently normal and does not prove the override worked.
Strongest Current Diagnosis For The MADDOCODE Test Case
The current install-side evidence makes this case much tighter than a generic -u experiment.
What is now verified in the GOG install:
MADDOCODEcontainsEUSECODE.FLX,OVERLOAD.DAT,UNKCOFF.DAT, andUNKDS.DATOVERLOAD.DAT,UNKCOFF.DAT, andUNKDS.DATinMADDOCODEare byte-identical to the working copies inUSECODE- only
EUSECODE.FLXdiffers between the stockUSECODEfolder andMADDOCODE
That makes the A/B interpretation unusually clean.
If:
- replacing
USECODE\EUSECODE.FLXwith the hacked file produces the expected obvious gameplay breakage - but launching with
-u MADDOCODEproduces stock gameplay instead
the strongest current read is not the alternate root loaded but behaved differently. The stronger current read is:
- the alternate root was probably never selected
- startup most likely fell back silently to stock usecode after the override path probe failed
Given the recovered code and the current install layout, the leading practical failure mode is now:
- the DOS-side file probe did not accept
MADDOCODEexactly as typed - the short alias
MADDOC~1is the next highest-probability working token
This is still a static-analysis conclusion rather than a runtime-tested acceptance matrix, so the exact relative-path and alias forms remain open. But the filename side is no longer the main uncertainty: the recovered helper is strongly telling us directory/root override for EUSECODE.FLX, not arbitrary filename override.
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_ItemCallEventat1420:0e3aUsecodeProcess_CreateProcessat1420:0f20Interpreter_NextUsecodeOpat1418:332cItem_GetDamagedat10a0: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:065ais empty, it returns without changing anything - if
1478:065ais non-empty, it resolves/loads an alternate source and then writes the resulting far pointer to1478: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:0d49writesDX -> 1478:66131420:0d4dwritesAX -> 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:8c7c1478:8c7e1478:8c801478: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
-usource 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:
- default startup path establishes the ordinary runtime state
startup_apply_u_override_if_presentruns once during startup- if
-uwas supplied, it replaces the root pointer and rebuilds the slot bases - 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_Everythingcallsstartup_apply_u_override_if_presentat1048:05d31478:6611/6613starts as zero in the loaded image- the only currently recovered explicit write to
1478:6611/6613in the live NE session is the-uoverride 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:
entity_vm_runtime_init_from_path_if_configured(000d:44df) builds the external path and creates the runtimeentity_vm_runtime_create(000d:4c99) creates the runtime objectentity_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/0x8c82from the four counts at0x6608..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/6613before gameplay without-uis 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
0x1319bytes when no existing object is supplied - clears the first
0x1300bytes through1420:1536 - stores several tail defaults in the
0x1300..0x1314range - calls
entity_vm_runtime_owner_resource_createwith the resolved path - stores the returned far pointer at
+0x1315/+0x1317
The zeroed first 0x1300 bytes are especially suggestive:
1420:1575later walks that area as0x80entries of stride0x26- for each entry it frees two far-pointer pairs at offsets
+0x1e/+0x20and+0x22/+0x24
So the safest current shape is:
- runtime object header/tail metadata at
0x1300..0x1318 - leading table of
0x80slot/runtime records occupying0x0000..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.flxpath 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:
-uis 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:
uselookhitgotHitequipunequipscheduleanim- 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
-usource 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:
-ugives 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 classonly one event bodypatch 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
That also means a standalone archive like FLICTEST.FLX is currently a poor first experiment even if its internals are valid. The live retail loader path is much more likely to accept a complete replacement EUSECODE.FLX rooted under a directory passed to -u.
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
- Which exact Filespec forms does
<arg>accept in practice: relative directory, absolute directory, resource alias, CD-rooted path, or more than one? - Which live-NE startup or game-start function establishes the stock
1478:6611/6613root when-uis not used? - What exact source/container format does the
-uloader expect on disk or in resources beyond the filename familyeusecode.flx, and how does that map onto the0x14-byte owner-resource helper plus the0x1319-byte runtime object? - Does the replacement need to be a full archive, or can the loader tolerate a narrower-but-still-valid subset?
- Are there any startup-visible sanity checks that reject malformed but syntactically valid usecode containers?
- What is the smallest safe experimental replacement that can prove the runtime path end-to-end without destabilizing unrelated startup systems?
Recommended Next Steps
- Close the remaining Filespec semantics for
s_usecode: confirm which directory/alias formsFilespec_GetFullPathaccepts in this call shape. - Identify the live-NE stock-runtime initialization site that seeds
1478:6611/6613when-uis absent. - Continue walking
entity_vm_runtime_create/entity_vm_runtime_owner_resource_createso more of the0x26-stride slot records and the owner/helper object fields are named concretely. - Compare the resolved runtime root against known EUSECODE/USECODE container structure to decide whether
-uexpects a full FLEX-style archive or another packaging layer. - Design the smallest conservative replacement experiment, ideally one structurally complete archive with one intentionally changed low-risk class/event body.
- Tie that future experiment back to the current parser/export pipeline so reconstructed bodies can eventually be runtime-tested through
-u.