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:
co-authored by
Claude Opus 5.5
parent
d16fabf61c
commit
62b6e23bfc
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user