diff --git a/audit/history.jsonl b/audit/history.jsonl index 1c544f4b..cd1c3cb0 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -529,3 +529,4 @@ {"time": "2026-09-24T02:00:00Z", "event": "port_round", "unit": "PORT-PLAN batch 2 P0: UserInterface/PythonCharacterManager.cpp + InstanceBaseMovement.cpp", "ported": ["UserInterface/PythonCharacterManager.cpp (54)", "UserInterface/InstanceBaseMovement.cpp (32)"], "adapted": [], "divergent": [], "needs_live": [], "tests": ["diff vs 40250: include case, comment encoding, 2 PORT edits only", "ctest all: 19/19 incl. port.login_live 192.168.21.203", "port_map.py check: 0 errors"], "todo": ["P1 queue: FlyingInstance, ActorInstanceData, FlyingObjectManager, FlyTrace"]} {"time": "2026-09-24T03:00:00Z", "event": "port_round", "unit": "PORT-PLAN batch 2 P1: flying objects + effect/fly Python modules", "ported": ["GameLib/FlyingData.cpp (12)", "GameLib/FlyingInstance.cpp (20)", "GameLib/FlyingObjectManager.cpp (12)", "GameLib/FlyTrace.cpp (11)", "GameLib/ActorInstanceData.cpp (12)", "UserInterface/PythonEffectModule.cpp (8)", "UserInterface/PythonFlyModule.cpp (3)"], "adapted": ["CMapManager::GetTerrainHeight over native height tiles; GetHeight forwards to it"], "divergent": [], "needs_live": [], "tests": ["port.login_flow: indexed fly .msf registered by playersettingmodule and parses", "ctest all: 19/19 incl. port.login_live 192.168.21.203", "script/python_game_render_test.sh offline: PASS", "port_map.py check: 0 errors"], "todo": ["CEffectManager is a platform stub: fly effects are not rendered", "P2 network units already copied into port/ need verification and marking"]} {"time": "2026-09-24T04:00:00Z", "event": "port_round", "unit": "PORT-PLAN batch 2: verify and mark 28 units copied into port/ in 2V1/2A", "ported": ["28 units, 372 functions: PythonNetworkStream{,PhaseLoading,PhaseLogin,PhaseSelect,PhaseHandShake,PhaseOffline,Event}, AccountConnector, NetStream, NetPacketHeaderMap, NetAddress, NetDevice, TextTag, EterPack, EterPackManager, EterPackCursor, EterPackPolicy_CSHybridCrypt, Timer, cipher, lzo, Random, Stl, tea, Arabic, StringCodec, StringCodec_Vietnamese, InsultChecker, AffectFlagContainer"], "adapted": [], "divergent": [], "needs_live": [], "tests": ["port_copy.py diff: 23 exact mechanical copies, 5 with // PORT: edits only; StringCodec_Vietnamese (not CP949) differs by BOM + include case", "no same-named platform/pending files", "ctest all 19/19 + render test on the same tree", "port_map.py check: 0 errors"], "todo": ["P2 ServerStateChecker (module still stubbed)", "P3 items/minimap/text tail"]} +{"time": "2026-09-24T05:00:00Z", "event": "port_round", "unit": "PORT-PLAN batch 2 P2: ServerStateChecker + module", "ported": ["UserInterface/ServerStateChecker.cpp", "UserInterface/ServerStateCheckerModule.cpp"], "adapted": [], "divergent": [], "needs_live": [], "tests": ["port.login_flow: fake state port answers CG_STATE_CHECKER, NotifyChannelState(2, 1)", "port.login_live 192.168.21.203: channel states {1: 1, 2: 1}", "ctest all 19/19, python_game_render_test offline PASS", "port_map.py check: 0 errors"], "todo": ["P3 item chain: ItemData, ItemManager, PythonItem, PythonExchange"]} diff --git a/audit/port-map/UserInterface/ServerStateChecker.cpp.json b/audit/port-map/UserInterface/ServerStateChecker.cpp.json new file mode 100644 index 00000000..6e306e1a --- /dev/null +++ b/audit/port-map/UserInterface/ServerStateChecker.cpp.json @@ -0,0 +1,50 @@ +{ + "reference": "UserInterface/ServerStateChecker.cpp", + "reference_sha256": "89f157c56f486a92c7bfe695853b9165b727d77dfdca5e360d256a9ff153d511", + "priority": "P2", + "contracts": [], + "functions": { + "CServerStateChecker::CServerStateChecker": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::CServerStateChecker" + ] + }, + "CServerStateChecker::~CServerStateChecker": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::~CServerStateChecker" + ] + }, + "CServerStateChecker::Create": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::Create" + ] + }, + "CServerStateChecker::AddChannel": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::AddChannel" + ] + }, + "CServerStateChecker::Request": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::Request" + ] + }, + "CServerStateChecker::Update": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::Update" + ] + }, + "CServerStateChecker::Initialize": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateChecker.cpp:CServerStateChecker::Initialize" + ] + } + } +} diff --git a/audit/port-map/UserInterface/ServerStateCheckerModule.cpp.json b/audit/port-map/UserInterface/ServerStateCheckerModule.cpp.json new file mode 100644 index 00000000..7156b63a --- /dev/null +++ b/audit/port-map/UserInterface/ServerStateCheckerModule.cpp.json @@ -0,0 +1,44 @@ +{ + "reference": "UserInterface/ServerStateCheckerModule.cpp", + "reference_sha256": "eb402b63df2c32bd69287d8c02f717e8ff586204917e02bb5850b956f75e3609", + "priority": "P4", + "contracts": [], + "functions": { + "ServerStateCheckerCreate": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateCheckerModule.cpp:ServerStateCheckerCreate" + ] + }, + "ServerStateCheckerUpdate": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateCheckerModule.cpp:ServerStateCheckerUpdate" + ] + }, + "ServerStateCheckerRequest": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateCheckerModule.cpp:ServerStateCheckerRequest" + ] + }, + "ServerStateCheckerAddChannel": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateCheckerModule.cpp:ServerStateCheckerAddChannel" + ] + }, + "ServerStateCheckerInitialize": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateCheckerModule.cpp:ServerStateCheckerInitialize" + ] + }, + "initServerStateChecker": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/ServerStateCheckerModule.cpp:initServerStateChecker" + ] + } + } +} diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index 145a6e81..87046694 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -432,6 +432,7 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 2V3-a(2026-09-23):`PythonPlayer`、`PythonPlayerInput`/`InputKeyboard`/`InputMouse`、`PythonPlayerEventHandler`、`PythonPlayerSkill`、`PythonPlayerModule`、`GameType` 原样移植(port-map 标 PORTED),`GameplayModules.cpp` 的 `player` 桩删除,`initPlayer()` 按 40250 顺序在 `initchrmgr()` 后调用;`pending/UserInterface/{PythonPlayer,GameType}.cpp` 替身删除,其余被调方(`CPythonSkill`、`CItemData`、`CItemManager`、`CPythonItem` 等)进 `pending`。64 位修正:`playerGetEmotionIconImage` 用 `Py_BuildPointer`,`PyDict_Next` 用 `Py_ssize_t`。根运动:Granny 运行时的 `GrannyUpdateModelMatrix` 原为直通,按 TrackGroups[0] 的 `AccumulationFlags`(AccumulationExtracted)/`LoopTranslation`(libgr2 新读出,战士 run = (0,-300,0),同 .msa `Accumulation`)按控制器本地时钟推进与权重累加位移(Delta × ModelMatrix);VDA 抽取未实现。`port.login_flow` 按 DIK_UP 经 `game.GameWindow` → `player.SetSingleDIKKeyState` 行走 >100 cm,假服解析 CG 包,收到 `CG_MOVE` FUNC_MOVE 后 FUNC_WAIT 且坐标变化;`port.login_live` 2026-09-23 对 192.168.21.203 走 132 cm 通过。(2V3-a 的待办见 2V3-b。) 2V3-b(2026-09-23):鼠标点地移动。`CScreen::SetCursorPosition`(GrpScreen.cpp:487)与 `CPythonGraphic::SetCursorPosition` 原样实现,`RenderGame` 每帧用光标建拾取射线;`CPythonBackground::GetPickingPoint`/`GetPickingPointWithRay`/`GetPickingPointWithRayOnlyTerrain` 按 `CMapOutdoor::__PickTerrainHeight` 原样沿射线步进,地形高度取 native `CMapManager::GetHeight`(`PR_FLOAT_TO_INT` 的 x87 取整按 floor),物件拾取随 `CMapOutdoor` 移植再接。`CMapManager::isAttrOn` 按 `CMapOutdoor::isAttrOn`+`CTerrain::isAttrOn` 读包内 `//attr.atr`(阻挡/水/禁 PK),`CheckAdvancing` 及两个碰撞检查器原样(地图物件未注册到 `CCullingManager` 前不会阻挡)。以上在 pending/platform 层,port-map 标 ADAPTED。`port.login_flow` 在按键行走后左键点击角色下方地面:`game.GameWindow.OnMouseLeftButtonDown` → `player.SetMouseState` → 角色走到拾取点(离线 334 cm),假服收到 FUNC_MOVE … FUNC_WAIT;另断言角色所在格不阻挡、所在地形块有阻挡格(离线 17814 格)。`port.login_live` 2026-09-23 对 192.168.21.203 通过(按键 133 cm、点地 296 cm、11406 阻挡格),`python_game_render_test.sh` 离线通过。`client_main` 的 `MT_TEST_MODE=python_ui` 加上 `python_3d_surface.gd`,新路径可手动登录进游戏走动。待办:默认路由切到 `port`(当前 `client_main` 默认仍走 GDScript `AppFlow`)后整体删除 legacy;攻击/拾取物品等 P1 战斗交互属批次 2 之后。 批次 2 P0 收尾(2026-09-24):`UserInterface/PythonCharacterManager.cpp`(54 函数)与 `InstanceBaseMovement.cpp`(32 函数)在 2V2-b 已原样拷入 `port/`,2V3 起即是运行路径(行走、点地移动、远程角色同步都经它们),本轮逐行比对 40250(仅 include 大小写、注释编码和两处 `// PORT:` 修正:`CreateInstance` 的 TraceError 传 `m_dwVID`,`NEW_SyncPixelPosition` 用 32 位 `LONG &`),确认 pending/platform 无重复定义后标 PORTED。P0 队列已清空;P1 飞行物(同日):`GameLib/FlyingData`、`FlyingInstance`、`FlyingObjectManager`、`FlyTrace` 与 Python 模块 `PythonEffectModule`(`effect`)、`PythonFlyModule`(`fly`)原样移植,删除 `pending/GameLib/FlyingObjectManager.cpp` 替身和两个模块的桩,`initeffect()`/`initfly()` 按 40250 顺序在 `initnet()` 后调用;`CFlyingManager::SetMapManagerPtr(&CPythonBackground)` 按 40250 构造函数接上;`FlyTrace.h` 加 `// PORT:` include 以过头文件门禁。`CMapManager::GetTerrainHeight`(飞行物落地判断用)在 pending 层按 native 地形高度实现,`GetHeight` 转发到它(地图物件高度待 `CMapOutdoor`)。`ActorInstanceData.cpp` 早已原样在 port/,一并标 PORTED。`port.login_flow` 断言 `playersettingmodule.py` 已经 `effect.RegisterIndexedFlyData` 注册并能解析 `.msf`。特效本体(`CEffectManager`)仍是平台桩,飞行物不可见;队列里 P0/P1 已清空,剩下 P2 网络单元(多数已在 2V1 原样拷入 port/,待逐个核对后标记)。同日核对:队列中 28 个早已拷入 port/ 的单元(网络流各阶段、`AccountConnector`、`NetStream`/`NetDevice`/`NetAddress`、`EterPack*`、`lzo`/`tea`/`cipher`/`Timer` 等,372 函数)用 `port_copy.py diff` 比对,23 个与机械拷贝完全一致,其余 5 个的手改全部带 `// PORT:`(CP949 转义、`e-` pp-number、32 位 `LONG`、包密钥外置、`lzo_uint` 宽度、`ActivateCipher` 解密同批已收密文),平台层无同名替身,标 PORTED;logic 层 54.8% → 67.9%。队列剩余 P2 仅 `ServerStateChecker`(`ServerStateChecker` 模块仍是桩),其后是 P3 物品/小地图/名牌。 + P2 收尾(2026-09-24):`ServerStateChecker.cpp` 与 `ServerStateCheckerModule.cpp` 原样移植,删除模块桩,实例按 `CPythonApplication::m_kServerStateChecker` 由 PythonBoot 持有,`initServerStateChecker()` 按 40250 顺序调用。`port.login_flow` 在选角后按 intrologin 的 `__RequestServerStateList` 调 `AddChannel`/`Request`/`Update`,假服新增状态端口按 `CInputHandshake`+`CInputDB::RespondChannelStatus` 应答(先握手字节,再 `210,n,TChannelStatus[n],1`);`TChannelStatus::nPort` 是 short,假服临时端口 >32767 对不上自身频道,离线以第二个频道 13002 为证。`port.login_live` 真服返回 {1: 1, 2: 1}。P2 队列清空,下一步 P3 物品链。 每个切片都必须同时跑 legacy/port 双路回归;只有 2V3 稳定并把默认路由切到 `port` 后,才能删除总开关和剩余 legacy 宿主。 diff --git a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp index e7c2b02c..d29d16bd 100644 --- a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp +++ b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp @@ -2,7 +2,6 @@ // 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" -#include "UserInterface/ServerStateChecker.h" #include "UserInterface/PythonSystem.h" #include "UserInterface/PythonBackground.h" #include "EterLib/StateManager.h" @@ -36,12 +35,6 @@ #include "UserInterface/PythonPlayer.h" #include "UserInterface/PythonTextTail.h" -// 40250 UserInterface/ServerStateCheckerModule.cpp -void AddStubConstants_ServerStateChecker(PyObject* poModule) -{ - (void) poModule; -} - // 40250 UserInterface/PythonBackgroundModule.cpp void AddStubConstants_background(PyObject* poModule) { diff --git a/extension/src/platform/ScriptLib/GameplayModules.cpp b/extension/src/platform/ScriptLib/GameplayModules.cpp index 2d075584..6a91cb66 100644 --- a/extension/src/platform/ScriptLib/GameplayModules.cpp +++ b/extension/src/platform/ScriptLib/GameplayModules.cpp @@ -5,7 +5,6 @@ #include "../EterLib/UIRenderCommands.h" // GameplayModuleConstants.cpp: each module's 40250 constants. -void AddStubConstants_ServerStateChecker(PyObject* poModule); void AddStubConstants_background(PyObject* poModule); void AddStubConstants_event(PyObject* poModule); void AddStubConstants_eventMgr(PyObject* poModule); @@ -34,20 +33,6 @@ PyObject* stub_call_named(const char* name, PyObject* result = nullptr) { return result ? result : Py_BuildValue("i", 0); } -// 40250 UserInterface/ServerStateCheckerModule.cpp: ServerStateChecker export names. -PyObject* stub_ServerStateChecker_Create(PyObject*, PyObject*) { return stub_call_named("ServerStateChecker.Create", Py_BuildNone()); } -PyObject* stub_ServerStateChecker_Update(PyObject*, PyObject*) { return stub_call_named("ServerStateChecker.Update", Py_BuildNone()); } -PyObject* stub_ServerStateChecker_Request(PyObject*, PyObject*) { return stub_call_named("ServerStateChecker.Request", Py_BuildNone()); } -PyObject* stub_ServerStateChecker_AddChannel(PyObject*, PyObject*) { return stub_call_named("ServerStateChecker.AddChannel", Py_BuildNone()); } -PyObject* stub_ServerStateChecker_Initialize(PyObject*, PyObject*) { return stub_call_named("ServerStateChecker.Initialize", Py_BuildNone()); } -PyMethodDef methods_ServerStateChecker[] = { - { "Create", stub_ServerStateChecker_Create, METH_VARARGS }, - { "Update", stub_ServerStateChecker_Update, METH_VARARGS }, - { "Request", stub_ServerStateChecker_Request, METH_VARARGS }, - { "AddChannel", stub_ServerStateChecker_AddChannel, METH_VARARGS }, - { "Initialize", stub_ServerStateChecker_Initialize, METH_VARARGS }, - { NULL, NULL } -}; // 40250 UserInterface/PythonBackgroundModule.cpp: background export names. PyObject* stub_background_IsSoftwareTiling(PyObject*, PyObject*) { return stub_call_named("background.IsSoftwareTiling", Py_BuildValue("i", 0)); } PyObject* stub_background_EnableSoftwareTiling(PyObject*, PyObject*) { return stub_call_named("background.EnableSoftwareTiling", Py_BuildNone()); } @@ -892,11 +877,6 @@ PyMethodDef methods_udp[] = { } // namespace void init_2v0_gameplay_stubs() { - { - PyObject* module = Py_InitModule("ServerStateChecker", methods_ServerStateChecker); - PyModule_AddIntConstant(module, "__port_stub__", 1); - AddStubConstants_ServerStateChecker(module); - } { PyObject* module = Py_InitModule("background", methods_background); PyModule_AddIntConstant(module, "__port_stub__", 1); diff --git a/extension/src/platform/ScriptLib/PythonBoot.cpp b/extension/src/platform/ScriptLib/PythonBoot.cpp index 1f277bdd..7612443a 100644 --- a/extension/src/platform/ScriptLib/PythonBoot.cpp +++ b/extension/src/platform/ScriptLib/PythonBoot.cpp @@ -10,6 +10,7 @@ #include "../EterLib/HostCursor.h" #include "UserInterface/PythonNetworkStream.h" #include "UserInterface/AccountConnector.h" +#include "UserInterface/ServerStateChecker.h" #include "UserInterface/PythonPlayer.h" #include "GameLib/FlyingObjectManager.h" #include "GameLib/RaceManager.h" @@ -58,9 +59,11 @@ std::unique_ptr g_timer; std::unique_ptr g_network_stream; std::unique_ptr g_account_connector; // 40250 CPythonApplication::m_pyPlayer, m_FlyingManager and m_kEftMgr: the loading phase clears them -// (CPythonNetworkStream::SetLoadingPhase). PythonPlayer and FlyingObjectManager are platform/pending stand-ins. +// (CPythonNetworkStream::SetLoadingPhase). std::unique_ptr g_player; std::unique_ptr g_flying_manager; +// 40250 CPythonApplication::m_kServerStateChecker: intrologin.py's channel status list. +std::unique_ptr g_server_state_checker; std::unique_ptr g_race_manager; std::unique_ptr g_game_event_manager; std::unique_ptr g_effect_manager; @@ -314,6 +317,7 @@ bool Start(const char* stdlib_path, std::string* error) g_account_connector = std::make_unique(); g_player = std::make_unique(); g_flying_manager = std::make_unique(); + g_server_state_checker = std::make_unique(); g_race_manager = std::make_unique(); g_game_event_manager = std::make_unique(); g_effect_manager = std::make_unique(); @@ -347,6 +351,7 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error) initnet(); initeffect(); initfly(); + initServerStateChecker(); init_2v0_gameplay_stubs(); ClearLogBoxMessage(); @@ -517,6 +522,7 @@ void Stop() g_effect_manager.reset(); g_game_event_manager.reset(); g_race_manager.reset(); + g_server_state_checker.reset(); g_flying_manager.reset(); g_player.reset(); g_account_connector.reset(); diff --git a/extension/src/port/UserInterface/ServerStateChecker.cpp b/extension/src/port/UserInterface/ServerStateChecker.cpp new file mode 100644 index 00000000..ab03af0b --- /dev/null +++ b/extension/src/port/UserInterface/ServerStateChecker.cpp @@ -0,0 +1,105 @@ +#include "StdAfx.h" +#include "Packet.h" +#include "ServerStateChecker.h" + +#include "../EterLib/NetAddress.h" + +#pragma pack(push) +#pragma pack(1) + +typedef unsigned char ServerStateChecker_Header; +typedef unsigned long ServerStateChecker_Key; +typedef unsigned long ServerStateChecker_Index; +typedef unsigned char ServerStateChecker_State; + +#pragma pack(pop) + +CServerStateChecker::CServerStateChecker() +{ + Initialize(); +} + +CServerStateChecker::~CServerStateChecker() +{ + Initialize(); + m_poWnd = NULL; +} + +void CServerStateChecker::Create(PyObject* poWnd) +{ + m_poWnd = poWnd; +} + +void CServerStateChecker::AddChannel(UINT uServerIndex, const char* c_szAddr, UINT uPort) +{ + TChannel c; + c.uServerIndex = uServerIndex; + c.c_szAddr = c_szAddr; + c.uPort = uPort; + + m_lstChannel.push_back(c); +} + +void CServerStateChecker::Request() +{ + if (m_lstChannel.empty()) { + return; + } + + if (!m_kStream.Connect(m_lstChannel.begin()->c_szAddr, m_lstChannel.begin()->uPort)) + { + for (std::list::const_iterator it = m_lstChannel.begin(); it != m_lstChannel.end(); ++it) { + PyCallClassMemberFunc(m_poWnd, "NotifyChannelState", Py_BuildValue("(ii)", it->uServerIndex, 0)); + } + return; + } + m_kStream.ClearRecvBuffer(); + m_kStream.SetSendBufferSize(1024); + m_kStream.SetRecvBufferSize(1024); + + BYTE bHeader = HEADER_CG_STATE_CHECKER; + if (!m_kStream.Send(sizeof(bHeader), &bHeader)) + { + for (std::list::const_iterator it = m_lstChannel.begin(); it != m_lstChannel.end(); ++it) { + PyCallClassMemberFunc(m_poWnd, "NotifyChannelState", Py_BuildValue("(ii)", it->uServerIndex, 0)); + } + Initialize(); + return; + } +} + +void CServerStateChecker::Update() +{ + m_kStream.Process(); + + BYTE bHeader; + if (!m_kStream.Recv(sizeof(bHeader), &bHeader)) { + return; + } + if (HEADER_GC_RESPOND_CHANNELSTATUS != bHeader) { + return; + } + int nSize; + if (!m_kStream.Recv(sizeof(nSize), &nSize)) { + return; + } + for (int i = 0; i < nSize; i++) { + TChannelStatus channelStatus; + if (!m_kStream.Recv(sizeof(channelStatus), &channelStatus)) { + return; + } + for (std::list::const_iterator it = m_lstChannel.begin(); it != m_lstChannel.end(); ++it) { + if (channelStatus.nPort == it->uPort) { + PyCallClassMemberFunc(m_poWnd, "NotifyChannelState", Py_BuildValue("(ii)", it->uServerIndex, channelStatus.bStatus)); + break; + } + } + } + Initialize(); +} + +void CServerStateChecker::Initialize() +{ + m_lstChannel.clear(); + m_kStream.Disconnect(); +} \ No newline at end of file diff --git a/extension/src/port/UserInterface/ServerStateCheckerModule.cpp b/extension/src/port/UserInterface/ServerStateCheckerModule.cpp new file mode 100644 index 00000000..6e36f3d0 --- /dev/null +++ b/extension/src/port/UserInterface/ServerStateCheckerModule.cpp @@ -0,0 +1,64 @@ +#include "StdAfx.h" +#include "ServerStateChecker.h" + +PyObject * ServerStateCheckerCreate(PyObject* poSelf, PyObject* poArgs) +{ + PyObject * poWnd; + if (!PyTuple_GetObject(poArgs, 0, &poWnd)) + return Py_BadArgument(); + + CServerStateChecker::Instance().Create(poWnd); + return Py_BuildNone(); +} + +PyObject * ServerStateCheckerUpdate(PyObject* poSelf, PyObject* poArgs) +{ + CServerStateChecker::Instance().Update(); + return Py_BuildNone(); +} + +PyObject * ServerStateCheckerRequest(PyObject* poSelf, PyObject* poArgs) +{ + CServerStateChecker::Instance().Request(); + return Py_BuildNone(); +} + +PyObject * ServerStateCheckerAddChannel(PyObject* poSelf, PyObject* poArgs) +{ + int nServerIndex; + if (!PyTuple_GetInteger(poArgs, 0, &nServerIndex)) + return Py_BuildException(); + + char * szAddr; + if (!PyTuple_GetString(poArgs, 1, &szAddr)) + return Py_BuildException(); + + int nPort; + if (!PyTuple_GetInteger(poArgs, 2, &nPort)) + return Py_BuildException(); + + CServerStateChecker::Instance().AddChannel(nServerIndex, szAddr, nPort); + return Py_BuildNone(); +} + +PyObject * ServerStateCheckerInitialize(PyObject* poSelf, PyObject* poArgs) +{ + CServerStateChecker::Instance().Initialize(); + return Py_BuildNone(); +} + +void initServerStateChecker() +{ + static PyMethodDef s_methods[] = + { + { "Create", ServerStateCheckerCreate, METH_VARARGS }, + { "Update", ServerStateCheckerUpdate, METH_VARARGS }, + { "Request", ServerStateCheckerRequest, METH_VARARGS }, + { "AddChannel", ServerStateCheckerAddChannel, METH_VARARGS }, + { "Initialize", ServerStateCheckerInitialize, METH_VARARGS }, + { NULL, NULL, NULL }, + }; + + //PyObject * poModule = Py_InitModule("ServerStateChecker", s_methods); + Py_InitModule("ServerStateChecker", s_methods); +} \ No newline at end of file diff --git a/extension/src/port/UserInterface/StdAfx.h b/extension/src/port/UserInterface/StdAfx.h index 50d5cb0b..9abfe81a 100644 --- a/extension/src/port/UserInterface/StdAfx.h +++ b/extension/src/port/UserInterface/StdAfx.h @@ -51,3 +51,4 @@ void initChat(); void initnet(); void initeffect(); void initfly(); +void initServerStateChecker(); diff --git a/extension/tests/port_login_flow_server.cpp b/extension/tests/port_login_flow_server.cpp index f87ff6f8..eb75344e 100644 --- a/extension/tests/port_login_flow_server.cpp +++ b/extension/tests/port_login_flow_server.cpp @@ -142,9 +142,11 @@ bool FakeLoginServer::Start() { m_auth_listener = listen_loopback(&m_auth_port); m_game_listener = listen_loopback(&m_game_port); - if (m_auth_listener < 0 || m_game_listener < 0) + m_state_listener = listen_loopback(&m_state_port); + if (m_auth_listener < 0 || m_game_listener < 0 || m_state_listener < 0) return false; m_thread = std::thread([this] { Run(); }); + m_state_thread = std::thread([this] { ServeState(); }); return true; } @@ -153,7 +155,9 @@ void FakeLoginServer::Stop() m_stop = true; if (m_thread.joinable()) m_thread.join(); - for (int* fd : {&m_auth_listener, &m_game_listener}) + if (m_state_thread.joinable()) + m_state_thread.join(); + for (int* fd : {&m_auth_listener, &m_game_listener, &m_state_listener}) if (*fd >= 0) { close(*fd); @@ -287,6 +291,52 @@ bool FakeLoginServer::Handshake(Connection& c, std::uint8_t phase_after) return c.Phase(phase_after) || Fail("send phase after handshake"); } +// CServerStateChecker::Request connects to the first channel and sends the bare header. The game server +// has already started the handshake (DESC::Setup), so the client reads past those bytes one per Update; +// CInputHandshake::Analyze(HEADER_CG_STATE_CHECKER) asks the db, and CInputDB::RespondChannelStatus writes +// the header, the count, the TChannelStatus {short nPort; BYTE bStatus} array and a success byte. +void FakeLoginServer::ServeState() +{ + Connection c; + c.stop = &m_stop; + for (;;) + { + if (m_stop) + return; + pollfd pfd = {m_state_listener, POLLIN, 0}; + if (poll(&pfd, 1, 50) > 0) + break; + } + c.fd = accept(m_state_listener, nullptr, nullptr); + if (c.fd < 0) + return; + mtnet::classic::Handshake hs = {HDR_HANDSHAKE, kHandshake, now_ms(), 0}; + if (!c.Phase(PHASE_HANDSHAKE) || !c.Send(hs)) + return; + std::uint8_t header = 0; + if (c.Read(&header, 1) != 1 || header != 206) + { + Fail("state checker: expected CG_STATE_CHECKER"); + return; + } + Event("state:checker"); + std::vector out = {210}; + const std::int32_t count = 2; + out.insert(out.end(), reinterpret_cast(&count), reinterpret_cast(&count) + 4); + for (int port : {m_state_port, kOtherChannelPort}) + { + const std::int16_t short_port = static_cast(port); + out.insert(out.end(), reinterpret_cast(&short_port), reinterpret_cast(&short_port) + 2); + out.push_back(static_cast(kOtherChannelStatus)); + } + out.push_back(1); + c.Send(out.data(), out.size()); + // Keep the socket open until the client disconnects (CServerStateChecker::Initialize). + std::uint8_t rest; + while (c.Read(&rest, 1) == 1) + ; +} + // Reads one client packet of a known header: the static size from packet_info.cpp plus the sequence // byte CInputProcessor::Process checks against gc_abSequence. static int read_packet(FakeLoginServer::Connection& c, std::uint8_t expect, void* packet, size_t size, diff --git a/extension/tests/port_login_flow_server.h b/extension/tests/port_login_flow_server.h index 5a9b10ad..f3ca3e45 100644 --- a/extension/tests/port_login_flow_server.h +++ b/extension/tests/port_login_flow_server.h @@ -30,6 +30,11 @@ public: int AuthPort() const { return m_auth_port; } int GamePort() const { return m_game_port; } + // A third game-server port that only answers intrologin's ServerStateChecker (CG_STATE_CHECKER). + int StatePort() const { return m_state_port; } + // The channel port the state answer reports besides StatePort(), with status kOtherChannelStatus. + static constexpr int kOtherChannelPort = 13002; + static constexpr int kOtherChannelStatus = 1; // Milestones in arrival order ("auth:login3", "game1:login2", ...), and the first protocol error. std::vector Events(); @@ -45,13 +50,15 @@ private: bool ServeAuth(Connection& c); bool ServeGame(Connection& c, int index); bool Handshake(Connection& c, std::uint8_t phase_after); + void ServeState(); bool Accept(int listener, Connection& c); void Event(const std::string& event); bool Fail(const std::string& error); int m_auth_listener = -1, m_game_listener = -1; - int m_auth_port = 0, m_game_port = 0; - std::thread m_thread; + int m_state_listener = -1; + int m_auth_port = 0, m_game_port = 0, m_state_port = 0; + std::thread m_thread, m_state_thread; std::atomic m_stop{false}; std::mutex m_mutex; std::vector m_events; diff --git a/extension/tests/port_login_flow_test.cpp b/extension/tests/port_login_flow_test.cpp index 2a1bb307..43676d00 100644 --- a/extension/tests/port_login_flow_test.cpp +++ b/extension/tests/port_login_flow_test.cpp @@ -269,6 +269,34 @@ int main(int argc, char** argv) "assert net.GetEmpireID() in (1, 2, 3), net.GetEmpireID()")); } + // 3b. intrologin's channel status list (__RequestServerStateList / NotifyChannelState): ServerStateChecker + // connects to the first channel, sends CG_STATE_CHECKER and reports each channel the answer lists + // by port. TChannelStatus::nPort is a short, so the fake server's ephemeral state port (> 32767) + // never matches its own channel; offline the proof is the second channel (13002) it also lists. + // Live the game port answers for the channels the db knows, the game port among them. + { + const int state_port = server ? server->StatePort() : game_port; + const int key = server ? 2 : 1; + char channels[256]; + std::snprintf(channels, sizeof(channels), + "ServerStateChecker.AddChannel(1, _host, %d)\nServerStateChecker.AddChannel(2, _host, %d)\n" + "_channel_key = %d\n", + state_port, FakeLoginServer::kOtherChannelPort, key); + CHECK(py(std::string("import ServerStateChecker\n" + "class _ChannelStates:\n" + " def __init__(self): self.states = {}\n" + " def NotifyChannelState(self, key, state): self.states[key] = state\n" + "_channel_states = _ChannelStates()\n" + "ServerStateChecker.Create(_channel_states)\n" + "ServerStateChecker.Initialize()\n") + + channels + "ServerStateChecker.Request()")); + CHECK(pump_until(10, [] { return py("ServerStateChecker.Update()") && py_true("_channel_key in _channel_states.states"); })); + py("import sys\nsys.stderr.write('port_login_flow_test: channel states %r\\n' % _channel_states.states)"); + CHECK(py_true("_channel_states.states.get(_channel_key, 0) > 0")); + if (server) + CHECK(server->Has("state:checker")); + } + // 4-5. Start the game on the slot: DirectEnter, the second game login and the loading phase. if (selecting) {