2V1-d: offline login -> select -> loading against a fake 40250 server
port.login_flow drives the real 40250 root scripts through the ported CPythonNetworkStream and CAccountConnector against a loopback auth + game server built from net/classic (wire layouts, sequence table, cipher) that answers like the 40250 desc.cpp/input*.cpp: handshake, key agreement, LOGIN3/AUTH_SUCCESS, LOGIN2, the character list, DirectEnter reconnect, CHARACTER_SELECT, the loading packets and CLIENT_VERSION, with every sequence byte checked. - NetStream::ActivateCipher decrypts ciphertext already buffered behind KEY_AGREEMENT_COMPLETED (PORT) - CPythonApplication is final (PORT): no application object exists on the platform yet, so its virtual overrides must be called directly - app.GetTime (GetGlobalTime) and camera/center-position stubs - SendClientVersionPacket verbatim in the PhaseGame stand-in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4d9b800efb
commit
8d9d863c7e
@@ -0,0 +1,187 @@
|
||||
// 批次 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> <python27.zip>
|
||||
//
|
||||
// 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 <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#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());
|
||||
}
|
||||
|
||||
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<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);
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user