MODDING.md calls modding a constraint on the exporter TODAY and nothing verified it -- the same shape as the black hold, skipped[], stop_bed and --focus. All five rules pass, so check-modding is a guard rather than a fix, and it is proved able to fail: a stripped .cmd header, a bogus.bmp, and one orphaned PNG each exit 1. It found one thing: the .cmd encode-cache sidecars sat in the modder-facing tree with nothing saying what they were. They now carry a header. The header is excluded from the cache key so rewording it does not re-encode four minutes of video, and the sidecar is refreshed whenever its text differs rather than only on re-encode -- otherwise a header change could never reach an existing export. Also partly answers my own question to the Decoder: there is no general capture-path floor, because the port matches live-title-press-a at 0.00093% full-frame and 0.000% across the band. The 0.301% is specific to that pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
93 lines
4.4 KiB
Bash
Executable File
93 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Check the export against MODDING.md's five rules.
|
|
#
|
|
# tools/port/check-modding
|
|
#
|
|
# `MODDING.md` opens by saying modding is a requirement and **a constraint on the
|
|
# exporter today, not a later feature**. Nothing checked it. That is the shape
|
|
# this port keeps finding: a rule stated, believed, and unexercised -- the black
|
|
# hold implemented and never called, `ScreenView.skipped` written and never read,
|
|
# `stop_bed` provided and never used, `--focus` parsed and overwritten.
|
|
#
|
|
# So this is a guard, not a fix: every rule passes as of 2026-08-30. Its value is
|
|
# that the next thing to break one of them says so.
|
|
#
|
|
# ⚠️ What it CANNOT check: rule 3's "modern, editable" is enforced by extension,
|
|
# which cannot tell a valid PNG from a renamed one, and rule 1's "one logical
|
|
# asset" is checked as one-file-per-reference -- an exporter that split a sprite
|
|
# and referenced both halves would pass. These are the rules' checkable shadows.
|
|
set -euo pipefail
|
|
cd "${PROJECT_DIR:-/work}"
|
|
EXPORT="${EXPORT:-export}"
|
|
fail=0
|
|
note() { printf ' %-6s %s\n' "$1" "$2"; [ "$1" = FAIL ] && fail=1 || true; }
|
|
|
|
echo "MODDING rule 1 -- one logical asset, one file"
|
|
python3 - "$EXPORT" <<'PY'
|
|
import json, glob, os, sys
|
|
E = sys.argv[1]
|
|
ref = set()
|
|
for p in glob.glob(f"{E}/screens/*/*.json"):
|
|
d = json.load(open(p))
|
|
for e in d["elements"]:
|
|
for k in ("sprite", "focus_sprite"):
|
|
if e.get(k): ref.add(e[k])
|
|
for sub in ("focus", "leaf"):
|
|
for fe in (e.get(sub) or {}).get("elements", []):
|
|
if fe.get("sprite"): ref.add(fe["sprite"])
|
|
files = {os.path.relpath(p, E) for p in glob.glob(f"{E}/sprites/**/*.png", recursive=True)}
|
|
missing, orphan = sorted(ref - files), sorted(files - ref)
|
|
split = [f for f in files if any(t in os.path.basename(f) for t in ("part", "seg", "chunk"))]
|
|
print(" %-6s %d sprites referenced, %d present" % ("OK" if not (missing or orphan) else "FAIL", len(ref), len(files)))
|
|
if missing: print(" FAIL referenced but absent:", missing[:5])
|
|
if orphan: print(" FAIL present but unreferenced:", orphan[:5])
|
|
if split: print(" FAIL split-looking names:", split[:5])
|
|
sys.exit(1 if (missing or orphan or split) else 0)
|
|
PY
|
|
[ $? -eq 0 ] || fail=1
|
|
|
|
echo "MODDING rule 2 -- names a person recognises"
|
|
hex=$(find "$EXPORT" -type f | grep -Ec '0x[0-9a-f]{6,}|/[0-9a-f]{8}\.' || true)
|
|
[ "$hex" -eq 0 ] && note OK "no hex or hash-shaped filenames" || note FAIL "$hex hash-shaped names"
|
|
|
|
echo "MODDING rule 3 -- modern, editable formats only"
|
|
bad=$(find "$EXPORT" -type f | sed 's/.*\.//' | sort -u | grep -vE '^(json|png|ogg|ogv|cmd)$' || true)
|
|
[ -z "$bad" ] && note OK "only json/png/ogg/ogv (+ .cmd sidecars)" || note FAIL "unexpected: $(echo $bad)"
|
|
# A `.cmd` is not an asset. It is allowed only because it SAYS SO in its own
|
|
# first line -- see video.rs. An unlabelled one reads as something to edit.
|
|
for c in $(find "$EXPORT" -name '*.cmd'); do
|
|
head -1 "$c" | grep -q '^# Generated by sylpheed-export' \
|
|
&& note OK "$(basename "$c") is self-describing" \
|
|
|| note FAIL "$(basename "$c") has no header saying what it is"
|
|
done
|
|
|
|
echo "MODDING rule 4 -- base and overrides, never one merged pile"
|
|
grep -q 'data/mods' .gitignore && note OK "data/mods contents are gitignored" \
|
|
|| note FAIL "data/mods is not gitignored -- a mod is usually a game asset"
|
|
grep -rq 'data/mods' crates/sylpheed-export/src/ && note OK "the exporter resolves overrides" \
|
|
|| note FAIL "nothing reads data/mods"
|
|
|
|
echo "MODDING rule 5 -- provenance in every generated file"
|
|
python3 - "$EXPORT" <<'PY'
|
|
import json, glob, os, sys
|
|
E = sys.argv[1]; miss = []
|
|
for p in sorted(glob.glob(f"{E}/**/*.json", recursive=True)):
|
|
d = json.load(open(p))
|
|
if os.path.basename(p) == "manifest.json":
|
|
# The manifest is the provenance -- it carries disc, exporter and the
|
|
# formats revision for the whole tree, so it has no `source` of its own.
|
|
if not all(k in d for k in ("disc", "exporter", "formats_rev")): miss.append(p)
|
|
continue
|
|
src = d.get("source") or {}
|
|
if not (isinstance(src, dict) and src): miss.append(p)
|
|
print(" %-6s %d json files carry provenance" % ("OK" if not miss else "FAIL",
|
|
len(glob.glob(f"{E}/**/*.json", recursive=True)) - len(miss)))
|
|
for m in miss[:5]: print(" FAIL ", m)
|
|
sys.exit(1 if miss else 0)
|
|
PY
|
|
[ $? -eq 0 ] || fail=1
|
|
|
|
echo
|
|
[ $fail -eq 0 ] && echo "all five rules pass" || echo "🔴 a MODDING rule is broken"
|
|
exit $fail
|