484 lines
16 KiB
Markdown
484 lines
16 KiB
Markdown
|
|
# COMBAT.DAT
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
|
||
|
|
This note documents the shipped `COMBAT.DAT` used by the Crusader builds in this workspace.
|
||
|
|
|
||
|
|
Verified corpus facts:
|
||
|
|
|
||
|
|
- `STATIC/COMBAT.DAT`, `STATIC_1.01/COMBAT.DAT`, `STATIC_DEMO/COMBAT.DAT`, `STATIC_JP/COMBAT.DAT`, `STATIC_REGRET/COMBAT.DAT`, and `STATIC_REGRET_DEMO/COMBAT.DAT` are byte-identical.
|
||
|
|
- All six files are `1734` bytes and share SHA-256 `c6097a721141a2e66ec6d0cd578427305f28ae9efe8a206bd5cf946ce2075faf`.
|
||
|
|
- ScummVM's Crusader support code and the live `CRUSADER.EXE` database agree that this file drives NPC ranged-combat tactics through a small bytecode interpreter.
|
||
|
|
|
||
|
|
This note uses three evidence sources together:
|
||
|
|
|
||
|
|
- direct binary parsing of the shipped `COMBAT.DAT`
|
||
|
|
- live `CRUSADER.EXE` function/type names already present in the workspace export map
|
||
|
|
- ScummVM Crusader-source behavior as a readable reference model for the tactic interpreter
|
||
|
|
|
||
|
|
## High-Level Findings
|
||
|
|
|
||
|
|
- The file is a small FLEX-style archive with `14` populated tactic records.
|
||
|
|
- Each tactic record has a zero-padded `16`-byte name, followed by four `uint16` block offsets, followed by bytecode.
|
||
|
|
- In the shipped data, only block `0` and block `1` matter. Block `0` acts like an entry/setup phase. Block `1` acts like the steady-state loop. The third and fourth offset slots are present but only contain near-EOF placeholder values in this file.
|
||
|
|
- The tactic bytecode is not just a name table. It is real AI scripting: face target, move, pathfind, test line-of-fire, loop, jump, sleep, and switch tactic/block.
|
||
|
|
- Several tactics are waypoint-driven. ScummVM identifies opcode `0xA6` as a search for nearby shape `0x33A` marker objects by frame number; that matches the naming of `OneToTwo`, `ThreeToFour`, `EggHopper1`, and `123_Shoot`.
|
||
|
|
- There is one notable model discrepancy to keep in mind: the shipped archive contains a valid entry `0` named `Dumb`, but ScummVM still special-cases tactic number `0` as a generic built-in attack path rather than always interpreting `COMBAT.DAT` entry `0`. Treat that as an open compatibility detail rather than silently preferring one model.
|
||
|
|
|
||
|
|
## Archive Layout
|
||
|
|
|
||
|
|
### Outer Container
|
||
|
|
|
||
|
|
Observed on-disk structure:
|
||
|
|
|
||
|
|
- file size: `0x06c6`
|
||
|
|
- first populated record starts at `0x0280`
|
||
|
|
- archive directory begins at `0x0080`
|
||
|
|
- directory entries are `8` bytes each: `<uint32 offset, uint32 length>`
|
||
|
|
- the shipped file leaves most directory slots empty and populates only `14` entries
|
||
|
|
|
||
|
|
Populated record table:
|
||
|
|
|
||
|
|
| Index | File offset | Length | Name |
|
||
|
|
|------:|------------:|-------:|------|
|
||
|
|
| 0 | `0x0280` | `0x004f` | `Dumb` |
|
||
|
|
| 1 | `0x02cf` | `0x003f` | `Pivot` |
|
||
|
|
| 2 | `0x030e` | `0x004f` | `Advance` |
|
||
|
|
| 3 | `0x035d` | `0x0047` | `Mental` |
|
||
|
|
| 4 | `0x03a4` | `0x0052` | `Careful` |
|
||
|
|
| 5 | `0x03f6` | `0x004c` | `OneToTwo` |
|
||
|
|
| 6 | `0x0442` | `0x004c` | `ThreeToFour` |
|
||
|
|
| 7 | `0x048e` | `0x0074` | `EggHopper1` |
|
||
|
|
| 8 | `0x0502` | `0x0056` | `StepOutShootNE` |
|
||
|
|
| 9 | `0x0558` | `0x005c` | `StepOutShootNW` |
|
||
|
|
| 10 | `0x05b4` | `0x0046` | `Random_CHAOS` |
|
||
|
|
| 11 | `0x05fa` | `0x003b` | `Static_Chaos` |
|
||
|
|
| 12 | `0x0635` | `0x0043` | `Stand_Choas` |
|
||
|
|
| 13 | `0x0678` | `0x004e` | `123_Shoot` |
|
||
|
|
|
||
|
|
Two names preserve the shipped typos/spelling:
|
||
|
|
|
||
|
|
- `Stand_Choas`
|
||
|
|
- `Random_CHAOS` / `Static_Chaos` mixed casing
|
||
|
|
|
||
|
|
### Inner Record Format
|
||
|
|
|
||
|
|
Per record:
|
||
|
|
|
||
|
|
| Offset | Size | Meaning |
|
||
|
|
|-------:|-----:|---------|
|
||
|
|
| `0x00` | `16` | ASCII tactic name, NUL-padded |
|
||
|
|
| `0x10` | `2` | block 0 start offset |
|
||
|
|
| `0x12` | `2` | block 1 start offset |
|
||
|
|
| `0x14` | `2` | block 2 start offset |
|
||
|
|
| `0x16` | `2` | block 3 start offset |
|
||
|
|
| `0x18+` | variable | bytecode stream |
|
||
|
|
|
||
|
|
Observed block-offset pattern:
|
||
|
|
|
||
|
|
- Most tactics use block 0 at `0x002c` and block 1 at `0x0031`.
|
||
|
|
- The waypoint tactics `OneToTwo`, `ThreeToFour`, and `123_Shoot` have a longer setup block and therefore move block 1 farther forward.
|
||
|
|
- The later two offset slots are usually `0x004d` and `0x004e` even when they point at the final sentinel bytes rather than real independent blocks.
|
||
|
|
|
||
|
|
ScummVM's comment in `combat_dat.h` says the format has `10x2-byte offsets`, but both its constructor and the live executable logic only use four offsets. The shipped file matches the implementation, not the stale comment.
|
||
|
|
|
||
|
|
## Live Executable Integration
|
||
|
|
|
||
|
|
The live `CRUSADER.EXE` export map already carries the important attack-process fields:
|
||
|
|
|
||
|
|
- `combatDatTacticPtr` at attack-process offset `0x45`
|
||
|
|
- `combatDatTacticPtr2` at `0x49`
|
||
|
|
- `combatDatTacticCurOffset` at `0x4d`
|
||
|
|
- `combatDatBlockNo` at `0x4f`
|
||
|
|
- `tacticNo` at `0x51`
|
||
|
|
|
||
|
|
Relevant live helpers:
|
||
|
|
|
||
|
|
- `1108:0586` `Attack_SetupForTacticNo`
|
||
|
|
- `1108:0506` `Attack_SetupForBlockNo`
|
||
|
|
- `10e8:3572` `NPC_GetNPCTacticNo`
|
||
|
|
- `10e8:358c` `NPC_SetNPCTacticNo`
|
||
|
|
|
||
|
|
Current best read of the runtime flow:
|
||
|
|
|
||
|
|
1. NPC state stores a tactic number in `ItemNPCData.field21_0x5c`.
|
||
|
|
2. `Attack_SetupForTacticNo` validates that the tactic slot has loaded COMBAT.DAT data, stores the selected tactic number in the attack process, and copies the far pointer to that archive entry into the process.
|
||
|
|
3. `Attack_SetupForBlockNo` selects one of the record's four offset words and seeds `combatDatTacticCurOffset` from it.
|
||
|
|
4. The attack-process main loop reads one opcode byte at a time from the selected block and executes it as AI logic.
|
||
|
|
|
||
|
|
ScummVM's Crusader attack-process code adds one more important detail:
|
||
|
|
|
||
|
|
- `readNextWordWithData()` treats any immediate operand `>= 33000` as an index into a `10`-entry tactic-local variable array instead of as a literal value.
|
||
|
|
|
||
|
|
The shipped `COMBAT.DAT` in this workspace does not appear to use that variable indirection. All operands decoded here are direct literals.
|
||
|
|
|
||
|
|
## Used Opcode Set
|
||
|
|
|
||
|
|
Only this subset is actually used by the shipped tactics:
|
||
|
|
|
||
|
|
| Opcode | Mnemonic | Meaning |
|
||
|
|
|------:|----------|---------|
|
||
|
|
| `0x84` | `set_target_objid` | Set attack target object id |
|
||
|
|
| `0x85` | `anim_walk` | Play walk/advance animation |
|
||
|
|
| `0x88` | `turn_left_90` | Turn `90` degrees left |
|
||
|
|
| `0x89` | `turn_right_90` | Turn `90` degrees right |
|
||
|
|
| `0x8a` | `fire_small_if_clear` | Fire if line-of-fire is valid |
|
||
|
|
| `0x8d` | `pathfind_home` | Pathfind to actor home position |
|
||
|
|
| `0x8f` | `pathfind_midpoint` | Pathfind to midpoint between actor and target |
|
||
|
|
| `0x93` | `sleep_scaled` | Sleep for N ticks, scaled by difficulty |
|
||
|
|
| `0x94` | `loiter` | Run a loiter sub-process for N ticks |
|
||
|
|
| `0x95` | `face_target` | Turn toward target center |
|
||
|
|
| `0x9a` | `jump_if_dist_lt_481` | Jump if target distance is under `481` |
|
||
|
|
| `0x9c` | `jump_if_shot_blocked` | Jump if `fireDistance()` fails |
|
||
|
|
| `0x9d` | `jump_if_shot_clear` | Jump if `fireDistance()` succeeds |
|
||
|
|
| `0x9f` | `loop_begin` | Set loop counter and remember current stream position |
|
||
|
|
| `0xa6` | `pathfind_marker_frame` | Find nearby shape `0x33a` marker by frame and pathfind to it |
|
||
|
|
| `0xa9` | `face_east` | Turn east |
|
||
|
|
| `0xaa` | `face_west` | Turn west |
|
||
|
|
| `0xc0` | `jump` | Unconditional jump to stream offset |
|
||
|
|
| `0xc1` | `loop_end` | Decrement loop counter and jump back while nonzero |
|
||
|
|
| `0xff` | `flip_to_block1_restart` | If still in block 0, switch to block 1; then restart current block |
|
||
|
|
|
||
|
|
Practical interpretation of `0xff` in shipped data:
|
||
|
|
|
||
|
|
- In block `0`, it is a one-way handoff into block `1`.
|
||
|
|
- In block `1`, it behaves like `restart block 1 forever`.
|
||
|
|
|
||
|
|
That is why most tactics have a very short block `0`: setup once, then live in block `1`.
|
||
|
|
|
||
|
|
## Tactic Catalog
|
||
|
|
|
||
|
|
### 0. `Dumb`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- simplest mobile shooter with midpoint fallback
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, then hand off to block 1
|
||
|
|
- block 1:
|
||
|
|
- if a shot is already clear, jump straight into a `3`-iteration fire loop
|
||
|
|
- otherwise pathfind to the midpoint between NPC and target, face target, and re-test line-of-fire
|
||
|
|
- if still not clear after midpoint attempts, loiter briefly
|
||
|
|
- once clear, loop `3` times: face target, fire, sleep `30`
|
||
|
|
- restart block 1
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- close some distance toward a firing lane, then burst-fire three times
|
||
|
|
|
||
|
|
### 1. `Pivot`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- stationary pivot-and-shoot burst
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, hand off
|
||
|
|
- block 1:
|
||
|
|
- if line-of-fire is clear, enter a `3`-shot loop immediately
|
||
|
|
- loop body: face target, fire, sleep `30`
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- no movement logic beyond facing the target; just turn and shoot in bursts
|
||
|
|
|
||
|
|
### 2. `Advance`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- move forward between bursts if a clear shot is available; midpoint fallback otherwise
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, hand off
|
||
|
|
- block 1:
|
||
|
|
- face target
|
||
|
|
- if shot is blocked, jump to midpoint-reposition logic
|
||
|
|
- otherwise run a `3`-iteration loop: face target, fire, loop
|
||
|
|
- sleep `30`
|
||
|
|
- play walk animation
|
||
|
|
- restart
|
||
|
|
- midpoint branch: pathfind midpoint, face target, re-test for clear shot, loiter briefly if still blocked, restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- pressure forward when able to shoot, otherwise drift toward a midpoint lane until a shot opens
|
||
|
|
|
||
|
|
### 3. `Mental`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- shoot if clear; otherwise midpoint hop and shoot
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, hand off
|
||
|
|
- block 1:
|
||
|
|
- if shot is blocked, pathfind to midpoint first
|
||
|
|
- run a `3`-iteration fire loop
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- simpler than `Advance`: it does not include the extra walk/sleep pressure cycle, only burst-fire with midpoint correction
|
||
|
|
|
||
|
|
### 4. `Careful`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- range-gated cautious shooter
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, hand off
|
||
|
|
- block 1:
|
||
|
|
- if target distance is not under `481`, loiter and restart
|
||
|
|
- if within `481` and shot is clear, fire once and restart
|
||
|
|
- if blocked, face target and test again
|
||
|
|
- if still blocked, pathfind midpoint and test again
|
||
|
|
- if still blocked, face target and test again
|
||
|
|
- if still blocked after all of that, loiter briefly and restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- only engages at medium/close range and spends more effort finding a clean shot before moving or firing
|
||
|
|
|
||
|
|
### 5. `OneToTwo`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- shuttle between marker frame `1` and marker frame `2`
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0:
|
||
|
|
- target object `1`
|
||
|
|
- pathfind to marker frame `1`
|
||
|
|
- face target
|
||
|
|
- hand off
|
||
|
|
- block 1:
|
||
|
|
- pathfind to marker frame `2`
|
||
|
|
- face target
|
||
|
|
- run a `3`-iteration fire loop while line-of-fire is clear
|
||
|
|
- return to marker frame `1`
|
||
|
|
- face target
|
||
|
|
- sleep `120`
|
||
|
|
- restart block 1
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- waypoint-based peek-and-return behavior across two authored combat markers
|
||
|
|
|
||
|
|
### 6. `ThreeToFour`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- same pattern as `OneToTwo`, but between marker frames `3` and `4`
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, pathfind marker `3`, hand off
|
||
|
|
- block 1:
|
||
|
|
- pathfind marker `4`
|
||
|
|
- face target
|
||
|
|
- if shot is clear, run a `3`-shot burst loop
|
||
|
|
- return to marker `3`
|
||
|
|
- face target
|
||
|
|
- sleep `120`
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- authored two-point lateral or cover movement using a second pair of marker frames
|
||
|
|
|
||
|
|
### 7. `EggHopper1`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- multi-marker patrol shooter
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, hand off
|
||
|
|
- block 1:
|
||
|
|
- pathfind marker `0`, attempt a `3`-shot loop
|
||
|
|
- pathfind marker `1`, attempt a `3`-shot loop
|
||
|
|
- pathfind marker `2`, attempt a `3`-shot loop
|
||
|
|
- pathfind marker `3`, attempt a `3`-shot loop
|
||
|
|
- pathfind home
|
||
|
|
- face target
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- sweep across four authored combat markers in order, trying a short firing burst at each stop, then return home
|
||
|
|
|
||
|
|
### 8. `StepOutShootNE`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- east-west step-out gunner, nominally biased to an east-facing start
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face east, hand off
|
||
|
|
- block 1:
|
||
|
|
- walk outward
|
||
|
|
- face target
|
||
|
|
- up to `3` short fire attempts with `5`-tick delays
|
||
|
|
- face west
|
||
|
|
- walk back
|
||
|
|
- face target
|
||
|
|
- another `3` short fire attempts with `5`-tick delays
|
||
|
|
- if blocked, wait `25` ticks instead of firing through
|
||
|
|
- face east
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- pop out from one side, take a short burst, step back across the lane, burst again, and reset orientation
|
||
|
|
|
||
|
|
### 9. `StepOutShootNW`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- mirror-image of `StepOutShootNE`, nominally biased to a west-facing start
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face west, hand off
|
||
|
|
- block 1:
|
||
|
|
- walk outward from the west-facing side
|
||
|
|
- face target and attempt short burst fire
|
||
|
|
- if blocked, delay `25`
|
||
|
|
- face east and walk across
|
||
|
|
- repeat the short-burst pattern
|
||
|
|
- face west and restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- same authored cover-pop logic as the northeast variant, but with opposite home orientation
|
||
|
|
|
||
|
|
### 10. `Random_CHAOS`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- immediate short burst, then aggressive midpoint pressure
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0:
|
||
|
|
- target object `1`
|
||
|
|
- face target
|
||
|
|
- run a `2`-shot loop with `30`-tick sleeps
|
||
|
|
- hand off
|
||
|
|
- block 1:
|
||
|
|
- face target
|
||
|
|
- pathfind midpoint
|
||
|
|
- face target
|
||
|
|
- run a `3`-shot loop with `30`-tick sleeps
|
||
|
|
- restart block 1 forever
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- start with a short static burst, then keep pressing toward midpoint and firing
|
||
|
|
|
||
|
|
### 11. `Static_Chaos`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- pure stationary burst shooter
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, face target, hand off
|
||
|
|
- block 1:
|
||
|
|
- face target
|
||
|
|
- loop `3` times: fire, loop
|
||
|
|
- sleep `30`
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- no pathfinding at all; just keep facing and firing from the current spot
|
||
|
|
|
||
|
|
### 12. `Stand_Choas`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- stationary turret-like spread pattern
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0: target object `1`, hand off
|
||
|
|
- block 1:
|
||
|
|
- face target
|
||
|
|
- if shot is clear, fire once and skip to the post-shot delay
|
||
|
|
- otherwise rotate and fire in a fixed sweep: left, original/right-adjusted, then right again
|
||
|
|
- sleep `30`
|
||
|
|
- face target
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- a stand-and-sweep pattern for targets that are partially obstructed or moving across the arc
|
||
|
|
|
||
|
|
### 13. `123_Shoot`
|
||
|
|
|
||
|
|
Role:
|
||
|
|
|
||
|
|
- two marker hops followed by midpoint pressure
|
||
|
|
|
||
|
|
Decoded behavior:
|
||
|
|
|
||
|
|
- block 0:
|
||
|
|
- target object `1`
|
||
|
|
- face target
|
||
|
|
- pathfind marker `0`
|
||
|
|
- face target
|
||
|
|
- pathfind marker `1`
|
||
|
|
- face target
|
||
|
|
- run a `2`-shot loop
|
||
|
|
- hand off
|
||
|
|
- block 1:
|
||
|
|
- face target
|
||
|
|
- pathfind midpoint
|
||
|
|
- run a `3`-shot loop
|
||
|
|
- restart
|
||
|
|
|
||
|
|
Human read:
|
||
|
|
|
||
|
|
- staged opening movement across authored markers, then transition into a more ordinary midpoint-pressure gunner
|
||
|
|
|
||
|
|
## Pattern Summary
|
||
|
|
|
||
|
|
Across the whole file, the tactics cluster into a few clear families:
|
||
|
|
|
||
|
|
| Family | Tactics | Shared idea |
|
||
|
|
|--------|---------|-------------|
|
||
|
|
| stationary shooters | `Pivot`, `Static_Chaos`, `Stand_Choas` | little or no movement; rely on facing and burst loops |
|
||
|
|
| midpoint pressers | `Dumb`, `Advance`, `Mental`, `Random_CHAOS`, `123_Shoot` block 1 | move toward a midpoint lane when a shot is blocked |
|
||
|
|
| cautious/range-gated | `Careful` | only engage inside a distance window and avoid overcommitting |
|
||
|
|
| authored marker shuttles | `OneToTwo`, `ThreeToFour`, `EggHopper1`, `123_Shoot` block 0 | follow placed map markers keyed by frame number |
|
||
|
|
| step-out cover shooters | `StepOutShootNE`, `StepOutShootNW` | walk out, burst, walk back, repeat |
|
||
|
|
|
||
|
|
This is enough to treat `COMBAT.DAT` as a compact authored AI-script table rather than a loose name list.
|
||
|
|
|
||
|
|
## Open Questions
|
||
|
|
|
||
|
|
1. Tactic `0` is the main remaining semantic mismatch. The archive contains `Dumb` at index `0`, while ScummVM still routes `_tactic == 0` through `genericAttack()`. The live executable-side helper accepts tactic `0` as a normal COMBAT.DAT slot, so this needs a later direct compiled-side pass if exact retail precedence matters.
|
||
|
|
2. The later two per-record offset slots are structurally present but operationally unimportant in this file. A future pass could still check whether any retail code path ever selects block `2` or `3`.
|
||
|
|
3. The marker-search opcode `0xA6` is strongly understood from ScummVM and the tactic names, but the live-game name of shape `0x33A` and the exact authored map placement conventions remain better documented on the map-data side than in the live NE database.
|
||
|
|
|
||
|
|
## Practical RE Use
|
||
|
|
|
||
|
|
For future compiled-side work, the main safe takeaways are:
|
||
|
|
|
||
|
|
- `tacticNo` in NPC data is not cosmetic; it selects a real bytecode program.
|
||
|
|
- block `0` is usually an initialization or reposition phase; block `1` is the stable loop.
|
||
|
|
- the named tactics are portable data labels because the shipped file is identical across the local Remorse/Regret variants.
|
||
|
|
- waypoint-driven tactics should be interpreted together with local marker placements, not only from executable code.
|