From 6ea0fa9889901fe32753fd7d1f6c7c4e93d6f98e Mon Sep 17 00:00:00 2001 From: shenlei Date: Thu, 24 Sep 2026 08:35:53 +0900 Subject: [PATCH] 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 --- audit/history.jsonl | 1 + audit/port-map/EterLib/IME.cpp.json | 712 ++++++++++++++++++ .../port-map/UserInterface/PythonIME.cpp.json | 122 +++ .../UserInterface/PythonIMEModule.cpp.json | 212 ++++++ docs/PORT-PLAN.md | 2 + extension/src/platform/EterLib/IME.cpp | 643 +++++++++++----- .../ScriptLib/GameplayModuleConstants.cpp | 5 - .../platform/ScriptLib/GameplayModules.cpp | 76 -- .../src/platform/ScriptLib/PythonBoot.cpp | 60 +- extension/src/platform/ScriptLib/PythonBoot.h | 3 + .../UserInterface/PythonApplication.cpp | 8 +- .../src/port/UserInterface/PythonIME.cpp | 119 +++ .../port/UserInterface/PythonIMEModule.cpp | 289 +++++++ extension/src/port/UserInterface/StdAfx.h | 1 + .../src/port/common/shim/win32/windows.h | 6 + extension/src/python_host_node.cpp | 6 + extension/src/python_host_node.h | 3 + extension/tests/port_login_flow_server.cpp | 16 + extension/tests/port_login_flow_test.cpp | 44 +- project/python_game_render_test.gd | 36 +- project/python_ui_surface.gd | 30 + 21 files changed, 2117 insertions(+), 277 deletions(-) create mode 100644 audit/port-map/EterLib/IME.cpp.json create mode 100644 audit/port-map/UserInterface/PythonIME.cpp.json create mode 100644 audit/port-map/UserInterface/PythonIMEModule.cpp.json create mode 100644 extension/src/port/UserInterface/PythonIME.cpp create mode 100644 extension/src/port/UserInterface/PythonIMEModule.cpp diff --git a/audit/history.jsonl b/audit/history.jsonl index d10c439a..e4f235dd 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -540,3 +540,4 @@ {"time": "2026-09-24T16:00:00Z", "event": "port_round", "unit": "batch 3-c: CMapOutdoor + PythonBackground + minimap", "ported": ["GameLib/{Area,AreaTerrain,DungeonBlock,MapBase,MapManager,MapOutdoor*,MapType,MonsterAreaInfo,Property*,Snow*,Terrain*}.cpp", "PRTerrainLib/{Terrain,TextureSet}.cpp", "UserInterface/PythonBackground(+Module).cpp", "EterLib/ColorTransitionHelper.cpp"], "adapted": ["CMapManager::BeginEnvironment: uintptr_t tag cast", "CTerrainImpl::LoadWaterMap: 32-bit long file layout", "EterBase/FileDir, TempFile (platform)", "MapOutdoorCharacterShadow texture create/release (platform file)"], "divergent": ["HTP/STP terrain render + RenderWater no-op (Godot draws terrain)", "BeginRenderCharacterShadowToTexture returns false"], "n_a": ["GameLib/AreaLoaderThread.cpp (dead in 40250)"], "needs_live": [], "tests": ["port.login_flow: IsMapOutdoor, miniMap atlas loaded, GetCurrentMapName", "python_game_render_test: minimap tiles through filter mask (9)", "port.login_live 192.168.21.203 passes", "ctest 19/19", "port_map check 0 errors"], "todo": ["weapon attach draws on the ground (since P3-b)", "default route switch"]} {"time": "2026-09-24T17:00:00Z", "event": "route_switch", "unit": "client_main default route -> port", "detail": "MT_PORT_PATH unset/port runs python_ui_surface + python_3d_surface (40250 system.py); MT_PORT_PATH=legacy runs AppFlow; smoke/playable test modes stay legacy; unset falls back to legacy when Metin2PythonHost or the 40250 pack is missing", "tests": ["client_main.tscn instantiated per route: default/port -> PythonUISurface+Python3DSurface with UI commands, legacy -> AppFlow+NetTrace"]} {"time": "2026-09-24T18:00:00Z", "event": "correction", "unit": "batch 3-c todo", "detail": "The sword next to the actor in python_game_render_test is the fake server ground item (GC_ITEM_GROUND_ADD vnum 19, P3-b), not a weapon-attach bug; the fake character has no weapon, AttachWeapon is never called. Todo withdrawn."} +{"time": "2026-09-24T19:00:00Z", "event": "port", "unit": "batch 4-a text input: EterLib/IME.cpp (ADAPTED), UserInterface/PythonIME.cpp, PythonIMEModule.cpp", "detail": "ime module ported; IMM/TSF-free CIME in platform/; Godot key events -> ui_ime_key/ui_char -> CWindowManager RunIME*; login typed through the IME; in-game chat CG_CHAT round-trip", "tests": ["port.login_flow (typed id/password with edits, chat send/echo/close)", "port.login_live", "python_game_render_test (Godot key events)"]} diff --git a/audit/port-map/EterLib/IME.cpp.json b/audit/port-map/EterLib/IME.cpp.json new file mode 100644 index 00000000..8e4ab04b --- /dev/null +++ b/audit/port-map/EterLib/IME.cpp.json @@ -0,0 +1,712 @@ +{ + "reference": "EterLib/IME.cpp", + "reference_sha256": "19a16dd5ac776591a7e2def2564102ea86d7e4a98f67c4bcd585cb8a7376f611", + "priority": "P4", + "contracts": [], + "functions": { + "CIME::CIME": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CIME" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::~CIME": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::~CIME" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::Initialize": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::Initialize" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::Uninitialize": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::Uninitialize" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::UseDefaultIME": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::UseDefaultIME" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::IsIMEEnabled": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::IsIMEEnabled" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::EnableIME": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::EnableIME" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::DisableIME": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::DisableIME" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::EnableCaptureInput": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::EnableCaptureInput" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::DisableCaptureInput": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::DisableCaptureInput" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::IsCaptureEnabled": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::IsCaptureEnabled" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::Clear": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::Clear" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetReading": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetReading" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetReadingError": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetReadingError" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetMax": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetMax" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetUserMax": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetUserMax" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetText": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetText" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetText": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetText" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCodePageText": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCodePageText" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCodePage": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCodePage" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCandidatePageCount": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCandidatePageCount" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCandidateCount": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCandidateCount" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCandidate": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCandidate" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCandidateSelection": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCandidateSelection" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetInputMode": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetInputMode" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetInputMode": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetInputMode" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetNumberMode": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetNumberMode" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetStringMode": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetStringMode" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::AddExceptKey": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::AddExceptKey" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::ClearExceptKey": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::ClearExceptKey" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::__IsWritable": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::__IsWritable" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::EnablePaste": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::EnablePaste" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::PasteTextFromClipBoard": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::PasteTextFromClipBoard" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::FinalizeString": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::FinalizeString" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCompLen": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCompLen" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetULBegin": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetULBegin" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetULEnd": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetULEnd" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CloseCandidateList": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CloseCandidateList" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CloseReadingInformation": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CloseReadingInformation" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::ChangeInputLanguage": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::ChangeInputLanguage" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::ChangeInputLanguageWorker": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::ChangeInputLanguageWorker" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetSupportLevel": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetSupportLevel" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::IncCurPos": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::IncCurPos" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::DecCurPos": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::DecCurPos" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCurPos": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCurPos" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetCurPos": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetCurPos" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::DelCurPos": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::DelCurPos" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::PasteString": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::PasteString" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::InsertString": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::InsertString" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::OnChar": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::OnChar" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetCodePageFromLang": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetCodePageFromLang" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CompositionProcess": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CompositionProcess" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CompositionProcessBuilding": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CompositionProcessBuilding" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::ResultProcess": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::ResultProcess" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::AttributeProcess": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::AttributeProcess" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CandidateProcess": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CandidateProcess" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::ReadingProcess": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::ReadingProcess" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::IsMax": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::IsMax" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetImeId": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetImeId" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::GetReadingWindowOrientation": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::GetReadingWindowOrientation" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::SetupImeApi": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::SetupImeApi" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "_strtoul": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:_strtoul" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CheckInputLocale": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CheckInputLocale" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::CheckToggleState": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::CheckToggleState" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::SetupSinks": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::SetupSinks" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::ReleaseSinks": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::ReleaseSinks" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::CUIElementSink": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::CUIElementSink" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::~CUIElementSink": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::~CUIElementSink" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::QueryInterface": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::QueryInterface" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::BeginUIElement": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::BeginUIElement" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::UpdateUIElement": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::UpdateUIElement" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::EndUIElement": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::EndUIElement" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::UpdateImeState": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::UpdateImeState" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::OnActivated": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::OnActivated" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CUIElementSink::OnChange": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CUIElementSink::OnChange" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::MakeReadingInformationString": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::MakeReadingInformationString" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::MakeCandidateStrings": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::MakeCandidateStrings" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::GetUIElement": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::GetUIElement" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::CurrentInputLocaleIsIme": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::CurrentInputLocaleIsIme" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::EnableUiUpdates": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::EnableUiUpdates" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::GetCompartments": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::GetCompartments" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CTsfUiLessMode::SetupCompartmentSinks": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CTsfUiLessMode::SetupCompartmentSinks" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::WMInputLanguage": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::WMInputLanguage" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::WMStartComposition": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::WMStartComposition" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::WMComposition": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::WMComposition" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::WMEndComposition": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::WMEndComposition" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::WMNotify": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::WMNotify" + ], + "note": "no IMM/TSF under Godot: composition arrives as committed text (PythonBoot::UIChar → InsertString + OnUpdate); body is a no-op/constant", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + }, + "CIME::WMChar": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/IME.cpp:CIME::WMChar" + ], + "note": "40250 body verbatim in platform/ (the file also holds the IMM/TSF-free no-ops); DelCurPos uses memmove for the overlapping wcscpy", + "test": "extension/tests/port_login_flow_test.cpp (login id/password typed via UIChar/UIIMEKeyDown incl. LEFT/DELETE/BACK/TAB/RETURN; in-game chat CG_CHAT) + project/python_game_render_test.gd (Godot key events)" + } + } +} diff --git a/audit/port-map/UserInterface/PythonIME.cpp.json b/audit/port-map/UserInterface/PythonIME.cpp.json new file mode 100644 index 00000000..5a041d99 --- /dev/null +++ b/audit/port-map/UserInterface/PythonIME.cpp.json @@ -0,0 +1,122 @@ +{ + "reference": "UserInterface/PythonIME.cpp", + "reference_sha256": "2fb5fa35249b8a2a9bb1c82d177d36b3ec083a2cafd4957d7cbea85d6be34009", + "priority": "P4", + "contracts": [], + "functions": { + "CPythonIME::CPythonIME": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::CPythonIME" + ] + }, + "CPythonIME::~CPythonIME": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::~CPythonIME" + ] + }, + "CPythonIME::Create": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::Create" + ] + }, + "CPythonIME::MoveLeft": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::MoveLeft" + ] + }, + "CPythonIME::MoveRight": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::MoveRight" + ] + }, + "CPythonIME::MoveHome": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::MoveHome" + ] + }, + "CPythonIME::MoveEnd": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::MoveEnd" + ] + }, + "CPythonIME::SetCursorPosition": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::SetCursorPosition" + ] + }, + "CPythonIME::Delete": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::Delete" + ] + }, + "CPythonIME::OnUpdate": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnUpdate" + ] + }, + "CPythonIME::OnTab": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnTab" + ] + }, + "CPythonIME::OnReturn": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnReturn" + ] + }, + "CPythonIME::OnEscape": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnEscape" + ] + }, + "CPythonIME::OnWM_CHAR": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnWM_CHAR" + ] + }, + "CPythonIME::OnChangeCodePage": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnChangeCodePage" + ] + }, + "CPythonIME::OnOpenCandidateList": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnOpenCandidateList" + ] + }, + "CPythonIME::OnCloseCandidateList": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnCloseCandidateList" + ] + }, + "CPythonIME::OnOpenReadingWnd": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnOpenReadingWnd" + ] + }, + "CPythonIME::OnCloseReadingWnd": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIME.cpp:CPythonIME::OnCloseReadingWnd" + ] + } + } +} diff --git a/audit/port-map/UserInterface/PythonIMEModule.cpp.json b/audit/port-map/UserInterface/PythonIMEModule.cpp.json new file mode 100644 index 00000000..5583afdb --- /dev/null +++ b/audit/port-map/UserInterface/PythonIMEModule.cpp.json @@ -0,0 +1,212 @@ +{ + "reference": "UserInterface/PythonIMEModule.cpp", + "reference_sha256": "9d2abae41a270b10f05aceb7e04709f18fe44248ea52a9c2f391c0848d10cc31", + "priority": "P4", + "contracts": [], + "functions": { + "imeEnable": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeEnable" + ] + }, + "imeDisable": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeDisable" + ] + }, + "imeEnableCaptureInput": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeEnableCaptureInput" + ] + }, + "imeDisableCaptureInput": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeDisableCaptureInput" + ] + }, + "imeSetMax": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetMax" + ] + }, + "imeSetUserMax": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetUserMax" + ] + }, + "imeSetText": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetText" + ] + }, + "imeGetText": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetText" + ] + }, + "imeGetCodePage": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetCodePage" + ] + }, + "imeGetCandidateCount": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetCandidateCount" + ] + }, + "imeGetCandidate": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetCandidate" + ] + }, + "imeGetCandidateSelection": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetCandidateSelection" + ] + }, + "imeGetReading": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetReading" + ] + }, + "imeGetReadingError": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetReadingError" + ] + }, + "imeEnableIME": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeEnableIME" + ] + }, + "imeDisableIME": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeDisableIME" + ] + }, + "imeSetInputMode": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetInputMode" + ] + }, + "imeSetNumberMode": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetNumberMode" + ] + }, + "imeAddExceptKey": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeAddExceptKey" + ] + }, + "imeClearExceptKey": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeClearExceptKey" + ] + }, + "imeSetStringMode": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetStringMode" + ] + }, + "imeGetInputMode": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeGetInputMode" + ] + }, + "imeMoveLeft": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeMoveLeft" + ] + }, + "imeMoveRight": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeMoveRight" + ] + }, + "imeMoveHome": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeMoveHome" + ] + }, + "imeMoveEnd": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeMoveEnd" + ] + }, + "imeSetCursorPosition": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeSetCursorPosition" + ] + }, + "imeDelete": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeDelete" + ] + }, + "imePasteTextFromClipBoard": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imePasteTextFromClipBoard" + ] + }, + "imeEnablePaste": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imeEnablePaste" + ] + }, + "imePasteString": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imePasteString" + ] + }, + "imePasteBackspace": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imePasteBackspace" + ] + }, + "imePasteReturn": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:imePasteReturn" + ] + }, + "initime": { + "status": "PORTED", + "impl": [ + "extension/src/port/UserInterface/PythonIMEModule.cpp:initime" + ] + } + } +} diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index e1cc9f2c..86d47eb0 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -450,6 +450,8 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 批次 3-b EffectLib(2026-09-24):`EffectLib/` 18 个 .cpp 原样移植(EffectManager、EffectData/Instance、EffectMesh、粒子系统、SimpleLight 等),删除 `platform/EffectLib` 生成的桩骨架;唯一改动是 `Type.h` 里 `BILLBOARD_TYPE_2FACE` 行尾注释的 `\` 被 clang 当作续行吞掉下一行,去掉反斜杠。`platform/EterBase/Utils.cpp` 剩下的 21 个桩按 §3 照抄 40250 函数体(`GetOnlyPathName`/`IsGlobalFileName` 缺失会让 .msf 以相对名挂的 .mse 丢目录,106 个 RegisterEffect 报错),Win32 调用换成 POSIX 的记 ADAPTED;平台 `CPythonResource` 补上 40250 的 `mde`(CEffectMesh)和 `mdatr`(CAttributeData)资源类型。`port.utils` 覆盖路径/分词/命令行辅助函数;`port.login_flow`:在主角脚下 `CreateEffect(level_up.mse)`,RenderGame 的 `CEffectManager::Render` 录出 levelup_1 贴图的 alpha 混合粒子绘制(dust、dropitem 同样出现)。40250 声明但未定义 `GetRenderingEffectCount`,测试不用。 批次 3-c CMapOutdoor(2026-09-24):`GameLib/` 的 Area、AreaTerrain、DungeonBlock、MapBase、MapManager、MapOutdoor(含 IndexBuffer/Load/Quadtree/Render/Update)、MapType、MonsterAreaInfo、Property/PropertyLoader/PropertyManager、SnowEnvironment/SnowParticle、TerrainDecal/Patch/Quadtree,`PRTerrainLib/` Terrain、TextureSet,`UserInterface/PythonBackground(+Module)`,`EterLib/ColorTransitionHelper` 原样移植,删除 pending 替身与 background 模块桩(含 GameplayModuleConstants 里的常量桩);`EterBase/FileDir`、`TempFile` 在 platform 下照抄 40250 函数体(Win32 查找/临时文件走 common 垫片,记 ADAPTED)。`TEMP_CAreaLoaderThread` 在 40250 已全部注释掉,.cpp 不编(N_A)。手改两处:`CMapManager::BeginEnvironment` 的 `ApplyLight` 标签指针经 `uintptr_t` 转 DWORD;`CTerrainImpl::LoadWaterMap` 按 32 位读 water.wtr 的 `long` 再逐个扩宽(LP64 下 `long` 是 8 字节,否则报 FILE DATA SIZE 16392 != 16400)。平台文件:HTP/STP 地形绘制与 `RenderWater` 为空(地形仍由 Godot 适配层画,§3),STP 的 Initialize/Create/Destroy、水面贴图载入、角色阴影贴图创建照抄;`BeginRenderCharacterShadowToTexture` 返回 false(记 DIVERGENT)。录制设备补齐 `CreateTexture/VertexBuffer/IndexBuffer/DepthStencilSurface`、`Get/SetRenderTarget`、`LightEnable`、`Clear`(CPU 表面/贴图),离屏渲染目标的绘制丢弃,light0 只在启用且为方向光时记录。小地图:`CPythonMiniMap` 在正交投影下画 minimap.dds 瓦片并以 stage 1 相机空间坐标 × `D3DTS_TEXTURE1` 采样 `minimap_image_filter.dds` 做圆形遮罩——录制设备把「正交投影 + 未变换顶点」的绘制按四边形转成 UI 命令(带 mask 贴图与四角 mask uv),`UIRenderAddImage` 按 D3DTS_WORLD 平移、按 TFACTOR 调色(标记点),`python_ui_surface.gd` 用按序的子 canvas item 画 UI 流,带遮罩的图各用一个 shader 材质。`port.login_flow`:Warp 后 `IsMapOutdoor()`、`miniMap.GetAtlasSize()` 已载入、`background.GetCurrentMapName()` 非空;`python_game_render_test` 新增「minimap tiles drawn through the filter mask」(9 块)。截图里主角旁地上的剑是假服 entergame 时按 `GC_ITEM_GROUND_ADD` 放的地面物品(vnum 19,P3-b 拾取测试用),不是武器挂点问题;假服角色没装备武器。 默认路由切换(2026-09-24):`client_main` 不设 `MT_PORT_PATH` 时走 port 路由(`python_ui_surface.gd` + `python_3d_surface.gd` 跑 40250 system.py),`MT_PORT_PATH=legacy` 回到 GDScript `AppFlow`,规则见 §2「迁移方式」。已验证:实例化 `client_main.tscn`,默认/`port` 起 PythonUISurface + Python3DSurface 并产出 UI 命令,`legacy` 起 AppFlow + NetTrace。下一步:port 路由稳定后删除 legacy 宿主与开关。 + 批次 4(2026-09-24 起):补齐 port 路由的玩法,以便删除 legacy。顺序:4-a 文字输入 → 4-b 战斗 → 4-c 商店 → 4-d 任务/NPC 对话 → 4-e 仓库 → 4-f 好友/公会/公会标记 → 4-g systemSetting → 4-h 声音。 + 批次 4-a 文字输入(2026-09-24):`UserInterface/PythonIME.cpp` 与 `PythonIMEModule.cpp` 原样移植(仅把 MSVC 专有的 `unsigned char(x)` 函数式转换改成 C 式转换),删除 ime 模块桩,`initime()` 按 40250 顺序在 `initdbg()` 之后,`CPythonApplication::Create` 按原序 `CPythonIME::Create` + `SetText("", 0)`。`EterLib/IME.cpp` 在 platform 下:缓冲区、光标、`WMChar`(退格、`|` 转义、按输入代码页 `MultiByteToWideChar`)、候选/读音状态、数字/字符串模式等函数体照抄;IMM/TSF 在 Godot 下没有对应物,组字相关处理器为空、`EnableIME` 恒关,已提交的非单字节文本由 `PythonBoot::UIChar` 模拟 `GCS_RESULTSTR`(`InsertString` + `OnUpdate`),`DelCurPos` 用 memmove 替代重叠的 wcscpy(全部记 ADAPTED)。`GameApplicationAdapter` 的 `RunIME*` 转发到 `CWindowManager`;注意 `RunIMEUpdate/RunIMEKeyDown` 只送达正在渲染的活动窗口(登录框未打开时输入被丢弃)。Godot 侧 `python_ui_surface.gd` 把按键送 `ui_ime_key`(VK)再送 `ui_char`(unicode)。`port.login_flow` 与 `port.login_live` 打开登录框后逐字键入账号(含 LEFT/DELETE/BACK 编辑)、TAB 切到密码、RETURN 登录(凭据只来自环境变量,测试随后删除 Python 局部变量);进游戏后 RETURN 打开聊天、键入 hello、RETURN 发出 `CG_CHAT`(假服回显 `GC_CHAT`),空行 RETURN 关闭聊天(40250 发完后只清空、不关闭)。`python_game_render_test` 用 Godot 按键事件走同一路径登录。 每个切片都必须同时跑 legacy/port 双路回归;默认路由已切到 `port`(2026-09-24);port 路由稳定后删除总开关和剩余 legacy 宿主。 diff --git a/extension/src/platform/EterLib/IME.cpp b/extension/src/platform/EterLib/IME.cpp index ead1408e..9dfbb3c8 100644 --- a/extension/src/platform/EterLib/IME.cpp +++ b/extension/src/platform/EterLib/IME.cpp @@ -1,381 +1,666 @@ -// Platform skeleton for EterLib/IME.h (40250 EterLib/IME.cpp), generated by platform_stub.py. -// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation. +// Platform implementation of EterLib/IME.h (40250 EterLib/IME.cpp). +// The edit buffer (m_wText, cursor, SetText/GetText, OnChar/InsertString, IsMax, the colour-tag aware cursor +// moves) and WMChar are the 40250 bodies verbatim. What 40250 gets from Win32 IMM/TSF — composition strings, +// the candidate list, the reading window, keyboard-layout code pages — has no counterpart under Godot: the +// host delivers committed characters only (Metin2PythonHost.ui_char -> WMChar, or InsertString for text the +// input code page cannot hold, which is what 40250's ResultProcess does with a GCS_RESULTSTR). Those entry +// points keep 40250's "no IME present" behaviour: no composition, empty candidate/reading lists. #include "EterLib/StdAfx.h" #include "EterLib/IME.h" +#include "EterLib/TextTag.h" +#include "EterLib/GrpTextInstance.h" +#include "EterLib/Util.h" -#include "../PlatformStub.h" +#include +#include +namespace +{ +// 40250 s_aszIndicator[INDICATOR_NON_IME] (IME.cpp:67); CheckInputLocale overwrites it with the locale's +// two-letter abbreviation, which here is the locale code page's language. +wchar_t s_aszNonImeIndicator[3] = L"En"; +} + +/*---------------------------------------------------------------------------*/ /* Public */ CIME::CIME() { - MT_PLATFORM_STUB(); + ms_hWnd = NULL; + + ms_bCandidateList = false; + ms_bReadingInformation = false; + + Clear(); + + m_max = 0; + m_userMax = 0; + + m_bOnlyNumberMode = FALSE; + m_hOrgIMC = NULL; + + m_bEnablePaste = false; + m_bUseDefaultIME = false; } CIME::~CIME() { - MT_PLATFORM_STUB(); } -auto CIME::Initialize(HWND) -> bool +// PORT: 40250 loads imm32.dll, disables Cicero, reads the keyboard layout and sets up the TSF sinks. The +// Godot host has no IMM context; only the part the edit buffer depends on — the input/output code page from +// CheckInputLocale and SetSupportLevel(2) — is kept. +bool CIME::Initialize(HWND hWnd) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} + if(ms_bInitialized) + return true; + ms_hWnd = hWnd; -auto CIME::Uninitialize() -> void -{ - MT_PLATFORM_STUB(); -} + ms_bDisableIMECompletely = true; + ms_bInitialized = true; -auto CIME::Clear() -> void -{ - MT_PLATFORM_STUB(); -} + CheckInputLocale(); + ChangeInputLanguageWorker(); + SetSupportLevel(2); -auto CIME::SetMax(int) -> void -{ - MT_PLATFORM_STUB(); -} + ms_bUILessMode = false; + CheckToggleState(); -auto CIME::SetUserMax(int) -> void -{ - MT_PLATFORM_STUB(); + return true; } -auto CIME::SetText(const char *, int) -> void +void CIME::Uninitialize() { - MT_PLATFORM_STUB(); + if ( !ms_bInitialized ) + return; + ms_hWnd = NULL; + m_hOrgIMC = NULL; + ms_bInitialized = false; } -auto CIME::GetText(std::string &, bool) -> int +void CIME::UseDefaultIME() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + m_bUseDefaultIME = true; } -auto CIME::GetCodePageText() -> const char * +bool CIME::IsIMEEnabled() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return ms_bImeEnabled; } -auto CIME::GetCodePage() -> int +// PORT: 40250 ImmAssociateContext()s the window; ms_bDisableIMECompletely (no imm32) forces bEnable=false, +// which is always the case here. +void CIME::EnableIME(bool bEnable) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (!ms_bInitialized || !ms_hWnd) + return; + if (ms_bDisableIMECompletely) + bEnable = false; + ms_bImeEnabled = bEnable; + if (bEnable) + CheckToggleState(); } -auto CIME::GetCandidateCount() -> int +void CIME::DisableIME() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + EnableIME(false); } -auto CIME::GetCandidatePageCount() -> int +void CIME::EnableCaptureInput() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + ms_bCaptureInput = true; } -auto CIME::GetCandidate(DWORD, std::string &) -> int +void CIME::DisableCaptureInput() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + ms_bCaptureInput = false; } -auto CIME::GetCandidateSelection() -> int +bool CIME::IsCaptureEnabled() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return ms_bCaptureInput; } -auto CIME::GetReading(std::string &) -> int +void CIME::Clear() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} + ms_lastpos = 0; + ms_curpos = 0; -auto CIME::GetReadingError() -> int -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + ms_compLen = 0; + ms_ulbegin = 0; + ms_ulend = 0; } -auto CIME::SetInputMode(DWORD) -> void +int CIME::GetReading(std::string & rstrText) { - MT_PLATFORM_STUB(); -} + char reading[IMEREADING_MAXLEN]; -auto CIME::GetInputMode() -> DWORD -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if(ms_wstrReading.size() == 0) + return 0; + int readingLen = WideCharToMultiByte(ms_uOutputCodePage, 0, &ms_wstrReading[0], ms_wstrReading.size(), reading, sizeof(reading), NULL, NULL); + + rstrText.append(GetCodePageText()); + rstrText.append(reading, reading + readingLen); + + return rstrText.size(); } -auto CIME::IsIMEEnabled() -> bool +int CIME::GetReadingError() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return ms_iReadingError; } -auto CIME::EnableIME(bool) -> void +void CIME::SetMax(int iMax) { - MT_PLATFORM_STUB(); + m_max = iMax; } -auto CIME::DisableIME() -> void +void CIME::SetUserMax(int iMax) { - MT_PLATFORM_STUB(); + m_userMax = iMax; } -auto CIME::EnableCaptureInput() -> void +void CIME::SetText(const char* szText, int len) { - MT_PLATFORM_STUB(); + ms_compLen = 0; + ms_ulbegin = 0; + ms_ulend = 0; + + const char* begin = szText; + const char* end = begin + len; + const char* iter = FindToken(begin, end); + + int m_wTextLen = sizeof(m_wText)/sizeof(wchar_t); + + ms_lastpos = MultiByteToWideChar(ms_uInputCodePage, 0, begin, iter-begin, m_wText, m_wTextLen); + + if (iter < end) + ms_lastpos += MultiByteToWideChar(ReadToken(iter), 0, (iter+5), end-(iter+5), m_wText+ms_lastpos, m_wTextLen-ms_lastpos); + + ms_curpos = std::min(ms_curpos, ms_lastpos); } -auto CIME::DisableCaptureInput() -> void +int CIME::GetText(std::string & rstrText, bool addCodePage) { - MT_PLATFORM_STUB(); + int outCodePage = ms_uOutputCodePage; + int dataCodePage; + switch (outCodePage) + { + //case 1256: // ARABIC + case 1268: // VIETNAM + dataCodePage = CP_UTF8; + break; + default: + dataCodePage = outCodePage; + } + + int len = 0; + char text[IMESTR_MAXLEN]; + + len += WideCharToMultiByte(dataCodePage, 0, m_wText, ms_curpos, text, sizeof(text)-len, NULL, NULL); + len += WideCharToMultiByte(dataCodePage, 0, m_wszComposition, ms_compLen, text+len, sizeof(text)-len, NULL, NULL); + len += WideCharToMultiByte(dataCodePage, 0, m_wText+ms_curpos, ms_lastpos-ms_curpos, text+len, sizeof(text)-len, NULL, NULL); + + int i; + for(i=0; i 0x7F) break; + + if(i == len) + { + rstrText.append(text, text+len); + } + else + { + rstrText.append(text, text+i); + + //if (addCodePage) + // rstrText.append(GetCodePageText()); + + rstrText.append(text+i, text+len); + } + + return rstrText.size(); } -auto CIME::IsCaptureEnabled() -> bool +const char* CIME::GetCodePageText() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + static char szCodePage[16]; + + const int defCodePage = GetDefaultCodePage(); + const int outCodePage = ms_uOutputCodePage; + + if (outCodePage != defCodePage) + { + sprintf(szCodePage, "@%04d", outCodePage); + } + else + { + szCodePage[0] = 0; + } + + return szCodePage; } -auto CIME::SetNumberMode() -> void +int CIME::GetCodePage() { - MT_PLATFORM_STUB(); + return ms_uOutputCodePage; } -auto CIME::SetStringMode() -> void +int CIME::GetCandidatePageCount() { - MT_PLATFORM_STUB(); + return ms_dwCandidatePageSize; } -auto CIME::__IsWritable(wchar_t) -> bool +int CIME::GetCandidateCount() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return ms_dwCandidateCount; } -auto CIME::AddExceptKey(wchar_t) -> void +int CIME::GetCandidate(DWORD index, std::string & rstrText) { - MT_PLATFORM_STUB(); + if(index >= MAX_CANDLIST) + return 0; + + LPCWSTR wszText = ms_wszCandidate[index]; + if(wszText == NULL) + return 0; + + int wTextLen = wcslen(wszText); + if(wTextLen == 0) + return 0; + + char text[IMESTR_MAXLEN]; + int len = ::WideCharToMultiByte(CP_UTF8, 0, wszText, wTextLen, text, sizeof(text), 0, 0); + + rstrText.append("@9999"); + rstrText.append(text, text+len); + + return wTextLen; } -auto CIME::ClearExceptKey() -> void +int CIME::GetCandidateSelection() { - MT_PLATFORM_STUB(); + return ms_dwCandidateSelection; } -auto CIME::PasteTextFromClipBoard() -> void +// PORT: 40250 ImmSetConversionStatus / ImmGetConversionStatus. Without an IMM context the conversion mode is +// the alphanumeric one (IME_CMODE_ALPHANUMERIC == 0) and cannot be changed. +void CIME::SetInputMode(DWORD) { - MT_PLATFORM_STUB(); } -auto CIME::EnablePaste(bool) -> void +DWORD CIME::GetInputMode() { - MT_PLATFORM_STUB(); + return 0; } -auto CIME::PasteString(const char *) -> void +void CIME::SetNumberMode() { - MT_PLATFORM_STUB(); + m_bOnlyNumberMode = TRUE; } -auto CIME::FinalizeString(bool) -> void +void CIME::SetStringMode() { - MT_PLATFORM_STUB(); + m_bOnlyNumberMode = FALSE; } -auto CIME::UseDefaultIME() -> void +void CIME::AddExceptKey(wchar_t key) { - MT_PLATFORM_STUB(); + m_exceptKey.push_back(key); } -auto CIME::GetCurPos() -> int +void CIME::ClearExceptKey() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + m_exceptKey.clear(); } -auto CIME::GetCompLen() -> int +bool CIME::__IsWritable(wchar_t key) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if ( m_exceptKey.end() == std::find(m_exceptKey.begin(),m_exceptKey.end(),key) ) + return true; + else + return false; } -auto CIME::GetULBegin() -> int +void CIME::EnablePaste(bool bFlag) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + m_bEnablePaste = bFlag; } -auto CIME::GetULEnd() -> int +// PORT: 40250 reads CF_TEXT from the Win32 clipboard. The host clipboard is not wired to the extension yet, +// so this pastes nothing (the same as an empty clipboard). +void CIME::PasteTextFromClipBoard() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (!m_bEnablePaste) + return; } -auto CIME::CloseCandidateList() -> void +// PORT: 40250 cancels the IMM composition string and closes the candidate list. There is no composition +// here; the candidate-list close (and its event) is kept. +void CIME::FinalizeString(bool) { - MT_PLATFORM_STUB(); + static bool s_bProcessing = false; // to avoid infinite recursion + if ( !ms_bInitialized || s_bProcessing ) + return; + s_bProcessing = true; + CloseCandidateList(); + s_bProcessing = false; } -auto CIME::CloseReadingInformation() -> void +int CIME::GetCompLen() { - MT_PLATFORM_STUB(); + return ms_compLen; } -auto CIME::ChangeInputLanguage() -> void +int CIME::GetULBegin() { - MT_PLATFORM_STUB(); + return ms_ulbegin; } -auto CIME::ChangeInputLanguageWorker() -> void +int CIME::GetULEnd() { - MT_PLATFORM_STUB(); + return ms_ulend; } -auto CIME::WMInputLanguage(HWND, UINT, WPARAM, LPARAM) -> LRESULT +void CIME::CloseCandidateList() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + ms_bCandidateList = false; + ms_dwCandidateCount = 0; + memset(&ms_wszCandidate, 0, sizeof(ms_wszCandidate)); + if(ms_pEvent) + ms_pEvent->OnCloseCandidateList(); } -auto CIME::WMStartComposition(HWND, UINT, WPARAM, LPARAM) -> LRESULT +void CIME::CloseReadingInformation() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + CIME::ms_bReadingInformation = false; + if(CIME::ms_pEvent) + CIME::ms_pEvent->OnCloseReadingWnd(); } -auto CIME::WMComposition(HWND, UINT, WPARAM, LPARAM) -> LRESULT +// PORT: 40250 re-reads the keyboard layout (GETLANG) and re-applies the IME support level when the language +// changed. The host has one fixed input locale, so only the toggle check and the code-page event remain. +void CIME::ChangeInputLanguage() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + CheckToggleState(); + ChangeInputLanguageWorker(); + + if(ms_pEvent) + ms_pEvent->OnChangeCodePage(); } -auto CIME::WMEndComposition(HWND, UINT, WPARAM, LPARAM) -> LRESULT +void CIME::ChangeInputLanguageWorker() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if ( !ms_bUILessMode ) + ms_iCandListIndexBase = 1; + SetupImeApi(); } -auto CIME::WMNotify(HWND, UINT, WPARAM, LPARAM) -> LRESULT +// PORT: 40250 forces level 3 for the Korean keyboard layout; there is no keyboard layout here. +void CIME::SetSupportLevel( DWORD dwImeLevel ) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if ( dwImeLevel < 2 || 3 < dwImeLevel ) + return; + ms_dwIMELevel = dwImeLevel; + // cancel current composition string. + FinalizeString(); } -auto CIME::WMChar(HWND, UINT, WPARAM, LPARAM) -> LRESULT +LRESULT CIME::WMInputLanguage(HWND, UINT, WPARAM, LPARAM) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + ChangeInputLanguage(); + return 0; } -auto CIME::IncCurPos() -> void +LRESULT CIME::WMStartComposition(HWND, UINT, WPARAM, LPARAM) { - MT_PLATFORM_STUB(); + return 1L; } -auto CIME::DecCurPos() -> void +// PORT: 40250 pulls GCS_RESULTSTR/GCS_COMPSTR out of the IMM context. The host never sends compositions. +LRESULT CIME::WMComposition(HWND, UINT, WPARAM, LPARAM) { - MT_PLATFORM_STUB(); + return 0; } -auto CIME::SetCurPos(int) -> void +LRESULT CIME::WMEndComposition(HWND, UINT, WPARAM, LPARAM) { - MT_PLATFORM_STUB(); + ms_compLen = 0; + ms_ulbegin = 0; + ms_ulend = 0; + + if(ms_pEvent) + ms_pEvent->OnUpdate(); + + return 0L; } -auto CIME::DelCurPos() -> void +// PORT: 40250 handles IMN_* candidate/reading/toggle notifications from IMM; none are sent here. +LRESULT CIME::WMNotify(HWND, UINT, WPARAM, LPARAM) { - MT_PLATFORM_STUB(); + return 0; } -auto CIME::CheckInputLocale() -> void +LRESULT CIME::WMChar(HWND /*hWnd*/, UINT /*uiMsg*/, WPARAM wParam, LPARAM lParam) { - MT_PLATFORM_STUB(); + unsigned char c = (unsigned char)(wParam & 0xff); + + switch (c) + { + case 8: + if(ms_bCaptureInput == false) + return 0; + if (ms_curpos > 0) + { + DecCurPos(); + DelCurPos(); + } + if(ms_pEvent) + ms_pEvent->OnUpdate(); + return 0; + break; + + default: + if(ms_pEvent) { + if (ms_pEvent->OnWM_CHAR(wParam, lParam)) + break; + } + if(ms_bCaptureInput == false) + return 0; + wchar_t w[10]; + MultiByteToWideChar(ms_uInputCodePage, 0, (char*)&c, 1, w, 1); + + OnChar(w[0]); + if (w[0] == L'|') + OnChar(w[0]); + if(ms_pEvent) + ms_pEvent->OnUpdate(); + break; + } + + return 0; } -auto CIME::CheckToggleState() -> void +/*---------------------------------------------------------------------------*/ /* Protected */ +void CIME::IncCurPos() { - MT_PLATFORM_STUB(); + if (ms_curpos < ms_lastpos) + { + int pos = FindColorTagEndPosition(m_wText + ms_curpos, ms_lastpos - ms_curpos); + + if (pos > 0) + ms_curpos = std::min(ms_lastpos, std::max(0, ms_curpos + (pos + 1))); + else + ++ms_curpos; + //++ms_curpos; + } } -auto CIME::SetSupportLevel(DWORD) -> void +void CIME::DecCurPos() { - MT_PLATFORM_STUB(); + if (ms_curpos > 0) + { + int pos = FindColorTagStartPosition(m_wText + ms_curpos - 1, ms_curpos); + + if (pos > 0) + ms_curpos = std::min(ms_lastpos, std::max(0, ms_curpos - (pos + 1))); + else + --ms_curpos; + //--ms_curpos; + } } -auto CIME::InsertString(wchar_t *, int) -> void +int CIME::GetCurPos() { - MT_PLATFORM_STUB(); + int pos = GetTextTagOutputLen(m_wText, ms_curpos); + return pos; + //return ms_curpos; } -auto CIME::OnChar(wchar_t) -> void +void CIME::SetCurPos(int offset) { - MT_PLATFORM_STUB(); + if (offset < 0 || offset > ms_lastpos) + { + ms_curpos = ms_lastpos; + return; + } + else + { + // offset is a position in the rendered text, so it has to be mapped back to the buffer. + //ms_curpos = min(ms_lastpos, offset); + ms_curpos = std::min(ms_lastpos, GetTextTagInternalPosFromRenderPos(m_wText, ms_lastpos, offset)); + } } -auto CIME::GetCodePageFromLang(LANGID) -> UINT +void CIME::DelCurPos() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (ms_curpos < ms_lastpos) + { + int eraseCount = FindColorTagEndPosition(m_wText + ms_curpos, ms_lastpos - ms_curpos) + 1; + // PORT: 40250 wcscpy()s the overlapping tail down; that is undefined for overlapping buffers, and the + // buffer is not guaranteed NUL-terminated at ms_lastpos, so move exactly the live tail. + memmove(m_wText + ms_curpos, m_wText + ms_curpos + eraseCount, sizeof(wchar_t) * (ms_lastpos - ms_curpos - eraseCount)); + ms_lastpos -= eraseCount; + ms_curpos = std::min(ms_lastpos, ms_curpos); + } } -auto CIME::ResultProcess(HIMC) -> void +void CIME::PasteString(const char * str) { - MT_PLATFORM_STUB(); + const char * begin = str; + const char * end = str + strlen(str); + wchar_t m_wText[IMESTR_MAXLEN]; + int wstrLen = MultiByteToWideChar(ms_uInputCodePage, 0, begin, end - begin, m_wText, IMESTR_MAXLEN); + InsertString(m_wText, wstrLen); + if(ms_pEvent) + ms_pEvent->OnUpdate(); } -auto CIME::CompositionProcessBuilding(HIMC) -> void +// PORT: 40250 reads the keyboard layout's default ANSI code page (GetLocaleInfoA LOCALE_IDEFAULTANSICODEPAGE). +// The host's input locale is the client locale, so its code page (SetDefaultCodePage from locale.cfg) is used. +void CIME::CheckInputLocale() { - MT_PLATFORM_STUB(); + static UINT s_uPrevCodePage = 0xFFFF; + + ms_uInputCodePage = GetDefaultCodePage(); + if ( s_uPrevCodePage == ms_uInputCodePage ) + return; + s_uPrevCodePage = ms_uInputCodePage; + + ms_bVerticalCandidate = false; + ms_wszCurrentIndicator = s_aszNonImeIndicator; + + if(ms_uOutputCodePage != 1256) { + ms_uOutputCodePage = ms_uInputCodePage; + Clear(); + } } -auto CIME::CompositionProcess(HIMC) -> void +// PORT: 40250 asks IMM whether an IME is open; there is none, so the state is IMEUI_STATE_OFF (0). +void CIME::CheckToggleState() { - MT_PLATFORM_STUB(); + CheckInputLocale(); + ms_bChineseIME = false; + ms_dwImeState = 0; } -auto CIME::AttributeProcess(HIMC) -> void +/*---------------------------------------------------------------------------*/ /* Private */ +void CIME::InsertString(wchar_t* wString, int iSize) { - MT_PLATFORM_STUB(); + if (IsMax(wString, iSize)) + return; + + if (ms_curpos < ms_lastpos) + memmove(m_wText+ms_curpos+iSize, m_wText+ms_curpos, sizeof(wchar_t)*(ms_lastpos-ms_curpos)); + + memcpy(m_wText+ms_curpos, wString, sizeof(wchar_t)*iSize); + + ms_curpos += iSize; + ms_lastpos += iSize; } -auto CIME::CandidateProcess(HIMC) -> void +void CIME::OnChar(wchar_t c) { - MT_PLATFORM_STUB(); + if (m_bOnlyNumberMode) + if (!iswdigit(c)) + return; + + if (!__IsWritable(c)) + return; + + InsertString(&c, 1); } -auto CIME::ReadingProcess(HIMC) -> void +// PORT: 40250 maps a keyboard-layout LANGID to its ANSI code page. Only CheckInputLocale's code page is used +// here (see there), so this answers with it. +UINT CIME::GetCodePageFromLang(LANGID) { - MT_PLATFORM_STUB(); + return GetDefaultCodePage(); } -auto CIME::IsMax(const wchar_t *, int) -> bool +// PORT: the IMM composition/candidate/reading processors (40250 IME.cpp:951-1227). No IMM context exists. +void CIME::ResultProcess(HIMC) {} +void CIME::CompositionProcessBuilding(HIMC) {} +void CIME::CompositionProcess(HIMC) {} +void CIME::AttributeProcess(HIMC) {} +void CIME::CandidateProcess(HIMC) {} +void CIME::ReadingProcess(HIMC) {} + +bool CIME::IsMax(const wchar_t* wInput, int len) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (ms_lastpos + len > IMESTR_MAXLEN) + return true; + + int textLen = WideCharToMultiByte(ms_uOutputCodePage, 0, m_wText, ms_lastpos, 0, 0, NULL, NULL); + int inputLen = WideCharToMultiByte(ms_uOutputCodePage, 0, wInput, len, 0, 0, NULL, NULL); + //return textLen + inputLen > m_max; + + if (textLen + inputLen > m_max) + return true; + else if (m_userMax != 0 && m_max != m_userMax) + { + std::wstring str = GetTextTagOutputString(m_wText, ms_lastpos); + std::wstring input = GetTextTagOutputString(wInput, len); + int textLen = WideCharToMultiByte(ms_uOutputCodePage, 0, str.c_str(), str.length(), 0, 0, NULL, NULL); + int inputLen = WideCharToMultiByte(ms_uOutputCodePage, 0, input.c_str(), input.length(), 0, 0, NULL, NULL); + return textLen + inputLen > m_userMax; + } + return false; } -auto CIME::GetImeId(UINT) -> DWORD +// PORT: 40250 identifies the CHT/CHS IME DLL version and the reading-window orientation. No IME DLL here. +DWORD CIME::GetImeId(UINT) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return 0; } -auto CIME::GetReadingWindowOrientation() -> bool +bool CIME::GetReadingWindowOrientation() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return true; } -auto CIME::SetupImeApi() -> void +void CIME::SetupImeApi() { - MT_PLATFORM_STUB(); + _GetReadingString = NULL; + _ShowReadingWindow = NULL; } decltype(CIME::_ImmLockIMC) CIME::_ImmLockIMC{}; diff --git a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp index 155237b4..a8fa32d1 100644 --- a/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp +++ b/extension/src/platform/ScriptLib/GameplayModuleConstants.cpp @@ -63,11 +63,6 @@ void AddStubConstants_guild(PyObject* poModule) } // 40250 UserInterface/PythonIMEModule.cpp -void AddStubConstants_ime(PyObject* poModule) -{ - (void) poModule; -} - // 40250 UserInterface/PythonMessenger.cpp void AddStubConstants_messenger(PyObject* poModule) { diff --git a/extension/src/platform/ScriptLib/GameplayModules.cpp b/extension/src/platform/ScriptLib/GameplayModules.cpp index e8cac9f9..bb4f60eb 100644 --- a/extension/src/platform/ScriptLib/GameplayModules.cpp +++ b/extension/src/platform/ScriptLib/GameplayModules.cpp @@ -8,7 +8,6 @@ void AddStubConstants_event(PyObject* poModule); void AddStubConstants_eventMgr(PyObject* poModule); void AddStubConstants_guild(PyObject* poModule); -void AddStubConstants_ime(PyObject* poModule); void AddStubConstants_messenger(PyObject* poModule); void AddStubConstants_profiler(PyObject* poModule); void AddStubConstants_quest(PyObject* poModule); @@ -154,76 +153,6 @@ PyMethodDef methods_guild[] = { { "Destroy", stub_guild_Destroy, METH_VARARGS }, { NULL, NULL } }; -// 40250 UserInterface/PythonIMEModule.cpp: ime export names. -PyObject* stub_ime_Enable(PyObject*, PyObject*) { return stub_call_named("ime.Enable", Py_BuildNone()); } -PyObject* stub_ime_Disable(PyObject*, PyObject*) { return stub_call_named("ime.Disable", Py_BuildNone()); } -PyObject* stub_ime_EnableCaptureInput(PyObject*, PyObject*) { return stub_call_named("ime.EnableCaptureInput", Py_BuildNone()); } -PyObject* stub_ime_DisableCaptureInput(PyObject*, PyObject*) { return stub_call_named("ime.DisableCaptureInput", Py_BuildNone()); } -PyObject* stub_ime_SetMax(PyObject*, PyObject*) { return stub_call_named("ime.SetMax", Py_BuildNone()); } -PyObject* stub_ime_SetUserMax(PyObject*, PyObject*) { return stub_call_named("ime.SetUserMax", Py_BuildNone()); } -PyObject* stub_ime_SetText(PyObject*, PyObject*) { return stub_call_named("ime.SetText", Py_BuildNone()); } -PyObject* stub_ime_GetText(PyObject*, PyObject*) { return stub_call_named("ime.GetText", Py_BuildValue("s", "")); } -PyObject* stub_ime_GetCodePage(PyObject*, PyObject*) { return stub_call_named("ime.GetCodePage", Py_BuildValue("i", 0)); } -PyObject* stub_ime_GetCandidateCount(PyObject*, PyObject*) { return stub_call_named("ime.GetCandidateCount", Py_BuildValue("i", 0)); } -PyObject* stub_ime_GetCandidate(PyObject*, PyObject*) { return stub_call_named("ime.GetCandidate", Py_BuildValue("si", "", 0)); } -PyObject* stub_ime_GetCandidateSelection(PyObject*, PyObject*) { return stub_call_named("ime.GetCandidateSelection", Py_BuildValue("i", 0)); } -PyObject* stub_ime_GetReading(PyObject*, PyObject*) { return stub_call_named("ime.GetReading", Py_BuildValue("s", "")); } -PyObject* stub_ime_GetReadingError(PyObject*, PyObject*) { return stub_call_named("ime.GetReadingError", Py_BuildValue("i", 0)); } -PyObject* stub_ime_EnableIME(PyObject*, PyObject*) { return stub_call_named("ime.EnableIME", Py_BuildNone()); } -PyObject* stub_ime_DisableIME(PyObject*, PyObject*) { return stub_call_named("ime.DisableIME", Py_BuildNone()); } -PyObject* stub_ime_GetInputMode(PyObject*, PyObject*) { return stub_call_named("ime.GetInputMode", Py_BuildValue("i", 0)); } -PyObject* stub_ime_SetInputMode(PyObject*, PyObject*) { return stub_call_named("ime.SetInputMode", Py_BuildNone()); } -PyObject* stub_ime_SetNumberMode(PyObject*, PyObject*) { return stub_call_named("ime.SetNumberMode", Py_BuildNone()); } -PyObject* stub_ime_SetStringMode(PyObject*, PyObject*) { return stub_call_named("ime.SetStringMode", Py_BuildNone()); } -PyObject* stub_ime_AddExceptKey(PyObject*, PyObject*) { return stub_call_named("ime.AddExceptKey", Py_BuildNone()); } -PyObject* stub_ime_ClearExceptKey(PyObject*, PyObject*) { return stub_call_named("ime.ClearExceptKey", Py_BuildNone()); } -PyObject* stub_ime_MoveLeft(PyObject*, PyObject*) { return stub_call_named("ime.MoveLeft", Py_BuildNone()); } -PyObject* stub_ime_MoveRight(PyObject*, PyObject*) { return stub_call_named("ime.MoveRight", Py_BuildNone()); } -PyObject* stub_ime_MoveHome(PyObject*, PyObject*) { return stub_call_named("ime.MoveHome", Py_BuildNone()); } -PyObject* stub_ime_MoveEnd(PyObject*, PyObject*) { return stub_call_named("ime.MoveEnd", Py_BuildNone()); } -PyObject* stub_ime_SetCursorPosition(PyObject*, PyObject*) { return stub_call_named("ime.SetCursorPosition", Py_BuildNone()); } -PyObject* stub_ime_Delete(PyObject*, PyObject*) { return stub_call_named("ime.Delete", Py_BuildNone()); } -PyObject* stub_ime_PasteString(PyObject*, PyObject*) { return stub_call_named("ime.PasteString", Py_BuildNone()); } -PyObject* stub_ime_PasteBackspace(PyObject*, PyObject*) { return stub_call_named("ime.PasteBackspace", Py_BuildNone()); } -PyObject* stub_ime_PasteReturn(PyObject*, PyObject*) { return stub_call_named("ime.PasteReturn", Py_BuildNone()); } -PyObject* stub_ime_PasteTextFromClipBoard(PyObject*, PyObject*) { return stub_call_named("ime.PasteTextFromClipBoard", Py_BuildNone()); } -PyObject* stub_ime_EnablePaste(PyObject*, PyObject*) { return stub_call_named("ime.EnablePaste", Py_BuildNone()); } -PyMethodDef methods_ime[] = { - { "Enable", stub_ime_Enable, METH_VARARGS }, - { "Disable", stub_ime_Disable, METH_VARARGS }, - { "EnableCaptureInput", stub_ime_EnableCaptureInput, METH_VARARGS }, - { "DisableCaptureInput", stub_ime_DisableCaptureInput, METH_VARARGS }, - { "SetMax", stub_ime_SetMax, METH_VARARGS }, - { "SetUserMax", stub_ime_SetUserMax, METH_VARARGS }, - { "SetText", stub_ime_SetText, METH_VARARGS }, - { "GetText", stub_ime_GetText, METH_VARARGS }, - { "GetCodePage", stub_ime_GetCodePage, METH_VARARGS }, - { "GetCandidateCount", stub_ime_GetCandidateCount, METH_VARARGS }, - { "GetCandidate", stub_ime_GetCandidate, METH_VARARGS }, - { "GetCandidateSelection", stub_ime_GetCandidateSelection, METH_VARARGS }, - { "GetReading", stub_ime_GetReading, METH_VARARGS }, - { "GetReadingError", stub_ime_GetReadingError, METH_VARARGS }, - { "EnableIME", stub_ime_EnableIME, METH_VARARGS }, - { "DisableIME", stub_ime_DisableIME, METH_VARARGS }, - { "GetInputMode", stub_ime_GetInputMode, METH_VARARGS }, - { "SetInputMode", stub_ime_SetInputMode, METH_VARARGS }, - { "SetNumberMode", stub_ime_SetNumberMode, METH_VARARGS }, - { "SetStringMode", stub_ime_SetStringMode, METH_VARARGS }, - { "AddExceptKey", stub_ime_AddExceptKey, METH_VARARGS }, - { "ClearExceptKey", stub_ime_ClearExceptKey, METH_VARARGS }, - { "MoveLeft", stub_ime_MoveLeft, METH_VARARGS }, - { "MoveRight", stub_ime_MoveRight, METH_VARARGS }, - { "MoveHome", stub_ime_MoveHome, METH_VARARGS }, - { "MoveEnd", stub_ime_MoveEnd, METH_VARARGS }, - { "SetCursorPosition", stub_ime_SetCursorPosition, METH_VARARGS }, - { "Delete", stub_ime_Delete, METH_VARARGS }, - { "PasteString", stub_ime_PasteString, METH_VARARGS }, - { "PasteBackspace", stub_ime_PasteBackspace, METH_VARARGS }, - { "PasteReturn", stub_ime_PasteReturn, METH_VARARGS }, - { "PasteTextFromClipBoard", stub_ime_PasteTextFromClipBoard, METH_VARARGS }, - { "EnablePaste", stub_ime_EnablePaste, METH_VARARGS }, - { NULL, NULL } -}; // 40250 UserInterface/PythonMessenger.cpp: messenger export names. PyObject* stub_messenger_RemoveFriend(PyObject*, PyObject*) { return stub_call_named("messenger.RemoveFriend", Py_BuildNone()); } PyObject* stub_messenger_IsFriendByName(PyObject*, PyObject*) { return stub_call_named("messenger.IsFriendByName", Py_BuildValue("i", 0)); } @@ -471,11 +400,6 @@ void init_2v0_gameplay_stubs() { PyModule_AddIntConstant(module, "__port_stub__", 1); AddStubConstants_guild(module); } - { - PyObject* module = Py_InitModule("ime", methods_ime); - PyModule_AddIntConstant(module, "__port_stub__", 1); - AddStubConstants_ime(module); - } { PyObject* module = Py_InitModule("messenger", methods_messenger); PyModule_AddIntConstant(module, "__port_stub__", 1); diff --git a/extension/src/platform/ScriptLib/PythonBoot.cpp b/extension/src/platform/ScriptLib/PythonBoot.cpp index 3eed577b..5c3499d4 100644 --- a/extension/src/platform/ScriptLib/PythonBoot.cpp +++ b/extension/src/platform/ScriptLib/PythonBoot.cpp @@ -88,14 +88,15 @@ public: } void BlendEventCamera(const SCameraSetting&, float) override {} void SetDefaultCamera() override { CPythonApplication::Instance().SetDefaultCamera(); } - void RunIMEUpdate() override {} - void RunIMETabEvent() override {} - void RunIMEReturnEvent() override {} - void RunIMEChangeCodePage() override {} - void RunIMEOpenCandidateListEvent() override {} - void RunIMECloseCandidateListEvent() override {} - void RunIMEOpenReadingWndEvent() override {} - void RunIMECloseReadingWndEvent() override {} + // 40250 CPythonApplication::RunIME* (PythonApplicationEvent.cpp:148-195): CPythonIME's event sink. + void RunIMEUpdate() override { UI::CWindowManager::Instance().RunIMEUpdate(); } + void RunIMETabEvent() override { UI::CWindowManager::Instance().RunIMETabEvent(); } + void RunIMEReturnEvent() override { UI::CWindowManager::Instance().RunIMEReturnEvent(); } + void RunIMEChangeCodePage() override { UI::CWindowManager::Instance().RunChangeCodePage(); } + void RunIMEOpenCandidateListEvent() override { UI::CWindowManager::Instance().RunOpenCandidate(); } + void RunIMECloseCandidateListEvent() override { UI::CWindowManager::Instance().RunCloseCandidate(); } + void RunIMEOpenReadingWndEvent() override { UI::CWindowManager::Instance().RunOpenReading(); } + void RunIMECloseReadingWndEvent() override { UI::CWindowManager::Instance().RunCloseReading(); } }; std::unique_ptr g_game_application; // The CPythonApplication members the game phase reaches through Instance() (批次 2V2), in 40250's member @@ -109,6 +110,7 @@ struct GameSingletons CItemManager kItemMgr; CPythonCharacterManager kChrMgr; CPythonGraphic pyGraphic; // owns CCullingManager (m_CullingManager) + CPythonIME pyIme; CPythonItem pyItem; CPythonShop pyShop; CPythonExchange pyExchange; @@ -266,6 +268,47 @@ void UIKey(int key, bool pressed) g_window_manager->RunKeyUp(key); } +// 40250 CPythonApplication::WindowProcedure (PythonApplicationProcedure.cpp:111-117): WM_CHAR goes to +// CPythonIME::WMChar and WM_KEYDOWN to OnIMEKeyDown (the VK code, next to DirectInput's DIK stream). The host +// delivers characters as Unicode code points; the 40250 window is an ANSI window, so a character that the input +// code page holds as one byte arrives as that byte, like WM_CHAR. PORT: anything else is what a Windows IME +// commits through WM_IME_COMPOSITION/GCS_RESULTSTR, which CIME::ResultProcess inserts as a wide string. +void UIChar(unsigned codepoint) +{ + if (!g_game_singletons || !codepoint) return; + CPythonIME& ime = CPythonIME::Instance(); + if (codepoint < 0x80) + { + ime.WMChar(NULL, WM_CHAR, codepoint, 0); + return; + } + const wchar_t wide = wchar_t(codepoint); + char bytes[8]; + BOOL used_default = FALSE; + const int n = WideCharToMultiByte(CIME::ms_uInputCodePage, 0, &wide, 1, bytes, sizeof(bytes), "?", &used_default); + if (n == 1 && !used_default) + { + ime.WMChar(NULL, WM_CHAR, (unsigned char)bytes[0], 0); + return; + } + // 40250 CIME::WMComposition with GCS_RESULTSTR: ResultProcess inserts the committed string, then OnUpdate. + if (!CIME::ms_bCaptureInput) + return; + struct ResultAccess : CPythonIME + { + static void Insert(CPythonIME& ime, wchar_t* text, int len) { (ime.*(&ResultAccess::InsertString))(text, len); } + }; + wchar_t text[1] = { wide }; + ResultAccess::Insert(ime, text, 1); + if (CIME::ms_pEvent) + CIME::ms_pEvent->OnUpdate(); +} + +void UIIMEKeyDown(int vkey) +{ + if (g_window_manager) g_window_manager->RunIMEKeyDown(vkey); +} + void UIUpdate() { // While system.py is inside app.Loop(), one host frame is one CPythonApplication::Process(), @@ -341,6 +384,7 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error) // The module initializers follow 40250's order; missing modules arrive in later 2V steps. initpack(); initdbg(); + initime(); initgrp(); initgrpImage(); initgrpText(); diff --git a/extension/src/platform/ScriptLib/PythonBoot.h b/extension/src/platform/ScriptLib/PythonBoot.h index e2317846..b6ed8a91 100644 --- a/extension/src/platform/ScriptLib/PythonBoot.h +++ b/extension/src/platform/ScriptLib/PythonBoot.h @@ -55,6 +55,9 @@ void SetUISize(int width, int height); void UIMouseMove(int x, int y); void UIMouseButton(int button, bool pressed, int x, int y); void UIKey(int key, bool pressed); +// WM_CHAR (a Unicode code point) and WM_KEYDOWN (a Win32 VK code) for CPythonIME and OnIMEKeyDown. +void UIChar(unsigned codepoint); +void UIIMEKeyDown(int vkey); // While IsAppLooping(), UIUpdate is AppFrame() and UIRender keeps the commands that Process() drew. void UIUpdate(); void UIRender(); diff --git a/extension/src/platform/UserInterface/PythonApplication.cpp b/extension/src/platform/UserInterface/PythonApplication.cpp index 25870202..20f951a0 100644 --- a/extension/src/platform/UserInterface/PythonApplication.cpp +++ b/extension/src/platform/UserInterface/PythonApplication.cpp @@ -68,7 +68,7 @@ void CPythonApplication::SetFrameSkip(bool) { MT_PLATFORM_STUB(); } // fonts and the IME here. The Godot window and its canvas already exist when the host starts the // script, so the window size is the canvas size (PythonBoot::SetUISize) rather than the width/height // passed from metin2.cfg, and the graphic device is the recording device (RecordingDevice.h) at that -// size. Cursors, sound, keyboard, fonts and the IME are not created here yet. +// size. Cursors, sound, keyboard and fonts are not created here yet. bool CPythonApplication::Create(PyObject*, const char*, int, int, int) { unsigned width = 0, height = 0; @@ -125,7 +125,11 @@ bool CPythonApplication::Create(PyObject*, const char*, int, int, int) // 40250 m_pyItem.Create() (PythonApplication.cpp:1253): registers dropitem.mse with CEffectManager. CPythonItem::Instance().Create(); - // 40250 PythonApplication.cpp:1260, after DefaultFont_Startup and the IME. + // 40250 PythonApplication.cpp:1258-1259 (after DefaultFont_Startup). + CPythonIME::Instance().Create(GetWindowHandle()); + CPythonIME::Instance().SetText("", 0); + + // 40250 PythonApplication.cpp:1260. CPythonTextTail::Instance().Initialize(); return true; } diff --git a/extension/src/port/UserInterface/PythonIME.cpp b/extension/src/port/UserInterface/PythonIME.cpp new file mode 100644 index 00000000..947d950d --- /dev/null +++ b/extension/src/port/UserInterface/PythonIME.cpp @@ -0,0 +1,119 @@ +#include "StdAfx.h" +#include "PythonIME.h" +#include "AbstractApplication.h" + +CPythonIME::CPythonIME() +: CIME() +{ + ms_pEvent = this; +} + +CPythonIME::~CPythonIME() +{ + Tracen("PythonIME Clear"); +} + +void CPythonIME::Create(HWND hWnd) +{ + Initialize(hWnd); +} + +void CPythonIME::MoveLeft() +{ + DecCurPos(); +} + +void CPythonIME::MoveRight() +{ + IncCurPos(); +} + +void CPythonIME::MoveHome() +{ + ms_curpos = 0; +} + +void CPythonIME::MoveEnd() +{ + ms_curpos = ms_lastpos; +} + +void CPythonIME::SetCursorPosition(int iPosition) +{ + SetCurPos(iPosition); +} + +void CPythonIME::Delete() +{ + DelCurPos(); +} + +void CPythonIME::OnUpdate() +{ + IAbstractApplication::GetSingleton().RunIMEUpdate(); +} + +void CPythonIME::OnTab() +{ + IAbstractApplication::GetSingleton().RunIMETabEvent(); +} + +void CPythonIME::OnReturn() +{ + IAbstractApplication::GetSingleton().RunIMEReturnEvent(); +} + +void CPythonIME::OnEscape() +{ +// IAbstractApplication::GetSingleton().RunIMEEscapeEvent(); +} + +bool CPythonIME::OnWM_CHAR( WPARAM wParam, LPARAM lParam ) +{ + unsigned char c = (unsigned char)(wParam & 0xff); // PORT: functional cast of a two-word type is MSVC-only. + + switch (c) + { + case VK_RETURN: + OnReturn(); + return true; + + case VK_TAB: + if(ms_bCaptureInput == false) + return 0; + OnTab(); + return true; + + case VK_ESCAPE: + if(ms_bCaptureInput == false) + return 0; + OnEscape(); + return true; + } + return false; +} + +void CPythonIME::OnChangeCodePage() +{ + IAbstractApplication::GetSingleton().RunIMEChangeCodePage(); +} + +void CPythonIME::OnOpenCandidateList() +{ + IAbstractApplication::GetSingleton().RunIMEOpenCandidateListEvent(); +} + +void CPythonIME::OnCloseCandidateList() +{ + IAbstractApplication::GetSingleton().RunIMECloseCandidateListEvent(); +} + +void CPythonIME::OnOpenReadingWnd() +{ + IAbstractApplication::GetSingleton().RunIMEOpenReadingWndEvent(); +} + +void CPythonIME::OnCloseReadingWnd() +{ + IAbstractApplication::GetSingleton().RunIMECloseReadingWndEvent(); +} diff --git a/extension/src/port/UserInterface/PythonIMEModule.cpp b/extension/src/port/UserInterface/PythonIMEModule.cpp new file mode 100644 index 00000000..cfc6845d --- /dev/null +++ b/extension/src/port/UserInterface/PythonIMEModule.cpp @@ -0,0 +1,289 @@ +#include "StdAfx.h" +#include "PythonIME.h" +#include "PythonApplication.h" + +PyObject* imeEnable(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().Initialize(CPythonApplication::Instance().GetWindowHandle()); + return Py_BuildNone(); +} + +PyObject* imeDisable(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().Uninitialize(); + return Py_BuildNone(); +} + +PyObject* imeEnableCaptureInput(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().EnableCaptureInput(); + return Py_BuildNone(); +} + +PyObject* imeDisableCaptureInput(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().DisableCaptureInput(); + return Py_BuildNone(); +} + +PyObject* imeSetMax(PyObject* poSelf, PyObject* poArgs) +{ + int iMax; + if (!PyTuple_GetInteger(poArgs, 0, &iMax)) + return Py_BuildException(); + + CPythonIME::Instance().SetMax(iMax); + return Py_BuildNone(); +} + +PyObject* imeSetUserMax(PyObject* poSelf, PyObject* poArgs) +{ + int iMax; + if (!PyTuple_GetInteger(poArgs, 0, &iMax)) + return Py_BuildException(); + + CPythonIME::Instance().SetUserMax(iMax); + return Py_BuildNone(); +} + +PyObject* imeSetText(PyObject* poSelf, PyObject* poArgs) +{ + char* szText; + if (!PyTuple_GetString(poArgs, 0, &szText)) + return Py_BuildException(); + + CPythonIME::Instance().SetText(szText, strlen(szText)); + return Py_BuildNone(); +} + +PyObject* imeGetText(PyObject* poSelf, PyObject* poArgs) +{ + int bCodePage; + if (!PyTuple_GetInteger(poArgs, 0, &bCodePage)) + bCodePage = 0; + + std::string strText; + CPythonIME::Instance().GetText(strText, bCodePage ? true : false); + return Py_BuildValue("s", strText.c_str()); +} + +PyObject* imeGetCodePage(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", CPythonIME::Instance().GetCodePage()); +} + +PyObject* imeGetCandidateCount(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", CPythonIME::Instance().GetCandidatePageCount()); +} + +PyObject* imeGetCandidate(PyObject* poSelf, PyObject* poArgs) +{ + int index; + if (!PyTuple_GetInteger(poArgs, 0, &index)) + return Py_BuildException(); + + std::string strText; + int iLength = CPythonIME::Instance().GetCandidate(index, strText); + return Py_BuildValue("si", strText.c_str(), iLength); +} + +PyObject* imeGetCandidateSelection(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", CPythonIME::Instance().GetCandidateSelection()); +} + +PyObject* imeGetReading(PyObject* poSelf, PyObject* poArgs) +{ + std::string strText; + CPythonIME::Instance().GetReading(strText); + return Py_BuildValue("s", strText.c_str()); +} + +PyObject* imeGetReadingError(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", CPythonIME::Instance().GetReadingError()); +} + +PyObject* imeEnableIME(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().EnableIME(); + return Py_BuildNone(); +} + +PyObject* imeDisableIME(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().DisableIME(); + return Py_BuildNone(); +} + +PyObject* imeSetInputMode(PyObject* poSelf, PyObject* poArgs) +{ + int mode; + if (!PyTuple_GetInteger(poArgs, 0, &mode)) + return Py_BuildException(); + + CPythonIME::Instance().SetInputMode(mode); + return Py_BuildNone(); +} + +PyObject * imeSetNumberMode(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().SetNumberMode(); + return Py_BuildNone(); +} + +PyObject * imeAddExceptKey(PyObject* poSelf, PyObject* poArgs) +{ + int key; + if (!PyTuple_GetInteger(poArgs, 0, &key)) + return Py_BuildException(); + + CPythonIME::Instance().AddExceptKey(key); + return Py_BuildNone(); +} + +PyObject * imeClearExceptKey(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().ClearExceptKey(); + return Py_BuildNone(); +} + +PyObject * imeSetStringMode(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().SetStringMode(); + return Py_BuildNone(); +} + +PyObject* imeGetInputMode(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", CPythonIME::Instance().GetInputMode()); +} + +PyObject* imeMoveLeft(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().MoveLeft(); + + return Py_BuildNone(); +} +PyObject* imeMoveRight(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().MoveRight(); + + return Py_BuildNone(); +} + +PyObject* imeMoveHome(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().MoveHome(); + + return Py_BuildNone(); +} +PyObject* imeMoveEnd(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().MoveEnd(); + + return Py_BuildNone(); +} + +PyObject* imeSetCursorPosition(PyObject* poSelf, PyObject* poArgs) +{ + int iPosition; + if (!PyTuple_GetInteger(poArgs, 0, &iPosition)) + return Py_BuildException(); + + CPythonIME::Instance().SetCursorPosition(iPosition); + + return Py_BuildNone(); +} + +PyObject* imeDelete(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().Delete(); + + return Py_BuildNone(); +} + +PyObject* imePasteTextFromClipBoard(PyObject* poSelf, PyObject* poArgs) +{ + CPythonIME::Instance().PasteTextFromClipBoard(); + return Py_BuildNone(); +} + +PyObject* imeEnablePaste(PyObject* poSelf, PyObject* poArgs) +{ + int iFlag; + if (!PyTuple_GetInteger(poArgs, 0, &iFlag)) + return Py_BuildException(); + + CPythonIME::Instance().EnablePaste(iFlag ? true : false); + return Py_BuildNone(); +} + +PyObject* imePasteString(PyObject * poSelf, PyObject * poArgs) +{ + char* szText; + if (!PyTuple_GetString(poArgs, 0, &szText)) + return Py_BuildException(); + CPythonIME::Instance().PasteString(szText); + return Py_BuildNone(); +} + +PyObject* imePasteBackspace(PyObject * poSelf, PyObject * poArgs) +{ + CPythonIME::Instance().WMChar(NULL, WM_CHAR, 0x08, NULL); + return Py_BuildNone(); +} + +PyObject* imePasteReturn(PyObject * poSelf, PyObject * poArgs) +{ + CPythonIME::Instance().WMChar(NULL, WM_CHAR, 0x0D, NULL); + return Py_BuildNone(); +} + +void initime() +{ + static PyMethodDef s_methods[] = + { + { "Enable", imeEnable, METH_VARARGS }, + { "Disable", imeDisable, METH_VARARGS }, + + { "EnableCaptureInput", imeEnableCaptureInput, METH_VARARGS }, + { "DisableCaptureInput", imeDisableCaptureInput, METH_VARARGS }, + { "SetMax", imeSetMax, METH_VARARGS }, + { "SetUserMax", imeSetUserMax, METH_VARARGS }, + { "SetText", imeSetText, METH_VARARGS }, + { "GetText", imeGetText, METH_VARARGS }, + { "GetCodePage", imeGetCodePage, METH_VARARGS }, + { "GetCandidateCount", imeGetCandidateCount, METH_VARARGS }, + { "GetCandidate", imeGetCandidate, METH_VARARGS }, + { "GetCandidateSelection", imeGetCandidateSelection, METH_VARARGS }, + { "GetReading", imeGetReading, METH_VARARGS }, + { "GetReadingError", imeGetReadingError, METH_VARARGS }, + { "EnableIME", imeEnableIME, METH_VARARGS }, + { "DisableIME", imeDisableIME, METH_VARARGS }, + { "GetInputMode", imeGetInputMode, METH_VARARGS }, + { "SetInputMode", imeSetInputMode, METH_VARARGS }, + + { "SetNumberMode", imeSetNumberMode, METH_VARARGS }, + { "SetStringMode", imeSetStringMode, METH_VARARGS }, + { "AddExceptKey", imeAddExceptKey, METH_VARARGS }, + { "ClearExceptKey", imeClearExceptKey, METH_VARARGS }, + + { "MoveLeft", imeMoveLeft, METH_VARARGS }, + { "MoveRight", imeMoveRight, METH_VARARGS }, + { "MoveHome", imeMoveHome, METH_VARARGS }, + { "MoveEnd", imeMoveEnd, METH_VARARGS }, + { "SetCursorPosition", imeSetCursorPosition, METH_VARARGS }, + { "Delete", imeDelete, METH_VARARGS }, + { "PasteString", imePasteString, METH_VARARGS }, + { "PasteBackspace", imePasteBackspace, METH_VARARGS }, + { "PasteReturn", imePasteReturn, METH_VARARGS }, + { "PasteTextFromClipBoard", imePasteTextFromClipBoard, METH_VARARGS }, + { "EnablePaste", imeEnablePaste, METH_VARARGS }, + + { NULL, NULL, NULL }, + }; + + Py_InitModule("ime", s_methods); +} \ No newline at end of file diff --git a/extension/src/port/UserInterface/StdAfx.h b/extension/src/port/UserInterface/StdAfx.h index 00861da8..c031b867 100644 --- a/extension/src/port/UserInterface/StdAfx.h +++ b/extension/src/port/UserInterface/StdAfx.h @@ -44,6 +44,7 @@ enum // The Python module initializers RunMainScript calls, each defined in its own Python*Module.cpp. // Listed here as 40250 lists them; only the ported ones are defined. void initpack(); +void initime(); void initapp(); void initchr(); void initchrmgr(); diff --git a/extension/src/port/common/shim/win32/windows.h b/extension/src/port/common/shim/win32/windows.h index 8db64a15..eee2f3c5 100644 --- a/extension/src/port/common/shim/win32/windows.h +++ b/extension/src/port/common/shim/win32/windows.h @@ -28,6 +28,12 @@ #define VK_HOME 0x24 #define VK_END 0x23 #define VK_DELETE 0x2E +#define VK_BACK 0x08 +#define VK_TAB 0x09 +#define VK_RETURN 0x0D +#define VK_ESCAPE 0x1B +#define WM_KEYDOWN 0x0100 +#define WM_CHAR 0x0102 // winuser.h functions the platform layer implements (platform/ScriptLib/PythonBoot.cpp). void PostQuitMessage(int nExitCode); diff --git a/extension/src/python_host_node.cpp b/extension/src/python_host_node.cpp index 9b19ea26..5dc2c828 100644 --- a/extension/src/python_host_node.cpp +++ b/extension/src/python_host_node.cpp @@ -71,6 +71,8 @@ void Metin2PythonHost::ui_mouse_button(int button, bool pressed, int x, int y) { PythonBoot::UIMouseButton(button, pressed, x, y); } void Metin2PythonHost::ui_key(int key, bool pressed) { PythonBoot::UIKey(key, pressed); } +void Metin2PythonHost::ui_char(int codepoint) { PythonBoot::UIChar(unsigned(codepoint)); } +void Metin2PythonHost::ui_ime_key(int vkey) { PythonBoot::UIIMEKeyDown(vkey); } void Metin2PythonHost::ui_update() { PythonBoot::UIUpdate(); } Array Metin2PythonHost::ui_render_commands() { PythonBoot::UIRender(); @@ -264,6 +266,8 @@ void Metin2PythonHost::set_ui_size(int, int) {} void Metin2PythonHost::ui_mouse_move(int, int) {} void Metin2PythonHost::ui_mouse_button(int, bool, int, int) {} void Metin2PythonHost::ui_key(int, bool) {} +void Metin2PythonHost::ui_char(int) {} +void Metin2PythonHost::ui_ime_key(int) {} void Metin2PythonHost::ui_update() {} Array Metin2PythonHost::ui_render_commands() { return Array(); } Ref Metin2PythonHost::memory_texture(const String &, int64_t) { return Ref(); } @@ -287,6 +291,8 @@ void Metin2PythonHost::_bind_methods() { ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_move", "x", "y"), &Metin2PythonHost::ui_mouse_move); ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_button", "button", "pressed", "x", "y"), &Metin2PythonHost::ui_mouse_button); ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_key", "key", "pressed"), &Metin2PythonHost::ui_key); + ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_char", "codepoint"), &Metin2PythonHost::ui_char); + ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_ime_key", "vkey"), &Metin2PythonHost::ui_ime_key); ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_update"), &Metin2PythonHost::ui_update); ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_render_commands"), &Metin2PythonHost::ui_render_commands); ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("memory_texture", "name", "known_revision"), diff --git a/extension/src/python_host_node.h b/extension/src/python_host_node.h index b696e5b3..5d107de1 100644 --- a/extension/src/python_host_node.h +++ b/extension/src/python_host_node.h @@ -38,6 +38,9 @@ public: static void ui_mouse_move(int x, int y); static void ui_mouse_button(int button, bool pressed, int x, int y); static void ui_key(int key, bool pressed); + // WM_CHAR (Unicode code point) and WM_KEYDOWN (Win32 VK code) for the 40250 IME. + static void ui_char(int codepoint); + static void ui_ime_key(int vkey); static void ui_update(); static godot::Array ui_render_commands(); // The pixels of a "mem:@" image command (the CGraphicFontTexture glyph pages) as an diff --git a/extension/tests/port_login_flow_server.cpp b/extension/tests/port_login_flow_server.cpp index efae2876..7f1b8ee4 100644 --- a/extension/tests/port_login_flow_server.cpp +++ b/extension/tests/port_login_flow_server.cpp @@ -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 " : " 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(packet + sizeof(head)), + strnlen(reinterpret_cast(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(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; diff --git a/extension/tests/port_login_flow_test.cpp b/extension/tests/port_login_flow_test.cpp index 7c8e1726..c66d989e 100644 --- a/extension/tests/port_login_flow_test.cpp +++ b/extension/tests/port_login_flow_test.cpp @@ -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); } diff --git a/project/python_game_render_test.gd b/project/python_game_render_test.gd index 73b51619..089dacd6 100644 --- a/project/python_game_render_test.gd +++ b/project/python_game_render_test.gd @@ -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") diff --git a/project/python_ui_surface.gd b/project/python_ui_surface.gd index 6674c15d..f90dc27c 100644 --- a/project/python_ui_surface.gd +++ b/project/python_ui_surface.gd @@ -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:" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the # "@" 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)