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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-24 08:35:53 +09:00
co-authored by Claude Opus 5.5
parent 1bda4390d6
commit 6ea0fa9889
21 changed files with 2117 additions and 277 deletions
+1
View File
@@ -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-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-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-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)"]}
+712
View File
@@ -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)"
}
}
}
@@ -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"
]
}
}
}
@@ -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"
]
}
}
}
+2
View File
@@ -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-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 拾取测试用),不是武器挂点问题;假服角色没装备武器。 批次 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 宿主与开关。 默认路由切换(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 宿主。 每个切片都必须同时跑 legacy/port 双路回归;默认路由已切到 `port`(2026-09-24);port 路由稳定后删除总开关和剩余 legacy 宿主。
+464 -179
View File
@@ -1,381 +1,666 @@
// Platform skeleton for EterLib/IME.h (40250 EterLib/IME.cpp), generated by platform_stub.py. // Platform implementation of EterLib/IME.h (40250 EterLib/IME.cpp).
// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation. // 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/StdAfx.h"
#include "EterLib/IME.h" #include "EterLib/IME.h"
#include "EterLib/TextTag.h"
#include "EterLib/GrpTextInstance.h"
#include "EterLib/Util.h"
#include "../PlatformStub.h" #include <algorithm>
#include <cwctype>
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() 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() 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(); if(ms_bInitialized)
return mt_platform_stub_return<bool>(); return true;
} ms_hWnd = hWnd;
auto CIME::Uninitialize() -> void ms_bDisableIMECompletely = true;
{ ms_bInitialized = true;
MT_PLATFORM_STUB();
}
auto CIME::Clear() -> void CheckInputLocale();
{ ChangeInputLanguageWorker();
MT_PLATFORM_STUB(); SetSupportLevel(2);
}
auto CIME::SetMax(int) -> void ms_bUILessMode = false;
{ CheckToggleState();
MT_PLATFORM_STUB();
}
auto CIME::SetUserMax(int) -> void return true;
{
MT_PLATFORM_STUB();
} }
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(); m_bUseDefaultIME = true;
return mt_platform_stub_return<int>();
} }
auto CIME::GetCodePageText() -> const char * bool CIME::IsIMEEnabled()
{ {
MT_PLATFORM_STUB(); return ms_bImeEnabled;
return mt_platform_stub_return<const char *>();
} }
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(); if (!ms_bInitialized || !ms_hWnd)
return mt_platform_stub_return<int>(); return;
if (ms_bDisableIMECompletely)
bEnable = false;
ms_bImeEnabled = bEnable;
if (bEnable)
CheckToggleState();
} }
auto CIME::GetCandidateCount() -> int void CIME::DisableIME()
{ {
MT_PLATFORM_STUB(); EnableIME(false);
return mt_platform_stub_return<int>();
} }
auto CIME::GetCandidatePageCount() -> int void CIME::EnableCaptureInput()
{ {
MT_PLATFORM_STUB(); ms_bCaptureInput = true;
return mt_platform_stub_return<int>();
} }
auto CIME::GetCandidate(DWORD, std::string &) -> int void CIME::DisableCaptureInput()
{ {
MT_PLATFORM_STUB(); ms_bCaptureInput = false;
return mt_platform_stub_return<int>();
} }
auto CIME::GetCandidateSelection() -> int bool CIME::IsCaptureEnabled()
{ {
MT_PLATFORM_STUB(); return ms_bCaptureInput;
return mt_platform_stub_return<int>();
} }
auto CIME::GetReading(std::string &) -> int void CIME::Clear()
{ {
MT_PLATFORM_STUB(); ms_lastpos = 0;
return mt_platform_stub_return<int>(); ms_curpos = 0;
}
auto CIME::GetReadingError() -> int ms_compLen = 0;
{ ms_ulbegin = 0;
MT_PLATFORM_STUB(); ms_ulend = 0;
return mt_platform_stub_return<int>();
} }
auto CIME::SetInputMode(DWORD) -> void int CIME::GetReading(std::string & rstrText)
{ {
MT_PLATFORM_STUB(); char reading[IMEREADING_MAXLEN];
}
auto CIME::GetInputMode() -> DWORD if(ms_wstrReading.size() == 0)
{ return 0;
MT_PLATFORM_STUB(); int readingLen = WideCharToMultiByte(ms_uOutputCodePage, 0, &ms_wstrReading[0], ms_wstrReading.size(), reading, sizeof(reading), NULL, NULL);
return mt_platform_stub_return<DWORD>();
rstrText.append(GetCodePageText());
rstrText.append(reading, reading + readingLen);
return rstrText.size();
} }
auto CIME::IsIMEEnabled() -> bool int CIME::GetReadingError()
{ {
MT_PLATFORM_STUB(); return ms_iReadingError;
return mt_platform_stub_return<bool>();
} }
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<len; ++i)
if((BYTE)text[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(); static char szCodePage[16];
return mt_platform_stub_return<bool>();
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 ms_dwCandidateCount;
return mt_platform_stub_return<bool>();
} }
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(); m_exceptKey.clear();
return mt_platform_stub_return<int>();
} }
auto CIME::GetCompLen() -> int bool CIME::__IsWritable(wchar_t key)
{ {
MT_PLATFORM_STUB(); if ( m_exceptKey.end() == std::find(m_exceptKey.begin(),m_exceptKey.end(),key) )
return mt_platform_stub_return<int>(); return true;
else
return false;
} }
auto CIME::GetULBegin() -> int void CIME::EnablePaste(bool bFlag)
{ {
MT_PLATFORM_STUB(); m_bEnablePaste = bFlag;
return mt_platform_stub_return<int>();
} }
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(); if (!m_bEnablePaste)
return mt_platform_stub_return<int>(); 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(); ms_bCandidateList = false;
return mt_platform_stub_return<LRESULT>(); 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(); CIME::ms_bReadingInformation = false;
return mt_platform_stub_return<LRESULT>(); 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(); CheckToggleState();
return mt_platform_stub_return<LRESULT>(); ChangeInputLanguageWorker();
if(ms_pEvent)
ms_pEvent->OnChangeCodePage();
} }
auto CIME::WMEndComposition(HWND, UINT, WPARAM, LPARAM) -> LRESULT void CIME::ChangeInputLanguageWorker()
{ {
MT_PLATFORM_STUB(); if ( !ms_bUILessMode )
return mt_platform_stub_return<LRESULT>(); 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(); if ( dwImeLevel < 2 || 3 < dwImeLevel )
return mt_platform_stub_return<LRESULT>(); 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(); ChangeInputLanguage();
return mt_platform_stub_return<LRESULT>(); 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(); if (ms_curpos < ms_lastpos)
return mt_platform_stub_return<UINT>(); {
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(); if (ms_lastpos + len > IMESTR_MAXLEN)
return mt_platform_stub_return<bool>(); 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 0;
return mt_platform_stub_return<DWORD>();
} }
auto CIME::GetReadingWindowOrientation() -> bool bool CIME::GetReadingWindowOrientation()
{ {
MT_PLATFORM_STUB(); return true;
return mt_platform_stub_return<bool>();
} }
auto CIME::SetupImeApi() -> void void CIME::SetupImeApi()
{ {
MT_PLATFORM_STUB(); _GetReadingString = NULL;
_ShowReadingWindow = NULL;
} }
decltype(CIME::_ImmLockIMC) CIME::_ImmLockIMC{}; decltype(CIME::_ImmLockIMC) CIME::_ImmLockIMC{};
@@ -63,11 +63,6 @@ void AddStubConstants_guild(PyObject* poModule)
} }
// 40250 UserInterface/PythonIMEModule.cpp // 40250 UserInterface/PythonIMEModule.cpp
void AddStubConstants_ime(PyObject* poModule)
{
(void) poModule;
}
// 40250 UserInterface/PythonMessenger.cpp // 40250 UserInterface/PythonMessenger.cpp
void AddStubConstants_messenger(PyObject* poModule) void AddStubConstants_messenger(PyObject* poModule)
{ {
@@ -8,7 +8,6 @@
void AddStubConstants_event(PyObject* poModule); void AddStubConstants_event(PyObject* poModule);
void AddStubConstants_eventMgr(PyObject* poModule); void AddStubConstants_eventMgr(PyObject* poModule);
void AddStubConstants_guild(PyObject* poModule); void AddStubConstants_guild(PyObject* poModule);
void AddStubConstants_ime(PyObject* poModule);
void AddStubConstants_messenger(PyObject* poModule); void AddStubConstants_messenger(PyObject* poModule);
void AddStubConstants_profiler(PyObject* poModule); void AddStubConstants_profiler(PyObject* poModule);
void AddStubConstants_quest(PyObject* poModule); void AddStubConstants_quest(PyObject* poModule);
@@ -154,76 +153,6 @@ PyMethodDef methods_guild[] = {
{ "Destroy", stub_guild_Destroy, METH_VARARGS }, { "Destroy", stub_guild_Destroy, METH_VARARGS },
{ NULL, NULL } { 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. // 40250 UserInterface/PythonMessenger.cpp: messenger export names.
PyObject* stub_messenger_RemoveFriend(PyObject*, PyObject*) { return stub_call_named("messenger.RemoveFriend", Py_BuildNone()); } 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)); } 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); PyModule_AddIntConstant(module, "__port_stub__", 1);
AddStubConstants_guild(module); 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); PyObject* module = Py_InitModule("messenger", methods_messenger);
PyModule_AddIntConstant(module, "__port_stub__", 1); PyModule_AddIntConstant(module, "__port_stub__", 1);
@@ -88,14 +88,15 @@ public:
} }
void BlendEventCamera(const SCameraSetting&, float) override {} void BlendEventCamera(const SCameraSetting&, float) override {}
void SetDefaultCamera() override { CPythonApplication::Instance().SetDefaultCamera(); } void SetDefaultCamera() override { CPythonApplication::Instance().SetDefaultCamera(); }
void RunIMEUpdate() override {} // 40250 CPythonApplication::RunIME* (PythonApplicationEvent.cpp:148-195): CPythonIME's event sink.
void RunIMETabEvent() override {} void RunIMEUpdate() override { UI::CWindowManager::Instance().RunIMEUpdate(); }
void RunIMEReturnEvent() override {} void RunIMETabEvent() override { UI::CWindowManager::Instance().RunIMETabEvent(); }
void RunIMEChangeCodePage() override {} void RunIMEReturnEvent() override { UI::CWindowManager::Instance().RunIMEReturnEvent(); }
void RunIMEOpenCandidateListEvent() override {} void RunIMEChangeCodePage() override { UI::CWindowManager::Instance().RunChangeCodePage(); }
void RunIMECloseCandidateListEvent() override {} void RunIMEOpenCandidateListEvent() override { UI::CWindowManager::Instance().RunOpenCandidate(); }
void RunIMEOpenReadingWndEvent() override {} void RunIMECloseCandidateListEvent() override { UI::CWindowManager::Instance().RunCloseCandidate(); }
void RunIMECloseReadingWndEvent() override {} void RunIMEOpenReadingWndEvent() override { UI::CWindowManager::Instance().RunOpenReading(); }
void RunIMECloseReadingWndEvent() override { UI::CWindowManager::Instance().RunCloseReading(); }
}; };
std::unique_ptr<GameApplicationAdapter> g_game_application; std::unique_ptr<GameApplicationAdapter> g_game_application;
// The CPythonApplication members the game phase reaches through Instance() (批次 2V2), in 40250's member // The CPythonApplication members the game phase reaches through Instance() (批次 2V2), in 40250's member
@@ -109,6 +110,7 @@ struct GameSingletons
CItemManager kItemMgr; CItemManager kItemMgr;
CPythonCharacterManager kChrMgr; CPythonCharacterManager kChrMgr;
CPythonGraphic pyGraphic; // owns CCullingManager (m_CullingManager) CPythonGraphic pyGraphic; // owns CCullingManager (m_CullingManager)
CPythonIME pyIme;
CPythonItem pyItem; CPythonItem pyItem;
CPythonShop pyShop; CPythonShop pyShop;
CPythonExchange pyExchange; CPythonExchange pyExchange;
@@ -266,6 +268,47 @@ void UIKey(int key, bool pressed)
g_window_manager->RunKeyUp(key); 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() void UIUpdate()
{ {
// While system.py is inside app.Loop(), one host frame is one CPythonApplication::Process(), // 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. // The module initializers follow 40250's order; missing modules arrive in later 2V steps.
initpack(); initpack();
initdbg(); initdbg();
initime();
initgrp(); initgrp();
initgrpImage(); initgrpImage();
initgrpText(); initgrpText();
@@ -55,6 +55,9 @@ void SetUISize(int width, int height);
void UIMouseMove(int x, int y); void UIMouseMove(int x, int y);
void UIMouseButton(int button, bool pressed, int x, int y); void UIMouseButton(int button, bool pressed, int x, int y);
void UIKey(int key, bool pressed); 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. // While IsAppLooping(), UIUpdate is AppFrame() and UIRender keeps the commands that Process() drew.
void UIUpdate(); void UIUpdate();
void UIRender(); void UIRender();
@@ -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 // 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 // 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 // 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) bool CPythonApplication::Create(PyObject*, const char*, int, int, int)
{ {
unsigned width = 0, height = 0; 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. // 40250 m_pyItem.Create() (PythonApplication.cpp:1253): registers dropitem.mse with CEffectManager.
CPythonItem::Instance().Create(); 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(); CPythonTextTail::Instance().Initialize();
return true; return true;
} }
@@ -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();
}
@@ -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);
}
@@ -44,6 +44,7 @@ enum
// The Python module initializers RunMainScript calls, each defined in its own Python*Module.cpp. // 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. // Listed here as 40250 lists them; only the ported ones are defined.
void initpack(); void initpack();
void initime();
void initapp(); void initapp();
void initchr(); void initchr();
void initchrmgr(); void initchrmgr();
@@ -28,6 +28,12 @@
#define VK_HOME 0x24 #define VK_HOME 0x24
#define VK_END 0x23 #define VK_END 0x23
#define VK_DELETE 0x2E #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). // winuser.h functions the platform layer implements (platform/ScriptLib/PythonBoot.cpp).
void PostQuitMessage(int nExitCode); void PostQuitMessage(int nExitCode);
+6
View File
@@ -71,6 +71,8 @@ void Metin2PythonHost::ui_mouse_button(int button, bool pressed, int x, int y) {
PythonBoot::UIMouseButton(button, pressed, x, y); PythonBoot::UIMouseButton(button, pressed, x, y);
} }
void Metin2PythonHost::ui_key(int key, bool pressed) { PythonBoot::UIKey(key, pressed); } 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(); } void Metin2PythonHost::ui_update() { PythonBoot::UIUpdate(); }
Array Metin2PythonHost::ui_render_commands() { Array Metin2PythonHost::ui_render_commands() {
PythonBoot::UIRender(); PythonBoot::UIRender();
@@ -264,6 +266,8 @@ void Metin2PythonHost::set_ui_size(int, int) {}
void Metin2PythonHost::ui_mouse_move(int, int) {} void Metin2PythonHost::ui_mouse_move(int, int) {}
void Metin2PythonHost::ui_mouse_button(int, bool, int, int) {} void Metin2PythonHost::ui_mouse_button(int, bool, int, int) {}
void Metin2PythonHost::ui_key(int, bool) {} void Metin2PythonHost::ui_key(int, bool) {}
void Metin2PythonHost::ui_char(int) {}
void Metin2PythonHost::ui_ime_key(int) {}
void Metin2PythonHost::ui_update() {} void Metin2PythonHost::ui_update() {}
Array Metin2PythonHost::ui_render_commands() { return Array(); } Array Metin2PythonHost::ui_render_commands() { return Array(); }
Ref<Image> Metin2PythonHost::memory_texture(const String &, int64_t) { return Ref<Image>(); } Ref<Image> Metin2PythonHost::memory_texture(const String &, int64_t) { return Ref<Image>(); }
@@ -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_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_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_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_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("ui_render_commands"), &Metin2PythonHost::ui_render_commands);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("memory_texture", "name", "known_revision"), ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("memory_texture", "name", "known_revision"),
+3
View File
@@ -38,6 +38,9 @@ public:
static void ui_mouse_move(int x, int y); 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_mouse_button(int button, bool pressed, int x, int y);
static void ui_key(int key, bool pressed); 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 void ui_update();
static godot::Array ui_render_commands(); static godot::Array ui_render_commands();
// The pixels of a "mem:<id>@<revision>" image command (the CGraphicFontTexture glyph pages) as an // The pixels of a "mem:<id>@<revision>" image command (the CGraphicFontTexture glyph pages) as an
@@ -655,6 +655,22 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
if (!ok) if (!ok)
return Fail(name + ": send GC_EXCHANGE"); return Fail(name + ": send GC_EXCHANGE");
} }
// CInputMain::Chat (input_main.cpp): the line comes back to the speaker (PacketAround includes them)
// as a CHAT_TYPE_TALKING GC_CHAT "<name> : <text>" carrying their vid and empire.
if (header == HDR_CG_CHAT)
{
CGChatHead head;
std::memcpy(&head, packet, sizeof(head));
const size_t text_len = size > sizeof(head) ? size - sizeof(head) : 0;
const std::string text(reinterpret_cast<const char*>(packet + sizeof(head)),
strnlen(reinterpret_cast<const char*>(packet + sizeof(head)), text_len));
Event(name + ":chat " + std::to_string(head.type) + " " + text);
const std::string line = std::string(kCharacterName) + " : " + text;
GCChatHead chat = {HDR_GC_CHAT, static_cast<std::uint16_t>(sizeof(GCChatHead) + line.size() + 1),
0 /* CHAT_TYPE_TALKING */, kMainVID, 1};
if (!c.Send(chat) || !c.Send(line.c_str(), line.size() + 1))
return Fail(name + ": send GC_CHAT");
}
if (header == HDR_CG_MOVE) if (header == HDR_CG_MOVE)
{ {
CGMove move; CGMove move;
+40 -4
View File
@@ -244,9 +244,29 @@ int main(int argc, char** argv)
char ports[128]; char ports[128];
std::snprintf(ports, sizeof(ports), "_auth_port, _game_port, _slot = %d, %d, %d", auth_port, game_port, slot); std::snprintf(ports, sizeof(ports), "_auth_port, _game_port, _slot = %d, %d, %d", auth_port, game_port, slot);
CHECK(py(ports)); CHECK(py(ports));
CHECK(py("_stream.SetConnectInfo(_host, _game_port, _host, _auth_port)\n" CHECK(py("_stream.SetConnectInfo(_host, _game_port, _host, _auth_port)"));
"_stream.curPhaseWindow.Connect(_login, _password)\n" // Typed into the login board through the 40250 IME (WM_CHAR -> CPythonIME::WMChar, WM_KEYDOWN ->
"del __main__._login, __main__._password")); // 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. // 3. The character list.
const bool selecting = pump_until(live ? 30 : 20, [&] { 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 // 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. // to the click that closed the dialog); spend it so step 8's click-release is not swallowed.
PythonBoot::UIMouseButton(1, false, 400, 520); 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 // 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) for (const auto& e : events)
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos && if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos &&
e.find(":pickup ") == 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); got.push_back(e);
CHECK(want == got); CHECK(want == got);
} }
+33 -3
View File
@@ -29,6 +29,25 @@ func _check(ok: bool, message: String) -> void:
func _py(source: String) -> bool: func _py(source: String) -> bool:
return Metin2PythonHost.run_line(source) == "" 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: func _pump_until(seconds: float, condition: Callable) -> bool:
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0) var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
while Time.get_ticks_msec() < deadline: while Time.get_ticks_msec() < deadline:
@@ -83,13 +102,24 @@ func run() -> void:
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)")) "assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"))
_check(at_login, "login window") _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" _check(_py("_env = os.environ\n"
+ "_slot = int(_env.get('MT_LIVE_SLOT', '0'))\n" + "_slot = int(_env.get('MT_LIVE_SLOT', '0'))\n"
+ "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), " + "_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" + "_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" + "_w = _stream.curPhaseWindow\n"
+ "del _env"), "LoginWindow.Connect") + "_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( var selecting := await _pump_until(30, func(): return _py(
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)")) "assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"))
_check(selecting, "character select") _check(selecting, "character select")
+30
View File
@@ -20,6 +20,19 @@ const KEY_TO_DIK := {
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0, KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
} }
# Win32 virtual-key codes of the keys EditLine.OnIMEKeyDown handles (WM_KEYDOWN → OnIMEKeyDown);
# letters and digits use their ASCII code, as VK_A..VK_Z / VK_0..VK_9 do.
const KEY_TO_VK := {
KEY_BACKSPACE: 0x08, KEY_TAB: 0x09, KEY_ENTER: 0x0d, KEY_KP_ENTER: 0x0d,
KEY_ESCAPE: 0x1b, KEY_SPACE: 0x20, KEY_END: 0x23, KEY_HOME: 0x24,
KEY_LEFT: 0x25, KEY_UP: 0x26, KEY_RIGHT: 0x27, KEY_DOWN: 0x28, KEY_DELETE: 0x2e,
}
# The control characters Windows turns these keys into (WM_CHAR); Godot reports them with unicode 0.
const KEY_TO_CHAR := {
KEY_BACKSPACE: 0x08, KEY_TAB: 0x09, KEY_ENTER: 0x0d, KEY_KP_ENTER: 0x0d, KEY_ESCAPE: 0x1b,
}
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the # "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
# "@<revision>" of a command's name moves on (a glyph was added to the page). # "@<revision>" of a command's name moves on (a glyph was added to the page).
var _memory_textures := {} 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) var dik: int = KEY_TO_DIK.get(event.physical_keycode, 0)
if dik != 0: if dik != 0:
Metin2PythonHost.ui_key(dik, event.pressed) 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 # 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) # shares one item; a masked image (CPythonMiniMap's terrain tiles under minimap_image_filter.dds)