From eea47d884ee7ef8d29d913cfccf711ff3d05b2bd Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Tue, 31 Mar 2026 00:35:13 +0200 Subject: [PATCH] Furthered knowledge --- .../idata/01/~00000015.db/change.data.gbf | Bin 98304 -> 98304 bytes .../idata/01/~00000015.db/change.map.gbf | Bin 32768 -> 32768 bytes .../01/~00000015.db/{db.34.gbf => db.36.gbf} | Bin 24461312 -> 24461312 bytes .../00/~00000008.db/{db.25.gbf => db.26.gbf} | Bin 81920 -> 81920 bytes .../00/~00000008.db/{db.24.gbf => db.27.gbf} | Bin 81920 -> 81920 bytes .../00/~0000000a.db/{db.5.gbf => db.6.gbf} | Bin 81920 -> 81920 bytes .../00/~0000000a.db/{db.4.gbf => db.7.gbf} | Bin 81920 -> 81920 bytes _tmp_scene_link_scan.ps1 | 69 ++ docs/map_renderer/egg-identification.md | 626 ++++++++++++++++++ plan-mid.md | 4 + 10 files changed, 699 insertions(+) rename Crusader.rep/idata/01/~00000015.db/{db.34.gbf => db.36.gbf} (99%) rename Crusader.rep/user/00/~00000008.db/{db.25.gbf => db.26.gbf} (99%) rename Crusader.rep/user/00/~00000008.db/{db.24.gbf => db.27.gbf} (99%) rename Crusader.rep/user/00/~0000000a.db/{db.5.gbf => db.6.gbf} (99%) rename Crusader.rep/user/00/~0000000a.db/{db.4.gbf => db.7.gbf} (99%) create mode 100644 _tmp_scene_link_scan.ps1 diff --git a/Crusader.rep/idata/01/~00000015.db/change.data.gbf b/Crusader.rep/idata/01/~00000015.db/change.data.gbf index 06682a8dcb76788ab4bb6cf00c3f58eb5f4c4b1c..5ed736d04a7448b9dddc17bc39b4bc600fe73b88 100644 GIT binary patch delta 46 zcmV+}0MY+|fChko1`sbWF)TDGEGi=EB=V8GYHqO*0YC=vT}bbcq_H97KansAvjl+r E|9El|g#Z8m delta 46 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 diff --git a/Crusader.rep/user/00/~00000008.db/db.25.gbf b/Crusader.rep/user/00/~00000008.db/db.26.gbf similarity index 99% rename from Crusader.rep/user/00/~00000008.db/db.25.gbf rename to Crusader.rep/user/00/~00000008.db/db.26.gbf index f1a68fdc9f542d9db5e1ac9eb8daab935b185a1f..088375898631def5110624e9b03f3849e6712604 100644 GIT binary patch delta 30 lcmZo@U~On%70@>@)G^W2(Q>;aM33T*%Y delta 30 lcmZo@U~On%70@>@)G^W2(R!{NZjc!9JG)U}s{rE%djN`c3BUjV diff --git a/Crusader.rep/user/00/~00000008.db/db.24.gbf b/Crusader.rep/user/00/~00000008.db/db.27.gbf similarity index 99% rename from Crusader.rep/user/00/~00000008.db/db.24.gbf rename to Crusader.rep/user/00/~00000008.db/db.27.gbf index f8adc2ae04239e4e404d062f9ae08b325f702bb2..b8bb43987496bc200b260ebd1d2d0c043e92ac9c 100644 GIT binary patch delta 286 zcmZo@U~On%70@>@)G^W2(F#1gQtp8k$LB_YsRE2VOtv8#6@6`{XWKA7;A3E55cP2k z4v7y4@^=rK?qkO&&BW9-J<*oYiNny)!pZ;$rVH3IN=!d($Eb)RWHMdBj!|d&cN<25 z=_&S%BI0hPdC4W2`FTO9C8b4qA(aKGZkee$Dbwri8G~8eLj9blciS;4pqL@S9poAk z8srxr5*gq+{eumoIA>{IN@`kWUTO-&+DrC~a?=mkGYWAAd%F8M0u@8lS;5sQ#K$M6 vq$cMWnx!QcDY%3rCgr5Yd*)@9XlN=a=-csf+35Qu7MH~PSBoM#6BjkHvx delta 163 zcmZo@U~On%70@>@)G^W2(duGw@Na#p_^?r6ssJMolb!oUMPHlg**1(1rYG1kiZafh zUS`Ya#9@$XVP#}&ZZVzFj!|Oz4O>P<6rqIak+zJ|OpMdPs@YA@)G^W2(F!`?{fZ?!u(eTOs{rE%djN_737h}` delta 30 lcmZo@U~On%70@>@)G^W2(b9ETs{rE%djNu+2^s(Z diff --git a/Crusader.rep/user/00/~0000000a.db/db.4.gbf b/Crusader.rep/user/00/~0000000a.db/db.7.gbf similarity index 99% rename from Crusader.rep/user/00/~0000000a.db/db.4.gbf rename to Crusader.rep/user/00/~0000000a.db/db.7.gbf index d77b23b0c2c1ad968b60e82ee02ffd77ba8dcf1a..757cff06b8a1ac69a272299aeace14a70a4d8ac7 100644 GIT binary patch delta 145 zcmZo@U~On%70@>@)G^W2(Mo@H@O$^RsV5o*rV23fFxfF}R6Js{{gDl$D&zF~HjL6t zjCZF0v0-$Y?qJWTz~SiP5)>cg8ZdpLE#s!?6?TkjiUmda=|zdTdZi_qIeLyMDMhKp t#cr9YIVnE*$%!SI`FYbf+A>N^e{RQUJbjuyqnv<^zE5ItNjyy24gf_}GJ^mB delta 241 zcmZo@U~On%70@>@)G^W2(P~Z#xZND8xVTYZssJMoldbDU#UnP`AK5UfGEQf)XS9*= za14qM@DEl}2#E}EwN=W@D^XGibMy%XvdyM%v}Kf-Zg0=1u9;X+pqG-GoS$2enUh+i zmy@5ISdy8ar{|KImROoo;sTeNUT@DB%;y&B=N#hc?-%dt7ZMaXeWER+g`AJ4pKH8d zs4q6FjEo?5U9xACn|{EaQHU$V-`mwMKEyR51f*MBQb9{!NkQL^m&-=qC$YFB-UsRe FI{@{UNtplu diff --git a/_tmp_scene_link_scan.ps1 b/_tmp_scene_link_scan.ps1 new file mode 100644 index 0000000..8101186 --- /dev/null +++ b/_tmp_scene_link_scan.ps1 @@ -0,0 +1,69 @@ +Set-Location 'k:\ghidra\crusader_map_viewer\map_renderer' + +function Get-Shape([object]$item) { + if ($null -eq $item -or $null -eq $item.shapeDefId) { return $null } + if ($item.shapeDefId -match '^shape:(\d+)$') { return [int]$Matches[1] } + return $null +} + +function Get-Qlo([object]$item) { + if ($null -eq $item -or $null -eq $item.quality) { return $null } + return ([int]$item.quality) -band 0xff +} + +function Get-Distance([object]$a, [object]$b) { + $dx = [double]$a.world.x - [double]$b.world.x + $dy = [double]$a.world.y - [double]$b.world.y + return [math]::Sqrt(($dx * $dx) + ($dy * $dy)) +} + +$pairs = @( + @{ Name = 'BRO_BOOT->SPANEL'; Source = 0x04fe; Target = 0x03aa; Distance = 768 }, + @{ Name = 'NPC_ONLY->CMD_LINK'; Source = 0x0366; Target = 0x04b1; Distance = 768 }, + @{ Name = 'DEATHBOX->CMD_LINK'; Source = 0x04e7; Target = 0x04b1; Distance = 768 }, + @{ Name = 'NPC_ONLY->TRIGGERISH'; Source = 0x0366; Target = 0x0361; Distance = 768 } +) + +$sceneFiles = Get-ChildItem '.\site\data\maps' -Recurse -Filter 'scene.json' | Sort-Object FullName +$lines = New-Object System.Collections.Generic.List[string] + +foreach ($pair in $pairs) { + $matchedSources = 0 + $sourceCount = 0 + $linkCount = 0 + $examples = New-Object System.Collections.Generic.List[string] + + foreach ($file in $sceneFiles) { + $scene = Get-Content $file.FullName -Raw | ConvertFrom-Json + $items = @($scene.items) + $sources = @($items | Where-Object { (Get-Shape $_) -eq $pair.Source }) + if ($sources.Count -eq 0) { continue } + $targets = @($items | Where-Object { (Get-Shape $_) -eq $pair.Target }) + + foreach ($source in $sources) { + $sourceCount += 1 + $qlo = Get-Qlo $source + if ($null -eq $qlo) { continue } + $matches = @($targets | Where-Object { + (Get-Qlo $_) -eq $qlo -and (Get-Distance $source $_) -le $pair.Distance + }) + if ($matches.Count -gt 0) { + $matchedSources += 1 + $linkCount += $matches.Count + if ($examples.Count -lt 6) { + $examples.Add(('{0}/{1}: source={2} qlo={3} targetIds={4}' -f $file.Directory.Parent.Name, $file.Directory.Name, $source.id, $qlo, (($matches | ForEach-Object { $_.id }) -join ','))) + } + } + } + } + + $rate = if ($sourceCount -gt 0) { [math]::Round(($matchedSources / $sourceCount) * 100, 1) } else { 0 } + $lines.Add(('PAIR {0}' -f $pair.Name)) + $lines.Add(('sources={0} matched={1} rate={2}% links={3}' -f $sourceCount, $matchedSources, $rate, $linkCount)) + foreach ($example in $examples) { + $lines.Add($example) + } + $lines.Add('') +} + +$lines | Set-Content 'k:\ghidra\Crusader_Decomp\_tmp_scene_link_scan.txt' \ No newline at end of file diff --git a/docs/map_renderer/egg-identification.md b/docs/map_renderer/egg-identification.md index a8bfdb6..83870ac 100644 --- a/docs/map_renderer/egg-identification.md +++ b/docs/map_renderer/egg-identification.md @@ -128,6 +128,632 @@ The current renderer catalog data already contains egg-oriented notes that inclu Those catalog references are evidence for the workflow and UI, but the renderer should still treat them as conventions layered on top of the raw egg-family decoding above. +## Non-Egg Teleporter Carriers + +Not every authored teleporter in the exported maps is itself an egg-family item. + +- Both retail games use shape `0x01DB` (`TELEPORTER_LIGHTS`) as a non-egg teleporter helper even though the exported shape definition is terrain-like rather than family `8` egg content. +- Current renderer scene exports show both frame `1` and some frame `0` `0x01DB` placements carrying small low-byte `quality` values that line up with real teleport-destination eggs. +- Verified No Remorse examples include map `13`, where frame `1` `0x01DB` items carry `quality & 0xFF = 14` and the map also contains teleport-destination egg `14`, and map `3`, where one frame `1` `0x01DB` carries low-byte `16` and the map contains teleport-destination egg `16`. +- Verified No Regret examples now include map `3` frame-`1` matches (`quality 4 -> destination egg 4`, `quality 2 -> destination egg 2`, `quality 9 -> destination egg 9`) plus the remaining frame-`0` telepad helpers (`quality 27 -> destination egg 27`, `quality 28 -> destination egg 28`), map `5` (`quality 78 -> destination egg 78`), map `10` (`quality 39 -> destination egg 39`, `quality 49 -> destination egg 49`), map `11` (`quality 19/20/21 -> destination eggs 19/20/21`), and map `15` (`quality 22 -> destination egg 22`). + +That evidence is strong enough for link-arrow visualization, but it is intentionally narrower than the family `8` egg rule above: + +- the renderer now treats frame `0` and frame `1` `0x01DB` placements as teleporter link sources for arrow rendering when they carry an integer destination id in `quality` +- the link key is the low byte of `quality` +- the destination side still comes from real teleport-destination eggs +- this does not yet claim that every `0x01DB` placement or every non-egg teleporter helper in Crusader uses the same schema + +## TELEPAD Hardcoding + +The current best executable-side explanation for `0x01DB` behavior comes from the checked Crusader usecode disassembly corpus, not from the generic ScummVM egg model. + +- `Usecode class 475 (01DB TELEPAD)` is a real authored telepad class in `crusader_disasm.txt`. +- `TELEPAD::gotHit` reads the pad's `quality` into local `theQual` with `Item::I_getQuality`. +- `quality == 0xFF` is treated as inert and exits without a teleport. +- `quality` in the low range `1..0x1d` uses the actor's current map together with the quality value as the egg id, then spawns `TELEPAD::ordinal20` to perform the move. + +That means the simple viewer rule for `0x01DB` is not arbitrary UI inference. It matches a real quality-driven telepad dispatch lane. + +The same `TELEPAD::gotHit` body also contains explicit hardcoded remaps and special cases rather than one universal `quality -> same-map egg` rule: + +- `0x1e -> map 0x28, egg 0x1e` +- `0x1f -> map 0x45, egg 0x45` +- `0x20 -> map 0x04, egg 0x10` +- `0x21 -> map 0x03, egg 0x11` +- `0x23 -> map 0x29, egg 0x1e` +- `0x64 -> map 0x05, egg 0x64` +- `0x65 -> map 0x05, egg 0x65` +- `0x82 -> map 0x19, egg 0x82` +- `0x83 -> map 0x19, egg 0x83` +- `0x84 -> map 0x19, egg 0x82` +- `0x85 -> map 0x19, egg 0x83` +- `0xd7/0xda/0xdb/0xdc -> map 0x05` with matching egg ids + +`TELEPAD::ordinal20` then confirms the actual move mechanism: + +- the normal lane calls `MainActor::I_teleportToEgg(...)` +- same-map and cross-map forms both appear +- map `0x28` and map `0x29` have extra hardcoded presentation/cutscene handling around the jump process, including `R01`, `B01`, `wec`, `1d`, and `14a` movie names + +So the current safest renderer claim is: + +- frame `0` and frame `1` `0x01DB` placements are useful cross-game teleporter-link sources when low-byte `quality` matches a real destination egg +- but the full authored gameplay semantics of `TELEPAD` include a hardcoded quality switch with special map-specific behavior, not just generic destination-egg matching + +## Elevator Helpers + +The checked usecode corpus also closes one separate same-map elevator lane used by visible elevator car objects rather than by egg-family source triggers. + +- `Usecode class 542 (021E ELEVATOR)` reads `Item::I_getQLo` and `Item::I_getQHi` from the elevator object itself. +- For same-map moves, `ELEVATOR::gotHit` dispatches `ELEVATOR::ordinal20` with destination egg ids derived from the low quality byte. +- The verified same-map cases are `QLo 1..0x0f -> egg 1..0x0f` and the explicit special case `QLo 0x10 -> egg 4`. +- `QLo 0x11` and `QLo 0x12` are cross-map special cases in the checked body, so the viewer does not currently draw same-map arrows for those values. +- `ELEVATOR::ordinal20` later confirms that the move itself is still a `MainActor::I_teleportToEgg(...)` hop using the caller-supplied egg id and map. + +Scene evidence lines up with that lane in No Remorse map `1`: + +- `shape:542` quality `0x0101` sits near destination egg `2`, which matches a source platform that teleports to egg `1`. +- `shape:542` quality `0x0202` sits near destination egg `1`, which matches the return platform that teleports to egg `2`. +- the same authored pattern repeats for the other nearby elevator pairs, including `0x0105 -> egg 5` from the far endpoint and the local return platform `0x0206 -> egg 6` beside destination egg `5`. + +So the public renderer now treats same-map `shape:542` frame-`0` placements as elevator-link sources when their checked `QLo` values map to a local destination egg id. + +## Current Elevator Gap + +- No Regret map `3` destination egg `102` does not sit on the same currently verified `ELEVATOR` (`shape:542`) or `LIFT` (`shape:307`) lanes. +- Current scene exports for that map show no nearby `shape:542` or `shape:307` objects at all. +- A nearby editor/helper record `item:1056:fixed:1201:0:41758:33694:0` carries `mapNum 102`, but there is not yet enough executable-side evidence to promote that into a viewer arrow rule. +- The map renderer therefore leaves egg `102` unresolved for now instead of hardcoding a speculative elevator source pattern. + +## Adjacent Editor Objects: `0x04E3` And `0x04B1` + +These two shapes came up during egg-browser work because they sit near teleporter/elevator/trap authoring patterns, but the current evidence says neither one should be folded into the egg model. + +### `0x04E3` frame `0`: `SKILLBOX` + +The recovered Crusader usecode corpus has a direct class label here: + +- `Usecode class 1251 (04E3 SKILLBOX)` +- the recovered handler is `SKILLBOX::func0A(sint16)` + +That body is difficulty-sensitive rather than egg-like. + +- it reads `Game::I_getDifficultyLevel()` +- it branches on the object's `frame` +- for `frame == 2`, it reads `Item::I_getQLo(item)`, temporarily adjusts that low-quality byte by difficulty, dispatches `TRIGGER::ordinal20`, then restores the original `QLo` +- for the other frames, it chooses between trigger lane `0` and trigger lane `1` based on a difficulty threshold derived from `frame + 2` +- when `Item::I_getMapArray(item)` is nonzero, the same lane choice is made but with `0x80` added to the dispatched trigger selector + +That gives the current best read of the family: + +- `frame 0` = difficulty gate that flips at difficulty level `2` +- `frame 1` = difficulty gate that flips at difficulty level `3` +- `frame 2` = special skill-routing form that rewrites `QLo` per difficulty before dispatch + +For the specific target in this pass, `0x04E3` frame `0` is therefore best read as a hidden difficulty gate or skill-route selector, not as a teleporter egg, generic egg id carrier, or NPC spawner. + +Scene-cache evidence supports that split. + +- across cached maps, `0x04E3` appears mostly as `frame 2`, then `frame 1`, with relatively few `frame 0` records (`64` in cached Remorse scenes and `25` in cached Regret scenes) +- `frame 2` payloads cluster strongly by `QLo`, which matches the recovered `Item::I_getQLo(...)` access +- `frame 0` payloads are much more heterogeneous, which fits a gate/controller role better than a simple numeric spawn id +- many dominant `frame 2` records reuse the same `npcNum/mapNum` pairs such as `208/134`, while the low byte of `quality` still varies meaningfully; that makes the low byte look like the authored control value and the other bytes look more like secondary metadata + +What helps in the editor: + +- label `0x04E3` as `SKILLBOX` instead of generic `Editor Object` +- show `quality` split into `QLo` and `QHi` +- for `frame 0` and `frame 1`, show a small derived note describing the difficulty switch point +- for `frame 2`, show the resolved difficulty lanes explicitly: `diff1 -> QLo`, `diff2 -> QLo + 1`, `diff3+ -> QLo + 2` +- keep `npcNum` and `mapNum` visible, but treat them as raw helper metadata rather than as DTABLE or egg ids + +### `0x04B1` frame `0`: trigger/link controller + +`0x04B1` still does not have as clean a recovered class label as `SKILLBOX`, but the current usecode evidence already points in one direction. + +- an earlier trigger pass found `TRIGGER.slot_20` iterating nearby `shape=0x04B1` items +- that trigger lane compares `Item.getQLo(item)` against a base link id +- it then branches on `Item.getMapNum(item)` flag bits before dispatching additional trigger logic + +This pass also re-confirmed that `0x04B1` is grouped with controller/helper shapes rather than with actor payload objects. + +- recovered control code such as `ELEVATOR::gotHit` explicitly scans nearby authored helper shapes and includes `0x04B1` in that control-side whitelist +- that keeps `0x04B1` in the same ecosystem as trigger/elevator helper markers rather than DTABLE-driven spawn objects + +The currently recovered `cmd` text is not yet a stable class name. It appears as a local variable name in recovered usecode exports, which fits a command/controller interpretation, but that is still weaker evidence than the `SKILLBOX` label above. + +Scene-cache evidence strengthens the controller model. + +- cached Remorse scenes overwhelmingly use `0x04B1` as `frame 0` +- cached Regret scenes use `0x04B1` across a much wider frame spread (`0` through `10` in the current cache) +- for Remorse `frame 0`, the most common low-byte values are small channel-like numbers such as `1`, `2`, `3`, `5`, `6`, `10`, `20`, `30`, `31`, `40`, `50`, and `60` +- the full `quality` word often changes while the low byte stays on the same small channel number, which is exactly what the earlier `Item.getQLo(...)` trigger evidence would predict +- the frequently repeated `npcNum/mapNum` pairs like `208/134` do not line up with a useful NPC-row interpretation and should stay raw for now + +Current best read: + +- `0x04B1` frame `0` is a command/link helper used by trigger-style controller logic +- `quality & 0xFF` is the strongest current candidate for the authored link or command id +- `mapNum` is not a destination map; its low bits are routing flags and its high bits contribute to the decoded target shape +- `npcNum` is not a DTABLE row here; it is the low byte of the decoded target selector used by `TRIGGER.slot_21` + +The recovered TRIGGER body now gives a more concrete field split for `0x04B1` itself: + +- `QLo` = current link id matched against the upstream trigger chain +- `QHi` = subcommand selector in the low three bits, plus a small argument in the upper bits +- `mapNum & 0x03` = command mode +- `mapNum & 0x04` = route into the item-targeting TRIGGER lanes instead of the NPC-triggering side +- `mapNum & 0x08` = phase gate (`set -> phase 0 / 0x80`, `clear -> phase 1 / 0x81`) +- `mapNum & 0x10` = low-priority/deferred execution bucket +- `((mapNum & 0xE0) * 8) + npcNum` = decoded target search shape or sentinel target code + +The executable-side read is now concrete enough to say more than "it probably triggers something": several `QHi` subcommands are visibly specific. + +- subcommand `0` is a helper-dispatch lane: it walks nearby `0x0476` helpers with the same `QLo` and forwards the upper-bit argument into `FREE.slot_30` using the helper's packed payload fields +- subcommand `1` is a direct target-mutation lane whose exact effect depends on mode: the recovered paths can write `QHi`, write `QLo`, call `equip`, set `frame`, or run a timed `TRIGGER.slot_22 -> DOOR.slot_21` pulse on matched targets +- subcommand `2` is no longer just a placeholder in the editor readout: in the direct item-targeting body it resolves to a frame-set lane using the upper-bit argument as the frame value +- subcommands `4` and `5` are verified link rewrites, adding to or subtracting from the active `QLo` before the controller continues scanning +- subcommand `6` is a create-and-drop lane: it again resolves payload through nearby `0x0476` helpers, creates the target item, copies `Q`, moves it to the helper coordinates, and unequips/drops it + +The strongest new practical result is that some authored `0x04B1` records can now be resolved to concrete nearby target shapes instead of staying purely abstract. + +- Remorse map `10` contains `0x04B1` items with decoded target shape `0x04D0`, and several of those have nearby same-`QLo` `0x04D0` helper matches. +- The same map also contains `0x04B1` items resolving to decoded target shapes `0x0476` and `0x04E3`, each with local same-`QLo` matches. +- Example authored records include `mapNum=134, npcNum=208 -> target shape 0x04D0`, `mapNum=135, npcNum=118 -> target shape 0x0476`, and `mapNum=134, npcNum=227 -> target shape 0x04E3`. + +That still does not mean every `0x04B1` record is fully solved. Some target codes still decode to shapes that need more map-side correlation, and the NPC-triggering versus item-targeting paths do not use every subcommand in exactly the same way. But the object is now clearly more than a generic relay: it is a compact local trigger program that encodes phase, priority, target domain, target shape, link rewrites, and several concrete operation lanes in the standard item fields. + +What helps in the editor: + +- relabel `0x04B1` from generic `Editor Object` to something like `Trigger Link Controller` or `Cmd Link` +- show `QLo` and `QHi` separately, with `QLo` emphasized +- decode `mapNum` into phase lane, priority, targeting mode, and high-bit target-shape contribution instead of only showing it raw +- decode `npcNum` as the low byte of the target selector +- show the derived target shape or sentinel family code directly in the tooltip +- list nearby same-`QLo` exact-shape candidates when the decoded target shape can be resolved in the current map +- do not attach DTABLE/NPC preview logic to this family +- keep a same-`QLo` highlight/filter action, because that remains the strongest local linkage field even after the target-shape decode + +The practical egg-browser outcome is that both shapes are adjacent to egg workflows, but neither one should currently be decoded as an egg-family object. `0x04E3` looks like a difficulty/skill gate, and `0x04B1` looks like a trigger-link controller. + +## More Editor Helpers + +The next helper batch follows the same pattern: these are controller-side authored objects that sit near eggs, doors, hazards, or alarm setups, but they should stay in their own helper bucket instead of being collapsed into one generic editor-object schema. + +### `0x0361` frame `0`: `EVENT` + +This one now has a direct recovered class label too. + +- `Usecode class 865 (0361 EVENT)` +- recovered body: `EVENT::equip` + +`EVENT::equip` is a large event multiplexer rather than a single-purpose marker. + +- it immediately reads `link = Item.getQLo(arg_06)` +- it branches on the incoming `event` number +- several branches dispatch `TRIGGER.slot_20` +- other branches touch camera state, audio, NPC actions, doors, and nearby `NUMBERS` helpers + +Current best read: + +- `0x0361` is a generic scripted event controller/helper +- `QLo` is the strongest authored linkage field currently visible in the recovered body +- `QHi` is used by at least some counter-style branches, so it is worth surfacing too +- `frame 0` should be treated as one authored placement of that broader event family, not as a special egg/NPC record + +What helps in the editor: + +- override placeholder-style catalog names with `EVENT` +- emphasize `QLo` and `QHi` +- describe it as a generic event controller, not a visible prop + +### `0x0500` frame `0`: `STEAMBOX` + +This helper also has a direct class label. + +- `Usecode class 1280 (0500 STEAMBOX)` +- recovered body: `STEAMBOX::equip` + +The recovered handler has two visible controller lanes. + +- for `event == 0`, it scans nearby steam-family shapes and matches them by `Item.getQLo(...)` +- for `event == 1`, it scans nearby `shape=0x03A9` items, again matched by `QLo` +- matching items then dispatch into `STEAMBOX.slot_20` or `STEAMBOX.slot_21` + +Current best read: + +- `0x0500` is a steam hazard/control relay, not a generic placeholder cube +- `QLo` is the authored channel/link id +- `frame` matters on nearby controlled shapes more than on the controller itself + +What helps in the editor: + +- label it `STEAMBOX` +- show `QLo/QHi` with `QLo` emphasized as the steam channel +- keep the role text focused on nearby steam/hazard linkage + +### `0x0561` frame `0`: `ALARMHAT` + +This family was already partially documented in the dedicated alarm note, and the extracted body matches that earlier read. + +- `Usecode class 1377 (0561 ALARMHAT)` +- recovered body: `ALARMHAT::equip` + +The visible structure is local alarm-state control. + +- `frame 0` scans nearby `shape=0x04D0` helpers and targets their `frame 0` state +- nonzero frames gate on `Item.isOnScreen(arg_06)` and nearby actor-family presence before scanning the same `0x04D0` family + +Current best read: + +- `0x0561` is a local alarm-state driver +- it is meant to flip or arm nearby `0x04D0` controller/spawner objects +- `frame 0` is the simple always-check local alarm form; nonzero frames add extra activation conditions + +What helps in the editor: + +- label it `ALARMHAT` +- add a frame note calling out `frame 0` as the direct local alarm lane +- keep `npcNum`/`mapNum` raw instead of forcing a DTABLE interpretation onto this helper itself + +### `0x0581` frame `0`: `ALRMTRIG` + +This one is compact and comparatively clean. + +- `Usecode class 1409 (0581 ALRMTRIG)` +- recovered body: `ALRMTRIG::equip` + +The whole handler is an alert-state relay. + +- it checks `Item.getMapArray(arg_06)` +- it checks `World.getAlertActive()` +- it dispatches `TRIGGER.slot_20` with lane `0`, `1`, `0x80`, or `0x81` + +Current best read: + +- `0x0581` is an alert/alarm trigger relay +- the important authored split is `mapArray == 0` versus nonzero +- the second split is world alert state inactive versus active + +What helps in the editor: + +- label it `ALRMTRIG` +- show the `mapNum`/`mapArray` byte in hex because it behaves like a flag lane selector here +- describe the four resulting trigger lanes explicitly + +### `0x04F8` frame `0`: door death helper + +This shape still lacks a clean No Remorse class label in the recovered tables, but the door-side behavior is already concrete enough to expose in the editor. + +- `DOOR.slot_23` iterates nearby `shape=0x04F8` items after the door damage/crush path +- it matches them by `Item.getQLo(deathBox) == Item.getQLo(arg_06)` +- it dispatches `TRIGGER.slot_20` on lane `0` when `Item.getMapArray(deathBox) == 0` +- otherwise it dispatches the `0x80`-offset lane + +Current best read: + +- `0x04F8` is a door-side death or crush trigger helper +- `QLo` is the local door/link key +- the map-array byte chooses the normal trigger lane versus the `+0x80` variant + +That is strong enough to support an editor-facing role hint even though the final class/name provenance is still incomplete. + +### `shape_04e3` / `0123only` + +The workspace did not yield an exact `0123only` string hit in the current source, catalogs, or extracted usecode artifacts. + +Current safest read: + +- there is no evidence yet that `0123only` is a separate helper family from `0x04E3` +- the only strong recovered identity for `shape 0x04E3` is still `SKILLBOX` +- treat `0123only` as an external nickname or pending label until a real source hit turns up + +So the editor integration should continue to show `SKILLBOX` for `0x04E3` and avoid promoting `0123only` to a canonical name. + +### `0x0120`: `FASTSKIL` + +This shape is a second skill-family controller, but it is not the same object as `SKILLBOX`. + +- `Usecode class 288 (0120 FASTSKIL)` +- recovered handler: `FASTSKIL::enterFastArea()` + +The recovered body is short enough to read directly. + +- it waits `5` ticks before doing anything +- it only runs the main lane while `Item::I_getMapArray(item) == 0` +- for `frame == 2`, it reads the current `QLo` as a base skill/link id and dispatches `TRIGGER::ordinal20` lane `0` through three difficulty lanes: `diff1 -> QLo`, `diff2 -> QLo + 1`, `diff3+ -> QLo + 2` +- for the non-`frame 2` forms, it does not rewrite the link id; instead it compares difficulty against `frame + 2` and dispatches `TRIGGER::ordinal20` lane `0` below that threshold and lane `1` at or above it + +That makes the current best read: + +- `0x0120` is a fast-area skill gate/controller, not a generic editor cube +- `frame 0` flips at difficulty `2` (`diff1 -> lane 0`, `diff2+ -> lane 1`) +- `frame 1` flips at difficulty `3` (`diff1/2 -> lane 0`, `diff3+ -> lane 1`) +- `frame 2` is the explicit skill-routing form that remaps the downstream trigger `QLo` by `+0/+1/+2` + +For editor arrows, the strongest conservative rule is the same local controller rule already used for other trigger sources, but with the frame-`2` rewrite made explicit. + +- `FASTSKIL` can point to nearby `0x04B1` controller helpers when they live in the same local trigger cluster +- for `frame 0` and `frame 1`, the relevant local link key is the source item's current `QLo` +- for `frame 2`, the relevant local link keys are `QLo`, `QLo + 1`, and `QLo + 2`, matching the recovered difficulty lanes +- one concrete Remorse map `13` example is especially strong: a `FASTSKIL` placement and a `0x04B1` controller helper sit at the same coordinates `(35390, 20894)` with matching raw quality `283` / `QLo 27` + +This still needs to stay conservative. + +- broader scene sweeps do not justify treating every nearby `0x04B1` as a `FASTSKIL` target +- the renderer should therefore stay on the existing local-distance plus explicit `QLo` lane rule instead of inventing a wider object-class relationship +- the tooltip should explain the frame-specific trigger lane or `QLo` rewrite directly so the object reads as a trigger program rather than as another anonymous skill box + +## Switch And Helper Follow-Up + +This pass tightened several of the still-generic switch/helper shapes that sit next to egg workflows. + +### `0x00A1` frame `0`: `PANELNS` + +This one now has a direct recovered class label. + +- `Usecode class 161 (00A1 PANELNS)` +- recovered handler: `PANELNS::use()` + +The body is small and consistent with a plain directional wall panel switch. + +- if `frame == 0`, the handler returns immediately +- if `Item::I_getMapArray(item) != 0`, the handler also exits without firing +- otherwise it plays SFX `0xAC` and dispatches `TRIGGER::ordinal20` with lane `0` + +Current best read: + +- `0x00A1` frame `0` is the idle/inactive visual state of a panel switch family +- the important authored controller key is still downstream `QLo`, because the spawned `TRIGGER` path uses that link namespace rather than `mapNum` or `npcNum` +- sampled Remorse scene caches show nearby same-`QLo` `0x04B1` controller helpers often enough to support editor arrows as a local-assistance layer + +### `0x031D` frame `0`: `CARD_NS` + +This one also closes cleanly from the recovered usecode corpus. + +- `Usecode class 797 (031D CARD_NS)` +- recovered handler: `CARD_NS::use()` + +`CARD_NS::use()` is only a thin wrapper. It immediately spawns `SWITCH::ordinal21` on the source item. + +`SWITCH::ordinal21` is the more useful body for editor work. + +- it reads `Item::I_getQLo(item)` and passes that to `MainActor::I_hasKeycard(...)` +- if the switch is already in `frame 4`, it routes straight into `TRIGGER::ordinal20` lane `0` +- otherwise, with the right card and no alert-state block, it flips to `frame 4`, plays the authorization SFX lane, then dispatches `TRIGGER::ordinal20` lane `0` +- the denied path eventually dispatches `TRIGGER::ordinal20` lane `1` + +Current best read: + +- `0x031D` frame `0` is a locked north/south keycard switch +- `QLo` is the keycard id and the strongest local link field for controller arrows +- same-map same-`QLo` `0x04B1` matches appear in sampled Remorse scenes, so the renderer can expose them as editor-helper arrows without claiming that the switch talks directly to those helpers in one hardcoded object-only lane + +### `0x03AA` frame `0`: `SPANEL` + +This shape now has both a direct class label and a second helper-side link path. + +- `Usecode class 938 (03AA SPANEL)` +- recovered handler: `SPANEL::use()` + +The direct switch body is simple. + +- it exits when `mapArray != 0` +- otherwise it conditionally plays SFX `0xAF` for the early-map lane +- it then dispatches `TRIGGER::ordinal20` lane `0` + +That closes the same switch-style controller story as `PANELNS`, with `QLo` remaining the practical local link key through the downstream `TRIGGER` chain. + +`SPANEL` also appears on the target side of another helper family. + +- `BRO_BOOT::enterFastArea()` scans nearby `shape 0x03AA` items +- it compares them by shared `QLo` +- depending on the active mission/global branch, it applies `ITEM::ordinal23` or `ITEM::ordinal24` to those matching `SPANEL` items + +Current best read: + +- `0x03AA` frame `0` is a switch panel source in the trigger/controller ecosystem +- it is also a helper target for `BRO_BOOT` +- broader exported-scene sweeps now show repeated local same-`QLo` `BRO_BOOT -> SPANEL` matches on Remorse maps `9`, `10`, `11`, `21`, `23`, `160`, and `246`, so the renderer can now expose that helper lane conservatively too + +### `0x0366` frame `0`: `NPC_ONLY` + +The earlier placeholder label is now closed. + +- `Usecode class 870 (0366 NPC_ONLY)` +- recovered handler: `NPC_ONLY::gotHit(uword, word)` + +The body is a gated trigger pad rather than a generic editor helper. + +- it rejects the hit unless the incoming actor/reference resolves to an NPC-like source +- it reads `Actor::I_GetNPCDataField0x63_00B(actor)` +- it reads `Item::I_getQLo(item)` from the pad and requires those values to match +- with `mapArray == 0` and the occupancy checks satisfied, it flips to `frame 1`, waits briefly, and dispatches `TRIGGER::ordinal20` lane `0` +- once the actor is no longer satisfying the occupancy lane, it restores `frame 0` and dispatches `TRIGGER::ordinal20` lane `1` + +Current best read: + +- `0x0366` frame `0` is an idle NPC-only trigger pad +- `QLo` is the author-selected NPC-group key, but the executable compares it against actor field `0x63`, not against the scene-export `npcNum` or a simple DTABLE row +- current scene exports do not expose that actor-side field directly, so there is not yet enough evidence to draw trustworthy `NPC_ONLY -> actor` arrows in the renderer +- a follow-up scene-cache sweep also failed to produce a convincing generic `NPC_ONLY -> 0x04B1` helper pattern; shared-`QLo` local matches look incidental rather than like a dedicated authored link lane + +That means the safe editor improvement here is naming and field emphasis, not a target-arrow rule yet. + +### `0x0403` frame `0`: `FLAMEBOX` + +This shape now has a direct class label and a usable local arrow rule. + +- `Usecode class 1027 (0403 FLAMEBOX)` +- recovered handler: `FLAMEBOX::equip(sint16)` + +The recovered body has two nearby-helper lanes keyed by shared `QLo`. + +- event `0` scans nearby flame-family shapes `0x043A`, `0x043B`, `0x050A`, and `0x0518` +- event `1` scans nearby flame editor/helper shapes `0x0438` and `0x0439` +- both lanes compare helper items by `Item::I_getQLo(...) == Item::I_getQLo(flamebox)` +- the event-`1` lane can replace the helper marker with an animated flame actor and then spawns the flame process + +Scene-cache checks on Remorse map `1` line up with that lane strongly enough to expose in the editor. + +- `FLAMEBOX qlo 24` at `(61310,60830)` has nearby helper `shape 0x0438` at `(61886,60798)` +- `FLAMEBOX qlo 7` at `(60350,49406)` has nearby helper `shape 0x050A` at `(59938,49294)` +- `FLAMEBOX qlo 21` at `(59166,5214)` has nearby `shape 0x0439` matches at `(58814,5790)` and `(59166,5790)` + +Current best read: + +- `0x0403` frame `0` is a local flame controller +- `QLo` is the authored flame channel id +- editor-helper arrows from `FLAMEBOX` to nearby same-`QLo` flame helper shapes are evidence-backed and now justified + +### `0x04E7` frame `0`: `DEATHBOX` + +The existing `npc death` nickname was directionally right but incomplete. The recovered class name and caller path now narrow the object down. + +- `Usecode class 1255 (04E7 DEATHBOX)` +- recovered helper body: `DEATHBOX::func0A(sint16)` +- recovered caller path: `NPCDEATH::ordinal20` scans nearby `shape 0x04E7` items directly + +The strong executable-side lane is in `NPCDEATH::ordinal20`. + +- it iterates nearby `DEATHBOX` items +- it compares the incoming death-link value against `Item::I_getQLo(deathBox)` +- if `Item::I_getMapArray(deathBox) == 0`, it dispatches `TRIGGER::ordinal20` lane `0` from the helper item +- otherwise it reads `QHi` and `npcNum` from the helper, can create a keycard with frame `QHi - 1`, can override that keycard's `QLo` from helper `npcNum`, and then dispatches the `0x80` trigger lane + +That makes the current best editor-facing read: + +- `0x04E7` is an `NPCDEATH` helper/controller, not just a raw death marker +- `QLo` is the death-link match key +- `QHi` carries drop/helper mode information, including the verified keycard lane for values `1..4` +- `npcNum` can act as a secondary payload for the spawned keycard helper path + +Current limitation: + +- the static scene export does not yet expose the originating NPC death-link values directly enough to draw reliable `DEATHBOX -> source NPC` arrows +- some maps do show local same-`QLo` `0x04B1` helpers near `DEATHBOX`, but the recovered executable path is still clearest as `NPC death event -> DEATHBOX -> TRIGGER`, and a broader follow-up sweep did not justify promoting `DEATHBOX -> 0x04B1` into a generic arrow rule + +### `0x04FE` frame `0` / frame `1`: `BRO_BOOT` + +This shape is no longer mysterious, even though its local authored targets are not yet consistently visible in sampled scenes. + +- recovered class label: `BRO_BOOT` +- recovered handlers include `BRO_BOOT::enterFastArea()` and `BRO_BOOT::leaveFastArea()` + +`BRO_BOOT::enterFastArea()` does three important things. + +- it branches on a mission/global state lane (`global [001F 01]` in the recovered output) +- it scans nearby `shape 0x03AA` items and compares them by shared `QLo` +- it applies `ITEM::ordinal23` or `ITEM::ordinal24` to those matching `SPANEL` items, then runs its own boot-style frame animation loop + +The broader scene cache now supports that link well enough for a renderer overlay. + +- follow-up exported-scene sweeps found repeated local same-`QLo` `BRO_BOOT -> SPANEL` matches across Remorse maps `9`, `10`, `11`, `21`, `23`, `160`, and `246` +- one concrete Remorse map `10` example has `SPANEL` item `4538` at `(21018, 13406)` with a nearby `BRO_BOOT` at `(20968, 13784)`, both carrying `QLo 50` + +Current best read: + +- `0x04FE` is a scripted helper tied to nearby `SPANEL` items, not a free-form generic editor object +- frame `0` and frame `1` are boot-sequence animation states, not independent target ids +- `QLo` is the local authored linkage field for the helper-to-`SPANEL` lane + +Current renderer stance: + +- this is strong enough to relabel the family as `BRO_BOOT` +- the exported-scene sweep is now broad enough to justify a generic local `BRO_BOOT -> SPANEL` arrow layer based on shared `QLo` plus local distance + +### `0x0080` frame `0`: `BOX_EW` + +This one closes as another switch-family trigger source. + +- `Usecode class 128 (0080 BOX_EW)` +- recovered handler: `BOX_EW::use()` + +The recovered body is compact but useful. + +- if `mapArray != 0`, it exits without firing +- `frame 0` plays the switch SFX and dispatches `TRIGGER::ordinal20` lane `1` +- nonzero frames dispatch `TRIGGER::ordinal20` lane `0` + +The map-side correlation is strong enough to promote a conservative helper-arrow rule, but only for the idle switch state. + +- sampled exported scenes show repeated local same-`QLo` `BOX_EW frame 0 -> 0x04B1` matches, including co-located pairs on Remorse map `9` +- the same broader sweep did not justify treating nonzero `BOX_EW` frames as the same generic cmd-link source lane + +Current best read: + +- `0x0080` is the `BOX_EW` switch family +- `QLo` remains the practical authored controller key for nearby helper scans +- the renderer can safely expose `BOX_EW frame 0 -> nearby 0x04B1` arrows when local distance and `QLo` both match + +### `0x04CD` frame `0`: `TRIGPAD` + +This family now has a direct class label, but not a new helper-arrow rule. + +- `Usecode class 1229 (04CD TRIGPAD)` +- recovered handler: `TRIGPAD::gotHit(...)` + +The recovered body behaves like a heavier floor trigger. + +- it gates on occupancy and surface checks before the pad arms +- with `mapArray == 0`, it waits briefly, dispatches `TRIGGER::ordinal20` lane `0`, and later dispatches lane `1` as the condition clears +- the same body also loops across nearby elevator-family items and can call `ELEVAT` control slots in that path + +Current best read: + +- `0x04CD` is a real trigger-pad class, not a generic editor placeholder +- it belongs in the trigger/helper ecosystem, but its executable behavior is broader than a simple `QLo -> cmd-link` switch +- the wider exported-scene sweep did not justify promoting a generic local `TRIGPAD -> 0x04B1` arrow rule, so the safe editor improvement is labeling plus tooltip decoding only + +### `0x033A` frame `0`: `NUMBERS` + +This family still does not have a cleaner recovered usecode class label than the catalog-facing name, but the scene role is much less ambiguous now. + +- exported scenes show `0x033A` as tiny glyph-sized frames, typically `10x16` or `12x16` +- those items repeatedly cluster beside larger sibling shapes such as `0x0501`, `0x0502`, `0x0503`, `0x0505`, and `0x0507` +- the same map-side checks do not make it look like another trigger or cmd-link helper family + +Current best read: + +- `0x033A` is best treated as a number/readout display helper family +- the renderer should label it clearly so editor users can spot display clusters +- current evidence does not justify helper arrows from `NUMBERS` + +## Editor Helper Arrow Overlay + +The renderer now exposes a separate nested toggle under `Show editor-only elements` called `Show editor helper arrows`. + +That overlay is intentionally narrower than the existing verified teleport/elevator arrow layer. It only draws local helper relationships that have direct usecode support and an evidence-backed authored key: + +- `ALARMHAT (0x0561) -> nearby 0x04D0` helpers, using the recovered local alarm scan behavior +- `STEAMBOX (0x0500) -> nearby steam-family targets`, matched by shared `QLo` +- `0x04F8 -> nearby door-family targets`, matched by shared `QLo` and local placement +- `BRO_BOOT (0x04FE) -> nearby SPANEL targets`, matched by shared `QLo` +- `PANELNS (0x00A1)`, `CARD_NS (0x031D)`, and `SPANEL (0x03AA) -> nearby 0x04B1` controller helpers when they share the same local `QLo` +- `BOX_EW (0x0080) frame 0 -> nearby 0x04B1` controller helpers when they share the same local `QLo` +- `FLAMEBOX (0x0403) -> nearby flame helper shapes`, matched by shared `QLo` +- `EVENT (0x0361)` and `SKILLBOX (0x04E3) -> nearby 0x04B1` controller helpers when they share the same local `QLo` +- `FASTSKIL (0x0120) -> nearby 0x04B1` controller helpers on the same local trigger lanes, with `frame 2` also exposing the recovered `QLo + 1` and `QLo + 2` difficulty variants +- `ALRMTRIG (0x0581) -> nearby 0x04B1` controller helpers by the same lane byte, as a weaker relay-style visualization rather than a claimed final object identity +- `0x04B1 -> nearby exact decoded target shapes` when the TRIGGER field split resolves to a concrete target shape and local same-`QLo` candidates exist + +Two newly decoded families stay intentionally out of the arrow layer for now. + +- `TRIGPAD (0x04CD)` is now named and decoded in tooltips, but the broader scene sweep did not justify a generic `TRIGPAD -> 0x04B1` helper rule +- `NUMBERS (0x033A)` looks like a display/readout marker family rather than a trigger/controller source, so it stays label-only + +The important constraint is that these arrows are editor-assistance overlays, not canonical gameplay graphs. They are meant to expose likely local controller lanes without pretending that every helper object participates in one global id namespace. + +## Live Ghidra Notes + +The live `CRUSADER.EXE` Ghidra database now also has comment-backed notes at `1020:029e` and `1020:02d0` documenting the startup teleporter precedence that matters for map/egg overrides: + +- if the X override remains `-1`, startup stays on the teleporter/egg path +- even when X/Y/Z coordinates are present, a nonnegative `-egg` override still wins and routes through `Teleporter_CreateProcessDirect` + +That executable-side note is adjacent to the broader teleporter system rather than to `0x01DB` specifically, but it closes one important runtime rule for interpreting teleport-id driven startup behavior. + ## Outcome For The Feature The egg browser added to the renderer now: diff --git a/plan-mid.md b/plan-mid.md index d909b2b..39f995c 100644 --- a/plan-mid.md +++ b/plan-mid.md @@ -62,6 +62,10 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan - That same warp-table lane is now exact across both retail DOS executables too. Byte checks against `CRUSADER.EXE` and `REGRET.EXE` now show matching 17-word `-warp mission` base-map tables (`0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,40`) at `1478:0488` and `1480:075c`, each followed by a `0,0` terminator. The public map renderer now also has a dedicated mission-table extractor and generated JSON cache, so scene metadata no longer has to treat mission/base-map usage as an unknown ownership question. - That same startup lane is now tighter at the argument level too. Current best parser/control-flow read in `REGRET.EXE` is `-warp [x y z]`, with X/Y/Z carried as positional argv tokens after the mission number rather than as separate recovered switches. The corresponding runtime branch in `Game_RunNewGameFlow` is also clearer: nonnegative `-egg` overrides beat the coordinate path, while the real eggless-map workaround is `-warp ` plus `-mapoff` with `-egg` omitted so the game falls into direct `NPC_Teleport` instead of the teleporter-egg lookup. - The matching No Remorse cross-check is now closed too. Live `CRUSADER.EXE` `HandleCommandlineArgs` at `1048:0adc` uses the same positional `-warp [x y z]` parser shape, and `Game_Start` at `1020:029e` / `1020:02d0` uses the same runtime precedence: direct coordinates only win when the egg override is still negative, otherwise the code falls back to `Teleporter_CreateProcessDirect`. The parameter-only eggless-map workaround is therefore shared across both retail games, not Regret-specific. +- The public map-renderer link lane is tighter again. Cross-game `0x01DB` support now covers both the earlier frame-`1` teleporter-light helpers and the remaining Regret map `3` frame-`0` telepad helper placements that carry destination ids `27/28` in `quality`. The same pass also adds the checked same-map `ELEVATOR` lane: frame-`0` `shape:542` sources now link to local teleport-destination eggs by verified `QLo` rules (`1..0x0f -> same egg id`, `0x10 -> egg 4`). Current best gap is still Regret map `3` egg `102`, which does not sit on the verified `shape:542` / `shape:307` elevator lanes yet. +- The editor-helper overlay lane is tighter too. A broader exported-scene sweep now shows that `BRO_BOOT` (`0x04FE`) really does form a repeatable local helper lane into nearby same-`QLo` `SPANEL` items, with concrete Remorse examples on maps `9`, `10`, `11`, `21`, `23`, `160`, and `246`, so the renderer now promotes `BRO_BOOT -> SPANEL` alongside the existing cmd-link, alarm, steam, door, and flame helper arrows. The same follow-up kept two tempting false positives out of the overlay: `NPC_ONLY -> 0x04B1` and `DEATHBOX -> 0x04B1` still read better as incidental local overlap than as a dedicated helper-source relationship. The latest tooltip pass also upgrades `0x04B1` from a mostly structural decode to concrete operation notes: helper dispatch via nearby `0x0476`, direct target mutation, timed pulses through `TRIGGER.slot_22` / `DOOR.slot_21`, verified link rewrites, and a create-and-drop lane. +- The skill-controller lane is tighter too. Shape `0x0120` is now closed as `FASTSKIL`, distinct from `SKILLBOX`: `enterFastArea` waits briefly, only runs while map-array is clear, uses frame `0/1` as difficulty thresholds for `TRIGGER.slot_20` lane `0` versus `1`, and uses frame `2` as an explicit `QLo/+1/+2` difficulty router. The renderer now exposes that decode in tooltips and adds conservative local `FASTSKIL -> 0x04B1` helper arrows, with frame-`2` variants for the recovered `QLo + 1` and `QLo + 2` lanes. +- The switch/pad clarification lane is tighter too. Shape `0x0080` now closes as `BOX_EW`, and sampled exported scenes are strong enough to promote a conservative `BOX_EW frame 0 -> nearby same-QLo 0x04B1` helper arrow rule. Shape `0x04CD` now closes as `TRIGPAD`, but its broader occupancy/elevator behavior and the negative scene sweep keep it metadata-only instead of promoting a generic cmd-link overlay. Shape `0x033A` now reads best as a tiny `NUMBERS` readout/display helper family clustered with nearby `0x0501/0x0502/0x0503/0x0505/0x0507` pieces, so it also stays label-only. - The command-line lane is tighter around `-u` now too. In live non-Japanese `CRUSADER.EXE`, the parser case at `1048:0a46` copies the following token into `1478:065a`, and the renamed `startup_apply_u_override_if_present` at `1420:0cdf` later consumes that buffer to resolve/load an alternate usecode/EUSECODE source into `1478:6611/6613`, mark `1478:6615`, and rebuild the cumulative slot-base words at `1478:8c7c..8c82`. Current best read is `real retail startup usecode override`, not `JP-only` and not `dead string-table residue`; the paired consequence is that the older CRUSADER-side `-setver` attribution should now be treated as reopened until its exact retail consumer is isolated directly. - That same `-u` lane is now tighter at the runtime-scope level too. The follow-up note `docs/usecode-startup-override.md` now records that retail `-u` appears to replace the single live usecode root at `1478:6611/6613`, not add a sidecar table: `startup_apply_u_override_if_present` overwrites that root directly, rebuilds the cumulative slot-base words, and later consumers including `Usecode_ItemCallEvent`, `UsecodeProcess_CreateProcess`, `Interpreter_NextUsecodeOp`, and `Item_GetDamaged` all read the same replacement root. Current safest tooling implication is `runtime swap for the existing Crusader usecode VM`, which makes `-u` a potentially important future validation hook for round-tripped/custom usecode archives once the accepted source format is nailed down. - The same `-u` lane is tighter at the token-shape level now too. Live `1420:0cdf` does not use the copied argv token as an arbitrary final filename; it treats `1478:065a` as the `Filespec_GetFullPath` path component while loading the fixed mutable filename template `eusecode.flx` from `1478:07a0` through `1478:06d6/06d8` and forcing the first byte to `'e'` before both the existence probe and the final load call. Current safest read is therefore `path/root override for standard EUSECODE archive naming`, not `free-form filename override`. The stock bootstrap side is also better scoped: `1478:6611/6613` starts zero in the live NE image and the only currently recovered explicit writer there is the `-u` helper, so the normal non-`-u` seed remains only cross-referenced through the verified raw-side VM bootstrap note rather than fully live-NE-closed.