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>
146 lines
6.0 KiB
Python
146 lines
6.0 KiB
Python
#!/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())
|