port: ServerStateChecker + module (batch 2 P2)

Verbatim 40250 CServerStateChecker and the ServerStateChecker Python
module replace the stub; PythonBoot owns the instance like
CPythonApplication::m_kServerStateChecker. The fake server gets a state
port that answers CG_STATE_CHECKER the way CInputHandshake and
CInputDB::RespondChannelStatus do; login_flow requests the channel list
after the character list (live: {1: 1, 2: 1}).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-24 00:27:35 +09:00
co-authored by Claude Opus 5.5
parent be5a68ff35
commit ce9f96398e
13 changed files with 362 additions and 32 deletions
+52 -2
View File
@@ -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<std::uint8_t> out = {210};
const std::int32_t count = 2;
out.insert(out.end(), reinterpret_cast<const std::uint8_t*>(&count), reinterpret_cast<const std::uint8_t*>(&count) + 4);
for (int port : {m_state_port, kOtherChannelPort})
{
const std::int16_t short_port = static_cast<std::int16_t>(port);
out.insert(out.end(), reinterpret_cast<const std::uint8_t*>(&short_port), reinterpret_cast<const std::uint8_t*>(&short_port) + 2);
out.push_back(static_cast<std::uint8_t>(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,
+9 -2
View File
@@ -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<std::string> 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<bool> m_stop{false};
std::mutex m_mutex;
std::vector<std::string> m_events;
+28
View File
@@ -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)
{