docs(parity): mirror 40250 layout for the logic layer; evaluate embedded Python UI
- SKILL.md: logic units port into extension/src/port/<Lib>/<File>, platform adapters under extension/src/platform; legacy GDScript owners are migration sources; python layer frozen until the embedding decision - port_map.py: logic/python/platform layers with per-layer progress, queue --layer, LEGACY layout check; inventory adds EterBase, EterPythonLib, ScriptLib, SpeedTreeLib, SphereLib, EterImageLib - py_embed_spike.py + docs/PYTHON-EMBED-EVAL.md: the 40250 system.py bootstrap loads 67/74 root modules unchanged under CPython 2.7 with stub natives Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,7 +122,7 @@ def manifest_data(repo: pathlib.Path) -> tuple[set[str], dict[str, list[str]]]:
|
||||
return listed, owners
|
||||
|
||||
|
||||
def active_project_sources(reference_root: pathlib.Path) -> set[str]:
|
||||
def active_project_sources(reference_root: pathlib.Path, dirs: tuple[str, ...] = REFERENCE_DIRS) -> set[str]:
|
||||
active: set[str] = set()
|
||||
for project in sorted((reference_root.parent / "vs_files").glob("*/*.vcxproj")):
|
||||
try:
|
||||
@@ -140,7 +140,7 @@ def active_project_sources(reference_root: pathlib.Path) -> set[str]:
|
||||
relative = candidate.relative_to(reference_root.resolve())
|
||||
except ValueError:
|
||||
continue
|
||||
if relative.parts and relative.parts[0] in REFERENCE_DIRS:
|
||||
if relative.parts and relative.parts[0] in dirs:
|
||||
if relative.suffix.lower() in SOURCE_SUFFIXES:
|
||||
active.add(relative.as_posix())
|
||||
return active
|
||||
|
||||
@@ -5,11 +5,16 @@ Units are the active 40250 C++ sources (from the .vcxproj files) plus the 40250
|
||||
root (`../../Client/Eternexus/root/*.py` relative to the reference root). Port-map entries
|
||||
live in `audit/port-map/<unit path>.json`; functions without an entry count as TODO.
|
||||
|
||||
port_map.py status [--lib UserInterface] coverage per library (and per unit with --units)
|
||||
port_map.py queue [--limit 20] next units by priority, then by TODO count
|
||||
port_map.py status [--lib UserInterface] coverage per library and per layer (per unit with --units)
|
||||
port_map.py queue [--limit 20] [--layer L] next units by priority, then by TODO count (default layer: logic)
|
||||
port_map.py init <unit> [<unit>...] create or extend entries (new functions as TODO)
|
||||
port_map.py show <unit> per-function status of one unit
|
||||
port_map.py check stale hashes, unknown functions, tag mismatches
|
||||
port_map.py check stale hashes, unknown functions, tag mismatches, layout
|
||||
|
||||
Layers: `logic` (ported into extension/src/port/<Lib>/<File>, same names as 40250), `python`
|
||||
(the embedded-Python track: Python binding modules, EterPythonLib window system, ScriptLib and the
|
||||
Python root) and `platform` (Direct3D/Granny/Miles/Win32 rendering, audio and OS plumbing, covered
|
||||
by adapters). Progress is reported per layer so platform code does not dilute the logic figure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,11 +29,34 @@ from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import refroot # script directory is on sys.path when run directly
|
||||
from audit_source_coverage import active_project_sources
|
||||
from audit_source_coverage import REFERENCE_DIRS, active_project_sources
|
||||
|
||||
STATUSES = ("TODO", "PORTED", "ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE")
|
||||
DONE = {"PORTED", "ADAPTED", "N_A"}
|
||||
PY_ROOT = "../../Client/Eternexus/root"
|
||||
UNIT_DIRS = REFERENCE_DIRS + ("EterBase", "EterPythonLib", "ScriptLib", "SpeedTreeLib", "SphereLib", "EterImageLib")
|
||||
PORT_DIR = "extension/src/port"
|
||||
LAYERS = ("logic", "python", "platform")
|
||||
# First matching pattern wins; anything unmatched is `logic`.
|
||||
LAYER_PATTERNS = [
|
||||
("python", [f"{PY_ROOT}/*.py", "EterPythonLib/*", "ScriptLib/*", "UserInterface/*Module.cpp",
|
||||
"UserInterface/*Moudle.cpp", "UserInterface/PythonSkill.cpp", "UserInterface/PythonGuild.cpp",
|
||||
"UserInterface/PythonShop.cpp", "UserInterface/PythonSafeBox.cpp", "UserInterface/PythonQuest.cpp",
|
||||
"UserInterface/PythonMessenger.cpp"]),
|
||||
("platform", ["EterGrnLib/*", "MilesLib/*", "SpeedTreeLib/*", "SphereLib/*", "EterImageLib/*", "EffectLib/*",
|
||||
"PRTerrainLib/*", "EterLib/Grp*.cpp", "EterLib/StateManager.cpp", "EterLib/MSApplication.cpp",
|
||||
"EterLib/MSWindow.cpp", "EterLib/Input.cpp", "EterLib/IME.cpp", "EterLib/SkyBox.cpp",
|
||||
"EterLib/LensFlare.cpp", "EterLib/ScreenFilter.cpp", "EterLib/Decal.cpp", "EterLib/EnvironmentMap.cpp",
|
||||
"EterLib/JpegFile.cpp", "EterLib/TargaResource.cpp", "EterLib/DibBar.cpp", "EterLib/BlockTexture.cpp",
|
||||
"EterLib/CullingManager.cpp", "EterLib/Thread.cpp", "EterLib/Mutex.cpp", "EterLib/FileLoaderThread.cpp",
|
||||
"EterLib/TextBar.cpp", "EterBase/*", "GameLib/MapOutdoorRender*.cpp", "GameLib/MapOutdoorWater.cpp",
|
||||
"GameLib/SnowParticle.cpp", "GameLib/SnowEnvironment.cpp", "GameLib/TerrainDecal.cpp",
|
||||
"GameLib/TerrainPatch.cpp", "UserInterface/PythonApplicationLogo.cpp", "UserInterface/MovieMan.cpp",
|
||||
"UserInterface/HackShield.cpp", "UserInterface/NProtectGameGuard.cpp", "UserInterface/ProcessCRC.cpp",
|
||||
"UserInterface/ProcessScanner.cpp", "UserInterface/WiseLogicXTrap.cpp",
|
||||
"UserInterface/PythonApplicationWebPage.cpp", "UserInterface/PythonExceptionSender.cpp",
|
||||
"UserInterface/UserInterface.cpp", "UserInterface/CheckLatestFiles.cpp"]),
|
||||
]
|
||||
|
||||
# Order follows SKILL.md "Priority"; the first matching pattern wins.
|
||||
PRIORITY = [
|
||||
@@ -100,7 +128,8 @@ def py_functions(path: Path) -> list[str]:
|
||||
|
||||
|
||||
def inventory(ref: Path) -> dict[str, Path]:
|
||||
units = {p: ref / p for p in sorted(active_project_sources(ref)) if p.endswith(".cpp")}
|
||||
units = {p: ref / p for p in sorted(active_project_sources(ref, UNIT_DIRS))
|
||||
if p.endswith(".cpp") and not p.lower().endswith("stdafx.cpp")}
|
||||
py_dir = (ref / PY_ROOT).resolve()
|
||||
if py_dir.is_dir():
|
||||
for p in sorted(py_dir.glob("*.py")):
|
||||
@@ -119,6 +148,18 @@ def priority(unit: str) -> str:
|
||||
return DEFAULT_PRIORITY
|
||||
|
||||
|
||||
def layer(unit: str) -> str:
|
||||
for name, patterns in LAYER_PATTERNS:
|
||||
if any(fnmatch.fnmatch(unit, pat) for pat in patterns):
|
||||
return name
|
||||
return "logic"
|
||||
|
||||
|
||||
def port_file(unit: str) -> str:
|
||||
"""Expected implementation path stem for a `logic` unit (`extension/src/port/GameLib/ActorInstance`)."""
|
||||
return f"{PORT_DIR}/{unit.rsplit('.', 1)[0]}"
|
||||
|
||||
|
||||
def entry_path(repo: Path, unit: str) -> Path:
|
||||
rel = unit.replace("../../Client/Eternexus/", "Client/")
|
||||
return repo / "audit" / "port-map" / (rel + ".json")
|
||||
@@ -162,6 +203,9 @@ def cmd_status(repo: Path, units: dict[str, Path], args) -> int:
|
||||
per_lib.setdefault(lib, Counter()).update(counts)
|
||||
rows.append((unit, counts))
|
||||
total = Counter()
|
||||
per_layer: dict[str, Counter] = {}
|
||||
for unit, counts in rows:
|
||||
per_layer.setdefault(layer(unit), Counter()).update(counts)
|
||||
print(f"{'library':<16}{'funcs':>7}{'done':>7} " + " ".join(f"{s:>10}" for s in STATUSES))
|
||||
for lib, c in sorted(per_lib.items()):
|
||||
total.update(c)
|
||||
@@ -172,6 +216,13 @@ def cmd_status(repo: Path, units: dict[str, Path], args) -> int:
|
||||
done = sum(total[s] for s in DONE)
|
||||
print(f"{'TOTAL':<16}{n:>7}{done:>7} " + " ".join(f"{total[s]:>10}" for s in STATUSES)
|
||||
+ f" ({100.0 * done / max(n, 1):.1f}% done)")
|
||||
print()
|
||||
for name in LAYERS:
|
||||
c = per_layer.get(name, Counter())
|
||||
n = sum(c.values())
|
||||
done = sum(c[s] for s in DONE)
|
||||
print(f"layer {name:<10}{n:>7}{done:>7} " + " ".join(f"{c[s]:>10}" for s in STATUSES)
|
||||
+ f" ({100.0 * done / max(n, 1):.1f}% done)")
|
||||
if args.units:
|
||||
for unit, c in rows:
|
||||
if sum(c.values()) != c["TODO"]:
|
||||
@@ -182,6 +233,8 @@ def cmd_status(repo: Path, units: dict[str, Path], args) -> int:
|
||||
def cmd_queue(repo: Path, units: dict[str, Path], args) -> int:
|
||||
rows = []
|
||||
for unit, path in units.items():
|
||||
if layer(unit) != args.layer:
|
||||
continue
|
||||
c = unit_counts(repo, unit, path)
|
||||
open_ = c["TODO"] + c["DIVERGENT"]
|
||||
if open_:
|
||||
@@ -235,6 +288,7 @@ def scan_tags(repo: Path) -> dict[str, list[str]]:
|
||||
|
||||
def cmd_check(repo: Path, units: dict[str, Path], args) -> int:
|
||||
problems = 0
|
||||
legacy = 0
|
||||
recorded_ported: set[str] = set()
|
||||
for entry_file in sorted((repo / "audit" / "port-map").rglob("*.json")):
|
||||
entry = json.loads(entry_file.read_text(encoding="utf-8"))
|
||||
@@ -265,6 +319,10 @@ def cmd_check(repo: Path, units: dict[str, Path], args) -> int:
|
||||
if status == "ADAPTED" and not info.get("test"):
|
||||
print(f"ERROR {label}: {fn} is ADAPTED without test")
|
||||
problems += 1
|
||||
if status in {"PORTED", "NEEDS_LIVE"} and layer(unit) == "logic" and not any(
|
||||
i.startswith(port_file(unit) + ".") for i in info.get("impl", [])):
|
||||
print(f"LEGACY {label}: {fn} impl is outside {port_file(unit)}.{{h,cpp}}; move it when the unit is ported")
|
||||
legacy += 1
|
||||
if status in {"PORTED", "ADAPTED"}:
|
||||
recorded_ported.add(fn.split("::")[-1] if "::" not in fn else fn)
|
||||
recorded_ported.add(fn)
|
||||
@@ -272,7 +330,8 @@ def cmd_check(repo: Path, units: dict[str, Path], args) -> int:
|
||||
untracked = sorted(t for t in tags if t not in recorded_ported)
|
||||
for tag in untracked:
|
||||
print(f"WARN tag `40250: {tag}` at {tags[tag][0]} has no PORTED/ADAPTED port-map entry")
|
||||
print(f"check: {problems} error(s), {len(untracked)} untracked tag(s), {len(tags)} tag(s) scanned")
|
||||
print(f"check: {problems} error(s), {legacy} legacy location(s), {len(untracked)} untracked tag(s), "
|
||||
f"{len(tags)} tag(s) scanned")
|
||||
return 1 if problems else 0
|
||||
|
||||
|
||||
@@ -281,6 +340,7 @@ def main() -> int:
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
p = sub.add_parser("status"); p.add_argument("--lib"); p.add_argument("--units", action="store_true")
|
||||
p = sub.add_parser("queue"); p.add_argument("--limit", type=int, default=20)
|
||||
p.add_argument("--layer", choices=LAYERS, default="logic")
|
||||
p = sub.add_parser("init"); p.add_argument("units", nargs="+")
|
||||
p = sub.add_parser("show"); p.add_argument("unit")
|
||||
sub.add_parser("check")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python2.7
|
||||
"""Embedded-Python feasibility spike (Python 2.7 only; see docs/PYTHON-EMBED-EVAL.md).
|
||||
|
||||
Emulates CPythonLauncher::Create + system.py bootstrap: stub native modules (every call returns 0),
|
||||
a directory-backed `pack` module over the 40250 Eternexus tree, then imports every root module
|
||||
through system.py's own `__pack_import`. Reports loaded/failed modules to --out (default
|
||||
py_embed_spike.txt); system.py redirects stdout, so nothing useful is printed.
|
||||
|
||||
python2.7 py_embed_spike.py [--out FILE] (reference root: MT_40250_SOURCE or audit manifest)
|
||||
"""
|
||||
import sys, types, os, glob, traceback, json
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.abspath(os.path.join(HERE, "../../../.."))
|
||||
_ref = os.environ.get("MT_40250_SOURCE") or json.load(open(os.path.join(REPO, "audit/manifest.json")))["reference_root"]
|
||||
E = os.path.normpath(os.path.join(REPO, _ref, "../../Client/Eternexus"))
|
||||
OUT = sys.argv[sys.argv.index("--out") + 1] if "--out" in sys.argv else "py_embed_spike.txt"
|
||||
FILES = {}
|
||||
for p in glob.glob(E + "/root/*"): FILES[os.path.basename(p).lower()] = p
|
||||
for base, pre in ((E + "/locale_en/locale/en", "locale/en/"), (E + "/uiscript/uiscript", "uiscript/")):
|
||||
for d, _, fs in os.walk(base):
|
||||
for f in fs:
|
||||
full = os.path.join(d, f)
|
||||
FILES[(pre + os.path.relpath(full, base)).lower()] = full
|
||||
LOG = open(OUT, "w")
|
||||
MISSING = {}
|
||||
class Stub(types.ModuleType):
|
||||
def __getattr__(self, name):
|
||||
if name.startswith("__"): raise AttributeError(name)
|
||||
key = "%s.%s" % (self.__name__, name)
|
||||
def f(*a, **k):
|
||||
MISSING[key] = MISSING.get(key, 0) + 1
|
||||
return 0
|
||||
setattr(self, name, f); return f
|
||||
NATIVE = "app net player chr chrmgr wndMgr grp grpImage grpText grpThing item skill snd chat background miniMap ime systemSetting guild safebox shop exchange quest messenger textTail nonplayer effect event eventMgr fly dbg profiler ServerStateChecker udp".split()
|
||||
for n in NATIVE: sys.modules[n] = Stub(n)
|
||||
pack = types.ModuleType("pack")
|
||||
def norm(n): return n.replace("\\", "/").lower().lstrip("/")
|
||||
pack.Exist = lambda n: norm(n) in FILES
|
||||
pack.Get = lambda n: open(FILES[norm(n)], "rb").read()
|
||||
sys.modules["pack"] = pack
|
||||
sys.modules["dbg"].Trace = sys.modules["dbg"].TraceError = lambda *a: None
|
||||
sys.modules["dbg"].LogBox = lambda m: LOG.write("LOGBOX: %s\n" % m)
|
||||
sys.modules["app"].Loop = lambda: LOG.write("app.Loop reached\n")
|
||||
STR = {"app.GetLocalePath": "locale/en", "app.GetLocaleName": "en", "app.GetLocaleServiceName": "en",
|
||||
"app.GetLocalePathCommon": "locale/common"}
|
||||
for k, v in STR.items():
|
||||
m, f = k.split(".")
|
||||
setattr(sys.modules[m], f, (lambda v: lambda *a: v)(v))
|
||||
import __builtin__
|
||||
__builtin__.TRUE = 1; __builtin__.FALSE = 0 # CPythonLauncher::Create
|
||||
import __main__
|
||||
__main__.__DEBUG__ = 0
|
||||
__main__.__COMMAND_LINE__ = ""
|
||||
src = pack.Get("system.py")
|
||||
try:
|
||||
exec compile(src, "system.py", "exec") in __main__.__dict__
|
||||
except BaseException:
|
||||
LOG.write("system.py raised:\n" + traceback.format_exc())
|
||||
ok, bad = [], []
|
||||
_pimp = __main__.__dict__["__pack_import"]
|
||||
for name in sorted(FILES):
|
||||
if not name.endswith(".py") or "/" in name: continue
|
||||
mod = name[:-3]
|
||||
real = [m for m in sys.modules if m.lower() == mod]
|
||||
if real: ok.append(mod); continue
|
||||
try:
|
||||
_pimp(mod); ok.append(mod)
|
||||
except BaseException as e:
|
||||
bad.append((mod, "%s: %s" % (type(e).__name__, str(e)[:100])))
|
||||
LOG.write("modules loaded %d, failed %d\n" % (len(ok), len(bad)))
|
||||
for m, e in bad: LOG.write(" FAIL %s %s\n" % (m, e))
|
||||
LOG.write("native functions called during bootstrap: %d\n" % len(MISSING))
|
||||
LOG.write(" " + " ".join(sorted(MISSING)) + "\n")
|
||||
LOG.close()
|
||||
Reference in New Issue
Block a user