2V1-e: live login smoke harness (port.login_live)
port_login_flow_test gains a `live` mode that runs the same script path against a real 40250 server and checks the client's own state: the character list, then LoadingWindow.LoadData from GC_MAIN_CHARACTER (which CPythonNetworkStream answers with CG_CLIENT_VERSION), exiting before the game phase. Host, ports, account and slot come only from MT_LIVE_* env vars, are handed to Python as objects and never printed; without them the test skips. Not yet run live: the server was unreachable today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8d9d863c7e
commit
2a8750e736
@@ -510,3 +510,4 @@
|
|||||||
{"time": "2026-09-23T05:25:43Z", "event": "port_round", "unit": "PORT-PLAN 2V1-a: EterLib network layer + EterBase cipher", "ported": ["EterLib/NetStream.cpp, NetAddress.cpp, NetDevice.cpp, NetPacketHeaderMap.cpp (port_copy verbatim)", "EterBase/cipher.cpp (port_copy verbatim, Crypto++ DH2 key agreement + CTR block ciphers)"], "adapted": ["shim/win32/winsock.h: Winsock names over BSD sockets; select ignores nfds and drops writable sockets without a peer (Winsock reports a failed connect only in exceptfds); EINPROGRESS -> WSAEWOULDBLOCK; ioctlsocket FIONBIO; WSAStartup ignores SIGPIPE"], "divergent": [], "needs_live": ["cipher agreement against the 40250 game server (2V1-e)"], "tests": ["port.net: pass (loopback connect, send/recv, peer close, refused-connect timeout, client/server cipher agreement)"], "todo": ["2V1-b PythonNetworkStream phases", "2V1-c net module", "2V1-d FakeServer", "2V1-e live smoke"]}
|
{"time": "2026-09-23T05:25:43Z", "event": "port_round", "unit": "PORT-PLAN 2V1-a: EterLib network layer + EterBase cipher", "ported": ["EterLib/NetStream.cpp, NetAddress.cpp, NetDevice.cpp, NetPacketHeaderMap.cpp (port_copy verbatim)", "EterBase/cipher.cpp (port_copy verbatim, Crypto++ DH2 key agreement + CTR block ciphers)"], "adapted": ["shim/win32/winsock.h: Winsock names over BSD sockets; select ignores nfds and drops writable sockets without a peer (Winsock reports a failed connect only in exceptfds); EINPROGRESS -> WSAEWOULDBLOCK; ioctlsocket FIONBIO; WSAStartup ignores SIGPIPE"], "divergent": [], "needs_live": ["cipher agreement against the 40250 game server (2V1-e)"], "tests": ["port.net: pass (loopback connect, send/recv, peer close, refused-connect timeout, client/server cipher agreement)"], "todo": ["2V1-b PythonNetworkStream phases", "2V1-c net module", "2V1-d FakeServer", "2V1-e live smoke"]}
|
||||||
{"time": "2026-09-23T06:00:34Z", "event": "port_round", "unit": "PORT-PLAN 2V1-b/c: PythonNetworkStream phases + AccountConnector + net module", "ported": ["UserInterface/PythonNetworkStream.cpp, PythonNetworkStreamEvent.cpp, PhaseHandShake/Login/Select/Loading/Offline (port_copy verbatim)", "UserInterface/AccountConnector.cpp, InsultChecker.cpp, AffectFlagContainer.cpp (port_copy verbatim)", "UserInterface/PythonNetworkStreamModule.cpp (net module, replaces the 2V0-e stub)", "PythonPackModule.cpp login-key functions compiled again", "Locale.cpp LocaleService_Is*/security key block (platform PythonApplication.cpp, verbatim)", "WiseLogicXTrap no-XTRAP branch"], "adapted": ["Packet.h long fields -> 32-bit LONG (PORT)", "singleton<T> pointer offset via intptr_t (PORT)", "grammar.h shim #undef BYTE (40250's bitset.h copy drops #define BYTE char)", "PythonBoot creates CPythonNetworkStream/CAccountConnector/CPythonPlayer/CFlyingManager/CEffectManager and calls initnet()", "app.Create creates CNetworkDevice; Process() runs CPythonNetworkStream then CAccountConnector", "port_gate.sh macos builds all; port-only config finds iconv"], "divergent": ["ProcessCRC stubbed (magic cube piece only feeds PhaseGame attack packets)", "unported logic units in the link closure (PythonPlayer, NetworkActorManager, FlyingObjectManager, GuildMarkDownloader/Uploader, MarkManager, PhaseGame*/PythonGuild members) are stand-ins under platform/pending/; loading-phase CharacterUpdate/PointChange/ItemSet/QuickSlotAdd stand-ins only consume the packet", "guild mark uploader/downloader Process not called yet"], "needs_live": ["login/select/loading against the 40250 server (2V1-e)"], "tests": ["port.packet: pass (38 login/select/loading packet sizes match wire_classic.h)", "ctest 33/33 pass", "script/port_gate.sh macos: PASS", "godot --headless --path project --script python_host_test.gd: pass (net is the ported module)"], "todo": ["2V1-d FakeServer HandShake->Login->Select->Loading", "2V1-e live smoke", "port the platform/pending units"]}
|
{"time": "2026-09-23T06:00:34Z", "event": "port_round", "unit": "PORT-PLAN 2V1-b/c: PythonNetworkStream phases + AccountConnector + net module", "ported": ["UserInterface/PythonNetworkStream.cpp, PythonNetworkStreamEvent.cpp, PhaseHandShake/Login/Select/Loading/Offline (port_copy verbatim)", "UserInterface/AccountConnector.cpp, InsultChecker.cpp, AffectFlagContainer.cpp (port_copy verbatim)", "UserInterface/PythonNetworkStreamModule.cpp (net module, replaces the 2V0-e stub)", "PythonPackModule.cpp login-key functions compiled again", "Locale.cpp LocaleService_Is*/security key block (platform PythonApplication.cpp, verbatim)", "WiseLogicXTrap no-XTRAP branch"], "adapted": ["Packet.h long fields -> 32-bit LONG (PORT)", "singleton<T> pointer offset via intptr_t (PORT)", "grammar.h shim #undef BYTE (40250's bitset.h copy drops #define BYTE char)", "PythonBoot creates CPythonNetworkStream/CAccountConnector/CPythonPlayer/CFlyingManager/CEffectManager and calls initnet()", "app.Create creates CNetworkDevice; Process() runs CPythonNetworkStream then CAccountConnector", "port_gate.sh macos builds all; port-only config finds iconv"], "divergent": ["ProcessCRC stubbed (magic cube piece only feeds PhaseGame attack packets)", "unported logic units in the link closure (PythonPlayer, NetworkActorManager, FlyingObjectManager, GuildMarkDownloader/Uploader, MarkManager, PhaseGame*/PythonGuild members) are stand-ins under platform/pending/; loading-phase CharacterUpdate/PointChange/ItemSet/QuickSlotAdd stand-ins only consume the packet", "guild mark uploader/downloader Process not called yet"], "needs_live": ["login/select/loading against the 40250 server (2V1-e)"], "tests": ["port.packet: pass (38 login/select/loading packet sizes match wire_classic.h)", "ctest 33/33 pass", "script/port_gate.sh macos: PASS", "godot --headless --path project --script python_host_test.gd: pass (net is the ported module)"], "todo": ["2V1-d FakeServer HandShake->Login->Select->Loading", "2V1-e live smoke", "port the platform/pending units"]}
|
||||||
{"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"]}
|
||||||
|
|||||||
+1
-1
@@ -412,7 +412,7 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
|
|||||||
| 2V1-b | `PythonNetworkStream` + HandShake/Login/Select/Loading phase + `AccountConnector` | 完成:`PythonNetworkStream{,Event,PhaseHandShake,PhaseLogin,PhaseSelect,PhaseLoading,PhaseOffline}.cpp`、`AccountConnector.cpp`、`InsultChecker.cpp`、`AffectFlagContainer.cpp` 原样 `port_copy`;`Packet.h` 里所有 `long` 字段改为 32 位 `LONG`(PORT 标注),`port.packet` 把登录/选角/Loading 用到的 38 个包的尺寸与已在真服 1:1 验证过的 `net/classic/wire_classic.h` 比对(两头宏冲突,分两个编译单元);`PythonPackModule.cpp` 的登录密钥函数恢复编译;`Locale.cpp` 的 `LocaleService_Is*`/安全密钥部分原样放进 platform `PythonApplication.cpp`;`WiseLogicXTrap` 取 40250 无 XTRAP 分支,`ProcessCRC` 为桩(魔方只用于 PhaseGame 攻击包)。链接闭包里尚未移植的逻辑单元(`PythonPlayer`、`NetworkActorManager`、`FlyingObjectManager`、`GuildMark*`、`MarkManager`、PhaseGame*/`PythonGuild` 的部分成员)放在 `extension/src/platform/pending/<Lib>/` 作为替身,移植对应单元的提交里删除;Loading 阶段会收到的 CharacterUpdate/PointChange/ItemSet/QuickSlotAdd 替身只吃掉包体,保证读流不断。`port_gate.sh macos` 改为构建 `all`(此前 ctest 会跑门禁没构建的测试),port-only 配置补 `find_library(iconv)` |
|
| 2V1-b | `PythonNetworkStream` + HandShake/Login/Select/Loading phase + `AccountConnector` | 完成:`PythonNetworkStream{,Event,PhaseHandShake,PhaseLogin,PhaseSelect,PhaseLoading,PhaseOffline}.cpp`、`AccountConnector.cpp`、`InsultChecker.cpp`、`AffectFlagContainer.cpp` 原样 `port_copy`;`Packet.h` 里所有 `long` 字段改为 32 位 `LONG`(PORT 标注),`port.packet` 把登录/选角/Loading 用到的 38 个包的尺寸与已在真服 1:1 验证过的 `net/classic/wire_classic.h` 比对(两头宏冲突,分两个编译单元);`PythonPackModule.cpp` 的登录密钥函数恢复编译;`Locale.cpp` 的 `LocaleService_Is*`/安全密钥部分原样放进 platform `PythonApplication.cpp`;`WiseLogicXTrap` 取 40250 无 XTRAP 分支,`ProcessCRC` 为桩(魔方只用于 PhaseGame 攻击包)。链接闭包里尚未移植的逻辑单元(`PythonPlayer`、`NetworkActorManager`、`FlyingObjectManager`、`GuildMark*`、`MarkManager`、PhaseGame*/`PythonGuild` 的部分成员)放在 `extension/src/platform/pending/<Lib>/` 作为替身,移植对应单元的提交里删除;Loading 阶段会收到的 CharacterUpdate/PointChange/ItemSet/QuickSlotAdd 替身只吃掉包体,保证读流不断。`port_gate.sh macos` 改为构建 `all`(此前 ctest 会跑门禁没构建的测试),port-only 配置补 `find_library(iconv)` |
|
||||||
| 2V1-c | `net` 模块(`PythonNetworkStreamModule.cpp`)替换 2V0-e 的桩;`app.Create`/`Process` 接网络 | 完成:`PythonNetworkStreamModule.cpp` 原样移植,`GameplayModules.cpp` 的 `net` 桩删除,`PythonBoot` 按 40250 `CPythonApplication` 成员顺序创建 `CPythonNetworkStream`/`CAccountConnector`/`CPythonPlayer`/`CFlyingManager`/`CEffectManager` 并调用 `initnet()`;`app.Create` 创建 `CNetworkDevice`,`Process()` 每帧先 `CPythonNetworkStream::Process` 再 `CAccountConnector::Process`(公会标记上传/下载的 Process 随 `GuildMark*` 移植)。`python_host_test.gd` 断言 `net` 不再是桩(`PHASE_WINDOW_SELECT == 2`、未连接) |
|
| 2V1-c | `net` 模块(`PythonNetworkStreamModule.cpp`)替换 2V0-e 的桩;`app.Create`/`Process` 接网络 | 完成:`PythonNetworkStreamModule.cpp` 原样移植,`GameplayModules.cpp` 的 `net` 桩删除,`PythonBoot` 按 40250 `CPythonApplication` 成员顺序创建 `CPythonNetworkStream`/`CAccountConnector`/`CPythonPlayer`/`CFlyingManager`/`CEffectManager` 并调用 `initnet()`;`app.Create` 创建 `CNetworkDevice`,`Process()` 每帧先 `CPythonNetworkStream::Process` 再 `CAccountConnector::Process`(公会标记上传/下载的 Process 随 `GuildMark*` 移植)。`python_host_test.gd` 断言 `net` 不再是桩(`PHASE_WINDOW_SELECT == 2`、未连接) |
|
||||||
| 2V1-d | FakeServer 离线跑完 HandShake→Login→Select→Loading | 完成:`port.login_flow` 用真实 40250 root 脚本(intrologin/introselect/introloading/networkmodule)驱动移植的 `CPythonNetworkStream` + `CAccountConnector`,对接 `tests/port_login_flow_server.cpp` 的回环 auth + game 假服(用已在真服 1:1 验证过的 `net/classic` 包布局、序列表与 cipher 按 40250 `desc.cpp`/`input*.cpp` 应答,与客户端不共享代码):握手 bias/重试 + 密钥协商 → `CG_LOGIN3`/`GC_AUTH_SUCCESS` → `CG_LOGIN2` → `GC_EMPIRE`/PHASE_SELECT/`GC_LOGIN_SUCCESS_NEWSLOT` 到 `SelectCharacterWindow` → `StartGame()` 3 秒后 `DirectEnter` 按槽位地址重连、再登录、直入 `LoadingWindow` 发 `CG_CHARACTER_SELECT` → PHASE_LOADING/`GC_MAIN_CHARACTER`/`GC_CHARACTER_POINTS` → 客户端回 `CG_CLIENT_VERSION(2)`;假服逐包校验序列字节。修正:`NetStream::ActivateCipher` 解密 COMPLETED 后已缓冲的密文(PORT,40250 靠服务器 pulse 间隔规避此竞争);`CPythonApplication` 加 `final`(PORT,platform 无应用对象,虚函数需直接调用),补 `GetGlobalTime`(`app.GetTime`)与 `SetCenterPosition`/`SetEventCamera`/`SetDefaultCamera` 桩;`SendClientVersionPacket` 原样放进 PhaseGame 替身。偏差:`GetExcutedFileName` 仍为桩(版本包文件名为空)。Loading 结束后 `net.StartGame()` 打开 `game.GameWindow` 属 2V2 |
|
| 2V1-d | FakeServer 离线跑完 HandShake→Login→Select→Loading | 完成:`port.login_flow` 用真实 40250 root 脚本(intrologin/introselect/introloading/networkmodule)驱动移植的 `CPythonNetworkStream` + `CAccountConnector`,对接 `tests/port_login_flow_server.cpp` 的回环 auth + game 假服(用已在真服 1:1 验证过的 `net/classic` 包布局、序列表与 cipher 按 40250 `desc.cpp`/`input*.cpp` 应答,与客户端不共享代码):握手 bias/重试 + 密钥协商 → `CG_LOGIN3`/`GC_AUTH_SUCCESS` → `CG_LOGIN2` → `GC_EMPIRE`/PHASE_SELECT/`GC_LOGIN_SUCCESS_NEWSLOT` 到 `SelectCharacterWindow` → `StartGame()` 3 秒后 `DirectEnter` 按槽位地址重连、再登录、直入 `LoadingWindow` 发 `CG_CHARACTER_SELECT` → PHASE_LOADING/`GC_MAIN_CHARACTER`/`GC_CHARACTER_POINTS` → 客户端回 `CG_CLIENT_VERSION(2)`;假服逐包校验序列字节。修正:`NetStream::ActivateCipher` 解密 COMPLETED 后已缓冲的密文(PORT,40250 靠服务器 pulse 间隔规避此竞争);`CPythonApplication` 加 `final`(PORT,platform 无应用对象,虚函数需直接调用),补 `GetGlobalTime`(`app.GetTime`)与 `SetCenterPosition`/`SetEventCamera`/`SetDefaultCamera` 桩;`SendClientVersionPacket` 原样放进 PhaseGame 替身。偏差:`GetExcutedFileName` 仍为桩(版本包文件名为空)。Loading 结束后 `net.StartGame()` 打开 `game.GameWindow` 属 2V2 |
|
||||||
| 2V1-e | 真服 smoke(凭据只从环境变量读) | 待做 |
|
| 2V1-e | 真服 smoke(凭据只从环境变量读) | 进行中:`port.login_live`(同一 `port_login_flow_test`,参数 `live`)走同一脚本路径连真服,以客户端自身状态为证据(选角窗口 + 槽位有名字 + 帝国号;`LoadingWindow.LoadData` 已由 `GC_MAIN_CHARACTER` 调用,随后同一调用发 `CG_CLIENT_VERSION`),在 `net.StartGame()` 进游戏前退出。`MT_LIVE_HOST`/`MT_LIVE_LOGIN`/`MT_LIVE_PASSWORD` 必填(缺则 SKIP),`MT_LIVE_AUTH_PORT`(11000)/`MT_LIVE_GAME_PORT`(13000)/`MT_LIVE_SLOT`(0) 可选,凭据不打印不入库。NEEDS_LIVE:2026-09-23 本机到 192.168.21.203 不通,尚未实跑 |
|
||||||
3. **2V2 — GamePhase/角色显示**:加入 GamePhase 的最小分派闭包、`PythonCharacterManager`、`InstanceBase`、`ActorInstance`
|
3. **2V2 — GamePhase/角色显示**:加入 GamePhase 的最小分派闭包、`PythonCharacterManager`、`InstanceBase`、`ActorInstance`
|
||||||
及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过
|
及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过
|
||||||
strict gate。
|
strict gate。
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
|||||||
add_test(NAME port.login_flow
|
add_test(NAME port.login_flow
|
||||||
COMMAND $<TARGET_FILE:port_login_flow_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP})
|
COMMAND $<TARGET_FILE:port_login_flow_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP})
|
||||||
set_tests_properties(port.login_flow PROPERTIES SKIP_RETURN_CODE 77)
|
set_tests_properties(port.login_flow PROPERTIES SKIP_RETURN_CODE 77)
|
||||||
|
# 2V1-e: the same path against a live 40250 server; skipped unless MT_LIVE_* is set (see the test).
|
||||||
|
add_test(NAME port.login_live
|
||||||
|
COMMAND $<TARGET_FILE:port_login_flow_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP} live)
|
||||||
|
set_tests_properties(port.login_live PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE)
|
||||||
|
|
||||||
add_executable(port_python_ui_modules_test ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_python_ui_modules_test.cpp)
|
add_executable(port_python_ui_modules_test ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_python_ui_modules_test.cpp)
|
||||||
target_link_libraries(port_python_ui_modules_test PRIVATE port_platform)
|
target_link_libraries(port_python_ui_modules_test PRIVATE port_platform)
|
||||||
|
|||||||
@@ -18,7 +18,16 @@
|
|||||||
// 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.
|
||||||
//
|
//
|
||||||
// port_login_flow_test <40250 Client dir> <python27.zip>
|
// 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
|
||||||
|
// the client's own state is the proof: the character list arrived, and the loading window got
|
||||||
|
// 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
|
||||||
|
// 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_AUTH_PORT (11000), MT_LIVE_GAME_PORT (13000, channel 1), MT_LIVE_SLOT (0)
|
||||||
//
|
//
|
||||||
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
|
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
|
||||||
#include "ScriptLib/StdAfx.h"
|
#include "ScriptLib/StdAfx.h"
|
||||||
@@ -27,12 +36,15 @@
|
|||||||
#include "../src/platform/UserInterface/UserInterface.h"
|
#include "../src/platform/UserInterface/UserInterface.h"
|
||||||
#include "port_login_flow_server.h"
|
#include "port_login_flow_server.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -91,24 +103,62 @@ static void dump(FakeLoginServer& server)
|
|||||||
std::fprintf(stderr, "\nserver error: %s\n", server.Error().c_str());
|
std::fprintf(stderr, "\nserver error: %s\n", server.Error().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string env(const char* name, const char* fallback = "")
|
||||||
|
{
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value && *value ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hands a C string to __main__ as a Python object, so account data never goes through source text.
|
||||||
|
static void py_set(const char* name, const std::string& value)
|
||||||
|
{
|
||||||
|
PyObject* object = PyString_FromString(value.c_str());
|
||||||
|
PyDict_SetItemString(PyModule_GetDict(PyImport_AddModule((char*) "__main__")), name, object);
|
||||||
|
Py_DECREF(object);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
const char* strict = std::getenv("MT_ASSETS_STRICT");
|
const char* strict = std::getenv("MT_ASSETS_STRICT");
|
||||||
const bool is_strict = strict && std::string(strict) == "1";
|
const bool is_strict = strict && std::string(strict) == "1";
|
||||||
const std::string client = argc > 1 ? argv[1] : "";
|
const std::string client = argc > 1 ? argv[1] : "";
|
||||||
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
|
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
|
||||||
|
const bool live = argc > 3 && std::string(argv[3]) == "live";
|
||||||
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
|
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
|
||||||
{
|
{
|
||||||
std::fprintf(stderr, "port_login_flow_test: no 40250 Client/pack at '%s'\n", client.c_str());
|
std::fprintf(stderr, "port_login_flow_test: no 40250 Client/pack at '%s'\n", client.c_str());
|
||||||
return is_strict ? 1 : 77;
|
return is_strict ? 1 : 77;
|
||||||
}
|
}
|
||||||
|
|
||||||
FakeLoginServer server;
|
std::string host = "127.0.0.1", login = FakeLoginServer::kLogin, password = FakeLoginServer::kPassword;
|
||||||
if (!server.Start())
|
int auth_port = 0, game_port = 0, slot = 0;
|
||||||
|
std::unique_ptr<FakeLoginServer> server;
|
||||||
|
if (live)
|
||||||
{
|
{
|
||||||
std::fprintf(stderr, "port_login_flow_test: cannot listen on loopback\n");
|
host = env("MT_LIVE_HOST");
|
||||||
return 1;
|
login = env("MT_LIVE_LOGIN");
|
||||||
|
password = env("MT_LIVE_PASSWORD");
|
||||||
|
if (host.empty() || login.empty() || password.empty())
|
||||||
|
{
|
||||||
|
std::fprintf(stderr, "port_login_flow_test: live mode needs MT_LIVE_HOST, MT_LIVE_LOGIN, MT_LIVE_PASSWORD\n");
|
||||||
|
return 77;
|
||||||
|
}
|
||||||
|
auth_port = std::atoi(env("MT_LIVE_AUTH_PORT", "11000").c_str());
|
||||||
|
game_port = std::atoi(env("MT_LIVE_GAME_PORT", "13000").c_str());
|
||||||
|
slot = std::atoi(env("MT_LIVE_SLOT", "0").c_str());
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
server = std::make_unique<FakeLoginServer>();
|
||||||
|
if (!server->Start())
|
||||||
|
{
|
||||||
|
std::fprintf(stderr, "port_login_flow_test: cannot listen on loopback\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auth_port = server->AuthPort();
|
||||||
|
game_port = server->GamePort();
|
||||||
|
}
|
||||||
|
auto failed = [&] { return server && server->Failed(); };
|
||||||
|
|
||||||
PackSingletons();
|
PackSingletons();
|
||||||
CHECK(PackInitialize("pack"));
|
CHECK(PackInitialize("pack"));
|
||||||
@@ -125,63 +175,95 @@ int main(int argc, char** argv)
|
|||||||
"__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"));
|
"__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"));
|
||||||
CHECK(pump_until(20, [] { return py_true("isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"); }));
|
CHECK(pump_until(20, [] { return py_true("isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"); }));
|
||||||
|
|
||||||
// 2. The login button's path, with the fake server for both the game and the auth address.
|
// 2. The login button's path (LoginWindow.Connect), pointed at the fake or the live server.
|
||||||
char connect[256];
|
py_set("_host", host);
|
||||||
std::snprintf(connect, sizeof(connect),
|
py_set("_login", login);
|
||||||
"_stream.SetConnectInfo('127.0.0.1', %d, '127.0.0.1', %d)\n"
|
py_set("_password", password);
|
||||||
"_stream.curPhaseWindow.Connect('%s', '%s')",
|
char ports[128];
|
||||||
server.GamePort(), server.AuthPort(), FakeLoginServer::kLogin, FakeLoginServer::kPassword);
|
std::snprintf(ports, sizeof(ports), "_auth_port, _game_port, _slot = %d, %d, %d", auth_port, game_port, slot);
|
||||||
CHECK(py(connect));
|
CHECK(py(ports));
|
||||||
|
CHECK(py("_stream.SetConnectInfo(_host, _game_port, _host, _auth_port)\n"
|
||||||
|
"_stream.curPhaseWindow.Connect(_login, _password)\n"
|
||||||
|
"del __main__._login, __main__._password"));
|
||||||
|
|
||||||
// 3. The character list.
|
// 3. The character list.
|
||||||
const bool selecting = pump_until(20, [&] {
|
const bool selecting = pump_until(live ? 30 : 20, [&] {
|
||||||
return server.Failed() ||
|
return failed() || ((!server || server->Has("game1:login2")) &&
|
||||||
(server.Has("game1:login2") && py_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"));
|
py_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"));
|
||||||
});
|
});
|
||||||
CHECK(selecting && !server.Failed());
|
CHECK(selecting && !failed());
|
||||||
CHECK(server.Has("auth:login3") && server.Has("auth:closed"));
|
if (!selecting)
|
||||||
CHECK(py("import net\n"
|
|
||||||
"_name = net.GetAccountCharacterSlotDataString(0, net.ACCOUNT_CHARACTER_SLOT_NAME)\n"
|
|
||||||
"assert _name == '" + std::string(FakeLoginServer::kCharacterName) + "', _name\n"
|
|
||||||
"assert net.GetEmpireID() == 1"));
|
|
||||||
|
|
||||||
// 4-5. Start the game on slot 0: DirectEnter, the second game login and the loading phase.
|
|
||||||
CHECK(py("_stream.curPhaseWindow.StartGame()"));
|
|
||||||
const bool loading = pump_until(30, [&] { return server.Failed() || server.Has("game2:client_version"); });
|
|
||||||
CHECK(loading && !server.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("isinstance(_stream.curPhaseWindow, introLoading.LoadingWindow)"));
|
|
||||||
|
|
||||||
const std::vector<std::string> expected = {
|
|
||||||
"auth:handshake", "auth:login3", "game1:handshake", "game1:login2", "auth:closed",
|
|
||||||
"game1:closed", "game2:handshake", "game2:login2", "game2:select", "game2:client_version",
|
|
||||||
};
|
|
||||||
auto events = server.Events();
|
|
||||||
// auth:closed races game1's handshake; compare the order within each connection.
|
|
||||||
for (const char* prefix : {"auth:", "game1:", "game2:"})
|
|
||||||
{
|
{
|
||||||
std::vector<std::string> want, got;
|
// The login window reports failures in its popup (LOGIN_FAILURE, connect errors).
|
||||||
for (const auto& e : expected)
|
py("import sys\n"
|
||||||
if (e.rfind(prefix, 0) == 0)
|
"_w = _stream.curPhaseWindow\n"
|
||||||
want.push_back(e);
|
"_msg = _stream.popupWindow and _stream.popupWindow.IsShow() and _stream.popupWindow.GetChild('message').GetText()\n"
|
||||||
for (const auto& e : events)
|
"sys.stderr.write('phase window %s, popup %r\\n' % (_w.__class__.__name__, _msg))");
|
||||||
if (e.rfind(prefix, 0) == 0)
|
}
|
||||||
got.push_back(e);
|
if (server)
|
||||||
CHECK(want == got);
|
{
|
||||||
|
CHECK(server->Has("auth:login3") && server->Has("auth:closed"));
|
||||||
|
CHECK(py("import net\n"
|
||||||
|
"_name = net.GetAccountCharacterSlotDataString(0, net.ACCOUNT_CHARACTER_SLOT_NAME)\n"
|
||||||
|
"assert _name == '" + std::string(FakeLoginServer::kCharacterName) + "', _name\n"
|
||||||
|
"assert net.GetEmpireID() == 1"));
|
||||||
|
}
|
||||||
|
else if (selecting)
|
||||||
|
{
|
||||||
|
CHECK(py("import net\n"
|
||||||
|
"assert net.GetAccountCharacterSlotDataString(_slot, net.ACCOUNT_CHARACTER_SLOT_NAME), 'slot is empty'\n"
|
||||||
|
"assert net.GetEmpireID() in (1, 2, 3), net.GetEmpireID()"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4-5. Start the game on the slot: DirectEnter, the second game login and the loading phase.
|
||||||
|
if (selecting)
|
||||||
|
{
|
||||||
|
CHECK(py("_stream.curPhaseWindow.SelectSlot(_slot)\n_stream.curPhaseWindow.StartGame()"));
|
||||||
|
// GC_MAIN_CHARACTER → LoadingWindow.LoadData(x, y), then CG_CLIENT_VERSION in the same call.
|
||||||
|
const char* loaded = "isinstance(_stream.curPhaseWindow, introLoading.LoadingWindow) and "
|
||||||
|
"getattr(_stream.curPhaseWindow, 'playerX', None) is not None";
|
||||||
|
const bool loading = pump_until(live ? 40 : 30, [&] {
|
||||||
|
return failed() || (server ? server->Has("game2:client_version") : py_true(loaded));
|
||||||
|
});
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> expected = {
|
||||||
|
"auth:handshake", "auth:login3", "game1:handshake", "game1:login2", "auth:closed",
|
||||||
|
"game1:closed", "game2:handshake", "game2:login2", "game2:select", "game2:client_version",
|
||||||
|
};
|
||||||
|
auto events = server->Events();
|
||||||
|
// auth:closed races game1's handshake; compare the order within each connection.
|
||||||
|
for (const char* prefix : {"auth:", "game1:", "game2:"})
|
||||||
|
{
|
||||||
|
std::vector<std::string> want, got;
|
||||||
|
for (const auto& e : expected)
|
||||||
|
if (e.rfind(prefix, 0) == 0)
|
||||||
|
want.push_back(e);
|
||||||
|
for (const auto& e : events)
|
||||||
|
if (e.rfind(prefix, 0) == 0)
|
||||||
|
got.push_back(e);
|
||||||
|
CHECK(want == got);
|
||||||
|
}
|
||||||
|
if (g_failures)
|
||||||
|
dump(*server);
|
||||||
}
|
}
|
||||||
if (g_failures)
|
|
||||||
dump(server);
|
|
||||||
|
|
||||||
CHECK(py("import app\napp.Exit()"));
|
CHECK(py("import app\napp.Exit()"));
|
||||||
pump_until(2, [] { return !PythonBoot::IsAppLooping(); });
|
pump_until(2, [] { return !PythonBoot::IsAppLooping(); });
|
||||||
CHECK(!PythonBoot::IsAppLooping());
|
CHECK(!PythonBoot::IsAppLooping());
|
||||||
PythonBoot::Stop();
|
PythonBoot::Stop();
|
||||||
server.Stop();
|
if (server)
|
||||||
|
server->Stop();
|
||||||
|
|
||||||
if (g_failures)
|
if (g_failures)
|
||||||
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
||||||
else
|
else
|
||||||
std::printf("port_login_flow_test: ok\n");
|
std::printf("port_login_flow_test: ok%s\n", live ? " (live)" : "");
|
||||||
return g_failures ? 1 : 0;
|
return g_failures ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user