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
+33 -3
View File
@@ -29,6 +29,25 @@ func _check(ok: bool, message: String) -> void:
func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == ""
func _key(keycode: Key, unicode: int, pressed: bool) -> void:
var event := InputEventKey.new()
event.keycode = keycode
event.physical_keycode = keycode
event.unicode = unicode
event.pressed = pressed
root.push_input(event)
func _press(keycode: Key) -> void:
_key(keycode, 0, true)
_key(keycode, 0, false)
func _type(text: String) -> void:
for i in text.length():
var ch := text.unicode_at(i)
var keycode := OS.find_keycode_from_string(text[i].to_upper())
_key(keycode, ch, true)
_key(keycode, ch, false)
func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline:
@@ -83,13 +102,24 @@ func run() -> void:
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"))
_check(at_login, "login window")
# LoginWindow.Connect with the account from the environment.
# The account from the environment is typed into the login board as key events (WM_KEYDOWN/WM_CHAR
# through PythonUISurface → CPythonIME); Return runs __OnClickLoginButton → LoginWindow.Connect.
_check(_py("_env = os.environ\n"
+ "_slot = int(_env.get('MT_LIVE_SLOT', '0'))\n"
+ "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), "
+ "_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_AUTH_PORT', '11000')))\n"
+ "_stream.curPhaseWindow.Connect(_env['MT_LIVE_LOGIN'], _env['MT_LIVE_PASSWORD'])\n"
+ "del _env"), "LoginWindow.Connect")
+ "_w = _stream.curPhaseWindow\n"
+ "_w._LoginWindow__OpenLoginBoard()\n"
+ "_w.idEditLine.SetText('')\n"
+ "_w.pwdEditLine.SetText('')\n"
+ "_w.idEditLine.SetFocus()"), "login board open")
_type(OS.get_environment("MT_LIVE_LOGIN"))
_press(KEY_TAB)
_type(OS.get_environment("MT_LIVE_PASSWORD"))
_check(_py("assert _w.idEditLine.GetText() == _env['MT_LIVE_LOGIN']\n"
+ "assert _w.pwdEditLine.GetText() == _env['MT_LIVE_PASSWORD']"), "account typed through the IME")
_check(_py("del _env, _w"), "login locals dropped")
_press(KEY_ENTER)
var selecting := await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"))
_check(selecting, "character select")
+30
View File
@@ -20,6 +20,19 @@ const KEY_TO_DIK := {
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
}
# Win32 virtual-key codes of the keys EditLine.OnIMEKeyDown handles (WM_KEYDOWN → OnIMEKeyDown);
# letters and digits use their ASCII code, as VK_A..VK_Z / VK_0..VK_9 do.
const KEY_TO_VK := {
KEY_BACKSPACE: 0x08, KEY_TAB: 0x09, KEY_ENTER: 0x0d, KEY_KP_ENTER: 0x0d,
KEY_ESCAPE: 0x1b, KEY_SPACE: 0x20, KEY_END: 0x23, KEY_HOME: 0x24,
KEY_LEFT: 0x25, KEY_UP: 0x26, KEY_RIGHT: 0x27, KEY_DOWN: 0x28, KEY_DELETE: 0x2e,
}
# The control characters Windows turns these keys into (WM_CHAR); Godot reports them with unicode 0.
const KEY_TO_CHAR := {
KEY_BACKSPACE: 0x08, KEY_TAB: 0x09, KEY_ENTER: 0x0d, KEY_KP_ENTER: 0x0d, KEY_ESCAPE: 0x1b,
}
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
# "@<revision>" of a command's name moves on (a glyph was added to the page).
var _memory_textures := {}
@@ -98,6 +111,23 @@ func _unhandled_key_input(event: InputEvent) -> void:
var dik: int = KEY_TO_DIK.get(event.physical_keycode, 0)
if dik != 0:
Metin2PythonHost.ui_key(dik, event.pressed)
if event.pressed:
_ime_key(event)
# 40250 gets every key press twice more through the window procedure: WM_KEYDOWN (a VK code, for
# OnIMEKeyDown) and then WM_CHAR (the typed character, for CPythonIME). Held keys repeat both.
func _ime_key(event: InputEventKey) -> void:
var keycode: int = event.keycode
var vk: int = KEY_TO_VK.get(keycode, 0)
if vk == 0 and ((keycode >= KEY_A and keycode <= KEY_Z) or (keycode >= KEY_0 and keycode <= KEY_9)):
vk = keycode
if vk != 0:
Metin2PythonHost.ui_ime_key(vk)
var ch: int = event.unicode
if ch == 0:
ch = KEY_TO_CHAR.get(keycode, 0)
if ch != 0:
Metin2PythonHost.ui_char(ch)
# The command stream is drawn into pooled child canvas items in order: a run of plain commands
# shares one item; a masked image (CPythonMiniMap's terrain tiles under minimap_image_filter.dds)