134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
|
|
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()
|