re: three ISL built-in names were wrong, including the most-used one
All re-read twice — the handler, and the thing it calls — because each had been named from its shape rather than its effect. * id 11 `yield` -> `end_coroutine`. 0x82272624 is li r11,1 ; li r3,3 ; stw r11,164(r31), and the dispatcher's r3==3 arm erases the thread from the active list and returns it to the free list. It destroys the thread. 2945 sites game-wide, 372 in Stage 02 — the most-used built-in there was. * id 5 `await_label` -> `kill_coroutine(label)`. sub_82273B08 kills the thread parked at the target pc, or itself if the target is its own pc. It waits for nothing. * id 100 `push_trigger` -> `reset_phase_threads`. It clears the trigger container and then frees every thread whose pc differs from the caller's — the opposite of pushing a trigger. Corroborated by usage: its 12 Stage 02 sites all sit in the phase terminator, next to timer_stop, clear_flag(-1) and MARK_LAST_PHASE. One name recovered from the game's own text: opcode 992 prints "RequestScriptMessage %s" at 0x820A5700, so id 64 is request_script_message (2683 sites). Return codes documented properly: 1 = restart the coroutine from its entry (previously not recorded at all), 3 = terminate. And the blocking set was wrong in two places — it is 102, 120, 137, 142, 143. Id 97 does NOT block; its handler ends `b 0x822724F8`, so it always returns 0. Unit-operand resolution settled from DATA over all 28 stages rather than by reading 147 handlers: a slot qualifies only if every value is a valid symtab-2 index, it takes >=15 distinct values, AND its maximum reaches most of the table — that last clause is what discriminates, since every small integer is trivially "in range". 31 built-ins at slot 4, 8 at slot 12, one at slot 20. It also refutes set_flag's slot 0, whose maximum overruns the table, and the resolver now declines rather than inventing a name. New and unexplained: symtab-2 holds two types, 2 and 8, and built-ins 95 and 128 take type 8 at slot 12 in 100% of their sites. A downstream inference is withdrawn with it: the note reading the live trigger counter attributed it to "the script arming watches as it goes" via built-in 100. The measurement stands; the attribution does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
@@ -78,18 +78,18 @@ KIND = {0: 'global', 1: 'imm', 2: 'special', 3: 'local'}
|
||||
# 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',
|
||||
5: 'kill_coroutine', 6: 'END_PHASE', 8: 'set_flag', 9: 'read_freg',
|
||||
10: 'random', 11: 'end_coroutine', 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', 46: 'squadron_trace', 47: 'squadron_attack',
|
||||
48: 'squadron_escort', 52: 'play_stream', 53: 'sound_busy', 54: 'stop_sound',
|
||||
56: 'unit_relation', 59: 'fade_sound', 62: 'FORCE_END_PHASE',
|
||||
56: 'unit_relation', 59: 'fade_sound', 62: 'FORCE_END_PHASE', 64: 'request_script_message',
|
||||
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',
|
||||
93: 'clear_flag', 94: 'is_engaged', 95: 'unit_hp_pct', 100: 'reset_phase_threads',
|
||||
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',
|
||||
@@ -135,10 +135,30 @@ def symbols(b, which):
|
||||
return out
|
||||
|
||||
|
||||
UNIT_ARG = {18, 20, 24, 26, 56, 69, 70, 94, 95, 105, 109} # unit idx at blob[4]
|
||||
# Built-ins 71 and 72 carry a SECOND unit index at blob[12] (handler 0x8226E568
|
||||
# indexes [phase+324] with both), so their second operand printed as a bare
|
||||
# integer until this was noticed. 56 may be the same shape -- unverified.
|
||||
# Built-ins whose operand blob carries a symbol-table-2 (unit) index, by slot.
|
||||
#
|
||||
# Derived from the DATA, not from reading 147 handlers: across all 28 stages a
|
||||
# slot qualifies only if every observed value is a valid symtab-2 index, it takes
|
||||
# >=15 distinct values, and its maximum reaches most of the table (symtab-2 tops
|
||||
# out at 122 entries, so a non-index slot overruns). That last clause is what
|
||||
# makes the test discriminating -- plain range-checking cannot separate an index
|
||||
# from a bool, because every small integer is "in range".
|
||||
#
|
||||
# It also refutes one tempting entry: `set_flag`'s slot 0 passes the range and
|
||||
# spread tests but its maximum EXCEEDS the table (flag indices run 0..31 against
|
||||
# tables as small as 40), so it is excluded. Slots are only listed here when the
|
||||
# ratio stayed below 1.0.
|
||||
UNIT_ARG = {2, 3, 7, 12, 15, 16, 18, 19, 20, 24, 25, 26, 28, 29, 30, 47, 48,
|
||||
56, 57, 58, 63, 69, 70, 79, 91, 92, 95, 105, 108, 128, 143}
|
||||
UNIT_ARG2 = {2, 18, 47, 48, 56, 79, 95, 128} # a SECOND unit index at blob[12]
|
||||
UNIT_ARG3 = {128} # and a third at blob[20]
|
||||
UNIT_SLOTS = {4: UNIT_ARG, 12: UNIT_ARG2, 20: UNIT_ARG3}
|
||||
|
||||
# Symbol table 2 holds TWO entity types: type 2 (1160 entries disc-wide) and
|
||||
# type 8 (249). They are not interchangeable -- built-ins 95 and 128 take a
|
||||
# type-2 unit at slot 4 and, at slot 12, an operand that is type 8 in 100% of
|
||||
# its 90 and 152 call sites respectively. What distinguishes the two classes is
|
||||
# not yet established.
|
||||
|
||||
|
||||
def dis(b, off, count=40, code_base=0x24, args=True, sym2=None):
|
||||
@@ -203,7 +223,11 @@ def dis(b, off, count=40, code_base=0x24, args=True, sym2=None):
|
||||
parts = []
|
||||
for slot, v in sorted(staged.items()):
|
||||
txt = ('0x%X' % v) if isinstance(v, int) else v
|
||||
if (sym2 and slot == 4 and words[0] in UNIT_ARG
|
||||
# Resolve only a slot that is declared a unit index AND
|
||||
# whose value really is one -- a resolver that invents a
|
||||
# name for a non-index is worse than one that prints the
|
||||
# raw number.
|
||||
if (sym2 and words[0] in UNIT_SLOTS.get(slot, ())
|
||||
and isinstance(v, int) and v in sym2):
|
||||
txt = sym2[v][1]
|
||||
parts.append(txt)
|
||||
|
||||
72
tools/re-capture/isl_report.py
Normal file
72
tools/re-capture/isl_report.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate the committed ISL artefacts under `docs/re/data/`.
|
||||
|
||||
isl_report.py <StageNN.ssb> calls -> the call-site census + a listing
|
||||
|
||||
The artefact was produced by an uncommitted one-off, so it drifted out of date
|
||||
twice: once when operand staging was fixed (calls printed with too few
|
||||
arguments) and once when three built-in names were corrected. Keeping the
|
||||
generator in the tree is the point of this file.
|
||||
|
||||
`data/isl-stage02-conditions.txt` still has no generator here. A first attempt
|
||||
is not committed because it printed most sites as bare `builtinN`: neither
|
||||
`isl.resync` (it gives up far from a valid start) nor a naive linear decode from
|
||||
the phase base reaches every call site, so producing that listing faithfully
|
||||
needs the coroutine entry points, which `start_coroutine`'s operand carries and
|
||||
this tool does not yet follow.
|
||||
"""
|
||||
import collections
|
||||
import sys
|
||||
|
||||
import isl
|
||||
|
||||
def census(b):
|
||||
cs = isl.call_sites(b)
|
||||
return cs, collections.Counter(bid for _, bid, _ in cs)
|
||||
|
||||
|
||||
def emit_calls(b, path):
|
||||
cs, h = census(b)
|
||||
s2 = isl.symbols(b, 2)
|
||||
print('# %s — ISL disassembly artefacts' % path)
|
||||
print()
|
||||
print('Generated by `tools/re-capture/isl_report.py calls`.')
|
||||
print()
|
||||
print('%d call sites, %d distinct built-ins' % (len(cs), len(h)))
|
||||
for bid, n in h.most_common():
|
||||
print(' builtin %-4d %5d site(s)' % (bid, n))
|
||||
print()
|
||||
print('## phase-control sites')
|
||||
for bid, nm in ((6, 'END_PHASE'), (62, 'FORCE_END_PHASE'),
|
||||
(39, 'MARK_LAST_PHASE'), (40, 'mark_not_last')):
|
||||
offs = [off for off, b2, _ in cs if b2 == bid]
|
||||
print('%-3d %-18s %2d: %s'
|
||||
% (bid, nm, len(offs), ' '.join('0x%x' % o for o in offs)))
|
||||
print()
|
||||
print('## named built-ins used, by traffic')
|
||||
for bid, n in h.most_common():
|
||||
nm = isl.BUILTIN.get(bid)
|
||||
if nm:
|
||||
print(' %-3d %-24s %4d' % (bid, nm, n))
|
||||
print()
|
||||
print('## phase code bases')
|
||||
print('Each phase has its OWN base; the file header offset is not it.')
|
||||
print(' ' + ' '.join('0x%x' % x for x in isl.phase_bases(b)))
|
||||
print()
|
||||
print('## disassembly into the first END_PHASE')
|
||||
target = [off for off, bid, _ in cs if bid == 6][0]
|
||||
start = isl.resync(b, target)
|
||||
print('resync from 0x%X' % start)
|
||||
for line in isl.dis(b, start, 64, code_base=0x24, sym2=s2):
|
||||
print(line)
|
||||
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
b = isl.load(path)
|
||||
name = path.replace('\\', '/').split('/')[-1]
|
||||
{'calls': emit_calls}[sys.argv[2]](b, name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user