port 4-a: text input through CPythonIME (login typed, in-game chat)

- UserInterface/PythonIME(+Module) ported verbatim; ime module stubs removed, initime() after initdbg()
- EterLib/IME.cpp in platform/: 40250 buffer/WMChar bodies, IMM/TSF processors no-op; committed
  multi-byte text via PythonBoot::UIChar emulating GCS_RESULTSTR
- GameApplicationAdapter RunIME* -> CWindowManager; Godot keys -> ui_ime_key/ui_char
- port.login_flow/login_live type the account through the IME; chat CG_CHAT round-trip on the fake server
- python_game_render_test logs in with Godot key events

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-24 08:35:53 +09:00
co-authored by Claude Opus 5.5
parent 1bda4390d6
commit 6ea0fa9889
21 changed files with 2117 additions and 277 deletions
@@ -655,6 +655,22 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
if (!ok)
return Fail(name + ": send GC_EXCHANGE");
}
// CInputMain::Chat (input_main.cpp): the line comes back to the speaker (PacketAround includes them)
// as a CHAT_TYPE_TALKING GC_CHAT "<name> : <text>" carrying their vid and empire.
if (header == HDR_CG_CHAT)
{
CGChatHead head;
std::memcpy(&head, packet, sizeof(head));
const size_t text_len = size > sizeof(head) ? size - sizeof(head) : 0;
const std::string text(reinterpret_cast<const char*>(packet + sizeof(head)),
strnlen(reinterpret_cast<const char*>(packet + sizeof(head)), text_len));
Event(name + ":chat " + std::to_string(head.type) + " " + text);
const std::string line = std::string(kCharacterName) + " : " + text;
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + line.size() + 1),
0 /* CHAT_TYPE_TALKING */, kMainVID, 1};
if (!c.Send(chat) || !c.Send(line.c_str(), line.size() + 1))
return Fail(name + ": send GC_CHAT");
}
if (header == HDR_CG_MOVE)
{
CGMove move;
+40 -4
View File
@@ -244,9 +244,29 @@ int main(int argc, char** argv)
char ports[128];
std::snprintf(ports, sizeof(ports), "_auth_port, _game_port, _slot = %d, %d, %d", auth_port, game_port, slot);
CHECK(py(ports));
CHECK(py("_stream.SetConnectInfo(_host, _game_port, _host, _auth_port)\n"
"_stream.curPhaseWindow.Connect(_login, _password)\n"
"del __main__._login, __main__._password"));
CHECK(py("_stream.SetConnectInfo(_host, _game_port, _host, _auth_port)"));
// Typed into the login board through the 40250 IME (WM_CHAR -> CPythonIME::WMChar, WM_KEYDOWN ->
// OnIMEKeyDown): Tab moves to the password line, Return runs __OnClickLoginButton -> Connect.
// The IME only reaches a rendering EditLine: open the login board as the server list's OK button does.
CHECK(py("_w = _stream.curPhaseWindow\n"
"_w._LoginWindow__OpenLoginBoard()\n"
"_w.idEditLine.SetText('')\n"
"_w.pwdEditLine.SetText('')\n"
"_w.idEditLine.SetFocus()"));
auto type = [](const std::string& text) {
for (unsigned char c : text)
PythonBoot::UIChar(c);
};
type(login + "xy");
PythonBoot::UIIMEKeyDown(VK_LEFT);
PythonBoot::UIIMEKeyDown(VK_DELETE); // y
PythonBoot::UIChar(VK_BACK); // x
CHECK(py("assert _w.idEditLine.GetText() == _login, repr(_w.idEditLine.GetText())"));
PythonBoot::UIChar(VK_TAB);
type(password);
CHECK(py("assert _w.pwdEditLine.GetText() == _password, len(_w.pwdEditLine.GetText())\n"
"del __main__._login, __main__._password, __main__._w"));
PythonBoot::UIChar(VK_RETURN);
// 3. The character list.
const bool selecting = pump_until(live ? 30 : 20, [&] {
@@ -614,6 +634,22 @@ int main(int argc, char** argv)
// uiexchange.CloseDialog armed wndMgr.OnceIgnoreMouseLeftButtonUpEvent (in the client that release belongs
// to the click that closed the dialog); spend it so step 8's click-release is not swallowed.
PythonBoot::UIMouseButton(1, false, 400, 520);
// Chat (批次 4-a): Return on the focused game.GameWindow → OnIMEReturn → interface.ToggleChat opens the
// chat line; the typed text goes through CPythonIME, a second Return sends CG_CHAT (TALKING) and the fake
// server echoes GC_CHAT back.
CHECK(py_true("not _stream.curPhaseWindow.interface.IsOpenChat()"));
PythonBoot::UIChar(VK_RETURN);
CHECK(pump_until(1, [] { return py_true("_stream.curPhaseWindow.interface.IsOpenChat()"); }));
for (char c : std::string("hello"))
PythonBoot::UIChar((unsigned char)c);
pump_until(0.05, [] { return false; });
PythonBoot::UIChar(VK_RETURN);
CHECK(pump_until(2, [&] { return server->Has("game2:chat 0 hello"); }));
// uichat.__SendTalkingChatPacket only resets the line; a Return on the empty line closes it (eventReturn).
CHECK(py_true("_stream.curPhaseWindow.interface.IsOpenChat() and __import__('ime').GetText(0) == ''"));
PythonBoot::UIChar(VK_RETURN);
CHECK(pump_until(1, [] { return py_true("not _stream.curPhaseWindow.interface.IsOpenChat()"); }));
}
// 8. (批次 2V3) Click-to-move: a left click on the ground below the actor reaches game.GameWindow's
@@ -664,7 +700,7 @@ int main(int argc, char** argv)
for (const auto& e : events)
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos &&
e.find(":pickup ") == std::string::npos &&
e.find(":exchange ") == std::string::npos)
e.find(":exchange ") == std::string::npos && e.find(":chat ") == std::string::npos)
got.push_back(e);
CHECK(want == got);
}