143 lines
6 KiB
Python
143 lines
6 KiB
Python
#!/usr/bin/env python3
|
|
# Which Forge ports play from their description alone, with none of the game's own Lua?
|
|
#
|
|
# The qte and kimmy behaviours load only what the description names, so deleting its "script" and
|
|
# "addons" lines leaves the loop nothing but the snapshot the converter wrote. This runs each
|
|
# framework title that way, against its original, through the same comparison the ports are proved
|
|
# with (FORGE.md 14.1), and writes the ones that agree to testScripts/ports/standalone.txt.
|
|
# util/forgePorts.py reads that list and assembles those ports without the game's Lua.
|
|
#
|
|
# python3 util/forgeStandalone.py [<title> ...] (no arguments does them all)
|
|
# python3 util/forgeStandalone.py --hosted [<title> ...]
|
|
#
|
|
# --hosted runs each title as it ships instead, with its Lua where it is: the check to run after
|
|
# regenerating descriptions, since a converter that has learned something writes a description the
|
|
# loop reads differently. It writes no list.
|
|
#
|
|
# SINGE the engine binary (default: the harness picks the newest build)
|
|
# SINGE_LIBRARY the ported originals (default: ~/claude/singetest/ported)
|
|
#
|
|
# A title is tested on a hard-linked copy of itself, so the library is never edited and the videos
|
|
# are not duplicated. A title whose description still needs its Lua stays off the list and keeps
|
|
# it: startConf drawing at random and specialScore's own scoring are code, not data.
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
LIBRARY = os.path.expanduser(os.environ.get("SINGE_LIBRARY", "~/claude/singetest/ported"))
|
|
LIST = os.path.join(REPO, "testScripts", "ports", "standalone.txt")
|
|
HOSTED = re.compile(r'^\s*(script|addons)\s*=\s*"[^"]*",\s*$')
|
|
|
|
|
|
def titles():
|
|
"""Every description the qte or kimmy behaviour plays, as (name, family, game argument,
|
|
description path). The library's own are a directory and a script; a Hypseus title is its
|
|
folder under hypseus/."""
|
|
found = []
|
|
for root, dirs, files in os.walk(LIBRARY):
|
|
for name in sorted(f for f in files if f.endswith(".forge")):
|
|
path = os.path.join(root, name)
|
|
text = open(path, errors="replace").read()
|
|
loop = re.search(r'behaviours = \{ \{ kind = "(\w+)" \} \}', text)
|
|
if not loop or loop.group(1) not in ("qte", "kimmy"):
|
|
continue
|
|
script = re.search(r'^\s*script\s*=\s*"([^"]+)",', text, re.M)
|
|
if not script:
|
|
continue
|
|
relative = os.path.relpath(path, LIBRARY)
|
|
if relative.startswith("hypseus" + os.sep):
|
|
title = relative.split(os.sep)[1]
|
|
found.append((title, "hypseus", title, path))
|
|
else:
|
|
folder = relative.split(os.sep)[0]
|
|
found.append((folder, "karis", "%s/%s" % (folder, script.group(1)), path))
|
|
return found
|
|
|
|
|
|
def strip(source, into):
|
|
"""The title, hard-linked so no video is copied, with its description's hosting lines gone.
|
|
The description itself is replaced rather than edited, since a hard link is shared."""
|
|
os.makedirs(into, exist_ok=True)
|
|
target = os.path.join(into, os.path.basename(source))
|
|
shutil.rmtree(target, ignore_errors=True)
|
|
subprocess.run(["cp", "-al", source, target], check=True)
|
|
return target
|
|
|
|
|
|
def unhost(descriptions):
|
|
for path in descriptions:
|
|
kept = [line for line in open(path, errors="replace").read().splitlines(True) if not HOSTED.match(line)]
|
|
os.remove(path) # The link, not the library's file.
|
|
with open(path, "w") as out:
|
|
out.writelines(kept)
|
|
|
|
|
|
def compare(family, game, library):
|
|
done = subprocess.run([os.path.join(REPO, "testScripts", "ports", "compare.sh"), family, game],
|
|
cwd=REPO, env=dict(os.environ, SINGE_LIBRARY=library),
|
|
capture_output=True, text=True, timeout=1800)
|
|
out = done.stdout + done.stderr
|
|
if "plays as the original does" in out:
|
|
return True, [line for line in out.splitlines() if "plays as" in line][0]
|
|
first = [line for line in out.splitlines() if ("differ" in line) or ("Error" in line) or ("FAIL" in line)]
|
|
return False, first[0] if first else "no verdict"
|
|
|
|
|
|
def main():
|
|
wanted = sys.argv[1:]
|
|
hosted = "--hosted" in wanted
|
|
|
|
if hosted:
|
|
wanted.remove("--hosted")
|
|
rows = [row for row in titles() if not wanted or row[0] in wanted]
|
|
if not rows:
|
|
sys.exit("forgeStandalone: no titles to test")
|
|
work = tempfile.mkdtemp(prefix="forgeStandalone.")
|
|
passed = []
|
|
|
|
for title, family, game, path in rows:
|
|
if hosted:
|
|
# As it ships: the library itself, nothing copied, nothing stripped.
|
|
ok, saying = compare(family, game, LIBRARY)
|
|
print("%-28s %s" % (title, "agrees" if ok else "DIFFERS"), flush=True)
|
|
print(" %s" % saying, flush=True)
|
|
if ok:
|
|
passed.append(title)
|
|
continue
|
|
# The title, and for a Hypseus one the hypseus/ folder the harness looks under.
|
|
if family == "hypseus":
|
|
library = os.path.join(work, "hypseus")
|
|
copied = strip(os.path.join(LIBRARY, "hypseus", title), library)
|
|
else:
|
|
library = work
|
|
copied = strip(os.path.join(LIBRARY, title), library)
|
|
inside = [os.path.join(r, f) for r, _, fs in os.walk(copied) for f in fs if f.endswith(".forge")]
|
|
unhost(inside)
|
|
ok, saying = compare(family, game, library)
|
|
print("%-28s %s" % (title, "STANDS ALONE" if ok else "needs its Lua"), flush=True)
|
|
print(" %s" % saying, flush=True)
|
|
if ok:
|
|
passed.append(title)
|
|
shutil.rmtree(copied, ignore_errors=True)
|
|
shutil.rmtree(work, ignore_errors=True)
|
|
|
|
if hosted:
|
|
print("\n%d of %d agree as they ship" % (len(passed), len(rows)))
|
|
elif not wanted:
|
|
with open(LIST, "w") as out:
|
|
out.write("# Titles whose Forge port plays from its description alone, proved by\n")
|
|
out.write("# util/forgeStandalone.py. util/forgePorts.py leaves the game's own Lua\n")
|
|
out.write("# out of these ports. Regenerate the list; do not edit it by hand.\n")
|
|
for title in sorted(passed):
|
|
out.write(title + "\n")
|
|
print("\n%d of %d stand alone; written to %s" % (len(passed), len(rows), LIST))
|
|
else:
|
|
print("\n%d of %d stand alone (the list is only written by a full run)" % (len(passed), len(rows)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|