PythonNetworkStreamPhaseGame/PhaseGameActor/PhaseGameItem/Command, NetworkActorManager and PythonChat/PythonChatModule are ported verbatim and dispatched at runtime; the 2V0 chat stub module is removed and initChat() runs before initnet() as in 40250. InstanceBase, PythonCharacterManager and GameLib ActorInstance/RaceData/RaceManager compile but stay TODO until 2V2-c/d wire them. Units not ported yet get their singletons from PythonBoot's GameSingletons (CPythonApplication member order) and platform/pending stand-ins generated by platform_stub.py pending; const char* stand-ins return "". port.login_flow's fake server now sends the CInputLogin::Entergame order (actor insert, NPC positions, PHASE_GAME, land list, time, channel, greet notice, ping) and a TraceError observer asserts no unknown packet header. port.login_live passes against the real server for 10 s of game phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
451 lines
18 KiB
Python
451 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the empty platform skeleton for mirrored platform-layer headers (docs/PORT-PLAN.md 2A).
|
|
|
|
platform_stub.py list platform-layer headers in the mirror, with their stub state
|
|
platform_stub.py gen [<header>...] [--force] write extension/src/platform/<Lib>/<File>.cpp
|
|
(no args: every platform header; never overwrites
|
|
without --force, so hand-written implementations stay)
|
|
platform_stub.py pending <header> <symbol>... [--ctor]
|
|
append stubs of just those members (the linker's
|
|
undefined-symbol names, demangled as ld prints them) to the stand-in of a not-yet-ported
|
|
logic unit, extension/src/platform/pending/<Lib>/<File>.cpp;
|
|
--ctor adds the class constructor/destructor so the
|
|
singleton can exist (its members still default-construct)
|
|
|
|
`<header>` is a mirror path such as `EterLib/GrpBase.h`. A header is platform-layer when its same-stem
|
|
40250 .cpp unit is (port_map.py LAYER_PATTERNS), or, without one, when its own path matches.
|
|
|
|
A stub file defines everything the header declares and does not define: non-pure, non-template member
|
|
functions, constructors/destructors, free functions, and static data members. Declarations come from
|
|
clang's AST of the header's gate translation unit (the library StdAfx.h + the header, with the exact
|
|
flags from the macOS gate build's compile_commands.json: `script/port_gate.sh macos` first), dumped per
|
|
class/function name with -ast-dump-filter. Definitions use trailing return types, so types nested in the
|
|
class resolve in class scope, and `decltype(C::x) C::x{}` for static data. Each body is
|
|
MT_PLATFORM_STUB() plus a default return (platform/PlatformStub.h); `grep -r MT_PLATFORM_STUB` is the
|
|
list of platform functions still to implement.
|
|
|
|
Not covered: platform .cpp units that define members of logic headers (GameLib/MapOutdoorRender*.cpp for
|
|
MapOutdoor.h, ...); those members get their stubs by hand when the logic side needs them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import json
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import port_map
|
|
import refroot # script directory is on sys.path when run directly
|
|
|
|
REPO = refroot.repo_root()
|
|
PORT = REPO / "extension" / "src" / "port"
|
|
PLATFORM = REPO / "extension" / "src" / "platform"
|
|
GATE_BUILD = REPO / "build-port-gate" / "macos"
|
|
MARK = "platform_stub.py"
|
|
|
|
COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
|
|
LITERAL = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'')
|
|
PREPROC = re.compile(r"^[ \t]*#(?:[^\n]*\\\n)*[^\n]*", re.M)
|
|
RECORD = re.compile(r"\b(?:class|struct|union)\s+(?:__declspec\s*\([^)]*\)\s*)?([A-Za-z_]\w*)\s*(?:final\s*)?(?::[^;{}]*)?$")
|
|
|
|
|
|
# --- header scan: names of top-level records and free functions --------------------------------------
|
|
|
|
def blank(text: str) -> str:
|
|
"""Comments, literals and preprocessor lines replaced by spaces (offsets and newlines kept)."""
|
|
keep_nl = lambda m: re.sub(r"[^\n]", " ", m.group(0))
|
|
text = COMMENT.sub(keep_nl, text)
|
|
text = LITERAL.sub(lambda m: '""', text)
|
|
return PREPROC.sub(keep_nl, text)
|
|
|
|
|
|
def scan(header: Path) -> tuple[list[str], list[str]]:
|
|
"""Record names defined, and function names declared, at namespace scope of the header."""
|
|
text = blank(header.read_text(encoding="utf-8"))
|
|
records: list[str] = []
|
|
functions: list[str] = []
|
|
stack: list[str] = [] # "ns" (namespace / extern "C") or "body"
|
|
stmt_start = 0
|
|
for i, ch in enumerate(text):
|
|
if ch == "{":
|
|
head = text[stmt_start:i].strip()
|
|
if re.search(r"\bnamespace\b[^;]*$|\bextern\s*\"\"\s*$", head):
|
|
stack.append("ns")
|
|
else:
|
|
if all(k == "ns" for k in stack) and (m := RECORD.search(head)) \
|
|
and not re.search(r"\btemplate\s*<", head):
|
|
records.append(m.group(1))
|
|
stack.append("body")
|
|
stmt_start = i + 1
|
|
elif ch == "}":
|
|
if stack:
|
|
stack.pop()
|
|
stmt_start = i + 1
|
|
elif ch == ";":
|
|
if all(k == "ns" for k in stack):
|
|
head = " ".join(text[stmt_start:i].split())
|
|
if (head and not re.match(r"(?:typedef|using|template|friend|static_assert|enum|class|struct|union)\b", head)
|
|
and not re.search(r"\binline\b", head)):
|
|
if m := re.search(r"([A-Za-z_]\w*|operator\s*\S+?)\s*\((?!\s*\*)", head):
|
|
if m.group(1) not in ("__declspec", "decltype", "sizeof", "alignas"):
|
|
functions.append(m.group(1))
|
|
stmt_start = i + 1
|
|
return list(dict.fromkeys(records)), list(dict.fromkeys(functions))
|
|
|
|
|
|
# --- clang AST ---------------------------------------------------------------------------------------
|
|
|
|
def gate_command(rel: str) -> tuple[list[str], str]:
|
|
db = GATE_BUILD / "compile_commands.json"
|
|
if not db.is_file():
|
|
raise SystemExit(f"{db}: missing; run `script/port_gate.sh macos` (it exports compile commands)")
|
|
tu = re.sub(r"[^A-Za-z0-9_]", "_", rel)
|
|
for entry in json.loads(db.read_text()):
|
|
if Path(entry["file"]).name == f"{tu}.cpp":
|
|
args = shlex.split(entry["command"])
|
|
out = args.index("-o")
|
|
del args[out:out + 2]
|
|
args.remove("-c")
|
|
args.remove(entry["file"])
|
|
return args, entry["file"]
|
|
raise SystemExit(f"{rel}: no gate translation unit in {db}; reconfigure the gate build")
|
|
|
|
|
|
def dump(args: list[str], tu: str, name: str) -> list[dict]:
|
|
cmd = args + ["-w", "-fsyntax-only", "-Xclang", "-ast-dump=json", "-Xclang", f"-ast-dump-filter={name}", tu]
|
|
res = subprocess.run(cmd, capture_output=True, text=True, cwd=GATE_BUILD)
|
|
if res.returncode:
|
|
raise SystemExit(f"clang failed for {tu} ({name}):\n{res.stderr[-2000:]}")
|
|
s, i, out = res.stdout, 0, []
|
|
dec = json.JSONDecoder()
|
|
while True:
|
|
while i < len(s) and s[i].isspace():
|
|
i += 1
|
|
if i >= len(s):
|
|
return out
|
|
if s[i] != "{": # "Dumping X:" banner
|
|
i = s.find("\n", i) + 1 or len(s)
|
|
continue
|
|
node, i = dec.raw_decode(s, i)
|
|
out.append(node)
|
|
|
|
|
|
def annotate_files(nodes: list[dict]) -> None:
|
|
"""Give every node `_file`: clang writes a loc's file only when it changes, in document order."""
|
|
last = [""]
|
|
|
|
def visit_loc(loc):
|
|
if isinstance(loc, dict):
|
|
for key in ("spellingLoc", "expansionLoc", "begin", "end"):
|
|
if key in loc:
|
|
visit_loc(loc[key])
|
|
if "file" in loc:
|
|
last[0] = loc["file"]
|
|
|
|
def visit(node):
|
|
loc = node.get("loc", {})
|
|
if "expansionLoc" in loc:
|
|
visit_loc(loc["spellingLoc"])
|
|
visit_loc(loc["expansionLoc"])
|
|
else:
|
|
visit_loc(loc)
|
|
node["_file"] = last[0]
|
|
visit_loc(node.get("range"))
|
|
for child in node.get("inner", []):
|
|
visit(child)
|
|
|
|
for n in nodes:
|
|
visit(n)
|
|
|
|
|
|
def has_body(node: dict) -> bool:
|
|
return any(c.get("kind") == "CompoundStmt" for c in node.get("inner", [])) \
|
|
or "explicitlyDefaulted" in node or "explicitlyDeleted" in node
|
|
|
|
|
|
def split_function_type(qual: str) -> tuple[str, str]:
|
|
"""`R (P...) quals` -> (R, quals)."""
|
|
depth = 0
|
|
for i in range(len(qual) - 1, -1, -1):
|
|
c = qual[i]
|
|
if c == ")":
|
|
depth += 1
|
|
elif c == "(":
|
|
depth -= 1
|
|
if depth == 0:
|
|
close = match_close(qual, i)
|
|
rest = qual[close + 1:].strip()
|
|
if re.fullmatch(r"(?:const|volatile|&&?|noexcept(?:\(.*\))?|throw\(.*\)|\s)*", rest):
|
|
return qual[:i].strip(), rest
|
|
raise ValueError(qual)
|
|
|
|
|
|
def match_close(s: str, open_idx: int) -> int:
|
|
depth = 0
|
|
for j in range(open_idx, len(s)):
|
|
if s[j] == "(":
|
|
depth += 1
|
|
elif s[j] == ")":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return j
|
|
raise ValueError(s)
|
|
|
|
|
|
def params(node: dict) -> str:
|
|
ps = [c["type"]["qualType"] for c in node.get("inner", []) if c.get("kind") == "ParmVarDecl"]
|
|
if node.get("variadic"):
|
|
ps.append("...")
|
|
return ", ".join(ps)
|
|
|
|
|
|
def function_def(qname: str, node: dict) -> str:
|
|
ret, quals = split_function_type(node["type"]["qualType"])
|
|
quals = f" {quals}" if quals else ""
|
|
kind = node["kind"]
|
|
if kind == "CXXDestructorDecl": # clang spells the implicit noexcept of a destructor into its type
|
|
quals = re.sub(r"\s*\bnoexcept\b", "", quals)
|
|
if kind in ("CXXConstructorDecl", "CXXDestructorDecl"):
|
|
return f"{qname}({params(node)}){quals}\n{{\n\tMT_PLATFORM_STUB();\n}}\n"
|
|
if kind == "CXXConversionDecl":
|
|
return (f"{qname}({params(node)}){quals}\n{{\n\tMT_PLATFORM_STUB();\n"
|
|
f"\treturn mt_platform_stub_return<{ret}>();\n}}\n")
|
|
body = "\tMT_PLATFORM_STUB();\n" + ("" if ret == "void" else f"\treturn mt_platform_stub_return<{ret}>();\n")
|
|
return f"auto {qname}({params(node)}){quals} -> {ret}\n{{\n{body}}}\n"
|
|
|
|
|
|
class Collector:
|
|
def __init__(self, header_file: str):
|
|
self.header_file = header_file
|
|
self.defined: set[str] = set() # mangled names with a body anywhere in the dump
|
|
self.decls: list[tuple[str, str, dict]] = [] # (kind, qualified name, node)
|
|
self.seen: set[str] = set()
|
|
|
|
def note_bodies(self, node: dict) -> None:
|
|
if node.get("mangledName") and has_body(node):
|
|
self.defined.add(node["mangledName"])
|
|
for c in node.get("inner", []):
|
|
self.note_bodies(c)
|
|
|
|
def record(self, node: dict, scope: str) -> None:
|
|
if node.get("kind") != "CXXRecordDecl" or not node.get("completeDefinition") or not node.get("name"):
|
|
return
|
|
q = f"{scope}{node['name']}"
|
|
for c in node.get("inner", []):
|
|
kind = c.get("kind")
|
|
if kind == "CXXRecordDecl" and c.get("isImplicit"):
|
|
continue
|
|
if kind == "CXXRecordDecl":
|
|
self.record(c, f"{q}::")
|
|
elif kind in ("CXXMethodDecl", "CXXConstructorDecl", "CXXDestructorDecl", "CXXConversionDecl"):
|
|
if c.get("isImplicit") or c.get("pure") or has_body(c):
|
|
continue
|
|
self.add("fn", f"{q}::{c['name']}", c)
|
|
elif kind == "VarDecl" and c.get("storageClass") == "static":
|
|
if c.get("init") or c.get("constexpr") or c.get("inline"):
|
|
continue
|
|
self.add("var", f"{q}::{c['name']}", c)
|
|
|
|
def add(self, kind: str, qname: str, node: dict) -> None:
|
|
key = node.get("mangledName") or qname
|
|
if key in self.seen:
|
|
return
|
|
self.seen.add(key)
|
|
self.decls.append((kind, qname, node))
|
|
|
|
def root(self, node: dict, scope: str) -> None:
|
|
if node["_file"] != self.header_file:
|
|
return
|
|
kind = node.get("kind")
|
|
if kind == "CXXRecordDecl":
|
|
self.record(node, scope)
|
|
elif kind == "FunctionDecl" and not has_body(node) and node.get("storageClass") != "static" \
|
|
and not node.get("inline"):
|
|
self.add("fn", f"{scope}{node['name']}", node)
|
|
|
|
def render(self, only=None) -> list[str]:
|
|
out = []
|
|
for kind, qname, node in self.decls:
|
|
if node.get("mangledName") in self.defined:
|
|
continue
|
|
if only is not None and not only(qname, node):
|
|
continue
|
|
if kind == "var":
|
|
out.append(f"decltype({qname}) {qname}{{}};\n")
|
|
else:
|
|
out.append(function_def(qname, node))
|
|
return out
|
|
|
|
|
|
def namespaces(header: Path) -> dict[str, str]:
|
|
"""Name -> enclosing namespace prefix, for names declared inside `namespace X {`."""
|
|
text = blank(header.read_text(encoding="utf-8"))
|
|
result: dict[str, str] = {}
|
|
for m in re.finditer(r"\bnamespace\s+(\w+)\s*\{", text):
|
|
depth, j = 1, m.end()
|
|
while depth and j < len(text):
|
|
depth += {"{": 1, "}": -1}.get(text[j], 0)
|
|
j += 1
|
|
for n in re.findall(r"\b(?:class|struct)\s+(\w+)|(\w+)\s*\(", text[m.end():j]):
|
|
result.setdefault(n[0] or n[1], f"{m.group(1)}::")
|
|
return result
|
|
|
|
|
|
def generate(rel: str, only=None) -> str:
|
|
"""The skeleton of `rel`; with `only(qname, node)`, just the declarations it accepts (the bodies alone)."""
|
|
header = PORT / rel
|
|
records, functions = scan(header)
|
|
args, tu = gate_command(rel)
|
|
ns = namespaces(header)
|
|
col = Collector(str(header))
|
|
with concurrent.futures.ThreadPoolExecutor(8) as pool:
|
|
dumps = list(pool.map(lambda n: (n, dump(args, tu, n)), records + functions))
|
|
for name, nodes in dumps:
|
|
annotate_files(nodes)
|
|
for n in nodes:
|
|
col.note_bodies(n)
|
|
for name, nodes in dumps:
|
|
for n in nodes:
|
|
if n.get("name") == name:
|
|
col.root(n, ns.get(name, ""))
|
|
body = col.render(only)
|
|
if not body or only is not None:
|
|
return "\n".join(body)
|
|
lib = rel.split("/")[0]
|
|
stdafx = next((p.name for p in (PORT / lib).glob("*") if p.name.lower() == "stdafx.h"), None)
|
|
unit = unit_of(rel)
|
|
lines = [f"// Platform skeleton for {rel}"
|
|
+ (f" (40250 {unit})" if unit else "") + f", generated by {MARK}.\n",
|
|
"// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.\n"]
|
|
if stdafx and f"{lib}/{stdafx}" != rel:
|
|
lines.append(f'#include "{lib}/{stdafx}"\n')
|
|
lines += [f'#include "{rel}"\n', "\n", f'#include "{"../" * rel.count("/")}PlatformStub.h"\n', "\n"]
|
|
return "".join(lines) + "\n".join(body)
|
|
|
|
|
|
# --- selection ---------------------------------------------------------------------------------------
|
|
|
|
_UNITS: dict[str, str] | None = None
|
|
|
|
|
|
def unit_of(rel: str) -> str | None:
|
|
global _UNITS
|
|
if _UNITS is None:
|
|
_UNITS = {u.lower(): u for u in port_map.inventory(refroot.reference_root())}
|
|
return _UNITS.get((rel.rsplit(".", 1)[0] + ".cpp").lower())
|
|
|
|
|
|
def platform_headers() -> list[str]:
|
|
out = []
|
|
for h in sorted(PORT.rglob("*.h")):
|
|
rel = h.relative_to(PORT).as_posix()
|
|
if rel.startswith("common/"):
|
|
continue
|
|
unit = unit_of(rel)
|
|
if port_map.layer(unit or rel) == "platform":
|
|
out.append(rel)
|
|
return out
|
|
|
|
|
|
def stub_path(rel: str) -> Path:
|
|
return PLATFORM / (rel.rsplit(".", 1)[0] + ".cpp")
|
|
|
|
|
|
def cmd_list(_args) -> int:
|
|
for rel in platform_headers():
|
|
p = stub_path(rel)
|
|
state = "-" if not p.exists() else ("generated" if MARK in p.read_text(encoding="utf-8")[:300] else "edited")
|
|
pending = p.read_text(encoding="utf-8").count("MT_PLATFORM_STUB();") if p.exists() else 0
|
|
print(f"{rel:48} {state:9} {pending:4} stubs")
|
|
return 0
|
|
|
|
|
|
def cmd_gen(args) -> int:
|
|
headers = platform_headers()
|
|
targets = headers if not args.headers else []
|
|
for h in args.headers:
|
|
hit = [r for r in headers if r.lower() == h.lower()]
|
|
if not hit:
|
|
raise SystemExit(f"{h}: not a platform-layer header in the mirror")
|
|
targets += hit
|
|
written = 0
|
|
for rel in targets:
|
|
path = stub_path(rel)
|
|
if path.exists() and not args.force:
|
|
print(f"skip {path.relative_to(REPO)} (exists; --force to regenerate)")
|
|
continue
|
|
text = generate(rel)
|
|
if not text:
|
|
print(f"none {rel} (declares nothing to define)")
|
|
continue
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
written += 1
|
|
print(f"wrote {path.relative_to(REPO)} ({text.count('MT_PLATFORM_STUB();')} stubs)")
|
|
print(f"{written} file(s) written")
|
|
return 0
|
|
|
|
|
|
def cmd_pending(args) -> int:
|
|
rel = next((h.relative_to(PORT).as_posix() for h in PORT.rglob("*.h")
|
|
if h.relative_to(PORT).as_posix().lower() == args.header.lower()), None)
|
|
if not rel:
|
|
raise SystemExit(f"{args.header}: not in the mirror")
|
|
wanted = {s.strip().strip('"') for s in args.symbols}
|
|
demangled: dict[str, str] = {}
|
|
|
|
def only(qname: str, node: dict) -> bool:
|
|
mangled = node.get("mangledName", "")
|
|
if mangled not in demangled:
|
|
demangled[mangled] = subprocess.run(["c++filt", mangled], capture_output=True, text=True).stdout.strip()
|
|
if demangled[mangled] in wanted:
|
|
return True
|
|
return args.ctor and node.get("kind") in ("CXXConstructorDecl", "CXXDestructorDecl") \
|
|
and qname.split("::")[-2:-1] == [node.get("name", "").lstrip("~")] and qname.count("::") == 1
|
|
body = generate(rel, only)
|
|
if not body:
|
|
print(f"none {rel} (no such undefined member)")
|
|
return 0
|
|
path = PLATFORM / "pending" / (rel.rsplit(".", 1)[0] + ".cpp")
|
|
if path.exists():
|
|
text = path.read_text(encoding="utf-8").rstrip("\n") + "\n\n" + body
|
|
else:
|
|
lib = rel.split("/")[0]
|
|
stdafx = next((p.name for p in (PORT / lib).glob("*") if p.name.lower() == "stdafx.h"), None)
|
|
unit = unit_of(rel) or rel
|
|
text = (f"// Stand-in for the 40250 logic unit {unit}, not ported yet: the members the ported units\n"
|
|
f"// reference, as {MARK} stubs. Delete this file in the commit that ports the unit into port/\n"
|
|
f"// (docs/PORT-PLAN.md).\n"
|
|
+ (f'#include "{lib}/{stdafx}"\n' if stdafx else "")
|
|
+ f'#include "{rel}"\n\n#include "{"../" * (rel.count("/") + 1)}PlatformStub.h"\n\n' + body)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
print(f"wrote {path.relative_to(REPO)} (+{body.count('MT_PLATFORM_STUB();')} stubs)")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
sub.add_parser("list").set_defaults(fn=cmd_list)
|
|
g = sub.add_parser("gen")
|
|
g.add_argument("headers", nargs="*")
|
|
g.add_argument("--force", action="store_true")
|
|
g.set_defaults(fn=cmd_gen)
|
|
p = sub.add_parser("pending")
|
|
p.add_argument("header")
|
|
p.add_argument("symbols", nargs="*")
|
|
p.add_argument("--ctor", action="store_true")
|
|
p.set_defaults(fn=cmd_pending)
|
|
args = ap.parse_args()
|
|
return args.fn(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|