Merge branch 'auto/isl-builtins-26-28-29'
# Conflicts: # docs/re/INDEX.md # tools/re-capture/isl.py
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).
|
||||
@@ -168,56 +171,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