Preserve current legacy presentation and options improvements
This commit is contained in:
1 parent
214789fe87
commit
eee082f2ad
15 files changed
+1090
-73
No files matched your search
@@ -0,0 +1,104 @@
|
||||
"""Spec for the re-authored data/ui/OPTS_SWD (catalog handle 0x1003f).
|
||||
|
||||
Same window/dialog as the original, grown from 13 to 23 fields: the original
|
||||
Music/FX/Detail rows plus FILTER, SCANLINES, RESOLUTION, FULLSCREEN and
|
||||
ASPECT rows and their selection icons. Window-relative coordinates assume
|
||||
the window at x=60, y=6, 200x188 (320x200 screen space).
|
||||
|
||||
Layout (window-relative):
|
||||
y=8 OPTIONS title (field 8)
|
||||
y=20 panel bumpin behind rows (field 2, 5,20 190x100)
|
||||
y=24 DETAIL row + PIC1 (fields 6, 3)
|
||||
y=40 FILTER row + PIC4 (13, 18)
|
||||
y=56 SCANLINES row + PIC5 (14, 19)
|
||||
y=72 RES row + PIC6 (15, 20)
|
||||
y=88 FULLSCR row + PIC7 (16, 21)
|
||||
y=104 ASPECT row + PIC8 (17, 22)
|
||||
y=122 MUSIC VOLUME label (9)
|
||||
y=131 music slider view/track (11, 0)
|
||||
y=146 SOUND FX VOLUME label (10)
|
||||
y=160 fx slider view/track (12, 1)
|
||||
y=174 EXIT button (7)
|
||||
|
||||
Build: python3 tools/swdbuild.py build tools/opts_swd_spec.py data/ui/OPTS_SWD
|
||||
(the printed size must be written into data/INDEX's `1003f` line)
|
||||
"""
|
||||
|
||||
from swdbuild import (BUMPIN_TEMPLATE, ICON_TEMPLATE, BUTTON_TEMPLATE,
|
||||
TEXT_TEMPLATE, VIEWAREA_TEMPLATE)
|
||||
|
||||
SWIN = {
|
||||
"version": 1, "swdsize": 0, "arrowflag": 0, "display": 0,
|
||||
"opt3": 0, "opt4": 0, "id": 2, "type": 0,
|
||||
"name": "OPTS", "item_name": "TEXTURE_PIC",
|
||||
"item": 65559, "picflag": 1, "lock": 1, "fldofs": 120, "txtofs": 0,
|
||||
"firstfld": 0, "opt": 1, "color": 1, "numflds": 23,
|
||||
"x": 60, "y": 6, "lx": 200, "ly": 188, "shadow": 1,
|
||||
# swdsize/txtofs are recomputed by the builder (file size / blob offset).
|
||||
}
|
||||
|
||||
# All rows share the button look; value texts are overwritten at runtime via
|
||||
# SWD_SetFieldText, so the stored strings are just the initial/label values.
|
||||
_ROW = {**BUTTON_TEMPLATE, "x": 30, "lx": 140, "maxchars": 22}
|
||||
_ICON = {**ICON_TEMPLATE, "x": 12, "y": 24}
|
||||
_LABEL = {**TEXT_TEMPLATE, "x": 30}
|
||||
|
||||
FIELDS = [
|
||||
# 0-2: bumpins (slider tracks + row panel), repositioned originals
|
||||
{**BUMPIN_TEMPLATE, "x": 30, "y": 136, "lx": 127, "ly": 3},
|
||||
{**BUMPIN_TEMPLATE, "x": 30, "y": 165, "lx": 127, "ly": 3},
|
||||
{**BUMPIN_TEMPLATE, "x": 5, "y": 20, "lx": 190, "ly": 100},
|
||||
|
||||
# 3-5: selection icons for detail / music / fx rows
|
||||
{**_ICON, "name": "PIC1", "y": 24},
|
||||
{**_ICON, "name": "PIC2", "y": 122},
|
||||
{**_ICON, "name": "PIC3", "y": 146},
|
||||
|
||||
# 6-7: buttons
|
||||
{**_ROW, "name": "DETAIL", "hotkey": 0x20, "y": 24, "text_index": 2},
|
||||
{**_ROW, "name": "EXIT", "id": 4, "hotkey": 0, "x": 118, "y": 174,
|
||||
"lx": 57, "maxchars": 5, "text_index": 3},
|
||||
|
||||
# 8-10: texts
|
||||
{**TEXT_TEMPLATE, "id": 200, "input_opt": 1, "item_name": "",
|
||||
"font_name": "FONT1_FNT", "fontid": 0x10003, "x": 68, "y": 8,
|
||||
"lx": 71, "ly": 14, "maxchars": 8, "text_index": 4},
|
||||
{**_LABEL, "id": 150, "y": 122, "lx": 104, "maxchars": 13,
|
||||
"text_index": 5},
|
||||
{**_LABEL, "id": 160, "input_opt": 1, "y": 146, "lx": 112,
|
||||
"maxchars": 16, "text_index": 6},
|
||||
|
||||
# 11-12: volume slider view areas
|
||||
{**VIEWAREA_TEMPLATE, "name": "VMUSIC", "x": 30, "y": 131},
|
||||
{**VIEWAREA_TEMPLATE, "name": "VFX", "x": 30, "y": 160},
|
||||
|
||||
# 13-17: video option rows (hotkeys: F, L, R, -, A)
|
||||
{**_ROW, "name": "FILTER", "hotkey": 0x21, "y": 40, "text_index": 7},
|
||||
{**_ROW, "name": "SCANLINES", "hotkey": 0x26, "y": 56, "text_index": 8},
|
||||
{**_ROW, "name": "RES", "hotkey": 0x13, "y": 72, "text_index": 9},
|
||||
{**_ROW, "name": "FULLSCR", "hotkey": 0, "y": 88, "text_index": 10},
|
||||
{**_ROW, "name": "ASPECT", "hotkey": 0x1E, "y": 104, "text_index": 11},
|
||||
|
||||
# 18-22: selection icons for the five video rows
|
||||
{**_ICON, "name": "PIC4", "y": 40},
|
||||
{**_ICON, "name": "PIC5", "y": 56},
|
||||
{**_ICON, "name": "PIC6", "y": 72},
|
||||
{**_ICON, "name": "PIC7", "y": 88},
|
||||
{**_ICON, "name": "PIC8", "y": 104},
|
||||
]
|
||||
|
||||
# 0-1 ("Y","N") are the original bumpin marks; nothing references them at
|
||||
# runtime but they are kept for parity with the extracted asset.
|
||||
TEXTS = [
|
||||
"Y", "N",
|
||||
"DETAIL HIGH ", # 2: overwritten at open by WIN_Opts
|
||||
"EXIT", # 3
|
||||
"OPTIONS", # 4
|
||||
"MUSIC VOLUME", # 5
|
||||
"SOUND FX VOLUME", # 6
|
||||
"FILTER SCALE 3X", # 7: overwritten at open
|
||||
"SCANLINES OFF", # 8: overwritten at open
|
||||
"RESOLUTION 960X720", # 9: overwritten at open
|
||||
"FULLSCREEN OFF", # 10: overwritten at open
|
||||
"ASPECT 4:3 ON", # 11: overwritten at open
|
||||
]
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dump and build Raptor .SWD dialog assets.
|
||||
|
||||
On-disk format (little-endian), mirrored from src/swdapi.h:
|
||||
|
||||
SWIN header 120 bytes <8i 16s 16s 14i>
|
||||
SFIELD32[] 148 bytes <8i 16s 16s i 16s 15i i> (sptr -> PlaceHolder)
|
||||
text blob NUL-padded strings
|
||||
|
||||
Each TEXT/BUTTON/INPUT/DRAGBAR field stores its text at `txtoff` RELATIVE
|
||||
TO ITS OWN RECORD (SWD_ReformatFieldData compensates the 64-bit struct
|
||||
growth with `(numflds - i) * 4`, which only closes under that reading).
|
||||
Other field types never dereference txtoff.
|
||||
|
||||
Modes:
|
||||
dump <file.swd> pretty-print SWIN, fields, text blob
|
||||
build <spec.py> <out.swd> assemble a .swd from a Python spec
|
||||
|
||||
Spec module contract (see tools/opts_swd_spec.py):
|
||||
from swdbuild import BUTTON_TEMPLATE, ... # clone-source records
|
||||
SWIN = {...} # all 24 ints + name/item_name strings
|
||||
FIELDS = [ {...}, ... ] # {**TEMPLATE, **overrides}; "text_index" links
|
||||
# the field to TEXTS[i] and computes txtoff
|
||||
TEXTS = ["...", ...] # blob entries, laid out in list order
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SWIN_STRUCT = struct.Struct("<8i16s16s14i")
|
||||
FIELD_STRUCT = struct.Struct("<8i16s16si16s15ii")
|
||||
|
||||
SWIN_KEYS = [
|
||||
"version", "swdsize", "arrowflag", "display", "opt3", "opt4", "id", "type",
|
||||
"name", "item_name",
|
||||
"item", "picflag", "lock", "fldofs", "txtofs", "firstfld", "opt", "color",
|
||||
"numflds", "x", "y", "lx", "ly", "shadow",
|
||||
]
|
||||
FIELD_KEYS = [
|
||||
"opt", "id", "hotkey", "kbflag", "opt3", "opt4", "input_opt", "bstatus",
|
||||
"name", "item_name", "item", "font_name",
|
||||
"fontid", "fontbasecolor", "maxchars", "picflag", "color", "lite", "mark",
|
||||
"saveflag", "shadow", "selectable", "x", "y", "lx", "ly",
|
||||
]
|
||||
|
||||
FLD_TEXT, FLD_BUTTON, FLD_INPUT, FLD_DRAGBAR = 1, 2, 5, 6 # txtoff-carrying
|
||||
|
||||
|
||||
def _s(raw: bytes) -> str:
|
||||
return raw.split(b"\0", 1)[0].decode("ascii", "replace")
|
||||
|
||||
|
||||
def _pack_str(text, field):
|
||||
raw = text.encode("ascii")
|
||||
if len(raw) > 16:
|
||||
raise ValueError(f"{field}: string longer than 16 chars: {text!r}")
|
||||
return raw.ljust(16, b"\0")
|
||||
|
||||
|
||||
def _check_keys(where, given, required, optional=()):
|
||||
missing = [k for k in required if k not in given]
|
||||
extra = [k for k in given if k not in required and k not in optional]
|
||||
if missing or extra:
|
||||
raise ValueError(f"{where}: missing {missing}, unknown {extra}")
|
||||
|
||||
BUMPIN_TEMPLATE = {
|
||||
"opt": 7, "id": 0, "hotkey": 0, "kbflag": 10, "opt3": 0, "opt4": 0,
|
||||
"input_opt": 0, "bstatus": 0,
|
||||
"name": "", "item_name": "",
|
||||
"item": 0, "font_name": "SYSTEM_FNT", "fontid": 5, "fontbasecolor": 0,
|
||||
"maxchars": 3, "picflag": 0, "color": 1, "lite": 15, "mark": 0,
|
||||
"saveflag": 0, "shadow": 0, "selectable": 0,
|
||||
"x": 0, "y": 0, "lx": 127, "ly": 3,
|
||||
}
|
||||
ICON_TEMPLATE = {
|
||||
"opt": 9, "id": 0, "hotkey": 0, "kbflag": 30, "opt3": 0, "opt4": 0,
|
||||
"input_opt": 0, "bstatus": 0,
|
||||
"name": "PIC1", "item_name": "",
|
||||
"item": -1, "font_name": "SYSTEM_FNT", "fontid": 5, "fontbasecolor": 0,
|
||||
"maxchars": 1, "picflag": 2, "color": 8, "lite": 15, "mark": 0,
|
||||
"saveflag": 0, "shadow": 0, "selectable": 0,
|
||||
"x": 11, "y": 35, "lx": 16, "ly": 11,
|
||||
}
|
||||
BUTTON_TEMPLATE = {
|
||||
"opt": 2, "id": 1, "hotkey": 0x20, "kbflag": 80, "opt3": 0, "opt4": 0,
|
||||
"input_opt": 0, "bstatus": 0,
|
||||
"name": "DETAIL", "item_name": "STEXTURE_PIC",
|
||||
"item": 0x10024, "font_name": "FONT2_FNT", "fontid": 0x10004,
|
||||
"fontbasecolor": 82, "maxchars": 13, "picflag": 1, "color": 8, "lite": 15,
|
||||
"mark": 0, "saveflag": 0, "shadow": 1, "selectable": 0,
|
||||
"x": 35, "y": 35, "lx": 118, "ly": 12,
|
||||
}
|
||||
TEXT_TEMPLATE = {
|
||||
"opt": 1, "id": 150, "hotkey": 0, "kbflag": 90, "opt3": 0, "opt4": 0,
|
||||
"input_opt": 0, "bstatus": 0,
|
||||
"name": "", "item_name": "",
|
||||
"item": 0, "font_name": "FONT2_FNT", "fontid": 0x10004, "fontbasecolor": 82,
|
||||
"maxchars": 13, "picflag": 0, "color": 8, "lite": 15, "mark": 0,
|
||||
"saveflag": 0, "shadow": 0, "selectable": 0,
|
||||
"x": 35, "y": 57, "lx": 104, "ly": 8,
|
||||
}
|
||||
VIEWAREA_TEMPLATE = {
|
||||
"opt": 11, "id": 0, "hotkey": 0, "kbflag": 110, "opt3": 0, "opt4": 0,
|
||||
"input_opt": 0, "bstatus": 0,
|
||||
"name": "VMUSIC", "item_name": "",
|
||||
"item": 0, "font_name": "SYSTEM_FNT", "fontid": 5, "fontbasecolor": 0,
|
||||
"maxchars": 1, "picflag": 0, "color": 8, "lite": 15, "mark": 0,
|
||||
"saveflag": 0, "shadow": 0, "selectable": 0,
|
||||
"x": 35, "y": 68, "lx": 127, "ly": 13,
|
||||
}
|
||||
def parse(path: Path):
|
||||
"""Return (swin dict, field dicts, blob bytes) for a .swd file."""
|
||||
data = path.read_bytes()
|
||||
if len(data) < SWIN_STRUCT.size:
|
||||
raise ValueError(f"{path}: {len(data)} bytes, shorter than SWIN header")
|
||||
|
||||
raw = SWIN_STRUCT.unpack_from(data, 0)
|
||||
swin = dict(zip(SWIN_KEYS[:8], raw[:8]))
|
||||
swin["name"] = _s(raw[8])
|
||||
swin["item_name"] = _s(raw[9])
|
||||
swin.update(dict(zip(SWIN_KEYS[10:], raw[10:])))
|
||||
|
||||
numflds = swin["numflds"]
|
||||
fields = []
|
||||
for i in range(numflds):
|
||||
off = swin["fldofs"] + i * FIELD_STRUCT.size
|
||||
if off + FIELD_STRUCT.size > len(data):
|
||||
raise ValueError(f"{path}: field {i} at {off} past EOF")
|
||||
fr = FIELD_STRUCT.unpack_from(data, off)
|
||||
fld = dict(zip(FIELD_KEYS[:8], fr[:8]))
|
||||
fld["name"] = _s(fr[8])
|
||||
fld["item_name"] = _s(fr[9])
|
||||
fld["item"] = fr[10]
|
||||
fld["font_name"] = _s(fr[11])
|
||||
fld.update(dict(zip(FIELD_KEYS[12:], fr[12:26])))
|
||||
fld["txtoff"] = fr[26]
|
||||
fld["_offset"] = off
|
||||
fields.append(fld)
|
||||
|
||||
blob_start = swin["fldofs"] + numflds * FIELD_STRUCT.size
|
||||
return swin, fields, data[blob_start:]
|
||||
|
||||
|
||||
def dump(path: Path) -> None:
|
||||
swin, fields, blob = parse(path)
|
||||
print(f"file: {path} ({path.stat().st_size} bytes, blob at "
|
||||
f"{SWIN_STRUCT.size + len(fields) * FIELD_STRUCT.size})")
|
||||
print("SWIN:")
|
||||
for k in SWIN_KEYS:
|
||||
print(f" {k:<12} {swin[k]!r}")
|
||||
for i, fld in enumerate(fields):
|
||||
print(f"field[{i:2d}] at +{fld['_offset']}:")
|
||||
for k in FIELD_KEYS:
|
||||
print(f" {k:<14} {fld[k]!r}")
|
||||
if fld["opt"] in (FLD_TEXT, FLD_BUTTON, FLD_INPUT, FLD_DRAGBAR):
|
||||
blob_start = SWIN_STRUCT.size + len(fields) * FIELD_STRUCT.size
|
||||
t = blob[fld["_offset"] + fld["txtoff"] - blob_start:]
|
||||
text = t.split(b"\0", 1)[0].decode("ascii", "replace")
|
||||
print(f" text {text!r}")
|
||||
print()
|
||||
|
||||
|
||||
def build(spec_path: Path, out_path: Path) -> None:
|
||||
# Import swdbuild itself under its own name so specs can pull templates.
|
||||
self_spec = importlib.util.spec_from_file_location(
|
||||
"swdbuild", Path(__file__).resolve())
|
||||
self_mod = importlib.util.module_from_spec(self_spec)
|
||||
sys.modules["swdbuild"] = self_mod
|
||||
self_spec.loader.exec_module(self_mod)
|
||||
spec = importlib.util.spec_from_file_location("swd_spec", spec_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
swin_in, fields_in, texts = mod.SWIN, mod.FIELDS, mod.TEXTS
|
||||
_check_keys("SWIN", swin_in, SWIN_KEYS)
|
||||
if len(fields_in) != swin_in["numflds"]:
|
||||
raise ValueError(f"SWIN.numflds={swin_in['numflds']} but "
|
||||
f"len(FIELDS)={len(fields_in)}")
|
||||
if swin_in["fldofs"] != SWIN_STRUCT.size:
|
||||
raise ValueError(f"SWIN.fldofs must be {SWIN_STRUCT.size}")
|
||||
|
||||
# Lay out the text blob: entry i is padded to the max slot size its
|
||||
# referencing fields need (SWD_SetFieldText writes maxchars bytes).
|
||||
slot_size = [len(t.encode("ascii")) + 1 for t in texts]
|
||||
for i, fld in enumerate(fields_in):
|
||||
_check_keys(f"FIELDS[{i}] (after template merge)", fld,
|
||||
FIELD_KEYS, optional=("text_index",))
|
||||
ti = fld.get("text_index")
|
||||
if ti is not None:
|
||||
if not 0 <= ti < len(texts):
|
||||
raise ValueError(f"FIELDS[{i}]: text_index {ti} out of range")
|
||||
slot_size[ti] = max(slot_size[ti], fld["maxchars"])
|
||||
|
||||
blob = bytearray()
|
||||
slot_off = []
|
||||
for text, size in zip(texts, slot_size):
|
||||
raw = text.encode("ascii")
|
||||
if len(raw) >= size:
|
||||
raise ValueError(f"TEXTS entry {text!r} does not fit slot {size}")
|
||||
slot_off.append(len(blob))
|
||||
blob += raw.ljust(size, b"\0")
|
||||
blob_start = SWIN_STRUCT.size + len(fields_in) * FIELD_STRUCT.size
|
||||
|
||||
# Header bookkeeping: total file size and blob offset (the original
|
||||
# asset stores exactly these; specs pass 0 and get the real values).
|
||||
swin = dict(swin_in)
|
||||
swin["swdsize"] = blob_start + len(blob)
|
||||
swin["txtofs"] = blob_start
|
||||
|
||||
out = bytearray()
|
||||
swin_vals = [swin[k] for k in SWIN_KEYS[:8]]
|
||||
out += SWIN_STRUCT.pack(*swin_vals,
|
||||
_pack_str(swin["name"], "SWIN.name"),
|
||||
_pack_str(swin["item_name"], "SWIN.item_name"),
|
||||
*[swin[k] for k in SWIN_KEYS[10:]])
|
||||
|
||||
for i, fld in enumerate(fields_in):
|
||||
rec_off = SWIN_STRUCT.size + i * FIELD_STRUCT.size
|
||||
txtoff = 0
|
||||
ti = fld.get("text_index")
|
||||
if ti is not None:
|
||||
txtoff = blob_start + slot_off[ti] - rec_off
|
||||
out += FIELD_STRUCT.pack(
|
||||
*[fld[k] for k in FIELD_KEYS[:8]],
|
||||
_pack_str(fld["name"], f"FIELDS[{i}].name"),
|
||||
_pack_str(fld["item_name"], f"FIELDS[{i}].item_name"),
|
||||
fld["item"],
|
||||
_pack_str(fld["font_name"], f"FIELDS[{i}].font_name"),
|
||||
*[fld[k] for k in FIELD_KEYS[12:]],
|
||||
txtoff, 0)
|
||||
|
||||
assert len(out) == blob_start
|
||||
out += blob
|
||||
out_path.write_bytes(out)
|
||||
print(f"{out_path}: {len(out)} bytes "
|
||||
f"({len(fields_in)} fields, {len(texts)} text slots)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="mode", required=True)
|
||||
sub.add_parser("dump").add_argument("swd", type=Path)
|
||||
bp = sub.add_parser("build")
|
||||
bp.add_argument("spec", type=Path)
|
||||
bp.add_argument("out", type=Path)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.mode == "dump":
|
||||
dump(args.swd)
|
||||
else:
|
||||
build(args.spec, args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user