Decode SPINIT H-scene gallery

This commit is contained in:
gamer147
2026-07-23 21:57:32 -04:00
parent 57c45a5e1c
commit 4c06cd9568
11 changed files with 336 additions and 8 deletions

View File

@@ -672,6 +672,11 @@ MAP_TEXTURE_SLOT_COUNT = 20
TERRAIN_DEFINITION_SPAN = 30
TERRAIN_SHIPPED_ID_MAX = 19
H_SCENE_GALLERY_SCRIPT_BASE = 0x6638B
H_SCENE_GALLERY_PAGE_COUNT = 8
H_SCENE_GALLERY_SLOTS_PER_PAGE = 15
H_SCENE_GALLERY_THUMBNAIL_BASE = 0x66421
def resolve(name: str) -> Path:
for cand in (paths.GAME_DIR / f"{name}.BIN", paths.DATA1 / f"{name}.BIN"):
@@ -3630,6 +3635,155 @@ def _terrain_definitions(max_terrain_id: int) -> list[dict]:
]
def extract_h_scene_gallery(scr):
"""Extract SPINIT's eight-page, fifteen-slot HMODE script registry."""
values: dict[tuple[int, int], int] = {}
classified_offsets = set()
static_write_count = 0
for ins in scr.instructions:
write = _static_global_write(ins)
if write is None:
continue
static_write_count += 1
destination, value = write
if not isinstance(value, int):
raise ValueError(
f"{scr.path.name}: non-static H-gallery value "
f"at 0x{ins.offset:x}"
)
index = destination - H_SCENE_GALLERY_SCRIPT_BASE
capacity = (
H_SCENE_GALLERY_PAGE_COUNT
* H_SCENE_GALLERY_SLOTS_PER_PAGE
)
if not 0 <= index < capacity:
raise ValueError(
f"{scr.path.name}: H-gallery write 0x{destination:x} "
f"outside the {capacity}-cell registry"
)
page, slot = divmod(index, H_SCENE_GALLERY_SLOTS_PER_PAGE)
_store_unique(values, (page, slot), value, page)
classified_offsets.add(ins.offset)
exit_offsets = {
ins.offset
for ins in scr.instructions
if sys4load.display_label(ins.opcode) == "exit"
}
classified_offsets.update(exit_offsets)
unclassified = [
f"0x{ins.offset:x}"
for ins in scr.instructions
if ins.offset not in classified_offsets
]
if unclassified:
raise ValueError(
f"{scr.path.name}: unclassified instructions "
+ ", ".join(unclassified)
)
if len(exit_offsets) != 1:
raise ValueError(
f"{scr.path.name}: expected one exit, found {len(exit_offsets)}"
)
init_scr = sys4load.load(resolve("INIT2"))
thumbnail_assets = {}
for ins in init_scr.instructions:
write = _static_global_write(ins)
if write is None:
continue
destination, value = write
page = destination - H_SCENE_GALLERY_THUMBNAIL_BASE
if 0 <= page < H_SCENE_GALLERY_PAGE_COUNT:
_store_unique(thumbnail_assets, page, value, page)
expected_pages = set(range(H_SCENE_GALLERY_PAGE_COUNT))
if set(thumbnail_assets) != expected_pages:
raise ValueError(
f"INIT2.BIN: H-gallery thumbnail pages are "
f"{sorted(thumbnail_assets)}, expected "
f"0..{H_SCENE_GALLERY_PAGE_COUNT - 1}"
)
names = callscript_names()
table_key = f"0x{H_SCENE_GALLERY_SCRIPT_BASE:x}"
records = []
empty_cells = []
for page in range(H_SCENE_GALLERY_PAGE_COUNT):
script_ids = []
scenes = []
raw_fields = {}
for slot in range(H_SCENE_GALLERY_SLOTS_PER_PAGE):
script_id = values.get((page, slot), 0)
script_ids.append(script_id)
if not script_id:
empty_cells.append({"page": page, "slot": slot})
continue
script_name = names.get(script_id, "")
scenes.append({
"slot": slot,
"script_resource_id": script_id,
"script_name": script_name,
})
raw_fields[
f"{table_key}/{H_SCENE_GALLERY_SLOTS_PER_PAGE}/{slot}"
] = script_id
thumbnail_asset_id = thumbnail_assets[page]
records.append({
"id": page,
"name": f"page_{page}",
"thumbnail_sheet_asset_id": thumbnail_asset_id,
"thumbnail_sheet_asset_name": names.get(
thumbnail_asset_id, ""
),
"script_resource_ids": script_ids,
"scenes": scenes,
"record_fields": raw_fields,
})
return records, {
"schema": "h-scene-gallery-pages",
"page_count": H_SCENE_GALLERY_PAGE_COUNT,
"slots_per_page": H_SCENE_GALLERY_SLOTS_PER_PAGE,
"registry_capacity": (
H_SCENE_GALLERY_PAGE_COUNT
* H_SCENE_GALLERY_SLOTS_PER_PAGE
),
"populated_scene_count": len(values),
"empty_cells": empty_cells,
"script_registry_base": table_key,
"thumbnail_sheet_array_base": (
f"0x{H_SCENE_GALLERY_THUMBNAIL_BASE:x}"
),
"thumbnail_sheet_source": "INIT2.BIN",
"array_layouts": {
table_key: {"stride": H_SCENE_GALLERY_SLOTS_PER_PAGE},
},
"schema_field_semantics": {
table_key: "h_scene_gallery_script_ids",
},
"static_write_count": static_write_count,
"classified_static_write_count": len(values),
"classified_instruction_count": len(classified_offsets),
"resolved_scene_script_count": sum(
bool(scene["script_name"])
for record in records
for scene in record["scenes"]
),
"resolved_thumbnail_sheet_count": sum(
bool(record["thumbnail_sheet_asset_name"])
for record in records
),
"consumer_contract": {
"HMODE.BIN": (
"compact the eight configured INIT2 thumbnail pages, scan "
"their fifteen SPINIT script slots, filter each populated "
"resource through opcode 0x19d, and call-script the selected "
"available scene"
),
},
}
def _map_stage_definitions() -> list[dict]:
"""Read the STINIT2 records that own all four terrain-atlas bounds."""
stage_scr = sys4load.load(resolve("STINIT2"))
@@ -4241,6 +4395,10 @@ def write_data_index(data_dir: Path) -> None:
"effect descriptions, four parallel topology/rendering arrays, the ten-column",
"combat-stat matrix, SKINIT traversal-skill joins, and shared texture fallbacks.",
"",
"SPINIT's dedicated H-scene gallery schema exposes eight fifteen-slot pages,",
"joins every page to its INIT2 SO027 thumbnail sheet, resolves all 118 populated",
"scene resources, and retains the two implicit empty cells in the final page.",
"",
"MPINIT's dedicated terrain-atlas schema exposes 1,472 authored rows of a sparse",
"53-column half-tile grid. It joins STINIT2's doubled tile-bound rectangles to 66",
"stage definitions, preserves implicit-zero rows and raw footer provenance, and",
@@ -4333,6 +4491,8 @@ def main() -> int:
extractor = extract_voice_configuration
elif mode == "name" and name == "LAINIT":
extractor = extract_terrain_definitions
elif mode == "numeric" and name == "SPINIT":
extractor = extract_h_scene_gallery
elif mode == "footer" and name == "MPINIT":
extractor = extract_map_terrain_atlas
recs, meta = extractor(scr)

View File

@@ -346,6 +346,25 @@ def profile_terrain_definitions(data: dict) -> dict:
}
def profile_h_scene_gallery(data: dict) -> dict:
"""Summarize SPINIT's HMODE page/slot script registry."""
if data.get("schema") != "h-scene-gallery-pages":
return {}
return {
"page_count": data.get("page_count", 0),
"slots_per_page": data.get("slots_per_page", 0),
"registry_capacity": data.get("registry_capacity", 0),
"populated_scene_count": data.get("populated_scene_count", 0),
"empty_cells": data.get("empty_cells", []),
"resolved_scene_script_count": data.get(
"resolved_scene_script_count", 0
),
"resolved_thumbnail_sheet_count": data.get(
"resolved_thumbnail_sheet_count", 0
),
}
def profile_messages(data: dict) -> dict:
"""Summarize the joined player-facing message evidence."""
records = data["records"]
@@ -473,7 +492,22 @@ def render_markdown(data: dict, rows: list[dict], limit: int) -> str:
f"- records: {data['record_count']}",
f"- populated fields: {len(rows)}",
]
if terrain_profile := profile_terrain_definitions(data):
if h_gallery_profile := profile_h_scene_gallery(data):
lines.extend([
f"- geometry: {h_gallery_profile['page_count']} pages × "
f"{h_gallery_profile['slots_per_page']} slots",
f"- populated scenes: "
f"{h_gallery_profile['populated_scene_count']}/"
f"{h_gallery_profile['registry_capacity']}",
f"- resolved scripts: "
f"{h_gallery_profile['resolved_scene_script_count']}/"
f"{h_gallery_profile['populated_scene_count']}",
f"- resolved thumbnail sheets: "
f"{h_gallery_profile['resolved_thumbnail_sheet_count']}/"
f"{h_gallery_profile['page_count']}",
f"- empty cells: {h_gallery_profile['empty_cells']}",
])
elif terrain_profile := profile_terrain_definitions(data):
lines.extend([
f"- shipped terrain ids: "
f"{terrain_profile['shipped_record_count']} inside a "
@@ -632,6 +666,7 @@ def main() -> int:
"banked_profile": profile_banked(data),
"map_atlas_profile": profile_map_atlas(data),
"terrain_definition_profile": profile_terrain_definitions(data),
"h_scene_gallery_profile": profile_h_scene_gallery(data),
"columns": sorted(rows, key=lambda row: (
int(row["base"], 16), row["stride"] or 0, row["column"] or 0
)),

View File

@@ -1523,6 +1523,55 @@ def test_terrain_definitions() -> None:
)
def test_h_scene_gallery() -> None:
scripts = paths.scripts()
script = sys4load.load(scripts["SPINIT.BIN"])
check(
extract_init.detect_mode(script) == "numeric",
"SPINIT remains compatible with numeric-mode auto-detection",
)
records, meta = extract_init.extract_h_scene_gallery(script)
check(
len(records) == 8
and meta["page_count"] == 8
and meta["slots_per_page"] == 15
and meta["registry_capacity"] == 120,
"SPINIT exposes HMODE's eight-by-fifteen page geometry",
)
check(
meta["static_write_count"] == 118
and meta["classified_static_write_count"] == 118
and meta["classified_instruction_count"] == 119
and meta["empty_cells"]
== [{"page": 7, "slot": 13}, {"page": 7, "slot": 14}],
"SPINIT classifies every scene cell and the two implicit empty slots",
)
check(
records[0]["thumbnail_sheet_asset_name"] == "SO027A.AGF"
and records[7]["thumbnail_sheet_asset_name"] == "SO027H.AGF"
and meta["resolved_thumbnail_sheet_count"] == 8,
"SPINIT pages join to all eight INIT2 HMODE thumbnail sheets",
)
check(
records[0]["scenes"][0]
== {
"slot": 0,
"script_resource_id": 0x151E,
"script_name": "SP0800.BIN",
}
and records[2]["scenes"][3]["script_name"] == "SP1200.BIN"
and records[7]["scenes"][-1]["script_name"] == "SP0179.BIN"
and meta["resolved_scene_script_count"] == 118,
"SPINIT resolves every populated cell to its call-script resource",
)
check(
len(records[7]["script_resource_ids"]) == 15
and records[7]["script_resource_ids"][-2:] == [0, 0]
and len(records[7]["record_fields"]) == 13,
"SPINIT preserves complete rows beside authored raw-cell provenance",
)
def test_condition_definitions() -> None:
scripts = paths.scripts()
script = sys4load.load(scripts["ILINIT.BIN"])
@@ -1738,6 +1787,7 @@ if __name__ == "__main__":
test_name_entry_palette()
test_voice_configuration()
test_terrain_definitions()
test_h_scene_gallery()
test_map_terrain_atlas()
test_condition_definitions()
test_field_semantics()

View File

@@ -245,6 +245,28 @@ def main() -> int:
assert "- shipped terrain ids: 20 inside a 30-row table" in rendered_terrain
assert "- combat-stat cells: 13" in rendered_terrain
h_gallery_fixture = {
"table": "SP",
"mode": "numeric",
"schema": "h-scene-gallery-pages",
"record_count": 8,
"page_count": 8,
"slots_per_page": 15,
"registry_capacity": 120,
"populated_scene_count": 118,
"empty_cells": [{"page": 7, "slot": 13}, {"page": 7, "slot": 14}],
"resolved_scene_script_count": 118,
"resolved_thumbnail_sheet_count": 8,
"records": [],
}
h_gallery_summary = profile.profile_h_scene_gallery(h_gallery_fixture)
assert h_gallery_summary["page_count"] == 8
assert h_gallery_summary["populated_scene_count"] == 118
assert h_gallery_summary["resolved_thumbnail_sheet_count"] == 8
rendered_h_gallery = profile.render_markdown(h_gallery_fixture, [], 40)
assert "- geometry: 8 pages × 15 slots" in rendered_h_gallery
assert "- populated scenes: 118/120" in rendered_h_gallery
messages = profile.profile_messages(fixture)
assert messages["population"] == 1
assert messages["coverage"] == 1 / 3