port 2V2-b: GamePhase dispatch consumes the Entergame burst

PythonNetworkStreamPhaseGame/PhaseGameActor/PhaseGameItem/Command,
NetworkActorManager and PythonChat/PythonChatModule are ported verbatim and
dispatched at runtime; the 2V0 chat stub module is removed and initChat()
runs before initnet() as in 40250. InstanceBase, PythonCharacterManager and
GameLib ActorInstance/RaceData/RaceManager compile but stay TODO until
2V2-c/d wire them.

Units not ported yet get their singletons from PythonBoot's GameSingletons
(CPythonApplication member order) and platform/pending stand-ins generated
by platform_stub.py pending; const char* stand-ins return "".

port.login_flow's fake server now sends the CInputLogin::Entergame order
(actor insert, NPC positions, PHASE_GAME, land list, time, channel, greet
notice, ping) and a TraceError observer asserts no unknown packet header.
port.login_live passes against the real server for 10 s of game phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 18:20:57 +09:00
co-authored by Claude Opus 5.5
parent d16fabf61c
commit 62b6e23bfc
76 changed files with 21087 additions and 753 deletions
@@ -440,6 +440,72 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
return Fail(name + ": CG_ENTERGAME: " + error);
Event(name + ":entergame");
// CInputLogin::Entergame order: ch->Show inserts the main actor while the client is still in
// the loading phase (CHARACTER::EncodeInsertPacket = ADD + ADDITIONAL_INFO, then UpdatePacket),
// then SendNPCPosition, PHASE_GAME, SendLandList, TIME, CHANNEL and the greet notice.
GCCharacterAdd actor = zeroed<GCCharacterAdd>();
actor.header = HDR_GC_CHARACTER_ADD;
actor.vid = kMainVID;
actor.x = kX;
actor.y = kY;
actor.type = 0; // CHAR_TYPE_PC
actor.race = 0; // warrior male
actor.moving_speed = 100;
actor.attack_speed = 100;
GCCharAddInfo info = zeroed<GCCharAddInfo>();
info.header = HDR_GC_CHAR_ADDITIONAL_INFO;
info.vid = kMainVID;
std::strncpy(info.name, kCharacterName, sizeof(info.name) - 1);
info.empire = 1;
info.level = 10;
GCCharacterUpdate update = zeroed<GCCharacterUpdate>();
update.header = HDR_GC_CHARACTER_UPDATE;
update.vid = kMainVID;
update.moving_speed = 100;
update.attack_speed = 100;
if (!c.Send(actor) || !c.Send(info) || !c.Send(update))
return Fail(name + ": send main actor");
// packet_npc_position = DynHead + count, then TNPCPosition entries.
NPCPosition34 npc = zeroed<NPCPosition34>();
npc.type = 1; // CHAR_TYPE_NPC
std::strncpy(npc.name, "Npc", sizeof(npc.name) - 1);
npc.x = kX + 500;
npc.y = kY + 500;
DynHead npc_head = {HDR_GC_NPC_POSITION, static_cast<std::uint16_t>(sizeof(DynHead) + sizeof(GCNPCPositionHead) + sizeof(npc))};
GCNPCPositionHead npc_count = {1};
if (!c.Send(npc_head) || !c.Send(npc_count) || !c.Send(npc))
return Fail(name + ": send NPC positions");
if (!c.Phase(PHASE_GAME))
return Fail(name + ": send PHASE_GAME");
LandPacketElement24 land = zeroed<LandPacketElement24>();
land.id = 1;
land.x = kX + 1000;
land.y = kY + 1000;
land.width = 2000;
land.height = 2000;
DynHead land_head = {HDR_GC_LAND_LIST, static_cast<std::uint16_t>(sizeof(DynHead) + sizeof(land))};
GCTime game_time = {HDR_GC_TIME, static_cast<std::int32_t>(now_ms())};
GCChannel channel = {HDR_GC_CHANNEL, 1};
if (!c.Send(land_head) || !c.Send(land) || !c.Send(game_time) || !c.Send(channel))
return Fail(name + ": send land/time/channel");
// SendGreetMessage: a CHAT_TYPE_NOTICE line with no speaker.
const char greet[] = "Welcome";
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + sizeof(greet)), 2 /* CHAT_TYPE_NOTICE */, 0, 1};
if (!c.Send(chat) || !c.Send(greet, sizeof(greet)))
return Fail(name + ": send greet notice");
Event(name + ":burst_sent");
// A ping after the burst proves that GamePhase consumed every packet above and can reply.
Blank ping = {HDR_GC_PING};
Blank pong = {};
if (!c.Send(ping) || read_packet(c, HDR_CG_PONG, &pong, sizeof(pong), &error) != 1)
return Fail(name + ": CG_PONG after the entergame burst: " + error);
Event(name + ":burst_pong");
// Hold the connection until the test stops us.
std::uint8_t byte = 0;
while (!m_stop && c.Read(&byte, 1) == 1)
+44 -2
View File
@@ -15,7 +15,9 @@
// 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.
// game.GameWindow, whose Open sends CG_ENTERGAME;
// 7. the server's Entergame answer (the actor's insert packets, PHASE_GAME, the Entergame burst) is
// read by the ported GamePhase without an unknown packet, and a final ping gets its pong.
//
// The server checks every sequence byte against the 40250 table, so a skipped or extra sequenced
// packet fails the run.
@@ -25,7 +27,8 @@
// 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
// CG_CLIENT_VERSION, game.GameWindow is open, which sent CG_ENTERGAME, and 10 s of the real game
// phase later it is still open and connected with no unknown packet. 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
@@ -36,6 +39,7 @@
#include "../src/platform/ScriptLib/PythonBoot.h"
#include "../src/platform/ScriptLib/PythonHost.h"
#include "../src/platform/UserInterface/UserInterface.h"
#include "../src/platform/EterBase/TraceErrorObserver.h"
#include "port_login_flow_server.h"
#include <memory>
@@ -44,6 +48,7 @@
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
@@ -59,6 +64,25 @@ static int g_failures = 0;
} \
} while (0)
// TraceError lines; the script thread writes them while the host thread waits for its baton.
static std::mutex g_syserr_mutex;
static std::vector<std::string> g_syserr;
static void on_trace_error(const char* line)
{
std::lock_guard<std::mutex> lock(g_syserr_mutex);
g_syserr.emplace_back(line);
}
static int count_syserr(const char* needle)
{
std::lock_guard<std::mutex> lock(g_syserr_mutex);
int n = 0;
for (const auto& line : g_syserr)
n += line.find(needle) != std::string::npos;
return n;
}
static bool py(const std::string& source)
{
std::string error;
@@ -162,6 +186,7 @@ int main(int argc, char** argv)
}
auto failed = [&] { return server && server->Failed(); };
SetTraceErrorObserver(on_trace_error);
PackSingletons();
CHECK(PackInitialize("pack"));
@@ -239,12 +264,29 @@ int main(int argc, char** argv)
return failed() || (py_true(gaming) && (!server || server->Has("game2:entergame")));
});
CHECK(entered && !failed());
// 7. (批次 2V2-b) The server answers CG_ENTERGAME with the character's own insert packets and
// PHASE_GAME, then the Entergame burst (PVP/land list, time, channel, greet chat, ...). GamePhase
// has to read all of it: an unknown or mis-sized packet makes CheckPacket log "Unknown packet
// header" and drop the receive buffer. Offline the fake server ends with a ping the client
// must answer; live the stream has to stay in the game window and connected for 10 s.
const bool burst = pump_until(live ? 10 : 30, [&] {
return failed() || (server && server->Has("game2:burst_pong"));
});
CHECK(live || (burst && !failed()));
CHECK(py_true(gaming));
CHECK(py("import net\nassert net.IsConnect(), 'disconnected in the game phase'"));
CHECK(count_syserr("Unknown packet header") == 0);
// The fake greet notice went through RecvChatPacket into the ported CPythonChat, where
// uichat's chat set (created by the game interface) counts it.
CHECK(live || py("import chat\nassert any(chat.GetLineCount(i) > 0 for i in range(4)), 'greet notice not in chat'"));
}
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",
"game2:burst_sent", "game2:burst_pong",
};
auto events = server->Events();
// auth:closed races game1's handshake; compare the order within each connection.