Files
mtgodot-poc/.agents/skills/metin2-40250-parity-audit/scripts/port_copy.py
T
shenleiandClaude Opus 5.5 62b6e23bfc port 2V2-b: GamePhase dispatch consumes the Entergame burst
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>
2026-09-23 18:20:57 +09:00

146 lines
6.0 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 its non-ASCII characters are written as their CP949
bytes in 3-digit octal escapes (`"\301\326..."`): the compiled bytes stay those of 40250.
- 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)
# Literals are found with comments blanked out; offsets are the same in `text`.
for m in reversed(list(LITERAL.finditer(code))):
if not m.group(0).isascii():
escaped = "".join(c if c.isascii() else "".join(f"\\{b:03o}" for b in c.encode("cp949"))
for c in m.group(0))
text = text[:m.start()] + escaped + text[m.end():]
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())