2V3-a: walk with the keyboard through the ported player chain

PythonPlayer, PythonPlayerInput{,Keyboard,Mouse}, PythonPlayerEventHandler,
PythonPlayerSkill, PythonPlayerModule and GameType are verbatim copies; the
`player` stub module and the pending PythonPlayer/GameType stand-ins are gone.
Unported callees (CPythonSkill, CItemData, CItemManager, CPythonItem, ...) are
pending stubs. 64-bit fixes: emotion icon handles use Py_BuildPointer, and
PyDict_Next takes Py_ssize_t.

The actor did not move because GrannyUpdateModelMatrix passed the model matrix
through. Walk/run motions use constant root-motion extraction
(AccumulationFlags=AccumulationExtracted, LoopTranslation e.g. (0,-300,0) for
the warrior run, same as the .msa Accumulation). libgr2 now reads both fields
from TrackGroups[0], and the runtime moves by LoopTranslation over each
control's local-clock span, blended by control weight. VDA extraction is not
implemented.

port.login_flow presses DIK_UP; the actor walks more than 100 cm, and the fake
server parses CG packets and sees CG_MOVE FUNC_MOVE, then FUNC_WAIT at the new
position. port.login_live against the real server walked 132 cm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 23:41:31 +09:00
co-authored by Claude Opus 5.5
parent 157f7c3bb4
commit ff0983d3c8
39 changed files with 10170 additions and 2039 deletions
+42 -5
View File
@@ -509,9 +509,46 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
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)
;
return true;
// Then hold the connection until the test stops us, reading what the game phase sends the way
// CInputProcessor::Process frames it (static size or TDynamicSizePacketHeader, then the sequence
// byte). A CG_MOVE (input_main.cpp CInputMain::Move) becomes "<name>:move <func> <arg> <rot> <x> <y>".
for (;;)
{
std::uint8_t packet[1024] = {};
if (c.Read(packet, 1) != 1)
return true;
const std::uint8_t header = packet[0];
size_t size = static_cast<size_t>(packet_size_cg(header));
if (is_dynamic_cg(header) || header == HDR_CG_SYNC_POSITION)
{
if (c.Read(packet + 1, sizeof(DynHead) - 1) != 1)
return true;
DynHead head;
std::memcpy(&head, packet, sizeof(head));
size = head.size;
if (size < sizeof(DynHead) || size > sizeof(packet) ||
(size > sizeof(DynHead) && c.Read(packet + sizeof(DynHead), size - sizeof(DynHead)) != 1))
return Fail(name + ": dynamic CG packet " + std::to_string(header) + " in the game phase");
}
else if (size == 0 || size > sizeof(packet))
return Fail(name + ": unknown CG header " + std::to_string(header) + " in the game phase");
else if (size > 1 && c.Read(packet + 1, size - 1) != 1)
return true;
if (is_sequence_cg(header))
{
std::uint8_t sequence = 0;
if (c.Read(&sequence, 1) != 1)
return true;
if (sequence != SEQUENCE_TABLE[c.sequence])
return Fail(name + ": SEQUENCE mismatch on CG header " + std::to_string(header));
c.sequence = (c.sequence + 1) % SEQUENCE_TABLE_SIZE;
}
if (header == HDR_CG_MOVE)
{
CGMove move;
std::memcpy(&move, packet, sizeof(move));
Event(name + ":move " + std::to_string(move.func) + " " + std::to_string(move.arg) + " " +
std::to_string(move.rot) + " " + std::to_string(move.x) + " " + std::to_string(move.y));
}
}
}
+37 -1
View File
@@ -377,6 +377,42 @@ int main(int argc, char** argv)
return false;
};
CHECK(pump_until(3, character_draw));
// 7. (批次 2V3) Keyboard movement through the original path: the key reaches game.GameWindow's
// onPressKeyDict, MoveUp calls player.SetSingleDIKKeyState(DIK_UP, True), CPythonPlayer turns
// it into NEW_MoveToDirection on the main instance, and CPythonPlayerEventHandler::OnMove sends
// CG_MOVE FUNC_MOVE. Releasing the key stops the actor (OnStop: FUNC_WAIT at the new position).
TPixelPosition start = {};
if (main_instance)
main_instance->NEW_GetPixelPosition(&start);
auto walked = [&] {
TPixelPosition now = {};
main_instance->NEW_GetPixelPosition(&now);
return std::hypot(now.x - start.x, now.y - start.y);
};
PythonBoot::UIKey(DIK_UP, true);
CHECK(main_instance && pump_until(3, [&] { return walked() > 100.0f; }));
PythonBoot::UIKey(DIK_UP, false);
CHECK(main_instance && pump_until(2, [&] { return !main_instance->IsWalking(); }));
const float distance = main_instance ? walked() : 0.0f;
std::printf("port_login_flow_test: DIK_UP walked %.0f cm\n", distance);
if (server)
{
// The fake server records "game2:move <func> <arg> <rot> <x> <y>"; x, y are the TPixelPosition
// (y negated back, as SendCharacterStatePacket does) plus the map base the server gave.
auto moves = [&] {
std::vector<std::string> out;
for (const auto& e : server->Events())
if (e.rfind("game2:move ", 0) == 0)
out.push_back(e);
return out;
};
CHECK(pump_until(2, [&] { auto m = moves(); return m.size() >= 2 && m.back().rfind("game2:move 0 ", 0) == 0; }));
const auto m = moves();
CHECK(!m.empty() && m.front().rfind("game2:move 1 0 ", 0) == 0);
for (const auto& e : m)
std::printf("port_login_flow_test: %s\n", e.c_str());
}
}
if (server)
{
@@ -394,7 +430,7 @@ int main(int argc, char** argv)
if (e.rfind(prefix, 0) == 0)
want.push_back(e);
for (const auto& e : events)
if (e.rfind(prefix, 0) == 0)
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos)
got.push_back(e);
CHECK(want == got);
}