port 2D steps 3-4: strict 40250 asset gate with a provenance manifest

tools/asset_manifest.py records path/size/sha256 of pack/Index, every .eix/.epk
and Eternexus/root/*.msm (audit/assets/40250-client.json). The gate script sets
MT_ASSETS_STRICT=1, verifies the manifest, runs the native pack/proto tests and
four Godot tests, and fails on a missing client, a mismatch or a skip.
MT_40250_CLIENT now comes from the environment for the native tests too.
remote_player_test accepts both weighted WAIT variants (wait/wait_1).
PORT-PLAN §5 records the 2D status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 01:41:38 +09:00
co-authored by Claude Opus 5
parent 240d2827c6
commit a9b31dec67
7 changed files with 1323 additions and 2 deletions
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Provenance manifest of the 40250 client assets the tests and the runtime read (docs/PORT-PLAN.md 2D step 3).
Records path (relative to the Client directory) + size + sha256 for pack/Index, every .eix/.epk in pack/ and the
loose Eternexus/root/*.msm race specs. `verify` fails on a missing, changed or untracked file, so a strict
run proves it read exactly the bytes the manifest names.
tools/asset_manifest.py generate <Client dir> [-o audit/assets/40250-client.json]
tools/asset_manifest.py verify <Client dir> [-m audit/assets/40250-client.json]
"""
import argparse
import hashlib
import json
import os
import sys
DEFAULT = os.path.join(os.path.dirname(__file__), "..", "audit", "assets", "40250-client.json")
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(1 << 20), b""):
h.update(block)
return h.hexdigest()
def tracked(client):
files = ["pack/Index"]
pack = os.path.join(client, "pack")
files += sorted("pack/" + n for n in os.listdir(pack) if n.lower().endswith((".eix", ".epk")))
root = os.path.join(client, "Eternexus", "root")
if os.path.isdir(root):
files += sorted("Eternexus/root/" + n for n in os.listdir(root) if n.lower().endswith(".msm"))
return files
def entry(client, rel):
p = os.path.join(client, rel)
return {"path": rel, "size": os.path.getsize(p), "sha256": sha256(p)}
def generate(args):
files = [entry(args.client, rel) for rel in tracked(args.client)]
out = {
"schema": 1,
"source": "40250 Server Client TMP4/Client",
"note": "path relative to the Client directory; regenerate with tools/asset_manifest.py generate",
"files": files,
}
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
with open(args.output, "w") as f:
json.dump(out, f, indent=1)
f.write("\n")
print("%d files, %d bytes -> %s" % (len(files), sum(e["size"] for e in files), args.output))
return 0
def verify(args):
try:
with open(args.manifest) as f:
want = json.load(f)["files"]
except (OSError, ValueError, KeyError) as e:
print("asset_manifest: cannot read manifest %s: %s" % (args.manifest, e), file=sys.stderr)
return 1
if not os.path.isfile(os.path.join(args.client, "pack", "Index")):
print("asset_manifest: no pack/Index under %s" % args.client, file=sys.stderr)
return 1
bad = 0
for e in want:
p = os.path.join(args.client, e["path"])
if not os.path.isfile(p):
print("missing %s" % e["path"], file=sys.stderr)
bad += 1
elif os.path.getsize(p) != e["size"] or sha256(p) != e["sha256"]:
print("changed %s" % e["path"], file=sys.stderr)
bad += 1
extra = sorted(set(tracked(args.client)) - {e["path"] for e in want})
for rel in extra:
print("untracked %s" % rel, file=sys.stderr)
bad += len(extra)
print("%d files checked, %d mismatch(es)" % (len(want), bad))
return 1 if bad else 0
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
g = sub.add_parser("generate")
g.add_argument("client")
g.add_argument("-o", "--output", default=DEFAULT)
v = sub.add_parser("verify")
v.add_argument("client")
v.add_argument("-m", "--manifest", default=DEFAULT)
args = ap.parse_args()
return generate(args) if args.cmd == "generate" else verify(args)
if __name__ == "__main__":
sys.exit(main())