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>
279 lines
11 KiB
C++
279 lines
11 KiB
C++
// 批次 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
|
|
// 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;
|
|
// 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
|
|
// packet fails the run.
|
|
//
|
|
// 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, the loading window got
|
|
// LoadData from GC_MAIN_CHARACTER, which CPythonNetworkStream::RecvMainCharacter answers with
|
|
// 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:
|
|
//
|
|
// 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.
|
|
#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 <memory>
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <unistd.h>
|
|
|
|
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<bool()>& done)
|
|
{
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration<double>(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());
|
|
}
|
|
|
|
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)
|
|
{
|
|
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();
|
|
const bool live = argc > 3 && std::string(argv[3]) == "live";
|
|
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;
|
|
}
|
|
|
|
std::string host = "127.0.0.1", login = FakeLoginServer::kLogin, password = FakeLoginServer::kPassword;
|
|
int auth_port = 0, game_port = 0, slot = 0;
|
|
std::unique_ptr<FakeLoginServer> server;
|
|
if (live)
|
|
{
|
|
host = env("MT_LIVE_HOST");
|
|
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();
|
|
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 (LoginWindow.Connect), pointed at the fake or the live server.
|
|
py_set("_host", host);
|
|
py_set("_login", login);
|
|
py_set("_password", password);
|
|
char ports[128];
|
|
std::snprintf(ports, sizeof(ports), "_auth_port, _game_port, _slot = %d, %d, %d", auth_port, game_port, slot);
|
|
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.
|
|
const bool selecting = pump_until(live ? 30 : 20, [&] {
|
|
return failed() || ((!server || server->Has("game1:login2")) &&
|
|
py_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"));
|
|
});
|
|
CHECK(selecting && !failed());
|
|
if (!selecting)
|
|
{
|
|
// The login window reports failures in its popup (LOGIN_FAILURE, connect errors).
|
|
py("import sys\n"
|
|
"_w = _stream.curPhaseWindow\n"
|
|
"_msg = _stream.popupWindow and _stream.popupWindow.IsShow() and _stream.popupWindow.GetChild('message').GetText()\n"
|
|
"sys.stderr.write('phase window %s, popup %r\\n' % (_w.__class__.__name__, _msg))");
|
|
}
|
|
if (server)
|
|
{
|
|
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());
|
|
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)
|
|
{
|
|
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", "game2:entergame",
|
|
};
|
|
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);
|
|
}
|
|
|
|
CHECK(py("import app\napp.Exit()"));
|
|
pump_until(2, [] { return !PythonBoot::IsAppLooping(); });
|
|
CHECK(!PythonBoot::IsAppLooping());
|
|
PythonBoot::Stop();
|
|
if (server)
|
|
server->Stop();
|
|
|
|
if (g_failures)
|
|
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
|
else
|
|
std::printf("port_login_flow_test: ok%s\n", live ? " (live)" : "");
|
|
return g_failures ? 1 : 0;
|
|
}
|