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>
This commit is contained in:
shenlei
2026-09-23 18:20:57 +09:00
co-authored by Claude Opus 5.5
parent d16fabf61c
commit 62b6e23bfc
76 changed files with 21087 additions and 753 deletions
@@ -5,6 +5,12 @@
platform_stub.py gen [<header>...] [--force] write extension/src/platform/<Lib>/<File>.cpp
(no args: every platform header; never overwrites
without --force, so hand-written implementations stay)
platform_stub.py pending <header> <symbol>... [--ctor]
append stubs of just those members (the linker's
undefined-symbol names, demangled as ld prints them) to the stand-in of a not-yet-ported
logic unit, extension/src/platform/pending/<Lib>/<File>.cpp;
--ctor adds the class constructor/destructor so the
singleton can exist (its members still default-construct)
`<header>` is a mirror path such as `EterLib/GrpBase.h`. A header is platform-layer when its same-stem
40250 .cpp unit is (port_map.py LAYER_PATTERNS), or, without one, when its own path matches.
@@ -262,11 +268,13 @@ class Collector:
and not node.get("inline"):
self.add("fn", f"{scope}{node['name']}", node)
def render(self) -> list[str]:
def render(self, only=None) -> list[str]:
out = []
for kind, qname, node in self.decls:
if node.get("mangledName") in self.defined:
continue
if only is not None and not only(qname, node):
continue
if kind == "var":
out.append(f"decltype({qname}) {qname}{{}};\n")
else:
@@ -288,7 +296,8 @@ def namespaces(header: Path) -> dict[str, str]:
return result
def generate(rel: str) -> str:
def generate(rel: str, only=None) -> str:
"""The skeleton of `rel`; with `only(qname, node)`, just the declarations it accepts (the bodies alone)."""
header = PORT / rel
records, functions = scan(header)
args, tu = gate_command(rel)
@@ -304,9 +313,9 @@ def generate(rel: str) -> str:
for n in nodes:
if n.get("name") == name:
col.root(n, ns.get(name, ""))
body = col.render()
if not body:
return ""
body = col.render(only)
if not body or only is not None:
return "\n".join(body)
lib = rel.split("/")[0]
stdafx = next((p.name for p in (PORT / lib).glob("*") if p.name.lower() == "stdafx.h"), None)
unit = unit_of(rel)
@@ -382,6 +391,44 @@ def cmd_gen(args) -> int:
return 0
def cmd_pending(args) -> int:
rel = next((h.relative_to(PORT).as_posix() for h in PORT.rglob("*.h")
if h.relative_to(PORT).as_posix().lower() == args.header.lower()), None)
if not rel:
raise SystemExit(f"{args.header}: not in the mirror")
wanted = {s.strip().strip('"') for s in args.symbols}
demangled: dict[str, str] = {}
def only(qname: str, node: dict) -> bool:
mangled = node.get("mangledName", "")
if mangled not in demangled:
demangled[mangled] = subprocess.run(["c++filt", mangled], capture_output=True, text=True).stdout.strip()
if demangled[mangled] in wanted:
return True
return args.ctor and node.get("kind") in ("CXXConstructorDecl", "CXXDestructorDecl") \
and qname.split("::")[-2:-1] == [node.get("name", "").lstrip("~")] and qname.count("::") == 1
body = generate(rel, only)
if not body:
print(f"none {rel} (no such undefined member)")
return 0
path = PLATFORM / "pending" / (rel.rsplit(".", 1)[0] + ".cpp")
if path.exists():
text = path.read_text(encoding="utf-8").rstrip("\n") + "\n\n" + body
else:
lib = rel.split("/")[0]
stdafx = next((p.name for p in (PORT / lib).glob("*") if p.name.lower() == "stdafx.h"), None)
unit = unit_of(rel) or rel
text = (f"// Stand-in for the 40250 logic unit {unit}, not ported yet: the members the ported units\n"
f"// reference, as {MARK} stubs. Delete this file in the commit that ports the unit into port/\n"
f"// (docs/PORT-PLAN.md).\n"
+ (f'#include "{lib}/{stdafx}"\n' if stdafx else "")
+ f'#include "{rel}"\n\n#include "{"../" * (rel.count("/") + 1)}PlatformStub.h"\n\n' + body)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
print(f"wrote {path.relative_to(REPO)} (+{body.count('MT_PLATFORM_STUB();')} stubs)")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
@@ -390,6 +437,11 @@ def main() -> int:
g.add_argument("headers", nargs="*")
g.add_argument("--force", action="store_true")
g.set_defaults(fn=cmd_gen)
p = sub.add_parser("pending")
p.add_argument("header")
p.add_argument("symbols", nargs="*")
p.add_argument("--ctor", action="store_true")
p.set_defaults(fn=cmd_pending)
args = ap.parse_args()
return args.fn(args)
@@ -9,7 +9,8 @@
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.
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`, ...),
@@ -77,9 +78,12 @@ def mechanical(ref: Path, index: dict[str, Path], src: Path) -> str:
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]}")
# 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)