re: ISL built-ins 26 / 29 / 101 named end to end; 28's field pinned, label 🟡
The three unnamed built-ins are one family, and the chain from the dispatch
table to the field write is now followed for all of them:
builtin -> ScriptPhase vtable slot -> interpreter command word 0xAB<op>BA
-> command-table thunk -> opcode handler -> unit message 0xED08nnDE
-> the GROUP pump sub_8232C4C0, which rebroadcasts to each child as
0xED09nnDE
-> the entity base handler sub_82398CC0, which writes the field.
26 -> [unit+532] = min(max(def.HP * pct, 0), def.HP) = set_unit_hp_pct
29 -> [unit+676] = pct, a multiplier on damage TAKEN = set_unit_damage_taken_pct
28 -> [unit+672] = pct, default 1.0 = 🟡 damage DEALT
101 -> the same message as 29 with a hard-wired 0.0, broadcast to every unit
= all_units_invulnerable
`damage_unit` is WITHDRAWN for 26. The handler sets an absolute value rather
than subtracting one, and 100 heals to full -- which no damage primitive does.
It is pinned three ways: [unit+496] is the unit definition (the constructor
sub_82393868 fills it from the same std::map::find built-in 15 uses), [def+84]
is HP in unit_definition_layout.txt and the constructor seeds [unit+532] from
it, and crossing zero loads [def+584] = Delay and raises a flag, i.e. the
destruction sequence. So 0 destroys, with the datasheet's own death delay.
29 is the strongest of the three: [unit+676] has three independent readers
(sub_8237B020, sub_823800A8, sub_82398CC0) and every one multiplies a damage
amount immediately before it reduces [unit+532].
28 is deliberately left 🟡. The write and the 1.0 default are certain, but the
field has exactly ONE reader in the whole image -- the craft update's projectile
spawn, where it ends up as a multiplicative term in the damage message. That is
the mirror of 29 and it is tidy, which is exactly the shape that produced the
wrong names this file has already had to withdraw. What is not established is
that it reaches every weapon; the sibling damage sender sub_82388FF8 has no
+672 term at all.
Three usage tests, all measured over the 28 stages:
* operand ceilings -- 26 is 97/97 inside [0,100] and 29 is 164/164, while 28
(identical signature, identical x0.01 conversion) reaches 2000;
* the craft cross-tab -- 26 splits cleanly into disposable props at 0,
warships at 30-80 and the tutorial player craft at 100; 29 lands on the
player, the tutorial boxes and the escorted TCAF hulls; 28 orders
boss > ace > elite > line > prop;
* the setup idiom -- activate_unit, then 15/29/28 as a speed/toughness/
firepower trio, with 26 added wherever a unit must arrive pre-damaged.
Refutations attempted are recorded, including the two that turned into
confirmations (Stage 28 makes each tutorial box invulnerable with 29 and then
removes it with 26) and the offset-search trap that produced three false
readers, because projectiles have their own fields at 672 and 676.
Also corrected: the state guards. 26 rejects states 3 and 4; 28 and 29 reject
1, 3 and 4. This file said otherwise for both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
Status: ✅ table encoding, calling convention and the `ScriptPhase` state layout;
|
||||
✅ ~135 of 147 handlers characterised from the disassembly; 🟡 three resolved
|
||||
only partially; ❔ the interpreter-command table is only partly recovered.
|
||||
✅ built-ins **26 / 29 / 101** are now named end to end (HP %, damage-taken %,
|
||||
broadcast invulnerability); 🟡 **28** writes the matching damage-**dealt** field
|
||||
but its one consumer is a single weapon path — see below.
|
||||
|
||||
Companion to [isl-bytecode](isl-bytecode.md) (the instruction encoding) and
|
||||
[mission-phase-advance](../mission-phase-advance.md) (why phases hinge on these).
|
||||
@@ -167,56 +170,269 @@ words**: a descriptor offset and the message id.
|
||||
That fixes the id format as `0xED08 nn DE`, and the ids already known from other
|
||||
work fit it: opcode 514 → `00DE`, 803 → `07DE`, 999 → `0FDE`.
|
||||
|
||||
🟡 The pump's arm for `0xED0802DE` does **not** apply an effect — it walks the
|
||||
✅ **The pump's arm for `0xED0802DE` applies no effect itself** — it walks the
|
||||
unit's child list at `[unit+320]`/`[unit+324]` and **rebroadcasts** to each child
|
||||
as `0xED0902DE`. So `0xED08…` is the to-unit family and `0xED09…` the to-child
|
||||
one, and the terminal effect is one link further on. ❌ Not followed; 26/28/29
|
||||
remain unnamed.
|
||||
as `0xED0902DE`. So `0xED08…` is the to-group family and `0xED09…` the
|
||||
to-individual one. That link is now followed all the way to the effect — see
|
||||
below.
|
||||
|
||||
### 🟡 Built-ins 26 / 28 / 29 are one family — and `damage_unit` looks mis-named
|
||||
### ✅ Built-ins 26 / 28 / 29 RESOLVED — HP, damage dealt, damage taken
|
||||
|
||||
Method-diffing put the structure beyond doubt but did not reach the semantics.
|
||||
The chain was followed end to end for all three, from the dispatch table to the
|
||||
field write. Nothing here is inferred from shape; every hop was read.
|
||||
|
||||
| built-in | vtable slot | method | opcode | sites |
|
||||
|---|---|---|---|---|
|
||||
| 26 `damage_unit` | 76 | `sub_8226ACD0` (67) | **800** | 97 |
|
||||
| **28** | 84 | `sub_82268F98` (69) | **801** | 410 |
|
||||
| **29** | 88 | `sub_822690B0` (69) | **802** | 164 |
|
||||
| 101 | 276 | `sub_822691C8` (78) | 802 (broadcast) | 133 |
|
||||
| built-in | vtable slot | method | opcode | cmd word | interp thunk | opcode handler | to-group msg | to-child msg | arm | writes |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| **26** | 76 | `sub_8226ACD0` | 800 | `0xAB0320BA` | `0x822FF118` | `sub_823008C8` | `0xED0802DE` | `0xED0902DE` | `0x82398E8C` | `[unit+532]` = **HP** |
|
||||
| **28** | 84 | `sub_82268F98` | 801 | `0xAB0321BA` | `0x822FF120` | `sub_823009B8` | `0xED0803DE` | `0xED0903DE` | `0x8239926C` | `[unit+672]` |
|
||||
| **29** | 88 | `sub_822690B0` | 802 | `0xAB0322BA` | `0x822FF128` | `sub_82300AA8` | `0xED0804DE` | `0xED0904DE` | `0x823994C0` | `[unit+676]` |
|
||||
|
||||
**28 and 29 differ in two words only** — the opcode (`0x21BA` vs `0x22BA`) and a
|
||||
descriptor pointer 8 bytes apart. Otherwise instruction-identical. All three take
|
||||
`(unit, double)`.
|
||||
The cmd word decodes as `0xAB` `opcode:16` `0xBA`, which is the same encoding
|
||||
already recorded for 256 (`0xAB0100BA`), 513 (`0xAB0201BA`) and 996
|
||||
(`0xAB03E4BA`) — `0x320/0x321/0x322` = 800/801/802.
|
||||
|
||||
26 differs from both by one guard: it rejects only state 3, while **28 and 29
|
||||
reject states 1 and 3** (2 = active, 1/3/4 = gone/dead/invalid).
|
||||
Two links that the previous attempt did not have:
|
||||
|
||||
#### The operand distributions separate them
|
||||
* the **to-group pump is `sub_8232C4C0`** (vtable `0x820AF1FC` slot 6), whose
|
||||
arms at `0x8232C824` / `0x8232CB48` / `0x8232CC40` each walk the child list and
|
||||
re-post with the `0xED09…` id, copying the float from `[msg+36]` unchanged;
|
||||
* the **to-individual handler is `sub_82398CC0`** (vtable `0x820B192C` slot 6),
|
||||
the entity base class. Its `0xED0902DE` arm is reached by a `sub.`/`cmplwi 0x8200`
|
||||
pair rather than a `lis`+`ori` compare, which is why a constant scan for
|
||||
`0xED0902DE` finds only the *sender*. That is worth remembering: **a
|
||||
subtract-based compare hides the constant from any scan that pairs
|
||||
`lis` with `ori`.**
|
||||
|
||||
| built-in | n | distinct | range | most common |
|
||||
|---|---|---|---|---|
|
||||
| 26 | 97 | 7 | **[0, 100]** | **0 ×69**, 80 ×10, 100 ×6 |
|
||||
| 28 | 410 | 13 | **[0, 2000]** | 200 ×116, 120 ×76, 300 ×74 |
|
||||
| 29 | 164 | 6 | **[0, 100]** | **0 ×64**, 100 ×53, 50 ×38 |
|
||||
#### ✅ 26 is `set_unit_hp_pct` — NOT `damage_unit`
|
||||
|
||||
26 and 29 are percentage-shaped; 28 is an absolute quantity an order of magnitude
|
||||
larger.
|
||||
`sub_8226ACD0` multiplies the double operand by the constant at `0x820C5688`,
|
||||
which is **0.01** — so the operand is a percentage and the message carries a
|
||||
fraction. `0x82398E8C` then does, in its own terms:
|
||||
|
||||
#### 🟡 `damage_unit` (26) is doubtful
|
||||
```
|
||||
new = min( max(def.HP * frac, 0.0), def.HP ) ; def = [unit+496]
|
||||
[unit+532] = new
|
||||
if new < FLT_EPSILON and old > 0: ; crossed to zero
|
||||
[unit+688] = 0.0 ; [unit+1340] = 1 ; [unit+692] = def.Delay
|
||||
```
|
||||
|
||||
**69 of its 97 calls pass 0.** Dealing zero damage is a no-op, so 71 % of the
|
||||
call sites would do nothing. *Setting* a percentage-valued property to 0 is a
|
||||
perfectly natural thing to do 69 times, and 29 has the same shape (0 ×64 of 164).
|
||||
The existing name predates this session and is not withdrawn, but it should not
|
||||
be relied on.
|
||||
Three independent facts pin the field:
|
||||
|
||||
#### ❌ Where this stopped
|
||||
* `[unit+496]` is the **unit definition** — the entity constructor
|
||||
`sub_82393868` sets it from `sub_82348830` (the per-member definition
|
||||
`std::map::find` already identified for built-in 15) at `0x82393BE4`;
|
||||
* `[def+84]` is **`HP`** in
|
||||
`crates/sylpheed-formats/data/unit_definition_layout.txt`, and the same
|
||||
constructor immediately does `[unit+532] = [def+84]` at `0x82393BF0` — so
|
||||
`+532` *is* current HP, seeded from the datasheet maximum;
|
||||
* the crossing branch loads **`[def+584]` = `Delay`** into `[unit+692]` and sets
|
||||
a flag — the destruction sequence. So a percentage of **0 destroys the unit**,
|
||||
with the definition's own death delay.
|
||||
|
||||
The three commands' descriptors sit at `0x820A8D10` / `+8` / `+16`. Following
|
||||
them lands on data pointing into `0x8210E5xx`, which is **below the disassembly
|
||||
DB's range** (it starts at `0x82150000`) and contains no code — so that route
|
||||
does not reach an execute method. Reaching opcodes 800–802's semantics needs the
|
||||
interpreter's command table, not the command objects.
|
||||
So the operand is *percent of maximum HP*, the write is absolute (a **set**, not
|
||||
a subtract), and `damage_unit` is withdrawn: 100 heals to full, which no
|
||||
"damage" primitive does.
|
||||
|
||||
#### ✅ 29 is `set_unit_damage_taken_pct` — `[unit+676]` scales incoming damage
|
||||
|
||||
`[unit+672]` and `[unit+676]` are **both initialised to 1.0** by the entity
|
||||
constructor (`0x8239395C`/`0x82393964`, from the float at `0x8208583C`), so both
|
||||
are multipliers whose neutral value is 1.0 and which built-ins 28/29 set to
|
||||
`operand/100`.
|
||||
|
||||
`[unit+676]` has **three independent readers**, and every one of them multiplies
|
||||
a damage amount immediately before it is subtracted from `[unit+532]`:
|
||||
|
||||
| reader | what it computes |
|
||||
|---|---|
|
||||
| `0x8237B20C` in `sub_8237B020` (craft subclass `0x820B0A3C` slot 6, the `0xED0200DE` damage arm) | `dmg = [msg+36] × def.ResistanceToPlayer × [unit+676]` |
|
||||
| `0x823803A4` in `sub_823800A8` (another subclass's damage arm) | the same product |
|
||||
| `0x82399FFC` in `sub_82398CC0` (the base class's continuous-damage arm) | `dmg = def.HP × globalRate × t × [unit+676]`, then `[unit+532] -= dmg` |
|
||||
|
||||
`def+128` is `ResistanceToPlayer` in the layout file, which is the second
|
||||
corroboration that the object is the entity and the quantity is damage.
|
||||
|
||||
#### 🟡 28 sets `[unit+672]`, and its one consumer scales damage DEALT
|
||||
|
||||
The write is certain and the field's default (1.0) is certain. The **label** is
|
||||
one link weaker than 29's, and this is the honest state:
|
||||
|
||||
`[unit+672]` is read in **exactly one place** in the image —
|
||||
`0x8237A44C`, inside `sub_823785A0`, which is vtable `0x820B0A3C` **slot 1**,
|
||||
i.e. the craft subclass's per-frame update. There it is copied into a 240-byte
|
||||
projectile spawn record at `+100`; `sub_8238F4D0` turns that record into a
|
||||
projectile with `[proj+300] = [rec+100]`; and `sub_8238FE10` builds the
|
||||
`0xED0200DE` damage message with **`[msg+36] = [proj+300] × charge × globalScale`**
|
||||
— the same `[msg+36]` that the readers above multiply by the *target's* `+676`.
|
||||
|
||||
So the two fields are the two ends of one damage product: **`+672` on the
|
||||
shooter, `+676` on the target.** That is a tidy story and it is exactly the
|
||||
shape that has produced wrong names in this file before, so it is 🟡 and not ✅.
|
||||
|
||||
❔ **What is not established**: that `+672` reaches *every* weapon. The one read
|
||||
site is on a charged-shot path (`[unit+2244]` is a charge accumulator compared
|
||||
against thresholds, `[unit+2268]` the resulting power), and the other damage
|
||||
sender in the same family, `sub_82388FF8`, computes
|
||||
`dmg = ammo.base × [proj+268] × k` with **no `+672` term at all**. Either the
|
||||
craft update funnels all firing through this one spawn, or `+672` scales only
|
||||
some weapons. Not settled.
|
||||
|
||||
#### ✅ The operand-range test discriminates the three
|
||||
|
||||
All three built-ins have the identical signature `(unit, double)` and the
|
||||
identical `× 0.01` conversion, so an operand ceiling is a real measurement rather
|
||||
than a coincidence:
|
||||
|
||||
| built-in | n | distinct | min | max | inside [0, 100] |
|
||||
|---|---|---|---|---|---|
|
||||
| 26 | 97 | 7 | 0 | **100** | **97 / 97** |
|
||||
| 29 | 164 | 6 | 0 | **100** | **164 / 164** |
|
||||
| 28 | 410 | 13 | 0.1 | **2000** | 42 / 410 |
|
||||
|
||||
26 and 29 respect a hard 100 ceiling — they are percentages of something with a
|
||||
natural maximum (HP; and "no more damage than normal"). 28 does not: it is a
|
||||
free multiplier that reaches 20×. 🟡 Note that 29 therefore **only ever makes a
|
||||
unit tougher**, never more fragile.
|
||||
|
||||
#### ✅ The craft cross-tab — the same test that settled built-in 15
|
||||
|
||||
Joining every call site to its squadron's craft through `UnitGroup_S<NN>.tbl`
|
||||
resolves **671 of 671** sites (26 + 28 + 29), none unknown. Full dump in
|
||||
[`data/isl-builtins-26-28-29-sites.txt`](../data/isl-builtins-26-28-29-sites.txt).
|
||||
|
||||
**26** splits into three groups with nothing in between:
|
||||
|
||||
| group | craft (sites) | values |
|
||||
|---|---|---|
|
||||
| disposable objects | `e201_ISCMissile` 17, `n001_TTRL_Box` 16, `f202_Cargo` 9, `e015_Puppy(_2)` 10, `mn500_FloatingMine` 3, `e011_Attacker_B` 2 | **0, and only 0** |
|
||||
| warships | `e106_Destroyer` 8, `f106_Destroyer` 8, `f105_Cruiser` 5, `e108_ASFrigate` 4, `e105_Cruiser` 2, `f101_Acropolis` 1 | 30 / 50 / 60 / 80 |
|
||||
| the player's craft in a tutorial | `..._Player_Ttrl1/2`, `..._T_Ttrl` (8) | **100** ×7, 40 ×1 |
|
||||
|
||||
Scripted removal of props, pre-damaged capital-ship spawns, and healing the
|
||||
player to full at the start of a tutorial section — one field, three uses, and
|
||||
no other reading covers all three.
|
||||
|
||||
**29** is dominated by the ships the mission must protect:
|
||||
|
||||
| craft (sites) | values |
|
||||
|---|---|
|
||||
| player + wingman `DeltaSaber` variants (`_T`, `_W`, `_A`, `_Player`) 113 | **0** and **100**, alternating |
|
||||
| `n001_TTRL_Box` 9 | **0** only |
|
||||
| escorted TCAF hulls — `f202_Cargo` 14, `f106_Destroyer` 2, `f105_Cruiser` 2, `f104_Battleship` 1, `f102_LightCarrier` 1, `f101_Acropolis` 1 | 50 / 75 |
|
||||
| named ADAN aces — `e001_Elan_GR`, `_GR_Violeta` 6; `e010_Attacker_S` 18 | 40 / 50 |
|
||||
|
||||
**28** stratifies by combat class and is applied almost only to ADAN craft:
|
||||
|
||||
| class | craft (sites) | value |
|
||||
|---|---|---|
|
||||
| named bosses | `e003_ElanPlus_Margras` 4, `e005_ElanTypeQ_Margras` 1 | **2000** |
|
||||
| named aces | `e001_Elan_GR(_Violeta)` 6, `e013_ElanPlus_Taskent` 2 | **1000** |
|
||||
| elite fighters | `e004_ElanPlus_N` 19, `e009_Phantom` 16, `e002_Elan_N` 9, `e011_Attacker_B_HF` 3 | **500–600** |
|
||||
| line fighters | `e001_Elan` 24, `e010_Attacker_S` 21, `e011_Attacker_B` 6 | 200–300 |
|
||||
| capital hulls | `e106_Destroyer` 105, `e104_Carrier` 55, `e105_CruiserEX` 52, `e102_BattleshipEX` 26 | 100–300 |
|
||||
| background props | `f106_Destroyer_Inv`, `f102_LightCarrier_Inv`, `f101_Acropolis`, `f105_Cruiser` (5) | **0.1** |
|
||||
|
||||
A monotone boss > ace > elite > line > prop ordering is what a firepower knob
|
||||
looks like. 🟡 It is also what several other knobs would look like, which is why
|
||||
the name stays PROBABLE and rests on the disassembly link above rather than on
|
||||
this table.
|
||||
|
||||
#### ✅ The idiom that ties all three together
|
||||
|
||||
The per-unit setup block that follows every deployment reads, e.g. Stage 06
|
||||
phase 3 at `0x0119AC` (from
|
||||
[`data/isl-builtins-26-28-29-sites.txt`](../data/isl-builtins-26-28-29-sites.txt)):
|
||||
|
||||
```
|
||||
activate_unit(TCN001) set_group_speed(TCN001, 400) 29(TCN001, 0)
|
||||
activate_unit(TCN002) set_group_speed(TCN002, 400) 29(TCN002, 0) 28(TCN002, 50)
|
||||
activate_unit(TCN003) set_group_speed(TCN003, 400)
|
||||
... 26(TCN306, 50)
|
||||
```
|
||||
|
||||
`15 / 29 / 28` is a **speed / toughness / firepower trio**, each a percentage
|
||||
override of the craft's datasheet, applied immediately after `activate_unit`;
|
||||
`26` joins it wherever a unit should arrive pre-damaged.
|
||||
|
||||
And the scripted-drama use is unambiguous. Stage 06 makes the wingman flight
|
||||
TCN002 invulnerable at phase start (`29(TCN002, 0)`), then walks its HP down as
|
||||
the voice lines fire:
|
||||
|
||||
```
|
||||
0x01A7B0 26(TCN002, 80) -> request_script_message(MSG_VOICE_D_065)
|
||||
0x01AAA0 26(TCN002, 50) -> request_script_message(MSG_VOICE_D_069)
|
||||
0x01ACEC 26(TCN002, 30)
|
||||
```
|
||||
|
||||
You would only combine those two built-ins that way if they meant exactly "this
|
||||
unit cannot be hurt by combat" and "set this unit's HP to N %".
|
||||
|
||||
#### ✅ Refutation attempts, and what they found
|
||||
|
||||
Recorded because two of them turned into confirmations.
|
||||
|
||||
* **"26 kills a unit the mission still needs."** Over all 28 stages there are
|
||||
200 (stage, unit) pairs where a predicate on `u` follows a `26(u, 0)` in file
|
||||
order. Every one inspected is a `unit_alive` / `unit_state` / `hp_pct_test`
|
||||
**poll waiting for that death** — which is what a scripted kill implies, not a
|
||||
contradiction. Not a counterexample, but the count is recorded rather than
|
||||
hidden.
|
||||
* **"29 makes a unit invulnerable that the player is required to destroy."**
|
||||
The nine `n001_TTRL_Box` targets in Stage 28 do get `29(box, 0)`. They are
|
||||
then destroyed by **`26(box, 0)`** later in the same file — nine boxes, nine
|
||||
pairs, invulnerability first (offsets 0x2E4C–0x48C8) and the scripted removal
|
||||
second (0x50F8–0x5FA0). The tutorial target cannot be shot down; the script
|
||||
removes it when the lesson ends. The counterexample became a confirmation.
|
||||
* **"28 is applied to something with no weapons."** Of the 35 craft classes it
|
||||
touches, none is an unarmed prop — no asteroid, box, cargo or mine ever
|
||||
receives 28, while 26 and 29 both do. The only near-miss is the four
|
||||
`…_Inv` / background hulls at 0.1, i.e. deliberately harmless.
|
||||
* **"`+672`/`+676` have another consumer that contradicts damage."** Searched
|
||||
every `lfs`/`stfs`/`addi`/`lfsx` reference to offsets 672 and 676 across the
|
||||
whole `.text`, then filtered by base register. **No further entity-side reader
|
||||
of either exists.** ⚠️ The search also produced the trap this file keeps
|
||||
warning about: `sub_8238E0F0` writes `676(r3)` and `sub_82389558` reads
|
||||
`672(r31)`, but in both `r3`/`r31` is a **projectile**, which has its own
|
||||
fields at those offsets. Three of the five "extra readers" the raw offset
|
||||
search returned were that mistake.
|
||||
* **What I could not refute and could not confirm**: that `+672` reaches every
|
||||
weapon (see the ❔ above).
|
||||
|
||||
#### ✅ Bonus: built-in 101 is `all_units_invulnerable`
|
||||
|
||||
`sub_822691C8` is 29's **broadcast** twin — same descriptor `0x820A8D20`, same
|
||||
opcode 802 — but it takes **no operand**: the float it sends is the constant at
|
||||
`0x8209FD28`, which is **0.0**, and it loops over the phase's whole unit array
|
||||
posting `0xED0804DE` to each live entry.
|
||||
|
||||
The usage confirms it. All **133** sites sit in one fixed phase-teardown idiom,
|
||||
with no exceptions:
|
||||
|
||||
```
|
||||
builtin116(0) -> builtin101 -> reset_phase_threads -> timer_stop -> clear_flag(-1) -> builtin118
|
||||
```
|
||||
|
||||
133/133 preceded by 116 and 133/133 followed by 100. Freezing all damage is
|
||||
exactly the first step of tearing a phase down, and no other reading of "post
|
||||
0.0 to every unit" fits a teardown.
|
||||
|
||||
#### ❌ Two corrections to this file's earlier entries
|
||||
|
||||
* **The state guards were mis-stated.** Read directly: **26 rejects states 3 and
|
||||
4**; **28 and 29 reject 1, 3 and 4**. This file said 26 rejects "only state 3"
|
||||
and 28/29 "states 1 and 3". So 26 alone will still act on a state-1 unit.
|
||||
* **"28 is an absolute quantity, 26 and 29 are percentage-shaped"** was half
|
||||
right for the wrong reason. All three are percentages of *something*; only 26
|
||||
and 29 are percentages of a thing with a ceiling.
|
||||
|
||||
#### 🗒️ The route that failed, kept
|
||||
|
||||
The three commands' descriptors at `0x820A8D10` / `+8` / `+16` are **vtables**,
|
||||
not data: slot 0 of each is `0x82301C20` and slot 1 points into `0x8210E5xx`,
|
||||
below the disassembly DB's range. Chasing them still leads nowhere. The route
|
||||
that works is the one this file already recommended — the **interpreter command
|
||||
table** → `0x822FF118/120/128` → the three opcode handlers — and from there the
|
||||
**unit-message pump**, which is the part that was missing.
|
||||
|
||||
### ✅ Built-in 108 is `deploy_squadron_ex` — `deploy_squadron` plus a `1 << n` selector
|
||||
|
||||
|
||||
Reference in New Issue
Block a user