Crusader_Decomp/_tmp_psx_mode1_live_row0_batch.py

134 lines
4.7 KiB
Python
Raw Permalink Normal View History

2026-03-30 00:19:01 +02:00
from __future__ import annotations
import json
import struct
import sys
from pathlib import Path
ROOT = Path(r"k:/ghidra/Crusader_Decomp")
L0_WDL_PATH = Path(r"e:/emu/psx/Crusader - No Remorse/LSET1/L0.WDL")
GPU_PATH = ROOT / "binary/Crusader - No Remorse (USA) GPU RAM.bin"
OUTPUT_DIR = ROOT / "out/psx_wdl/L0/mode1_live_clut_row_f0_x0"
ROW_BYTES = 2048
LIVE_CLUT_Y = 0xF0
LIVE_CLUT_X = 0
sys.path.insert(0, str(ROOT / "tools"))
from psx_extract_wdl import (
colorize_indexed_pixels,
parse_lset_wdl,
scan_sprite_bundles,
write_bundle_atlas,
write_overview_grid,
write_png_rgba,
)
def main() -> None:
l0_data = L0_WDL_PATH.read_bytes()
gpu = GPU_PATH.read_bytes()
summary = parse_lset_wdl(l0_data)
if summary is None:
raise SystemExit("failed to parse L0.WDL")
region = next(region for region in summary["regions"] if region["name"] == "post_audio_region_04")
region_data = l0_data[region["offset"] : region["offset"] + region["size"]]
bundles = scan_sprite_bundles(region_data, max_candidates=160)
row = gpu[LIVE_CLUT_Y * ROW_BYTES : (LIVE_CLUT_Y + 1) * ROW_BYTES]
row_words = struct.unpack("<1024H", row)
palette = list(row_words[LIVE_CLUT_X : LIVE_CLUT_X + 256])
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
entries: list[dict[str, object]] = []
summary_rows: list[dict[str, object]] = []
mode1_count = 0
for bundle in bundles:
if bundle["mode"] != 1 or not bundle["frames"]:
continue
mode1_count += 1
bundle_dir = OUTPUT_DIR / f"bundle_{bundle['offset']:08X}"
bundle_dir.mkdir(parents=True, exist_ok=True)
rendered_frames: list[dict[str, object]] = []
frame_rows: list[dict[str, object]] = []
for frame in bundle["frames"]:
rgba = colorize_indexed_pixels(frame["pixels"], frame["width"], frame["height"], bundle["mode"], palette)
write_png_rgba(bundle_dir / f"frame_{frame['index']:03d}_live_row_f0_x0.png", rgba, frame["width"], frame["height"])
rendered_frames.append(
{
"width": frame["width"],
"height": frame["height"],
"rgba": rgba,
}
)
frame_rows.append(
{
"index": frame["index"],
"width": frame["width"],
"height": frame["height"],
"origin_x": frame["origin_x"],
"origin_y": frame["origin_y"],
"data_start": frame["data_start"],
"consumed": frame["consumed"],
}
)
write_bundle_atlas(bundle_dir / "atlas_live_row_f0_x0.png", rendered_frames)
metadata = {
"offset": bundle["offset"],
"mode": bundle["mode"],
"palette_formula": "live_gpu_row_0xF0_x0_contiguous_256",
"palette_source": {
"gpu_dump": str(GPU_PATH),
"x": LIVE_CLUT_X,
"y": LIVE_CLUT_Y,
},
"frame_count": bundle["frame_count"],
"exported_frames": frame_rows,
}
(bundle_dir / "palette_formula.json").write_text(json.dumps(metadata, indent=2), encoding="ascii")
first_frame = bundle["frames"][0]
first_rgba = rendered_frames[0]["rgba"]
entries.append(
{
"width": first_frame["width"],
"height": first_frame["height"],
"rgba": first_rgba,
"offset": bundle["offset"],
"area": first_frame["width"] * first_frame["height"],
}
)
summary_rows.append(
{
"offset": bundle["offset"],
"width": first_frame["width"],
"height": first_frame["height"],
"frame_count": bundle["frame_count"],
}
)
entries.sort(key=lambda entry: entry["area"], reverse=True)
overview_entries = [{"width": entry["width"], "height": entry["height"], "rgba": entry["rgba"]} for entry in entries]
write_overview_grid(OUTPUT_DIR / "overview_live_row_f0_x0.png", overview_entries, columns=4)
summary_rows.sort(key=lambda row: row["width"] * row["height"], reverse=True)
(OUTPUT_DIR / "summary.json").write_text(
json.dumps(
{
"palette_formula": "live_gpu_row_0xF0_x0_contiguous_256",
"mode1_bundle_count": mode1_count,
"bundles": summary_rows,
},
indent=2,
),
encoding="ascii",
)
print(f"mode1_bundles={mode1_count}")
print(f"overview={OUTPUT_DIR / 'overview_live_row_f0_x0.png'}")
print(f"summary={OUTPUT_DIR / 'summary.json'}")
if __name__ == "__main__":
main()