// 批次 2V1-d: the real 40250 login → select → loading path, offline. // // 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), // which answers the way the 40250 game server does. The test only plays the player: // // 1. frames advance the logo phase into introLogin.LoginWindow; // 2. LoginWindow.Connect(id, pwd) → net.ConnectToAccountServer: handshake + key agreement with the // auth server, CG_LOGIN3, GC_AUTH_SUCCESS; the account connector hands the login key to the stream, // which connects to the game server and sends CG_LOGIN2; // 3. GC_EMPIRE, PHASE_SELECT, GC_LOGIN_SUCCESS_NEWSLOT open introSelect.SelectCharacterWindow; // 4. SelectCharacterWindow.StartGame() → (3 s later) net.DirectEnter(0): the stream reconnects to the // 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; // 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. // // The server checks every sequence byte against the 40250 table, so a skipped or extra sequenced // packet fails the run. // // port_login_flow_test <40250 Client dir> // // Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead. #include "ScriptLib/StdAfx.h" #include "../src/platform/ScriptLib/PythonBoot.h" #include "../src/platform/ScriptLib/PythonHost.h" #include "../src/platform/UserInterface/UserInterface.h" #include "port_login_flow_server.h" #include #include #include #include #include #include #include static int g_failures = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \ ++g_failures; \ } \ } while (0) static bool py(const std::string& source) { std::string error; if (PythonBoot::RunLine(source.c_str(), &error)) return true; std::fprintf(stderr, "python: %s\n%s\n", source.c_str(), error.c_str()); return false; } // Evaluates a Python expression on the host thread without raising. static bool py_true(const char* expression) { PyObject* globals = PyModule_GetDict(PyImport_AddModule((char*) "__main__")); PyObject* result = PyRun_String(expression, Py_eval_input, globals, globals); if (!result) { PyErr_Clear(); return false; } const bool truth = PyObject_IsTrue(result) == 1; Py_DECREF(result); return truth; } // Runs frames (~60 per second of wall time) until `done` or the deadline. static bool pump_until(double seconds, const std::function& done) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration(seconds); while (std::chrono::steady_clock::now() < deadline && PythonBoot::IsAppLooping()) { PythonBoot::UIUpdate(); if (done()) return true; std::this_thread::sleep_for(std::chrono::milliseconds(16)); } return done(); } static void dump(FakeLoginServer& server) { std::fprintf(stderr, "server events:"); for (const auto& e : server.Events()) std::fprintf(stderr, " %s", e.c_str()); std::fprintf(stderr, "\nserver error: %s\n", server.Error().c_str()); } int main(int argc, char** argv) { const char* strict = std::getenv("MT_ASSETS_STRICT"); const bool is_strict = strict && std::string(strict) == "1"; const std::string client = argc > 1 ? argv[1] : ""; const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath(); 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()); return is_strict ? 1 : 77; } FakeLoginServer server; if (!server.Start()) { std::fprintf(stderr, "port_login_flow_test: cannot listen on loopback\n"); return 1; } PackSingletons(); CHECK(PackInitialize("pack")); std::string error; CHECK(PythonBoot::Start(stdlib.c_str(), &error)); PythonBoot::SetUISize(800, 600); const bool ran = PythonBoot::RunMainScript("", &error); if (!ran) std::fprintf(stderr, "RunMainScript: %s\n", error.c_str()); CHECK(ran && PythonBoot::IsAppLooping()); CHECK(py("import __main__, networkModule, introLogin, introSelect, introLoading\n" "__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)"); })); // 2. The login button's path, with the fake server for both the game and the auth address. char connect[256]; std::snprintf(connect, sizeof(connect), "_stream.SetConnectInfo('127.0.0.1', %d, '127.0.0.1', %d)\n" "_stream.curPhaseWindow.Connect('%s', '%s')", server.GamePort(), server.AuthPort(), FakeLoginServer::kLogin, FakeLoginServer::kPassword); CHECK(py(connect)); // 3. The character list. const bool selecting = pump_until(20, [&] { return server.Failed() || (server.Has("game1:login2") && py_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)")); }); CHECK(selecting && !server.Failed()); 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")); // 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 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 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); CHECK(py("import app\napp.Exit()")); pump_until(2, [] { return !PythonBoot::IsAppLooping(); }); CHECK(!PythonBoot::IsAppLooping()); PythonBoot::Stop(); server.Stop(); if (g_failures) std::fprintf(stderr, "%d check(s) failed\n", g_failures); else std::printf("port_login_flow_test: ok\n"); return g_failures ? 1 : 0; }