182 lines
8.3 KiB
Python
182 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
# The ledger of FORGE.md section 17: every capability the library's converted titles use, which
|
|
# titles use it, and how the converter maps it, gathered from util/forgePortKaris.lua's report
|
|
# (FORGE_LEDGER) for each description it made. Nothing is run but the converter; no game plays.
|
|
#
|
|
# python3 util/forgeLedger.py [<out>] (default: testScripts/ports/ledger.md)
|
|
#
|
|
# SINGE_LIBRARY the ported originals (default: ~/claude/singetest/ported)
|
|
#
|
|
# A status is the converter's own: "forge" is a field of Forge's model, "framework" something
|
|
# Forge plays under the framework's names or numbers, "lua" something the game's own Lua still does.
|
|
# The runs-without-its-Lua column is testScripts/ports/standalone.txt as last proved, with its date.
|
|
import collections
|
|
import datetime
|
|
import hashlib
|
|
import os
|
|
import re
|
|
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"))
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPO, "testScripts", "ports", "ledger.md")
|
|
STANDING = os.path.join(REPO, "testScripts", "ports", "standalone.txt")
|
|
MADE_BY = "by the port converter"
|
|
SCRIPT = re.compile(r'^\s*script\s*=\s*"([^"]+)"', re.M)
|
|
STATUSES = ("lua", "framework", "forge")
|
|
EXAMPLES = 6
|
|
|
|
|
|
def descriptions():
|
|
"""Every description the converter made, as (title, directory, main script, name)."""
|
|
found = []
|
|
for root, dirs, files in os.walk(LIBRARY):
|
|
dirs.sort()
|
|
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()
|
|
if MADE_BY not in text:
|
|
continue
|
|
script = SCRIPT.search(text)
|
|
if not script:
|
|
continue
|
|
title = os.path.relpath(path, LIBRARY)[:-len(".forge")]
|
|
found.append((title, root, script.group(1), name[:-len(".forge")]))
|
|
return found
|
|
|
|
|
|
def others():
|
|
"""The descriptions another tool made, named so the ledger says what it does not cover."""
|
|
missing = []
|
|
for root, dirs, files in os.walk(LIBRARY):
|
|
for name in files:
|
|
path = os.path.join(root, name)
|
|
if name.endswith(".forge") and MADE_BY not in open(path, errors="replace").read():
|
|
missing.append(os.path.relpath(path, LIBRARY))
|
|
return sorted(missing)
|
|
|
|
|
|
def addonsHash(directory):
|
|
path = os.path.join(directory, "Script", "addons.singe")
|
|
return hashlib.md5(open(path, "rb").read()).hexdigest() if os.path.exists(path) else None
|
|
|
|
|
|
def report(directory, script, name):
|
|
with tempfile.NamedTemporaryFile(suffix=".ledger", delete=False) as handle:
|
|
path = handle.name
|
|
try:
|
|
done = subprocess.run(["lua5.4", os.path.join("util", "forgePortKaris.lua"), directory, script, name],
|
|
cwd=REPO, env=dict(os.environ, FORGE_LEDGER=path), capture_output=True, text=True, timeout=120)
|
|
lines = [line.rstrip("\n").split("\t") for line in open(path) if line.strip()]
|
|
return lines, (done.stderr.strip().splitlines() or [""])[-1] if done.returncode else None
|
|
finally:
|
|
os.remove(path)
|
|
|
|
|
|
def main():
|
|
standing = set()
|
|
proved = "never"
|
|
if os.path.exists(STANDING):
|
|
standing = {line.strip() for line in open(STANDING) if line.strip() and not line.startswith("#")}
|
|
proved = datetime.date.fromtimestamp(os.path.getmtime(STANDING)).isoformat()
|
|
|
|
uses = collections.defaultdict(set) # (status, capability) -> titles
|
|
details = collections.defaultdict(set) # (status, capability) -> details
|
|
perTitle = {}
|
|
failed = []
|
|
titles = descriptions()
|
|
# The add-ons most titles ship unchanged are the framework's own: their hooks are the stock
|
|
# ones, not the game's Lua.
|
|
hashes = collections.Counter(h for h in (addonsHash(row[1]) for row in titles) if h)
|
|
stock = hashes.most_common(1)[0][0] if hashes else None
|
|
for title, directory, script, name in titles:
|
|
lines, error = report(directory, script, name)
|
|
if error is not None:
|
|
failed.append((title, error))
|
|
continue
|
|
if stock and addonsHash(directory) == stock:
|
|
lines = [["framework", capability, "stock add-ons"] if capability.startswith("hook.") else [status, capability, detail] for status, capability, detail in lines]
|
|
counts = collections.Counter()
|
|
family = "?"
|
|
for status, capability, detail in lines:
|
|
uses[(status, capability)].add(title)
|
|
if detail:
|
|
details[(status, capability)].add(detail)
|
|
counts[status] += 1
|
|
if capability.startswith("family."):
|
|
family = capability[len("family."):]
|
|
folder = title.split("/")[1] if title.startswith("hypseus/") else title.split("/")[0]
|
|
perTitle[title] = (family, counts, "yes" if folder in standing else "not on the list")
|
|
|
|
out = []
|
|
out.append("# The ledger (FORGE.md section 17)")
|
|
out.append("")
|
|
out.append("Generated by `util/forgeLedger.py` on %s from the converter's report for each" % datetime.date.today().isoformat())
|
|
out.append("description it made; do not edit by hand. **forge** is a field of Forge's own model,")
|
|
out.append("**framework** something Forge plays under the framework's names or numbers, **lua**")
|
|
out.append("something the game's own Lua still does. Rows are sorted by status, then by how many")
|
|
out.append("titles use them: the top of the table is what most blocks titles from being Forge's own.")
|
|
out.append("A hook from the add-ons file most titles ship unchanged (%d of them) is the framework's" % (hashes[stock] if stock else 0))
|
|
out.append("stock one, and counts as framework. The globals are every global the game's script left")
|
|
out.append("that nothing names: most are the framework's own state and picture numbers, not data.")
|
|
out.append("")
|
|
out.append("%d titles reported; %d failed to report; %d descriptions made by another tool (not covered)." % (len(perTitle), len(failed), len(others())))
|
|
out.append("")
|
|
|
|
# The capabilities, with the globals and settings folded into one row each per status: there
|
|
# are hundreds, and their names are listed in the section after.
|
|
folded = collections.defaultdict(set)
|
|
for (status, capability), titles in uses.items():
|
|
if capability.startswith("global.") or capability.startswith("setting."):
|
|
folded[(status, capability.split(".")[0] + ".*")] |= titles
|
|
else:
|
|
folded[(status, capability)] |= titles
|
|
out.append("## Capabilities")
|
|
out.append("")
|
|
out.append("| Status | Capability | Titles | Some of them | Details |")
|
|
out.append("|---|---|---|---|---|")
|
|
for (status, capability), titles in sorted(folded.items(), key=lambda item: (STATUSES.index(item[0][0]), -len(item[1]), item[0][1])):
|
|
names = sorted(titles)
|
|
more = ", ..." if len(names) > EXAMPLES else ""
|
|
shown = ", ".join(title.split("/")[-1] for title in names[:EXAMPLES]) + more
|
|
found = sorted(details.get((status, capability), []))
|
|
note = "; ".join(found[:8]) + ("; ..." if len(found) > 8 else "")
|
|
out.append("| %s | %s | %d | %s | %s |" % (status, capability, len(names), shown, note))
|
|
out.append("")
|
|
|
|
for prefix, heading in (("setting.", "Settings under the framework's names"), ("global.", "Globals nothing names yet")):
|
|
rows = sorted(((capability, len(titles)) for (status, capability), titles in uses.items() if capability.startswith(prefix)), key=lambda row: (-row[1], row[0]))
|
|
out.append("## %s (%d)" % (heading, len(rows)))
|
|
out.append("")
|
|
out.append("By how many titles use each: " + ", ".join("%s %d" % (capability[len(prefix):], count) for capability, count in rows) + ".")
|
|
out.append("")
|
|
|
|
out.append("## Titles")
|
|
out.append("")
|
|
out.append("Runs without its Lua: `standalone.txt`, last proved %s." % proved)
|
|
out.append("")
|
|
out.append("| Title | Family | forge | framework | lua | Runs without its Lua |")
|
|
out.append("|---|---|---|---|---|---|")
|
|
for title, (family, counts, standalone) in sorted(perTitle.items(), key=lambda item: (item[1][1]["lua"], item[1][1]["framework"], item[0])):
|
|
out.append("| %s | %s | %d | %d | %d | %s |" % (title, family, counts["forge"], counts["framework"], counts["lua"], standalone))
|
|
out.append("")
|
|
if failed:
|
|
out.append("## Failed to report")
|
|
out.append("")
|
|
for title, error in failed:
|
|
out.append("* %s: %s" % (title, error))
|
|
out.append("")
|
|
out.append("## Made by another tool")
|
|
out.append("")
|
|
out.append(", ".join(others()) + ".")
|
|
out.append("")
|
|
|
|
with open(OUT, "w") as handle:
|
|
handle.write("\n".join(out))
|
|
print("wrote %s: %d titles, %d capabilities, %d failed" % (OUT, len(perTitle), len(folded), len(failed)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|