re: the 147 ISL built-ins characterised; Stage 02 gates on units, not counts

Table at 0x8227226C is 147 big-endian absolute VAs (verified structurally: it
ends exactly where the first handler begins, all targets inside sub_82272220).
Arguments are not in the instruction -- every handler does c_str() on
[phase+20], a packed blob, which is what the local[] staging fills. Return 2 =
yield; five built-ins block by skipping the pc advance.

Recovered the ScriptPhase state layout: 32-entry float and flag register files,
int/double result registers, the timer block, and the runtime unit array at
+324 indexed by symbol-table-2 index -- a direct hook from bytecode call sites
to the two .ssb symbol tables.

Spot-checked two claims against the disassembly rather than trusting them: id 4
loads a DOUBLE into the thread countdown and returns 2 (wait_s), and id 24 reads
current/initial squadron member counts (squad_survival_pct). Both exact.

Counting Stage02.ssb: unit_state 255, hp_pct_test 167, dist_lt 92, unit_alive
71, unit_relation 52 -- and squad_survival_pct, group_ratio_pct and the two
global counters are NOT called at all. So Stage 02's phases are gated on named
units (destroyed / HP / proximity), never on an aggregate count, even though the
kill-counter primitives exist in the VM. That answers the standing 'next wave
after N kills or after an event?' question for this stage: specific units, not a
number.

isl.py now names the built-ins, so the run-up to the first END_PHASE reads
wait_cmds_drained / fade_sound(3) / builtin85(3) / wait_s(3) / END_PHASE.

Not settled: 3 handlers unresolved (55, 75, 105); the 1024-slot interpreter
command table is only partly recovered.
This commit is contained in:
Sylpheed RE agent
2026-08-25 12:36:02 +00:00
parent 3f13a8ceef
commit 70cff9ca21
3 changed files with 219 additions and 40 deletions

View File

@@ -67,6 +67,30 @@ CODE_BASE_FIELD = 0x08 # .ssb header: code offset (0x24 in every file)
# opcode -> (mnemonic, handler VA) from the jump table
KIND = {0: 'global', 1: 'imm', 2: 'special', 3: 'local'}
# Built-in names, from the 147-entry table at 0x8227226C. Only the ones whose
# handler was actually read are named; the rest print as a bare id rather than a
# guess. See docs/re/structures/isl-builtins.md.
BUILTIN = {
1: 'start_coroutine', 2: 'deploy_squadron', 3: 'move_order', 4: 'wait_s',
5: 'await_label', 6: 'END_PHASE', 8: 'set_flag', 9: 'read_freg',
10: 'random', 11: 'yield', 13: 'play_se', 14: 'play_bgm',
17: 'wait_frames', 18: 'dist_lt', 20: 'hp_pct_test', 24: 'squad_survival_pct',
26: 'damage_unit', 30: 'objective_marker', 31: 'objective_marker_at_route',
33: 'global_counter0', 34: 'global_counter1', 36: 'screen_fade',
39: 'MARK_LAST_PHASE', 40: 'mark_not_last', 43: 'play_voice',
45: 'play_voice_vol', 52: 'play_stream', 53: 'sound_busy', 54: 'stop_sound',
56: 'unit_relation', 59: 'fade_sound', 62: 'FORCE_END_PHASE',
69: 'unit_state', 70: 'unit_alive', 72: 'group_ratio_pct', 73: 'timer_start',
74: 'timer_limit', 88: 'camera_at', 90: 'camera_at_route',
93: 'clear_flag', 94: 'is_engaged', 95: 'unit_hp_pct', 100: 'push_trigger',
102: 'prompt_yes_no', 109: 'set_unit_flags', 115: 'named_event',
120: 'wait_cmds_drained', 123: 'timer_resume', 124: 'timer_stop',
125: 'timer_reset', 126: 'timer_elapsed', 127: 'timer_set',
132: 'player_gauge0_test', 133: 'player_gauge1_test', 134: 'player_byte',
137: 'wait_units_ready', 139: 'fade_to_black_end', 142: 'deploy_and_wait',
143: 'deploy_and_wait2', 145: 'random_rand',
}
OPS = {
0: 'set.i', 1: 'set.f',
2: 'cmp.a', 4: 'cmp.a', 6: 'cmp.a', 8: 'cmp.a',
@@ -129,7 +153,7 @@ def dis(b, off, count=40, code_base=0x24, args=True):
elif k0 == 3 and k1 == 2 and pending is not None:
staged[words[0]] = pending
if op == 19 and words:
extra = ' builtin=%d' % words[0]
extra = ' %s' % BUILTIN.get(words[0], 'builtin%d' % words[0])
if args and staged:
extra += '(' + ', '.join(
'%s' % (('0x%X' % v) if isinstance(v, int) else v)