This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/isl-schedule.md
Sylpheed RE agent 3cad5d063f re: read_freg counts SECONDS -- the 32 "flags" are a stopwatch bank
The open question was the unit, not the array.  Following the writers of
[phase+88] settles it:

  * sub_822710D0(phase, dt), called from ScriptPhase::Update, does
    prev[i] = cur[i] then, only while [phase+120][i] == 1, cur[i] += dt --
    so +88/+104/+120 are current / previous / running, 32 entries each.
  * dt is seconds by a non-circular round trip: frames * (1/60) * 10000
    -> clamp 3200 -> * 1e-4, in the timing singleton at [0x828F35B4].
    The clamp is 0.32 s, a frame ceiling.
  * 675/675 timeline kinds are indices their own phase starts (control
    11.2 %), which is why kind is only ever 0 or 5.

Corrects isl-builtins.md twice: set_flag writes 0.0 not 1.0, and
clear_flag clears the running flag rather than the value.  Confirms its
grouping of 8/9/93 as one family.  Docs only -- all seven ISL artefacts
regenerate byte-identical.
2026-08-27 08:48:49 +00:00

7.0 KiB
Raw Blame History

Each phase carries a TIMELINE — 675 scheduled routines across the disc

The trailing data table found at the end of every phase region (isl-stream-entry-points) is decoded. It is the mission's scripted event schedule.

Layout

After the phase's code ends — at the first value of that phase's mission-level 0x1883 record — comes a run of 8-byte typed records, tag 0x19 = int, 0x1A = IEEE float:

int   N                                     -- entry count
N x [ int offset ; float t ; int kind ]

1 + 3N matches the record count in every phase measured: Stage 02's three phases hold 76, 40 and 55 records for N = 25, 13 and 18.

The checks

schedule entries disc-wide 675
0x1A float records disc-wide 675 — the same number, independently counted
offsets landing on the instruction stream 675 / 675 = 100.0 %
control: random 4-aligned offsets 33.3 %

The float count and the entry count are derived by different routes and agree exactly, and every single offset resolves. kind is 0 (556) or 5 (119).

The floats are seconds

The distribution is unmistakable: 0, 0.5, 1, 4, 5, 30, 50, 60, 90, 120, 150, 170, 180, 210, 240, 270, 300, 330, 360, 420, 570, 1020, 1080, 1140, 1170 … mission times, not fractions.

And the targets are what a schedule would point at — small one-shot coroutines:

002C04:  set.i  local[0] = 1
002C1C:  set.i  local[4] = 1
002C34:  set.f  local[8] = 0.4
002C50:  call   builtin106(0x1, 0x1, 0.4)
002C5C:  call   end_coroutine

🟡 A runtime cross-check — consistent, and not proof

The closed REMAINING OB work measured, on the emulator over n=5 runs, that Stage 02's squadrons arrive at t = 0, 120 and 210 s. All three times are in phase 1's static schedule, and 120 and 210 each appear twice, which is what two squadrons arriving together would look like.

⚠️ Stated as consistency rather than confirmation: these are round numbers, and phase 1 has ~22 distinct times spread over 01170, so three specified round values all being present is not by itself unlikely. The doubling is the sharper detail, and it was not predicted in advance.

Artefacts

data/isl-stage02-schedule.txt and data/isl-schedule-all.txt (all 28 stages, 675 entries), generator isl_report.py schedule. The four earlier artefacts regenerate byte-identical.

The CONSUMER — sub_822748D0, called from ScriptPhase::Update

Found statically. The phase initialiser sub_82270DF8 takes the 0x1883 record's six words as arguments and stores

82270FC8  stw  r26, 232(r30)     ; [phase+232] = code base        (already documented)
82270FDC  add  r10, r26, r21     ; base + entry_a
82270FEC  stw  r10, 240(r30)     ; [phase+240] = THE TABLE POINTER

[phase+240] has exactly two readers in the ISL region, and one of them, sub_822748D0, is called from sub_82263408 = ScriptPhase::Update. It is the timeline walker, and it confirms every field of the decode independently:

822748E0  lwz  r11, 240(r30)      ; the table
822748E4  lwz  r10,   4(r11)      ; N -- the count, at table+4          ✓
822748E8  addi r11, r11, 8        ; skip the count record               ✓
822748F4  addi r31, r11, 20       ; -> group+20
822748FC  lwz  r11,   0(r31)      ; kind   at group+20                  ✓
82274900  lfs  f0,   -8(r31)      ; t      at group+12, as a FLOAT      ✓
82274904  lwz  r10, 104(r30)      ; clock array A
82274908  rlwinm r11, r11, 2,0,29 ; kind * 4
8227490C  lfsx f13, r10, r11      ; A[kind]
82274910  fcmpu cr6, f13, f0
82274914  bc   ... skip
82274918  lwz  r10,  88(r30)      ; clock array B
8227491C  lfsx f13, r10, r11      ; B[kind]
82274920  fcmpu cr6, f0, f13
82274924  bc   ... skip
8227492C  lwz  r5, -16(r31)       ; offset at group+4                   ✓
82274930  lwz  r4, 232(r30)       ; the code base
82274934  bl   0x822737C8         ; start the routine
8227493C  addi r31, r31, 24       ; STRIDE 24 = three 8-byte records    ✓

Count position, float position, offset position and the 24-byte stride are all confirmed by the engine's own reader rather than by my structural inference.

kind is a CLOCK INDEX, not a flag

🔑 (2026-08-27) And the array it indexes is the one read_freg exposes. Built-in 9's stub bounds-checks 0 <= local[0] < 32 and then

822725E8  lwz  r10, 88(r31)          ; [phase+88]
822725EC  rlwinm r11, r11, 2, 0, 29  ; i * 4
822725F0  lfsx f0, r11, r10          ; a FLOAT

— exactly the array and the indexing the timeline walker uses. So kind is not a selector over some small private set of clocks: it is an index into the same 32-entry float register file the script reads with read_freg(i). A clear condition like read_freg(0) < 1200 and a timeline entry with kind = 0 read the same register.

🔴 The "six clocks" framing below was mine and is too narrow — the file has 32 slots; the timeline data merely never uses more than indices 0 and 5.

rlwinm r11, r11, 2, 0, 29 then lfsxkind is multiplied by 4 and used to index float arrays at [phase+104] and [phase+88]. So kind selects which timer the entry is scheduled against, and 0 / 5 are two of at least six.

The entry fires only when its t lies between the two clock readings — the classic "which events did this frame cross" test, with one array holding the previous value and the other the current. 🟡 Which array is which follows from the comparison directions (A[kind] > t skips; t >= B[kind] skips) and is stated as a reading of those two branches, not measured at runtime.

And the clock is PER PHASE

Both arrays are fields of the phase object, so each phase has its own timers — which is why every phase's table restarts at t = 0. That was previously an inference from the layout; it is now a read.

🟡 Not settled

  • This is not what starts the unreachable code. All 675 targets are already reachable — 0 are unreached run-starts — so the ~15 % gap stands.
  • What the six clocks are. SETTLED — isl-timers. The two arrays are a bank of 32 stopwatches: [phase+88] is the current value, [phase+104] last frame's, [phase+120] a running flag, and sub_822710D0 — called from ScriptPhase::Update — does prev[i] = cur[i]; if running[i] then cur[i] += dt every frame. Built-in 8 starts a timer, 9 reads it, 93 stops it. So the walker's prev <= t < cur test is "did timer kind cross t this frame", and t is in SECONDSdt is frames * (1/60) * 10000 ticks scaled back by 1e-4, clamped at 3200 ticks = 0.32 s. That the numbers in the table looked like mission times is no longer the evidence. 675 / 675 timeline entries name a timer their own phase starts (control 11.2 %), which is why kind is only ever 0 or 5: every phase opens set_flag(5); set_flag(0).
  • sub_822737C8, which actually starts the routine, is not read — it is presumably the same spawner start_coroutine uses, but that is unchecked.