audit(2A): include-dependency tool, 2V0-2V3 slice unit lists and batch-2 order
port_deps.py resolves explicit #include edges plus the implicit edges each 40250 library's StdAfx.h supplies (symbol index restricted to StdAfx-visible headers), writes cumulative slice lists and the P0 topological order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,9 @@ behavior verified from code inspection alone.
|
||||
— function inventory (active `.vcxproj` sources + the 40250 Python root), progress per library and
|
||||
per layer, and tag/port-map/layout consistency. Run `check` before every commit that touches
|
||||
`audit/port-map/`.
|
||||
- `scripts/port_deps.py closure <file> | slices [--write] | order [--write]` — `#include` graph with the
|
||||
implicit edges each library's `StdAfx.h` supplies; writes the 2V0-2V3 unit lists and the batch-2
|
||||
topological order to `audit/slices/`. Regenerate after changing a slice definition.
|
||||
- `scripts/py_embed_spike.py` (Python 2.7) — runs the 40250 `system.py` bootstrap with stub native
|
||||
modules; used by the embedded-Python evaluation.
|
||||
- `scripts/audit_ledger.py refresh --write | report --write | validate` — run once per batch, not
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/env python3
|
||||
"""40250 `#include` dependency graph: slice unit lists and batch-2 port order.
|
||||
|
||||
40250 headers lean on each library's precompiled `StdAfx.h` for most of their types, so the
|
||||
explicit `#include` graph alone is not a compile closure. This tool adds *implicit* edges: an
|
||||
identifier a file uses but does not see through its explicit includes is resolved to the header
|
||||
that declares it, restricted to the headers the file's own library `StdAfx.h` makes visible in
|
||||
40250 (so an implicit edge is exactly one that a slim per-library StdAfx must supply).
|
||||
|
||||
port_deps.py closure <unit|header> [...] compile closure (explicit + implicit), with reasons
|
||||
port_deps.py slices [--write] audit/slices/2V*.json: cumulative 2V0-2V3 unit lists
|
||||
port_deps.py order [--write] audit/slices/batch2-order.json: topological P0 order
|
||||
|
||||
Header ownership: `Lib/X.h` belongs to `Lib/X.cpp` when that is an active unit; a header-only
|
||||
file belongs to the first unit (in port order) whose closure needs it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import refroot # script directory is on sys.path when run directly
|
||||
from port_map import PY_ROOT, UNIT_DIRS, inventory, layer, priority
|
||||
|
||||
HEADER_SUFFIXES = {".h", ".hpp", ".inl"}
|
||||
SOURCE_DIRS = UNIT_DIRS + ("Discord", "CWebBrowser")
|
||||
# Global names that the index must not turn into edges (Win32/CRT/STL spellings every lib uses).
|
||||
IGNORED_SYMBOLS = {"TRUE", "FALSE", "NULL", "BOOL", "BYTE", "WORD", "DWORD", "UINT", "LONG", "INT", "VOID"}
|
||||
|
||||
# Cumulative vertical slices from docs/PORT-PLAN.md section 4 (2V0-2V3). `cpp` globs are real
|
||||
# units of the slice; `py` seeds are root scripts whose top-level import closure the slice runs.
|
||||
SLICES = {
|
||||
"2V0": {
|
||||
"cpp": ["ScriptLib/PythonLauncher.cpp", "UserInterface/PythonPackModule.cpp",
|
||||
"EterPythonLib/PythonWindow*.cpp", "EterPythonLib/PythonWindowManager*.cpp",
|
||||
"EterPythonLib/PythonGraphic*Module.cpp", "UserInterface/PythonApplicationModule.cpp"],
|
||||
"py": ["system.py", "prototype.py", "intrologo.py"],
|
||||
},
|
||||
"2V1": {
|
||||
"cpp": ["UserInterface/PythonNetworkStream.cpp", "UserInterface/PythonNetworkStreamPhaseHandShake.cpp",
|
||||
"UserInterface/PythonNetworkStreamPhaseLogin.cpp", "UserInterface/PythonNetworkStreamPhaseSelect.cpp",
|
||||
"UserInterface/PythonNetworkStreamPhaseLoading.cpp", "UserInterface/PythonNetworkStreamModule.cpp",
|
||||
"UserInterface/AccountConnector.cpp"],
|
||||
"py": ["intrologin.py", "introempire.py", "introselect.py", "introcreate.py", "introloading.py"],
|
||||
},
|
||||
"2V2": {
|
||||
"cpp": ["UserInterface/PythonNetworkStreamPhaseGame.cpp", "UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/InstanceBase.cpp", "GameLib/ActorInstance.cpp"],
|
||||
"py": ["game.py"],
|
||||
},
|
||||
"2V3": {
|
||||
"cpp": ["UserInterface/PythonPlayer.cpp", "UserInterface/PythonPlayerInput*.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp", "UserInterface/InstanceBaseMovement.cpp",
|
||||
"GameLib/ActorInstanceMotion.cpp", "GameLib/ActorInstanceEvent.cpp",
|
||||
"GameLib/ActorInstancePosition.cpp", "GameLib/ActorInstanceRotation.cpp",
|
||||
"GameLib/ActorInstanceCollisionDetection.cpp"],
|
||||
"py": [],
|
||||
},
|
||||
}
|
||||
|
||||
_STRIP = re.compile(r"//[^\n]*|/\*.*?\*/|\"(?:\\.|[^\"\\\n])*\"|'(?:\\.|[^'\\\n])*'", re.S)
|
||||
_INCLUDE = re.compile(r"^[ \t]*#[ \t]*include[ \t]*([<\"])([^>\"]+)[>\"]", re.M)
|
||||
_IDENT = re.compile(r"\b[A-Za-z_]\w*\b")
|
||||
_TYPE_DECL = re.compile(r"\b(class|struct|union|enum)\s+(?:__declspec\s*\([^)]*\)\s*)?([A-Za-z_]\w*)\s*"
|
||||
r"(?:final\s*)?(?::[^;{()]*)?\{")
|
||||
_ENUM_BODY = re.compile(r"\benum\b[^;{()]*\{")
|
||||
_TYPEDEF = re.compile(r"\btypedef\b")
|
||||
_DEFINE = re.compile(r"^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(?:\(|[ \t]+\S)", re.M)
|
||||
_FREE_FN = re.compile(r"^(?:extern\s+|inline\s+|static\s+|__forceinline\s+)*(?!return\b|typedef\b|else\b|if\b|"
|
||||
r"while\b|for\b|switch\b|case\b|class\b|struct\b|enum\b|template\b|using\b|friend\b)"
|
||||
r"[A-Za-z_][\w:<>, \t\*&]*?[ \t\*&]([A-Za-z_]\w*)[ \t]*\(", re.M)
|
||||
_EXTERN_VAR = re.compile(r"^extern\s+[^;()]*?\b([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*;", re.M)
|
||||
_PY_IMPORT = re.compile(r"^(?:import\s+([\w, \t]+)|from\s+(\w+)\s+import\b)", re.M)
|
||||
_PY_INIT = re.compile(r"Py_InitModule4?\(\s*\"(\w+)\"")
|
||||
|
||||
|
||||
def text(path: Path, keep_strings: bool = False) -> str:
|
||||
def repl(m: re.Match) -> str:
|
||||
tok = m.group(0)
|
||||
if tok.startswith("/*"):
|
||||
return "\n" * tok.count("\n")
|
||||
return tok if keep_strings and tok[0] in "\"'" else " "
|
||||
return _STRIP.sub(repl, path.read_bytes().decode("latin-1"))
|
||||
|
||||
|
||||
def matching_brace(src: str, start: int) -> int:
|
||||
depth = 0
|
||||
for i in range(start, len(src)):
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
return len(src) - 1
|
||||
|
||||
|
||||
def top_level(src: str) -> str:
|
||||
"""`src` with every brace-enclosed body blanked (newlines kept), leaving namespace-scope text."""
|
||||
out, depth = [], 0
|
||||
for ch in src:
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth = max(depth - 1, 0)
|
||||
elif depth == 0 or ch == "\n":
|
||||
out.append(ch)
|
||||
continue
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def declared(src: str) -> set[str]:
|
||||
"""Names a header declares at namespace scope: types, typedefs, enumerators, macros, free functions."""
|
||||
names = {m.group(2) for m in _TYPE_DECL.finditer(src)}
|
||||
for m in _ENUM_BODY.finditer(src):
|
||||
body = src[m.end():matching_brace(src, m.end() - 1)]
|
||||
for item in body.split(","):
|
||||
if ident := re.match(r"\s*([A-Za-z_]\w*)", item):
|
||||
names.add(ident.group(1))
|
||||
for m in _TYPEDEF.finditer(src):
|
||||
rest = src[m.end():]
|
||||
brace, semi = rest.find("{"), rest.find(";")
|
||||
if 0 <= brace < semi:
|
||||
end = matching_brace(rest, brace)
|
||||
tail = rest[end + 1:rest.find(";", end)]
|
||||
names.update(re.findall(r"[A-Za-z_]\w*", tail))
|
||||
elif semi >= 0:
|
||||
decl = rest[:semi]
|
||||
fnptr = re.search(r"\(\s*\w*\s*\*\s*([A-Za-z_]\w*)\s*\)", decl)
|
||||
last = re.findall(r"([A-Za-z_]\w*)\s*(?:\[[^\]]*\]\s*)*$", decl.strip())
|
||||
if fnptr:
|
||||
names.add(fnptr.group(1))
|
||||
elif last:
|
||||
names.add(last[0])
|
||||
names.update(_DEFINE.findall(src))
|
||||
top = top_level(src)
|
||||
names.update(m.group(1) for m in _FREE_FN.finditer(top))
|
||||
names.update(_EXTERN_VAR.findall(top))
|
||||
return names - IGNORED_SYMBOLS
|
||||
|
||||
|
||||
class Graph:
|
||||
def __init__(self, ref: Path):
|
||||
self.ref = ref
|
||||
self.files: dict[str, str] = {} # lower-case rel path -> real rel path
|
||||
for lib in SOURCE_DIRS:
|
||||
base = ref / lib
|
||||
if base.is_dir():
|
||||
for p in base.rglob("*"):
|
||||
if p.suffix.lower() in HEADER_SUFFIXES | {".cpp", ".c"}:
|
||||
rel = p.relative_to(ref).as_posix()
|
||||
self.files[rel.lower()] = rel
|
||||
self._src: dict[str, str] = {}
|
||||
self._ids: dict[str, set[str]] = {}
|
||||
self._inc: dict[str, list[tuple[str, str]]] = {}
|
||||
self._explicit: dict[str, tuple[list[str], list[str]]] = {}
|
||||
self.decl: dict[str, set[str]] = {}
|
||||
self.symbols: dict[str, set[str]] = defaultdict(set) # name -> declaring headers
|
||||
for rel in self.files.values():
|
||||
if Path(rel).suffix.lower() in HEADER_SUFFIXES:
|
||||
self.decl[rel] = declared(self.src(rel))
|
||||
for name in self.decl[rel]:
|
||||
self.symbols[name].add(rel)
|
||||
self._visible: dict[str, set[str]] = {}
|
||||
|
||||
def src(self, rel: str) -> str:
|
||||
if rel not in self._src:
|
||||
self._src[rel] = text(self.ref / rel)
|
||||
return self._src[rel]
|
||||
|
||||
def idents(self, rel: str) -> set[str]:
|
||||
if rel not in self._ids:
|
||||
self._ids[rel] = set(_IDENT.findall(self.src(rel)))
|
||||
return self._ids[rel]
|
||||
|
||||
def includes(self, rel: str) -> list[tuple[str, str]]:
|
||||
if rel not in self._inc:
|
||||
self._inc[rel] = _INCLUDE.findall(text(self.ref / rel, keep_strings=True))
|
||||
return self._inc[rel]
|
||||
|
||||
def resolve(self, frm: str, inc: str) -> str | None:
|
||||
inc = inc.replace("\\", "/")
|
||||
for base in (Path(frm).parent, Path(".")):
|
||||
parts: list[str] = []
|
||||
for part in (base / inc).as_posix().split("/"):
|
||||
if part == "..":
|
||||
if parts:
|
||||
parts.pop()
|
||||
elif part not in ("", "."):
|
||||
parts.append(part)
|
||||
hit = self.files.get("/".join(parts).lower())
|
||||
if hit:
|
||||
return hit
|
||||
return None
|
||||
|
||||
def explicit(self, rel: str) -> tuple[list[str], list[str]]:
|
||||
"""(resolved project includes, system includes); StdAfx includes are dropped."""
|
||||
if rel not in self._explicit:
|
||||
local, system = [], []
|
||||
for kind, inc in self.includes(rel):
|
||||
if Path(inc).name.lower() == "stdafx.h" and kind == '"':
|
||||
continue
|
||||
hit = self.resolve(rel, inc) if kind == '"' else None
|
||||
if hit:
|
||||
local.append(hit)
|
||||
elif kind == "<" or not hit:
|
||||
system.append(inc.lower())
|
||||
self._explicit[rel] = (local, system)
|
||||
return self._explicit[rel]
|
||||
|
||||
def explicit_closure(self, roots: list[str]) -> set[str]:
|
||||
seen: set[str] = set()
|
||||
stack = list(roots)
|
||||
while stack:
|
||||
rel = stack.pop()
|
||||
if rel in seen:
|
||||
continue
|
||||
seen.add(rel)
|
||||
# StdAfx chains are walked here (unlike explicit()), since that is what they supply.
|
||||
for kind, inc in self.includes(rel):
|
||||
hit = self.resolve(rel, inc) if kind == '"' else None
|
||||
if hit:
|
||||
stack.append(hit)
|
||||
return seen
|
||||
|
||||
def visible(self, rel: str) -> set[str]:
|
||||
"""Headers the 40250 StdAfx of `rel`'s library makes visible."""
|
||||
lib = rel.split("/")[0]
|
||||
if lib not in self._visible:
|
||||
stdafx = self.files.get(f"{lib}/stdafx.h".lower())
|
||||
self._visible[lib] = self.explicit_closure([stdafx]) - {stdafx} if stdafx else set()
|
||||
return self._visible[lib]
|
||||
|
||||
def closure(self, roots: list[str]) -> tuple[dict[str, str], set[str], dict[str, list[str]]]:
|
||||
"""Compile closure of `roots`: {file: reason}, system includes, ambiguous symbols."""
|
||||
reason: dict[str, str] = {r: "root" for r in roots}
|
||||
system: set[str] = set()
|
||||
ambiguous: dict[str, list[str]] = {}
|
||||
queue = list(roots)
|
||||
scanned: set[str] = set()
|
||||
known: set[str] = set()
|
||||
while queue:
|
||||
# Explicit edges first, so implicit resolution sees the full explicit closure.
|
||||
while queue:
|
||||
rel = queue.pop()
|
||||
known |= self.decl.get(rel, set())
|
||||
local, sys_inc = self.explicit(rel)
|
||||
system.update(sys_inc)
|
||||
for hit in local:
|
||||
if hit not in reason:
|
||||
reason[hit] = f"include from {rel}"
|
||||
queue.append(hit)
|
||||
for rel in sorted(set(reason) - scanned):
|
||||
scanned.add(rel)
|
||||
vis = self.visible(rel)
|
||||
lib = rel.split("/")[0]
|
||||
for name in sorted(self.idents(rel) - known):
|
||||
cands = sorted(self.symbols.get(name, set()) & vis)
|
||||
if not cands:
|
||||
continue
|
||||
pick = [c for c in cands if c.split("/")[0] == lib] or cands
|
||||
if len(pick) > 1:
|
||||
ambiguous[name] = pick
|
||||
hit = pick[0]
|
||||
known |= self.decl[hit]
|
||||
if hit not in reason:
|
||||
reason[hit] = f"implicit {name} from {rel}"
|
||||
queue.append(hit)
|
||||
return reason, system, ambiguous
|
||||
|
||||
|
||||
def header_layer(units: dict[str, Path], rel: str) -> str:
|
||||
stem = rel.rsplit(".", 1)[0]
|
||||
return layer(stem + ".cpp") if stem + ".cpp" in units else layer(rel)
|
||||
|
||||
|
||||
def expand(units: dict[str, Path], globs: list[str]) -> list[str]:
|
||||
import fnmatch
|
||||
out: list[str] = []
|
||||
for pat in globs:
|
||||
hits = sorted(u for u in units if fnmatch.fnmatch(u, pat))
|
||||
if not hits:
|
||||
raise SystemExit(f"slice pattern {pat!r} matches no active 40250 unit")
|
||||
out += [h for h in hits if h not in out]
|
||||
return out
|
||||
|
||||
|
||||
def py_closure(ref: Path, seeds: list[str], native: dict[str, str]) -> tuple[list[str], dict[str, str]]:
|
||||
root = (ref / PY_ROOT).resolve()
|
||||
files = {p.stem.lower(): p for p in root.glob("*.py")}
|
||||
seen: list[str] = []
|
||||
mods: dict[str, str] = {}
|
||||
stack = [Path(s).stem.lower() for s in seeds]
|
||||
while stack:
|
||||
name = stack.pop()
|
||||
if name in seen:
|
||||
continue
|
||||
seen.append(name)
|
||||
for a, b in _PY_IMPORT.findall(files[name].read_bytes().decode("latin-1")):
|
||||
for mod in ([m.strip() for m in a.split(",")] if a else [b]):
|
||||
if not mod:
|
||||
continue
|
||||
if mod.lower() in files:
|
||||
stack.append(mod.lower())
|
||||
else:
|
||||
mods[mod] = native.get(mod, "stdlib")
|
||||
return sorted(files[n].name for n in seen), dict(sorted(mods.items()))
|
||||
|
||||
|
||||
def native_modules(ref: Path, units: dict[str, Path]) -> dict[str, str]:
|
||||
out = {}
|
||||
for unit, path in units.items():
|
||||
if unit.endswith(".cpp"):
|
||||
for name in _PY_INIT.findall(path.read_bytes().decode("latin-1")):
|
||||
out[name] = unit
|
||||
return out
|
||||
|
||||
|
||||
def owner_map(units: dict[str, Path], order: list[str], closures: dict[str, set[str]]) -> dict[str, str]:
|
||||
owner = {}
|
||||
for unit in units:
|
||||
if unit.endswith(".cpp"):
|
||||
for suffix in (".h", ".hpp", ".inl"):
|
||||
owner[unit[:-4] + suffix] = unit
|
||||
for unit in order:
|
||||
for rel in sorted(closures[unit]):
|
||||
owner.setdefault(rel, unit)
|
||||
return owner
|
||||
|
||||
|
||||
def build_slices(ref: Path, g: Graph, units: dict[str, Path]) -> dict[str, dict]:
|
||||
native = native_modules(ref, units)
|
||||
cpp: list[str] = []
|
||||
py_seeds: list[str] = []
|
||||
out = {}
|
||||
for name, spec in SLICES.items():
|
||||
new = [u for u in expand(units, spec["cpp"]) if u not in cpp]
|
||||
cpp += new
|
||||
py_seeds += spec["py"]
|
||||
reason, system, ambiguous = g.closure(cpp)
|
||||
headers = sorted(r for r in reason if Path(r).suffix.lower() in HEADER_SUFFIXES)
|
||||
scripts, mods = py_closure(ref, py_seeds, native)
|
||||
out[name] = {
|
||||
"slice": name,
|
||||
"generated_by": ".agents/skills/metin2-40250-parity-audit/scripts/port_deps.py slices --write",
|
||||
"units": [{"unit": u, "layer": layer(u), "new": u in new} for u in cpp],
|
||||
"headers": [{"header": h, "layer": header_layer(units, h), "reason": reason[h],
|
||||
"owner_in_slice": (h.rsplit(".", 1)[0] + ".cpp") in cpp}
|
||||
for h in headers],
|
||||
"stdafx_supplied": sorted(h for h in headers if reason[h].startswith("implicit")),
|
||||
"system_includes": sorted(system),
|
||||
"python_scripts": scripts,
|
||||
"native_modules": [{"module": m, "provider": p, "real": p in cpp,
|
||||
"layer": layer(p) if p in units else "stdlib"} for m, p in mods.items()],
|
||||
"ambiguous_symbols": ambiguous,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def build_order(g: Graph, units: dict[str, Path]) -> dict:
|
||||
p0 = [u for u in units if priority(u) == "P0" and layer(u) == "logic"]
|
||||
closures = {u: set(g.closure([u])[0]) for u in p0}
|
||||
stems = {u[:-4].lower(): u for u in units if u.endswith(".cpp")}
|
||||
deps: dict[str, set[str]] = {}
|
||||
for u in p0:
|
||||
deps[u] = set()
|
||||
for rel in closures[u]:
|
||||
owner = stems.get(rel.rsplit(".", 1)[0].lower())
|
||||
if owner and owner != u and owner in closures:
|
||||
deps[u].add(owner)
|
||||
# Tarjan SCC, then Kahn on the condensation; ties broken by the roadmap's candidate order.
|
||||
index, low, stack, on, sccs = {}, {}, [], set(), []
|
||||
|
||||
def strong(v: str, counter=[0]) -> None:
|
||||
index[v] = low[v] = counter[0]
|
||||
counter[0] += 1
|
||||
stack.append(v)
|
||||
on.add(v)
|
||||
for w in sorted(deps[v]):
|
||||
if w not in index:
|
||||
strong(w)
|
||||
low[v] = min(low[v], low[w])
|
||||
elif w in on:
|
||||
low[v] = min(low[v], index[w])
|
||||
if low[v] == index[v]:
|
||||
comp = []
|
||||
while True:
|
||||
w = stack.pop()
|
||||
on.discard(w)
|
||||
comp.append(w)
|
||||
if w == v:
|
||||
break
|
||||
sccs.append(sorted(comp))
|
||||
|
||||
sys.setrecursionlimit(10000)
|
||||
for v in sorted(p0):
|
||||
if v not in index:
|
||||
strong(v)
|
||||
comp_of = {u: i for i, c in enumerate(sccs) for u in c}
|
||||
# Tarjan emits SCCs in reverse topological order of the "depends on" edges: dependencies first.
|
||||
steps = []
|
||||
for comp in sccs:
|
||||
after = sorted({w for u in comp for d in deps[u] if comp_of[d] != comp_of[u] for w in sccs[comp_of[d]]})
|
||||
steps.append({"units": comp, "serial_group": len(comp) > 1, "after": after})
|
||||
return {
|
||||
"generated_by": ".agents/skills/metin2-40250-parity-audit/scripts/port_deps.py order --write",
|
||||
"note": "P0 logic units; a unit starts only after every unit in `after` has landed. `serial_group` units "
|
||||
"include each other's headers and are ported as one serial group.",
|
||||
"steps": steps,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
c = sub.add_parser("closure")
|
||||
c.add_argument("files", nargs="+")
|
||||
for name in ("slices", "order"):
|
||||
s = sub.add_parser(name)
|
||||
s.add_argument("--write", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
repo = refroot.repo_root()
|
||||
ref = refroot.reference_root(repo)
|
||||
units = inventory(ref)
|
||||
g = Graph(ref)
|
||||
out_dir = repo / "audit" / "slices"
|
||||
|
||||
if args.cmd == "closure":
|
||||
roots = []
|
||||
for f in args.files:
|
||||
hit = g.files.get(f.lower())
|
||||
if not hit:
|
||||
raise SystemExit(f"{f}: not a 40250 source file")
|
||||
roots.append(hit)
|
||||
reason, system, ambiguous = g.closure(roots)
|
||||
for rel in sorted(reason):
|
||||
print(f"{rel:55} {header_layer(units, rel):8} {reason[rel]}")
|
||||
print(f"\nsystem includes: {' '.join(sorted(system))}")
|
||||
for name, cands in sorted(ambiguous.items()):
|
||||
print(f"ambiguous {name}: {', '.join(cands)}")
|
||||
return 0
|
||||
|
||||
data = build_slices(ref, g, units) if args.cmd == "slices" else {"batch2-order": build_order(g, units)}
|
||||
for name, payload in data.items():
|
||||
body = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
||||
if args.write:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
(out_dir / f"{name}.json").write_text(body, encoding="utf-8")
|
||||
print(f"wrote audit/slices/{name}.json")
|
||||
elif args.cmd == "order":
|
||||
for i, step in enumerate(payload["steps"], 1):
|
||||
tag = " (serial)" if step["serial_group"] else ""
|
||||
print(f"{i:2}. {', '.join(step['units'])}{tag} after: {', '.join(step['after']) or '-'}")
|
||||
else:
|
||||
print(f"{name}: {len(payload['units'])} units, {len(payload['headers'])} headers "
|
||||
f"({len(payload['stdafx_supplied'])} via StdAfx), {len(payload['python_scripts'])} scripts, "
|
||||
f"{sum(m['real'] for m in payload['native_modules'])}/{len(payload['native_modules'])} native modules real")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -28,7 +28,7 @@ python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --lim
|
||||
| 3 | 剩余 Python 模块和窗口 → P1 战斗/技能 → P2 游戏阶段封包 → P3 物品 | 未开始 |
|
||||
| 4 | `NEEDS_LIVE` 真服验证批 | 未开始 |
|
||||
|
||||
## 批次 2 单元(顺序由 2A 的依赖图重新生成;下表仅为候选)
|
||||
## 批次 2 单元(顺序以 `audit/slices/batch2-order.json` 为准,由 `port_deps.py order --write` 生成;下表仅为候选)
|
||||
|
||||
实现位置一律是镜像文件 `extension/src/port/<Lib>/<File>.{h,cpp}`;下面列出的是需要删减旧逻辑的迁移来源。
|
||||
目录、CMake 目标和公共头由 2A 建立。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"generated_by": ".agents/skills/metin2-40250-parity-audit/scripts/port_deps.py order --write",
|
||||
"note": "P0 logic units; a unit starts only after every unit in `after` has landed. `serial_group` units include each other's headers and are ported as one serial group.",
|
||||
"steps": [
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": []
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstanceCollisionDetection.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstanceEvent.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstanceMotion.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstanceMotionEvent.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstancePosition.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstanceRotation.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"GameLib/ActorInstanceSync.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
],
|
||||
"serial_group": true,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBaseBattle.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBaseEffect.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBaseEvent.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBaseMotion.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBaseMovement.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/InstanceBaseTransform.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/PythonPlayerInput.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/PythonPlayerInputKeyboard.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"units": [
|
||||
"UserInterface/PythonPlayerInputMouse.cpp"
|
||||
],
|
||||
"serial_group": false,
|
||||
"after": [
|
||||
"GameLib/ActorInstance.cpp",
|
||||
"UserInterface/InstanceBase.cpp",
|
||||
"UserInterface/PythonCharacterManager.cpp",
|
||||
"UserInterface/PythonPlayer.cpp",
|
||||
"UserInterface/PythonPlayerEventHandler.cpp"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
-1
@@ -101,7 +101,9 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
|
||||
1. `extension/src/port/common/`:`Win32Types.h`(`BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT` 等标量的定宽映射;
|
||||
`HANDLE`/窗口句柄/消息参数另用指针宽度的平台类型)、
|
||||
40250 用到的 Win32/CRT 宏和函数(`ZeroMemory`、`_snprintf`、`stricmp`、`timeGetTime` 等)的最小实现、
|
||||
`StdAfx.h` 等价物。
|
||||
`StdAfx.h` 等价物。GameLib 的逻辑头直接使用 D3DX 数学类型(`TPixelPosition` 就是 `D3DXVECTOR3`),所以还要一个
|
||||
与平台无关的 D3DX 数学层(`D3DXVECTOR2/3/4`、`D3DXMATRIX`、`D3DXQUATERNION`、`D3DXCOLOR` 及逻辑层用到的 `D3DX*` 函数),
|
||||
按 D3DX8 的 float 语义实现。
|
||||
2. 参考公共头的最小闭包:从第一批要移植的单元出发(`PythonPlayerEventHandler.h` 依赖 `ActorInstance.h`、`FlyHandler.h`、
|
||||
`PythonNetworkStream.h`、`InstanceBase.h`),用脚本列出 `#include` 闭包,把闭包里的头文件先照抄为可编译的声明。
|
||||
3. `port_logic` 静态库 CMake 目标,链接进 `libmtgodot`;macOS、Android、iOS、Linux、Windows 五个平台分别编译。
|
||||
@@ -112,6 +114,10 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
|
||||
6. port-map 重新基线(见第 6 节)。
|
||||
7. 用 `#include` 依赖图生成批次 2 的移植顺序(拓扑序),替换原来"互不共享实现文件即可并行"的假设:共享头文件的单元,
|
||||
头文件由先做的那个单元负责,后面的单元只能在它合入后开始。
|
||||
**已完成**:`port_deps.py`(显式 `#include` + 按符号解析的 StdAfx 隐式依赖)生成 `audit/slices/2V0–2V3.json`
|
||||
和 `audit/slices/batch2-order.json`。结论:每个切片的头文件闭包约 210 个(其中平台层约 110 个,来自
|
||||
`ThingInstance.h`/`GrpBase.h` 等);P0 单元里 `InstanceBase`、`PythonCharacterManager`、`PythonPlayer`、
|
||||
`PythonPlayerEventHandler` 的头文件互相包含,必须作为一组串行移植,`ActorInstance.cpp` 排在最前。
|
||||
|
||||
### 批次 2R:资源包能力盘点
|
||||
|
||||
|
||||
Reference in New Issue
Block a user