126 lines
4.1 KiB
Python
Executable File
126 lines
4.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate and summarize the exact game471 stage-note wire format."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import collections
|
|
import pathlib
|
|
import struct
|
|
|
|
|
|
NOTE = struct.Struct(">IBB9hB3fBB4f3IfI5fI")
|
|
TYPE_NAMES = (
|
|
"NONE", "NORMAL", "FLICK", "HOLD", "SCRATCH", "BEAT", "MERRY GO ROUND", "HIDDEN",
|
|
"HIDDEN2", "CRITICAL", "SLIDE HOLD", "SLIDE COUNTER", "TURN", "SPIN", "FINISH", "DUAL HOLD",
|
|
)
|
|
|
|
|
|
def u32be(blob: bytes, off: int) -> int:
|
|
return struct.unpack_from(">I", blob, off)[0]
|
|
|
|
|
|
def decode(path: pathlib.Path):
|
|
blob = path.read_bytes()
|
|
if len(blob) < 20:
|
|
raise ValueError("short header")
|
|
notes_off = u32be(blob, 12)
|
|
camera_off = u32be(blob, 16)
|
|
if not (0 <= notes_off < camera_off <= len(blob)):
|
|
raise ValueError("invalid note/camera offsets")
|
|
|
|
off = notes_off
|
|
name_count = u32be(blob, off)
|
|
off += 4
|
|
names = []
|
|
for _ in range(name_count):
|
|
if off >= camera_off:
|
|
raise ValueError("truncated note name")
|
|
size = blob[off]
|
|
off += 1
|
|
if off + size > camera_off:
|
|
raise ValueError("truncated note name bytes")
|
|
names.append(blob[off : off + size].rstrip(b"\0").decode("utf-8", "replace"))
|
|
off += size
|
|
|
|
if off + 4 > camera_off:
|
|
raise ValueError("missing note count")
|
|
count = u32be(blob, off)
|
|
off += 4
|
|
expected = off + count * NOTE.size
|
|
if expected != camera_off:
|
|
raise ValueError(f"size mismatch: count={count}, expected=0x{expected:x}, camera=0x{camera_off:x}")
|
|
return names, [NOTE.unpack_from(blob, off + i * NOTE.size) for i in range(count)]
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("path", type=pathlib.Path, help="stage .dat or directory")
|
|
parser.add_argument("--samples", action="store_true", help="show one decoded record per raw type")
|
|
args = parser.parse_args()
|
|
|
|
if args.path.is_dir():
|
|
paths = [
|
|
p for p in sorted(args.path.glob("*.dat"))
|
|
if not p.name.endswith(("_ext.dat", "_clip.dat"))
|
|
]
|
|
else:
|
|
paths = [args.path]
|
|
|
|
types = collections.Counter()
|
|
name_counts = collections.Counter()
|
|
override_count = 0
|
|
overrides = collections.Counter()
|
|
note_count = 0
|
|
fly_in_count = 0
|
|
fly_trail_count = 0
|
|
fly_bounce_count = 0
|
|
samples = {}
|
|
failures = []
|
|
for path in paths:
|
|
try:
|
|
names, notes = decode(path)
|
|
except (OSError, ValueError, struct.error) as exc:
|
|
failures.append((path, str(exc)))
|
|
continue
|
|
name_counts[len(names)] += 1
|
|
note_count += len(notes)
|
|
for note in notes:
|
|
raw_type = note[1]
|
|
types[raw_type] += 1
|
|
override_count += note[2] != 0
|
|
if note[2] != 0:
|
|
overrides[(raw_type, note[2])] += 1
|
|
samples.setdefault(raw_type, (path, note))
|
|
flag37 = note[16]
|
|
flag38 = note[17]
|
|
cycles = note[24]
|
|
fly_in_count += flag37 != 0
|
|
fly_trail_count += flag38 != 0
|
|
fly_bounce_count += flag37 != 0 and cycles != 0
|
|
|
|
print(f"record_size={NOTE.size}")
|
|
print(f"files={len(paths)} valid={len(paths) - len(failures)} invalid={len(failures)} notes={note_count}")
|
|
print("name_counts=" + " ".join(f"{key}:{value}" for key, value in sorted(name_counts.items())))
|
|
print("types=" + " ".join(
|
|
f"0x{key:02x}/{TYPE_NAMES[key] if key < len(TYPE_NAMES) else 'UNKNOWN'}:{value}"
|
|
for key, value in sorted(types.items())
|
|
))
|
|
print(f"type_override_nonzero={override_count}")
|
|
print(f"fly_in={fly_in_count} fly_trail={fly_trail_count} fly_bounce={fly_bounce_count}")
|
|
print("overrides=" + " ".join(
|
|
f"0x{raw:02x}/0x{override:02x}:{count}"
|
|
for (raw, override), count in sorted(overrides.items())
|
|
))
|
|
|
|
if args.samples:
|
|
for raw_type, (path, note) in sorted(samples.items()):
|
|
print(f"sample 0x{raw_type:02x} {path.name}: {note}")
|
|
for path, error in failures[:10]:
|
|
print(f"invalid {path.name}: {error}")
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|