Files
mtgodot-poc/.agents/skills/metin2-40250-parity-audit/scripts/port_copy.py
T
shenleiandClaude Opus 5 fc47582017 port 2A step 2: copy the 40250 header closure into the port mirror
76 closure headers (EterBase, EterLocale, EterPack, EterLib, GameLib, MilesLib,
EffectLib, SphereLib, EterImageLib) copied by the new port_copy.py (CP949->UTF-8,
LF, include-path case only); platform-class headers go to the mirror path too, as
the interface platform/ implements. Six manual `// PORT:` edits (Random, Singleton,
Pool, Stl, Utils, FlyTarget).

- common/D3D8Types.h: D3D8 value types with SDK values/layout, opaque COM interfaces
- common/shim/sdk (d3d8/d3dx8/mss) on every platform, common/shim/win32 off Windows
- Win32Types/Win32Crt: HRESULT, GUID, LARGE_INTEGER, LOGFONT/TEXTMETRIC, _atoi64,
  CRITICAL_SECTION on std::recursive_mutex
- header gate prepends each library's StdAfx.h (40250's precompiled header)

port_gate: macOS, Android, iOS, Windows (mingw) PASS; Linux BLOCKED (no toolchain).
ScriptLib/EterPythonLib/UserInterface wait for 2P (their StdAfx includes Python).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:27:57 +09:00

142 lines
5.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Copy 40250 sources into the port mirror (extension/src/port/<Lib>/<File>) with mechanical edits only.
port_copy.py copy <rel> [<rel>...] [--force] write the mechanical copy (refuses to overwrite without --force)
port_copy.py diff [<rel>...] show the manual edits: port file vs its mechanical copy
(no args: every mirrored 40250 file that differs)
`<rel>` is a reference path such as `GameLib/ActorInstance.h`, matched case-insensitively.
Mechanical edits, and nothing else:
- CP949 is decoded and written as UTF-8 (comments are Korean). A non-ASCII string or character
literal would change bytes at runtime, so such a file is refused and must be handled by hand.
- CRLF becomes LF.
- `#include "..."` paths get the on-disk case of the file they name (40250 builds on a
case-insensitive file system and writes `../eterLib/StdAfx.h`, `Parser.h` for `parser.h`, ...),
keeping the relative form, so the original include resolves the same way on every platform.
Everything else in a port file that differs from this copy is a manual edit, marked `// PORT:`.
"""
from __future__ import annotations
import argparse
import difflib
import re
import sys
from pathlib import Path
import refroot # script directory is on sys.path when run directly
REPO = refroot.repo_root()
PORT = REPO / "extension" / "src" / "port"
INCLUDE = re.compile(r'^(\s*#\s*include\s*")([^"]+)(")', re.M)
LITERAL = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'')
COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
def ref_index(ref: Path) -> dict[str, Path]:
return {p.relative_to(ref).as_posix().lower(): p for p in ref.rglob("*") if p.is_file()}
def canonical(ref: Path, index: dict[str, Path], path: Path) -> str:
return index[path.relative_to(ref).as_posix().lower()].relative_to(ref).as_posix()
def resolve(ref: Path, index: dict[str, Path], rel: str) -> Path:
hit = index.get(rel.replace("\\", "/").lower())
if not hit:
raise SystemExit(f"{rel}: not in the 40250 reference tree")
return hit
def fix_include(ref: Path, index: dict[str, Path], src: Path, target: str) -> str:
base = src.parent.relative_to(ref).as_posix()
parts = [] if base == "." else base.split("/")
for piece in target.replace("\\", "/").split("/"):
if piece == "..":
if not parts:
return target
parts.pop()
elif piece != ".":
parts.append(piece)
hit = index.get("/".join(parts).lower())
if not hit:
return target # unresolved (commented-out or absent in 40250): leave untouched
real = hit.relative_to(ref).as_posix().split("/")
# Keep the original shape: same number of `..` and segments, only the case changes.
kept = [p for p in target.replace("\\", "/").split("/") if p in ("..", ".")]
tail = real[len(real) - (len(target.replace("\\", "/").split("/")) - len(kept)):]
return "/".join(kept + tail)
def mechanical(ref: Path, index: dict[str, Path], src: Path) -> str:
raw = src.read_bytes()
try:
text = raw.decode("cp949")
except UnicodeDecodeError as e:
raise SystemExit(f"{src.relative_to(ref)}: not CP949 ({e})")
text = text.replace("\r\n", "\n").lstrip("")
code = COMMENT.sub(lambda m: " " * len(m.group(0)), text)
bad = [m.group(0) for m in LITERAL.finditer(code) if not m.group(0).isascii()]
if bad:
raise SystemExit(f"{src.relative_to(ref)}: non-ASCII literal(s), copy by hand: {bad[:3]}")
return INCLUDE.sub(lambda m: m.group(1) + fix_include(ref, index, src, m.group(2)) + m.group(3), text)
def cmd_copy(args) -> int:
ref = refroot.reference_root()
index = ref_index(ref)
for rel in args.rel:
src = resolve(ref, index, rel)
dst = PORT / canonical(ref, index, src)
if dst.exists() and not args.force:
print(f"skip {dst.relative_to(REPO)} (exists; --force overwrites manual edits)")
continue
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_text(mechanical(ref, index, src), encoding="utf-8")
print(f"copy {dst.relative_to(REPO)}")
return 0
def cmd_diff(args) -> int:
ref = refroot.reference_root()
index = ref_index(ref)
if args.rel:
pairs = [(resolve(ref, index, r), PORT / canonical(ref, index, resolve(ref, index, r))) for r in args.rel]
else:
pairs = []
for dst in sorted(PORT.rglob("*")):
key = dst.relative_to(PORT).as_posix().lower()
if dst.is_file() and key in index and not key.startswith("common/"):
pairs.append((index[key], dst))
changed = 0
for src, dst in pairs:
if not dst.exists():
print(f"missing {dst.relative_to(REPO)}")
continue
want = mechanical(ref, index, src).splitlines(keepends=True)
have = dst.read_text(encoding="utf-8").splitlines(keepends=True)
diff = list(difflib.unified_diff(want, have, f"40250/{src.relative_to(ref).as_posix()}",
dst.relative_to(REPO).as_posix()))
if diff:
changed += 1
sys.stdout.writelines(diff)
print(f"{changed} of {len(pairs)} file(s) with manual edits", file=sys.stderr)
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("copy")
c.add_argument("rel", nargs="+")
c.add_argument("--force", action="store_true")
d = sub.add_parser("diff")
d.add_argument("rel", nargs="*")
args = ap.parse_args()
return {"copy": cmd_copy, "diff": cmd_diff}[args.cmd](args)
if __name__ == "__main__":
sys.exit(main())