2V2-a: game.GameWindow opens after Loading and sends CG_ENTERGAME

The 2V0 stub modules now carry the 40250 module constants (copied from each
module's init function by stub_constants.py) and return zero values shaped like
each reference function's Py_BuildValue, so interfaceModule.MakeInterface runs
through (uiSafebox no longer divides by a zero SAFEBOX_SLOT_Y_COUNT).

UserInterface/StdAfx.h includes Locale.h before GameType.h as the 40250 PCH
does; the reversed order dropped GameType.h's ENABLE_NEW_EQUIPMENT_SYSTEM
branches.

port.login_flow's fake server checks CG_ENTERGAME; port.login_live reaches the
GameWindow on 192.168.21.203.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 16:56:23 +09:00
co-authored by Claude Opus 5.5
parent 0073469dd1
commit d16fabf61c
9 changed files with 2048 additions and 833 deletions
@@ -155,6 +155,9 @@ behavior verified from code inspection alone.
skeleton `extension/src/platform/<Lib>/<File>.cpp` for platform-layer mirror headers from clang's AST skeleton `extension/src/platform/<Lib>/<File>.cpp` for platform-layer mirror headers from clang's AST
(needs `script/port_gate.sh macos` for compile commands); never overwrites without `--force`. (needs `script/port_gate.sh macos` for compile commands); never overwrites without `--force`.
`grep -r MT_PLATFORM_STUB extension/src/platform` lists the platform functions still unimplemented. `grep -r MT_PLATFORM_STUB extension/src/platform` lists the platform functions still unimplemented.
- `scripts/stub_constants.py [--check]` — regenerates the 2V0 stub modules' constants
(`platform/ScriptLib/GameplayModuleConstants.cpp`) and stub return shapes (`GameplayModules.cpp`)
from the 40250 module sources; rerun when a stub module is ported for real and removed.
- `scripts/port_deps.py closure <file> | slices [--write] | order [--write]` — `#include` graph with the - `scripts/port_deps.py closure <file> | slices [--write] | order [--write]` — `#include` graph with the
implicit edges each library's `StdAfx.h` supplies; writes the 2V0-2V3 unit lists and the batch-2 implicit edges each library's `StdAfx.h` supplies; writes the 2V0-2V3 unit lists and the batch-2
topological order to `audit/slices/`. Regenerate after changing a slice definition. topological order to `audit/slices/`. Regenerate after changing a slice definition.
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Write the module constants and return shapes of the 2V0 native stub modules from the 40250 sources.
stub_constants.py [--check]
The stub modules in extension/src/platform/ScriptLib/GameplayModules.cpp only export names. Their
constants are data the root scripts compute with (uiSafebox divides by safebox.SAFEBOX_SLOT_Y_COUNT),
so each stub module gets its 40250 init function's `PyModule_Add{Int,String}Constant` lines verbatim,
with the preprocessor lines around them and the source file's includes, in
extension/src/platform/ScriptLib/GameplayModuleConstants.cpp as `AddStubConstants_<module>(poModule)`.
A module that is ported for real drops out of the stub list and out of this file.
The stub functions themselves (`stub_<module>_<Name>` in GameplayModules.cpp) return a zero value of
the shape their 40250 function returns on its last `return`: the `Py_BuildValue` format with every
number 0, every string "" and every object None, or None for `Py_BuildNone()`. uiTaskBar unpacks
`player.GetLocalQuickSlot` as a pair, so an `int` 0 stops the script where the real module would not.
Functions whose last return is not a build call (lists built by hand, ...) keep returning 0.
"""
from __future__ import annotations
import argparse
import re
import sys
import refroot
REPO = refroot.repo_root()
STUBS = REPO / "extension/src/platform/ScriptLib/GameplayModules.cpp"
OUT = REPO / "extension/src/platform/ScriptLib/GameplayModuleConstants.cpp"
INIT = re.compile(r'Py_InitModule\("([A-Za-z_]+)"')
KEEP = re.compile(r'^\s*(PyModule_Add(Int|String)Constant\s*\(|#\s*(if|ifdef|ifndef|elif|else|endif)\b)')
INCLUDE = re.compile(r'^\s*#\s*include\s*"([^"]+)"')
def read(path) -> list[str]:
return path.read_bytes().decode("cp949", "replace").replace("\r", "").split("\n")
def init_body(lines: list[str], module: str) -> list[str]:
start = next(i for i, l in enumerate(lines) if f'Py_InitModule("{module}"' in l and not l.strip().startswith("//"))
body = []
for line in lines[start + 1:]:
if line.startswith("}"):
break
if KEEP.match(line):
body.append(line.strip() if line.lstrip().startswith("#") else "\t" + line.strip())
# Preprocessor pairs with nothing between them carry no constant.
changed = True
while changed:
changed = False
for i in range(len(body) - 1):
if re.match(r"#\s*if", body[i]) and re.match(r"#\s*endif", body[i + 1]):
del body[i:i + 2]
changed = True
break
return body
def enums(lines: list[str]) -> list[str]:
"""The file's constant definitions and enum blocks (file scope or inside the init function) that constants name."""
out, inside = [], False
for line in lines:
if re.match(r"extern const \w+\s+\w+\s*=", line):
out.append(line)
continue
if re.match(r"\s*enum\b", line):
inside = True
if inside:
out.append(line[1:] if line.startswith("\t") and out and out[0].startswith("\t") else line)
if "};" in line:
inside = False
out.append("")
return out
ZERO = {"i": "0", "b": "0", "h": "0", "B": "0", "H": "0", "I": "0u", "c": "0", "l": "0L", "k": "0UL",
"L": "0LL", "K": "0ULL", "n": "(Py_ssize_t) 0", "f": "0.0", "d": "0.0", "s": '""', "z": '""',
"O": "Py_None", "S": "Py_None", "N": "Py_None"}
def zero_value(fmt: str) -> str | None:
"""A C expression building `fmt` with zeros, or None when the format has a converter."""
out_fmt, args, i = "", [], 0
while i < len(fmt):
ch = fmt[i]
if ch in "()[]{}:, \t":
out_fmt += ch
elif ch in ZERO:
out_fmt += "O" if ch in "SN" else ch
args.append(ZERO[ch])
if i + 1 < len(fmt) and fmt[i + 1] == "#":
out_fmt += "#"
args.append("0")
i += 1
elif ch == "O" and i + 1 < len(fmt) and fmt[i + 1] in "&!":
return None
else:
return None
i += 1
return "Py_BuildValue(" + ", ".join([f'"{out_fmt}"'] + args) + ")"
def function_bodies(ref) -> dict[str, str]:
bodies = {}
for path in ref.glob("*.cpp"):
text = "\n".join(read(path))
for m in re.finditer(r"PyObject\s*\*\s*(\w+)\s*\(\s*PyObject\s*\*\s*\w*\s*,\s*PyObject\s*\*\s*\w*\s*\)\s*\{", text):
depth, j = 1, m.end()
while depth and j < len(text):
depth += {"{": 1, "}": -1}.get(text[j], 0)
j += 1
bodies.setdefault(m.group(1), text[m.end():j - 1])
return bodies
def return_shape(body: str) -> str | None:
body = re.sub(r"//[^\n]*|/\*.*?\*/", "", body, flags=re.S)
returns = [r.strip() for r in re.findall(r"\breturn\b([^;]*);", body)]
returns = [r for r in returns if not r.startswith("Py_BuildException")]
if not returns:
return None
last = returns[-1]
if last.startswith("Py_BuildNone"):
return "Py_BuildNone()"
m = re.match(r'Py_BuildValue\s*\(\s*"([^"]*)"', last)
return zero_value(m.group(1)) if m else None
def shapes() -> tuple[str, int]:
"""GameplayModules.cpp with every stub returning its 40250 shape; also the count left at 0."""
ref = refroot.reference_root() / "UserInterface"
bodies = function_bodies(ref)
tables = {}
for path in ref.glob("*.cpp"):
text = "\n".join(read(path))
m = re.search(r'Py_InitModule\("(\w+)"', text)
if not m:
continue
for name, func in re.findall(r'\{\s*"(\w+)"\s*,\s*(\w+)\s*,\s*METH_VARARGS', text):
tables.setdefault(m.group(1), {}).setdefault(name, func)
left = 0
def fix(m):
nonlocal left
module, name = m.group(2).split(".", 1)
shape = return_shape(bodies.get(tables.get(module, {}).get(name, ""), ""))
if not shape:
left += 1
return f'{m.group(1)}"{m.group(2)}"); }}'
return f'{m.group(1)}"{m.group(2)}", {shape}); }}'
text = re.sub(r'(\{ return stub_call_named\()"([\w.]+)"(?:, .*)?\); \}$', fix, STUBS.read_text(), flags=re.M)
return text, left
def generate() -> str:
ref = refroot.reference_root() / "UserInterface"
modules = INIT.findall(STUBS.read_text())
sources = {}
for path in sorted(ref.glob("*.cpp")):
for line in read(path):
m = INIT.search(line)
if m and not line.strip().startswith("//"):
sources[m.group(1)] = path
out = [
"// Generated by .agents/skills/metin2-40250-parity-audit/scripts/stub_constants.py; do not edit.",
"// The module constants of the 2V0 stub modules (GameplayModules.cpp), copied from each 40250",
"// module's init function with the preprocessor lines around them.",
'#include "UserInterface/StdAfx.h"',
]
root = REPO / "extension/src/port"
port = {p.relative_to(root).as_posix().lower(): p.relative_to(root).as_posix() for p in root.rglob("*.h")}
includes, bodies = [], []
for module in modules:
path = sources[module]
body = init_body(read(path), module)
for line in read(path):
m = INCLUDE.match(line)
if m and m.group(1).lower() != "stdafx.h":
rel = m.group(1)[3:] if m.group(1).startswith("../") else "UserInterface/" + m.group(1)
if rel.lower() not in port:
continue # not mirrored (e.g. the UDP datagram unit); its constants need none of it
inc = f'#include "{port[rel.lower()]}"'
if inc not in includes:
includes.append(inc)
blocks = enums(read(path))
if blocks:
bodies.append(f"\n// 40250 UserInterface/{path.name}")
bodies.extend(l for l in blocks[:-1])
bodies.append(f"\n// 40250 UserInterface/{path.name}\nvoid AddStubConstants_{module}(PyObject* poModule)\n{{")
bodies.append("\t(void) poModule;")
bodies.extend(body)
bodies.append("}")
return "\n".join(out + includes + bodies) + "\n"
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--check", action="store_true", help="fail if the file is out of date")
args = ap.parse_args()
text = generate()
stubs, left = shapes()
if args.check:
stale = [p for p, t in ((OUT, text), (STUBS, stubs)) if not p.exists() or p.read_text() != t]
for p in stale:
print(f"{p.relative_to(REPO)} is out of date; run stub_constants.py", file=sys.stderr)
return 1 if stale else 0
OUT.write_text(text)
STUBS.write_text(stubs)
print(f"wrote {OUT.relative_to(REPO)} and {STUBS.relative_to(REPO)} ({left} stubs keep returning 0)")
return 0
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -512,3 +512,4 @@
{"time": "2026-09-23T06:18:52Z", "event": "port_round", "unit": "PORT-PLAN 2V1-d: offline FakeServer login -> select -> loading", "ported": ["PythonNetworkStreamPhaseGame SendClientVersionPacket body (verbatim, in the platform/pending stand-in)"], "adapted": ["NetStream::ActivateCipher decrypts the unread receive buffer (PORT: ciphertext pipelined behind KEY_AGREEMENT_COMPLETED)", "CPythonApplication marked final (PORT: no application object on the platform, virtual overrides must be called directly)", "platform CPythonApplication::GetGlobalTime reads CTimer (40250 caches it in m_fGlobalTime each Process); SetCenterPosition/SetEventCamera/SetDefaultCamera stubs (2V3)"], "divergent": ["GetExcutedFileName is a platform stub, so CG_CLIENT_VERSION carries an empty filename", "SendClientVersionPacket lives in the PhaseGame stand-in until PhaseGame is ported"], "needs_live": ["login/select/loading against the 40250 server (2V1-e)"], "tests": ["port.login_flow: pass (auth + two game connections, every sequence byte checked; 5x repeat stable)", "ctest 34/34 pass", "script/port_gate.sh macos: PASS", "godot python_host_test.gd: pass"], "todo": ["2V1-e live smoke", "port the platform/pending units", "game.GameWindow after loading (2V2)"]} {"time": "2026-09-23T06:18:52Z", "event": "port_round", "unit": "PORT-PLAN 2V1-d: offline FakeServer login -> select -> loading", "ported": ["PythonNetworkStreamPhaseGame SendClientVersionPacket body (verbatim, in the platform/pending stand-in)"], "adapted": ["NetStream::ActivateCipher decrypts the unread receive buffer (PORT: ciphertext pipelined behind KEY_AGREEMENT_COMPLETED)", "CPythonApplication marked final (PORT: no application object on the platform, virtual overrides must be called directly)", "platform CPythonApplication::GetGlobalTime reads CTimer (40250 caches it in m_fGlobalTime each Process); SetCenterPosition/SetEventCamera/SetDefaultCamera stubs (2V3)"], "divergent": ["GetExcutedFileName is a platform stub, so CG_CLIENT_VERSION carries an empty filename", "SendClientVersionPacket lives in the PhaseGame stand-in until PhaseGame is ported"], "needs_live": ["login/select/loading against the 40250 server (2V1-e)"], "tests": ["port.login_flow: pass (auth + two game connections, every sequence byte checked; 5x repeat stable)", "ctest 34/34 pass", "script/port_gate.sh macos: PASS", "godot python_host_test.gd: pass"], "todo": ["2V1-e live smoke", "port the platform/pending units", "game.GameWindow after loading (2V2)"]}
{"time": "2026-09-23T06:23:33Z", "event": "port_round", "unit": "PORT-PLAN 2V1-e: live smoke harness (port.login_live)", "ported": [], "adapted": ["port_login_flow_test live mode: server and account from MT_LIVE_* env vars only, passed to Python as objects, never printed"], "divergent": [], "needs_live": ["login -> character list -> loading against 192.168.21.203 (host unreachable from this machine on 2026-09-23)", "the live loading-phase packet burst through the platform/pending stand-ins", "auth/login failure popups (wrong password, already logged in, blocked)", "CG_CLIENT_VERSION with an empty filename (GetExcutedFileName stub) accepted by the server"], "tests": ["port.login_flow: pass", "port.login_live: skipped without MT_LIVE_*", "script/port_gate.sh macos: PASS"], "todo": ["run port.login_live once the server is reachable", "port the platform/pending units"]} {"time": "2026-09-23T06:23:33Z", "event": "port_round", "unit": "PORT-PLAN 2V1-e: live smoke harness (port.login_live)", "ported": [], "adapted": ["port_login_flow_test live mode: server and account from MT_LIVE_* env vars only, passed to Python as objects, never printed"], "divergent": [], "needs_live": ["login -> character list -> loading against 192.168.21.203 (host unreachable from this machine on 2026-09-23)", "the live loading-phase packet burst through the platform/pending stand-ins", "auth/login failure popups (wrong password, already logged in, blocked)", "CG_CLIENT_VERSION with an empty filename (GetExcutedFileName stub) accepted by the server"], "tests": ["port.login_flow: pass", "port.login_live: skipped without MT_LIVE_*", "script/port_gate.sh macos: PASS"], "todo": ["run port.login_live once the server is reachable", "port the platform/pending units"]}
{"time": "2026-09-23T07:40:54Z", "event": "port_round", "unit": "PORT-PLAN 2V1-e: live smoke against 192.168.21.203", "ported": [], "adapted": [], "divergent": [], "needs_live": ["the loading-phase packet burst after GC_MAIN_CHARACTER and CG_ENTERGAME (2V2 GameWindow)", "auth/login failure popups", "server handling of CG_CLIENT_VERSION with an empty filename (not observable client-side)"], "tests": ["port.login_live: pass x2 against 192.168.21.203 (auth handshake + key agreement + LOGIN3, game LOGIN2, character list, DirectEnter reconnect, CHARACTER_SELECT, GC_MAIN_CHARACTER -> LoadData + CG_CLIENT_VERSION; no unknown packets)", "port.login_flow: pass"], "todo": ["2V2", "port the platform/pending units"]} {"time": "2026-09-23T07:40:54Z", "event": "port_round", "unit": "PORT-PLAN 2V1-e: live smoke against 192.168.21.203", "ported": [], "adapted": [], "divergent": [], "needs_live": ["the loading-phase packet burst after GC_MAIN_CHARACTER and CG_ENTERGAME (2V2 GameWindow)", "auth/login failure popups", "server handling of CG_CLIENT_VERSION with an empty filename (not observable client-side)"], "tests": ["port.login_live: pass x2 against 192.168.21.203 (auth handshake + key agreement + LOGIN3, game LOGIN2, character list, DirectEnter reconnect, CHARACTER_SELECT, GC_MAIN_CHARACTER -> LoadData + CG_CLIENT_VERSION; no unknown packets)", "port.login_flow: pass"], "todo": ["2V2", "port the platform/pending units"]}
{"time": "2026-09-23T07:52:21Z", "event": "port_round", "unit": "PORT-PLAN 2V2-a: game.GameWindow opens after Loading, CG_ENTERGAME", "ported": [], "adapted": ["2V0 stub modules: 40250 constants (verbatim init lines, stub_constants.py) and zero return values shaped like each reference function's last Py_BuildValue"], "divergent": [], "fixed": ["UserInterface/StdAfx.h included GameType.h before Locale.h, dropping GameType.h's ENABLE_NEW_EQUIPMENT_SYSTEM branches; 40250 PCH order restored"], "needs_live": ["PHASE_GAME and the Entergame burst: GamePhase/SetGamePhase still stand-ins (2V2-b)"], "tests": ["port.login_flow: pass (fake server receives CG_ENTERGAME with the right sequence byte, GameWindow shown)", "port.login_live: pass against 192.168.21.203 (GameWindow shown)", "port.packet: pass"], "todo": ["2V2-b PhaseGame dispatch", "2V2-c PythonCharacterManager", "2V2-d InstanceBase/ActorInstance", "2V2-e render"]}
+8
View File
@@ -416,6 +416,14 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
3. **2V2 — GamePhase/角色显示**:加入 GamePhase 的最小分派闭包、`PythonCharacterManager`、`InstanceBase`、`ActorInstance` 3. **2V2 — GamePhase/角色显示**:加入 GamePhase 的最小分派闭包、`PythonCharacterManager`、`InstanceBase`、`ActorInstance`
及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过 及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过
strict gate。 strict gate。
| 子步骤 | 内容 | 状态 |
| --- | --- | --- |
| 2V2-a | Loading 结束后打开 `game.GameWindow`,发 `CG_ENTERGAME` | 完成:2V0 桩模块的常量与返回值形状改为按 40250 生成(`scripts/stub_constants.py`:常量逐行抄各模块 init 的 `PyModule_Add*Constant` 及其预处理行/枚举/`extern const` 定义,写到 `platform/ScriptLib/GameplayModuleConstants.cpp`;桩函数返回参考函数最后一个 `return` 的 `Py_BuildValue` 格式的零值或 None,而不是一律 `int 0`),`uiSafebox.SetTableSize` 的除零和 `uiTaskBar` 解包失败随之消失,`interfaceModule.MakeInterface` 走完。修正:`UserInterface/StdAfx.h` 按 40250 PCH 顺序先 `Locale.h` 后 `GameType.h`(之前反了,`GameType.h` 里 `ENABLE_NEW_EQUIPMENT_SYSTEM` 分支全被丢掉)。`port.login_flow` 假服在 `CG_CLIENT_VERSION` 后收 `CG_ENTERGAME`(校验序列字节),测试等到 `GameWindow` 显示;`port.login_live` 2026-09-23 对 192.168.21.203 同样走到 `GameWindow`。`GamePhase`/`SetGamePhase` 仍是替身:服务器的 PHASE_GAME 及 Loading 阶段落到 `GamePhase()` 的包会卡住读流 |
| 2V2-b | `PythonNetworkStreamPhaseGame*.cpp` 分派闭包:`SetGamePhase`/`GamePhase`,PHASE_GAME 后的 Entergame 包洪流(`GC_TIME`/`GC_CHANNEL`/`GC_CHARACTER_ADD`/`GC_CHAR_ADDITIONAL_INFO`/`GC_CHARACTER_UPDATE`/技能/物品/快捷栏…)读流不断 | TODO |
| 2V2-c | `PythonCharacterManager` + `chrmgr`/`chr` 模块:`playerSettingModule` 的种族/动作/特效注册走真实实现 | TODO |
| 2V2-d | `InstanceBase` + `ActorInstance` + `CGraphicThingInstance` adapter:`GC_CHARACTER_ADD` 创建主角实例(msm 模型 + 待机动作) | TODO |
| 2V2-e | Godot 显示主角(渲染 adapter),离线 + 真服 smoke:进游戏看到可辨认的静止角色 | TODO |
4. **2V3 — 本地移动**:加入 `PythonPlayer`、`PythonPlayerInput*`、`PythonPlayerEventHandler`、 4. **2V3 — 本地移动**:加入 `PythonPlayer`、`PythonPlayerInput*`、`PythonPlayerEventHandler`、
`InstanceBaseMovement`、`ActorInstance{Motion,Event,Position,Rotation,CollisionDetection}` 的实际依赖闭包。验收为输入经过新路径 `InstanceBaseMovement`、`ActorInstance{Motion,Event,Position,Rotation,CollisionDetection}` 的实际依赖闭包。验收为输入经过新路径
改变位置/朝向、发送与 40250 相同的移动封包,并在离线边界测试和真服 smoke 中通过。接通时删除对应 legacy 玩法逻辑。 改变位置/朝向、发送与 40250 相同的移动封包,并在离线边界测试和真服 smoke 中通过。接通时删除对应 legacy 玩法逻辑。
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,8 +11,8 @@
#include "../GameLib/StdAfx.h" // 40250 PCH order: GameLib types precede UserInterface/GameType.h. #include "../GameLib/StdAfx.h" // 40250 PCH order: GameLib types precede UserInterface/GameType.h.
#include "../GameLib/RaceData.h" #include "../GameLib/RaceData.h"
#include "../EterPythonLib/StdAfx.h" #include "../EterPythonLib/StdAfx.h"
#include "Locale.h" // Before GameType.h, as in 40250: Locale_inc.h's feature macros shape its tables.
#include "GameType.h" // 40250's UserInterface precompiled header supplies these item types. #include "GameType.h" // 40250's UserInterface precompiled header supplies these item types.
#include "Locale.h"
#include "../EterBase/tea.h" #include "../EterBase/tea.h"
#include <dinput.h> #include <dinput.h>
+7 -1
View File
@@ -434,7 +434,13 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE; c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE;
Event(name + ":client_version"); Event(name + ":client_version");
// Hold the connection (the loading screen) until the test stops us. // game.GameWindow.Open (after the loading steps' net.StartGame()) sends CG_ENTERGAME.
auto enter = zeroed<CGEnterGame>();
if (read_packet(c, HDR_CG_ENTERGAME, &enter, sizeof(enter), &error) != 1)
return Fail(name + ": CG_ENTERGAME: " + error);
Event(name + ":entergame");
// Hold the connection until the test stops us.
std::uint8_t byte = 0; std::uint8_t byte = 0;
while (!m_stop && c.Read(&byte, 1) == 1) while (!m_stop && c.Read(&byte, 1) == 1)
; ;
+17 -8
View File
@@ -1,4 +1,4 @@
// 批次 2V1-d: the real 40250 login → select → loading path, offline. // 批次 2V1-d/2V2: the real 40250 login → select → loading → game path, offline.
// //
// The 40250 root scripts (intrologin.py, introselect.py, introloading.py, networkmodule.py) drive the // The 40250 root scripts (intrologin.py, introselect.py, introloading.py, networkmodule.py) drive the
// ported CPythonNetworkStream and CAccountConnector against FakeLoginServer (port_login_flow_server.h), // ported CPythonNetworkStream and CAccountConnector against FakeLoginServer (port_login_flow_server.h),
@@ -13,7 +13,9 @@
// slot's addr/port, logs in again, and in direct-enter mode the select phase goes straight to // slot's addr/port, logs in again, and in direct-enter mode the select phase goes straight to
// introLoading.LoadingWindow, which sends CG_CHARACTER_SELECT; // introLoading.LoadingWindow, which sends CG_CHARACTER_SELECT;
// 5. PHASE_LOADING, GC_MAIN_CHARACTER, GC_CHARACTER_POINTS: the loading phase answers the main // 5. PHASE_LOADING, GC_MAIN_CHARACTER, GC_CHARACTER_POINTS: the loading phase answers the main
// character with CG_CLIENT_VERSION(2), the server's proof the client reached Loading. // character with CG_CLIENT_VERSION(2), the server's proof the client reached Loading;
// 6. (批次 2V2) the loading steps end in net.StartGame(): networkModule.SetGamePhase opens
// game.GameWindow, whose Open sends CG_ENTERGAME.
// //
// The server checks every sequence byte against the 40250 table, so a skipped or extra sequenced // The server checks every sequence byte against the 40250 table, so a skipped or extra sequenced
// packet fails the run. // packet fails the run.
@@ -21,9 +23,9 @@
// port_login_flow_test <40250 Client dir> <python27.zip> [live] // port_login_flow_test <40250 Client dir> <python27.zip> [live]
// //
// With `live` (批次 2V1-e, ctest port.login_live) the same path runs against a real 40250 server and // With `live` (批次 2V1-e, ctest port.login_live) the same path runs against a real 40250 server and
// the client's own state is the proof: the character list arrived, and the loading window got // the client's own state is the proof: the character list arrived, the loading window got
// LoadData from GC_MAIN_CHARACTER, which CPythonNetworkStream::RecvMainCharacter answers with // LoadData from GC_MAIN_CHARACTER, which CPythonNetworkStream::RecvMainCharacter answers with
// CG_CLIENT_VERSION. The test exits before net.StartGame() would enter the game. Everything about the // CG_CLIENT_VERSION, and game.GameWindow is open, which sent CG_ENTERGAME. Everything about the
// server comes from the environment and nothing of it is printed: // server comes from the environment and nothing of it is printed:
// //
// MT_LIVE_HOST, MT_LIVE_LOGIN, MT_LIVE_PASSWORD required; without them the test is skipped // MT_LIVE_HOST, MT_LIVE_LOGIN, MT_LIVE_PASSWORD required; without them the test is skipped
@@ -226,16 +228,23 @@ int main(int argc, char** argv)
return failed() || (server ? server->Has("game2:client_version") : py_true(loaded)); return failed() || (server ? server->Has("game2:client_version") : py_true(loaded));
}); });
CHECK(loading && !failed()); CHECK(loading && !failed());
// Checked right away: introLoading's own update steps end in net.StartGame(), after which the stream
// opens game.GameWindow itself (CPythonNetworkStream::Process, m_isStartGame), which is 2V2.
CHECK(py_true(loaded)); CHECK(py_true(loaded));
}
// 6. introLoading's update steps end in net.StartGame(); the next CPythonNetworkStream::Process calls
// networkModule's SetGamePhase, which builds game.GameWindow (interfaceModule.MakeInterface) and
// opens it, and GameWindow.Open sends CG_ENTERGAME.
CHECK(py("import game"));
const char* gaming = "isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()";
const bool entered = pump_until(30, [&] {
return failed() || (py_true(gaming) && (!server || server->Has("game2:entergame")));
});
CHECK(entered && !failed());
}
if (server) if (server)
{ {
const std::vector<std::string> expected = { const std::vector<std::string> expected = {
"auth:handshake", "auth:login3", "game1:handshake", "game1:login2", "auth:closed", "auth:handshake", "auth:login3", "game1:handshake", "game1:login2", "auth:closed",
"game1:closed", "game2:handshake", "game2:login2", "game2:select", "game2:client_version", "game1:closed", "game2:handshake", "game2:login2", "game2:select", "game2:client_version", "game2:entergame",
}; };
auto events = server->Events(); auto events = server->Events();
// auth:closed races game1's handshake; compare the order within each connection. // auth:closed races game1's handshake; compare the order within each connection.