From 8b4700f008a59be8f02f8d84a0331bec69f17cea Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 23 Sep 2026 23:57:34 +0900 Subject: [PATCH] 2V3-b: click on the ground to walk; read the terrain attribute map CScreen::SetCursorPosition and CPythonGraphic::SetCursorPosition are the 40250 bodies, so RenderGame builds the pick ray under the cursor each frame. CPythonBackground::GetPickingPoint* march that ray as CMapOutdoor::__PickTerrainHeight does, against the native CMapManager height (PR_FLOAT_TO_INT's x87 rounding becomes floor); object picking waits for CMapOutdoor. CMapManager::isAttrOn reads the packed //attr.atr as CMapOutdoor::isAttrOn + CTerrain::isAttrOn do, and CheckAdvancing with its collision checkers is verbatim (no map objects are registered yet). All of this stays in the pending/platform layer and is marked ADAPTED. port.login_flow clicks the ground below the actor after the key walk. The actor walks to the picked point (334 cm offline), and the fake server sees FUNC_MOVE, then FUNC_WAIT. It also checks that the actor's cell is free and that its terrain block has blocked cells. port.login_live against the real server passed (key 133 cm, click 296 cm), and python_game_render_test.sh passes offline. MT_TEST_MODE=python_ui now adds the 3D surface, so the port route can be played by hand. Co-Authored-By: Claude Opus 5.5 --- audit/history.jsonl | 1 + audit/port-map/EterLib/GrpScreen.cpp.json | 151 ++++++++++++++++++ .../EterPythonLib/PythonGraphic.cpp.json | 85 ++++++++++ audit/port-map/GameLib/MapManager.cpp.json | 55 +++++-- .../UserInterface/PythonBackground.cpp.json | 64 ++++++-- docs/PORT-PLAN.md | 3 +- extension/src/platform/EterLib/GrpScreen.cpp | 30 +++- .../platform/EterPythonLib/PythonGraphic.cpp | 6 +- .../platform/pending/GameLib/MapManager.cpp | 75 ++++++++- .../UserInterface/PythonBackground.cpp | 146 +++++++++++++++-- extension/tests/port_login_flow_test.cpp | 49 ++++++ project/client_main.gd | 8 +- 12 files changed, 627 insertions(+), 46 deletions(-) create mode 100644 audit/port-map/EterLib/GrpScreen.cpp.json create mode 100644 audit/port-map/EterPythonLib/PythonGraphic.cpp.json diff --git a/audit/history.jsonl b/audit/history.jsonl index 53afd1d5..bb3bb3b8 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -525,3 +525,4 @@ {"time":"2026-09-23T12:54:00Z","event":"port_round","unit":"PORT-PLAN 2V2-e.5: native map height and character light transition adapter","ported":[],"adapted":["CMapManager Warp-facing AtlasInfo/Setting.txt/height.raw/.msenv loader and 40250 triangle height interpolation","CPythonBackground native map selection/local coordinates/character and background SetLight","Python 3D surface consumes native map name/local frame, removes initial terrain elevation shift and rebuilds on map change","EterBase/Utils MIN/MAX/MINMAX and fMIN/fMAX/fMINMAX exact 40250 pure functions"],"divergent":["CMapOutdoor and placed-object height, attributes, fog, minimap and portal lifecycle remain unported; native map functions are marked ADAPTED/TODO rather than PORTED"],"needs_live":["MT_LIVE_* credentials absent; real-server terrain and cross-map warp screenshots pending"],"tests":["script/python_game_render_test.sh offline: PASS (4/4 native light0, finite view, native map, actor foot-ground delta 0.1 cm, textured ground screenshot)","extension/tests/port_utils_test.cpp: PASS","port_map.py check: 0 errors"],"todo":["full CMapOutdoor/MapManager/PythonBackground unit port","real-server and cross-map visual smoke","2V3 player/input/event/movement chain"]} {"time":"2026-09-23T13:09:00Z","event":"test_run","id":"PORT-PLAN 2V2-e.5","result":"PASS","tests":["script/port_gate.sh macos: PASS (12 port tests; port.login_live skipped without MT_LIVE_*)","script/python_game_render_test.sh: PASS (4/4 native light0, finite camera matrix, 0.1 cm foot-ground difference, 559 terrain texture contrasts)","port_map.py check: 0 errors","git diff --check: PASS"],"reason":"The first sandboxed port gate run denied localhost sockets; loopback-enabled rerun passed. Full CMapOutdoor and live/cross-map evidence remain outstanding."} {"time": "2026-09-23T14:45:00Z", "event": "port_round", "unit": "PORT-PLAN 2V3-a: player/input/event handler chain and Granny root motion", "ported": ["UserInterface/PythonPlayer.cpp", "UserInterface/PythonPlayerInput.cpp", "UserInterface/PythonPlayerInputKeyboard.cpp", "UserInterface/PythonPlayerInputMouse.cpp", "UserInterface/PythonPlayerEventHandler.cpp", "UserInterface/PythonPlayerSkill.cpp", "UserInterface/PythonPlayerModule.cpp", "UserInterface/GameType.cpp"], "adapted": ["GrannyUpdateModelMatrix constant-extraction root motion from TrackGroups[0] AccumulationFlags/LoopTranslation (libgr2)"], "divergent": ["VDA accumulation not implemented", "CPythonSkill/CItemData/CItemManager/CPythonItem callees remain pending stubs"], "needs_live": [], "tests": ["port.login_flow: DIK_UP walked 108 cm; fake server got CG_MOVE FUNC_MOVE then FUNC_WAIT at moved y", "port.login_live 192.168.21.203: PASS, walked 132 cm", "ctest all: PASS (login_live skipped without MT_LIVE_*)", "port_map.py check: 0 errors"], "todo": ["mouse click-to-move and attack", "CheckAdvancing/map attribute stubs", "delete legacy movement when default route switches to port"]} +{"time": "2026-09-23T15:00:00Z", "event": "port_round", "unit": "PORT-PLAN 2V3-b: click-to-move, terrain picking and attribute map", "ported": [], "adapted": ["CScreen::SetCursorPosition / CPythonGraphic::SetCursorPosition verbatim in platform layer", "CPythonBackground::GetPickingPoint* terrain half via CMapOutdoor::__PickTerrainHeight over native height", "CMapManager::isAttrOn via CMapOutdoor/CTerrain::isAttrOn over packed attr.atr", "CPythonBackground::CheckAdvancing + collision checkers verbatim (no map objects registered yet)"], "divergent": ["object picking and object collision wait for CMapOutdoor"], "needs_live": [], "tests": ["port.login_flow: click walked 334 cm, FUNC_MOVE..FUNC_WAIT; 17814 blocked cells", "port.login_live 192.168.21.203: PASS (key 133 cm, click 296 cm, 11406 blocked cells)", "script/python_game_render_test.sh offline: PASS", "ctest all: 19/19 (login_live skipped without env)", "port_map.py check: 0 errors"], "todo": ["switch client_main default to the port route and delete the legacy GDScript client"]} diff --git a/audit/port-map/EterLib/GrpScreen.cpp.json b/audit/port-map/EterLib/GrpScreen.cpp.json new file mode 100644 index 00000000..3a3aa51c --- /dev/null +++ b/audit/port-map/EterLib/GrpScreen.cpp.json @@ -0,0 +1,151 @@ +{ + "reference": "EterLib/GrpScreen.cpp", + "reference_sha256": "3efb287b5acc846f557ade7b828fa76026699a393407d1f0151f5fd5e1dcbebc", + "priority": "P4", + "contracts": [], + "functions": { + "CScreen::RenderLine3d": { + "status": "TODO" + }, + "CScreen::RenderBox3d": { + "status": "TODO" + }, + "CScreen::RenderBar3d": { + "status": "TODO" + }, + "CScreen::RenderGradationBar3d": { + "status": "TODO" + }, + "CScreen::RenderLineCube": { + "status": "TODO" + }, + "CScreen::RenderCube": { + "status": "TODO" + }, + "CScreen::RenderLine2d": { + "status": "TODO" + }, + "CScreen::RenderBox2d": { + "status": "TODO" + }, + "CScreen::RenderBar2d": { + "status": "TODO" + }, + "CScreen::RenderGradationBar2d": { + "status": "TODO" + }, + "CScreen::RenderCircle2d": { + "status": "TODO" + }, + "CScreen::RenderCircle3d": { + "status": "TODO" + }, + "CScreen::RenderD3DXMesh": { + "status": "TODO" + }, + "CScreen::RenderSphere": { + "status": "TODO" + }, + "CScreen::RenderCylinder": { + "status": "TODO" + }, + "CScreen::RenderTextureBox": { + "status": "TODO" + }, + "CScreen::RenderBillboard": { + "status": "TODO" + }, + "CScreen::DrawMinorGrid": { + "status": "TODO" + }, + "CScreen::DrawGrid": { + "status": "TODO" + }, + "CScreen::SetCursorPosition": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterLib/GrpScreen.cpp:CScreen::SetCursorPosition" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "40250 body verbatim in the platform layer (pick ray through the cursor)." + }, + "CScreen::GetCursorPosition": { + "status": "TODO" + }, + "CScreen::GetCursorXYPosition": { + "status": "TODO" + }, + "CScreen::GetCursorZPosition": { + "status": "TODO" + }, + "CScreen::GetPickingPosition": { + "status": "TODO" + }, + "CScreen::SetDiffuseColor": { + "status": "TODO" + }, + "CScreen::SetClearColor": { + "status": "TODO" + }, + "CScreen::SetClearDepth": { + "status": "TODO" + }, + "CScreen::SetClearStencil": { + "status": "TODO" + }, + "CScreen::ClearDepthBuffer": { + "status": "TODO" + }, + "CScreen::Clear": { + "status": "TODO" + }, + "CScreen::IsLostDevice": { + "status": "TODO" + }, + "CScreen::RestoreDevice": { + "status": "TODO" + }, + "CScreen::Begin": { + "status": "TODO" + }, + "CScreen::End": { + "status": "TODO" + }, + "CScreen::Show": { + "status": "TODO" + }, + "CScreen::ProjectPosition": { + "status": "TODO" + }, + "CScreen::UnprojectPosition": { + "status": "TODO" + }, + "CScreen::SetColorOperation": { + "status": "TODO" + }, + "CScreen::SetDiffuseOperation": { + "status": "TODO" + }, + "CScreen::SetBlendOperation": { + "status": "TODO" + }, + "CScreen::SetOneColorOperation": { + "status": "TODO" + }, + "CScreen::SetAddColorOperation": { + "status": "TODO" + }, + "CScreen::Identity": { + "status": "TODO" + }, + "CScreen::CScreen": { + "status": "TODO" + }, + "CScreen::~CScreen": { + "status": "TODO" + }, + "CScreen::BuildViewFrustum": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterPythonLib/PythonGraphic.cpp.json b/audit/port-map/EterPythonLib/PythonGraphic.cpp.json new file mode 100644 index 00000000..e704be5f --- /dev/null +++ b/audit/port-map/EterPythonLib/PythonGraphic.cpp.json @@ -0,0 +1,85 @@ +{ + "reference": "EterPythonLib/PythonGraphic.cpp", + "reference_sha256": "82fc01bd616637f92fcbc17e1eb2c7dd8ab78615695dc2aaabe9e5bf2f204632", + "priority": "P4", + "contracts": [], + "functions": { + "CPythonGraphic::Destroy": { + "status": "TODO" + }, + "CPythonGraphic::GetD3D": { + "status": "TODO" + }, + "CPythonGraphic::GetOrthoDepth": { + "status": "TODO" + }, + "CPythonGraphic::SetInterfaceRenderState": { + "status": "TODO" + }, + "CPythonGraphic::SetGameRenderState": { + "status": "TODO" + }, + "CPythonGraphic::SetCursorPosition": { + "status": "ADAPTED", + "impl": [ + "extension/src/platform/EterPythonLib/PythonGraphic.cpp:CPythonGraphic::SetCursorPosition" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "40250 body verbatim in the platform layer (pick ray through the cursor)." + }, + "CPythonGraphic::SetOmniLight": { + "status": "TODO" + }, + "CPythonGraphic::SetViewport": { + "status": "TODO" + }, + "CPythonGraphic::RestoreViewport": { + "status": "TODO" + }, + "CPythonGraphic::SetGamma": { + "status": "TODO" + }, + "GenScreenShotTag": { + "status": "TODO" + }, + "CPythonGraphic::SaveJPEG": { + "status": "TODO" + }, + "CPythonGraphic::SaveScreenShot": { + "status": "TODO" + }, + "CPythonGraphic::PushState": { + "status": "TODO" + }, + "CPythonGraphic::PopState": { + "status": "TODO" + }, + "CPythonGraphic::RenderImage": { + "status": "TODO" + }, + "CPythonGraphic::RenderAlphaImage": { + "status": "TODO" + }, + "CPythonGraphic::RenderCoolTimeBox": { + "status": "TODO" + }, + "CPythonGraphic::GenerateColor": { + "status": "TODO" + }, + "CPythonGraphic::RenderDownButton": { + "status": "TODO" + }, + "CPythonGraphic::RenderUpButton": { + "status": "TODO" + }, + "CPythonGraphic::GetAvailableMemory": { + "status": "TODO" + }, + "CPythonGraphic::CPythonGraphic": { + "status": "TODO" + }, + "CPythonGraphic::~CPythonGraphic": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/GameLib/MapManager.cpp.json b/audit/port-map/GameLib/MapManager.cpp.json index 5abd971c..ce138c27 100644 --- a/audit/port-map/GameLib/MapManager.cpp.json +++ b/audit/port-map/GameLib/MapManager.cpp.json @@ -12,13 +12,17 @@ }, "CMapManager::CMapManager": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::CMapManager"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::CMapManager" + ], "note": "Constructor matches 40250; kept in pending file until CMapOutdoor is ported.", "test": "project/python_game_render_test.gd" }, "CMapManager::~CMapManager": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::~CMapManager"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::~CMapManager" + ], "note": "Calls transition adapter Destroy, which frees pack-backed map state.", "test": "project/python_game_render_test.gd" }, @@ -30,25 +34,33 @@ }, "CMapManager::Initialize": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Initialize"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Initialize" + ], "note": "40250 AtlasInfo loading order; pack text parsed in transition adapter.", "test": "project/python_game_render_test.gd" }, "CMapManager::Create": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Create"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Create" + ], "note": "No CMapOutdoor allocation yet; Metin2World renders terrain.", "test": "project/python_game_render_test.gd" }, "CMapManager::Destroy": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Destroy"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Destroy" + ], "note": "Frees transition map and environment data; CMapOutdoor cleanup remains TODO.", "test": "project/python_game_render_test.gd" }, "CMapManager::Clear": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Clear"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::Clear" + ], "note": "Clears pack-backed terrain state, not CMapOutdoor.", "test": "project/python_game_render_test.gd" }, @@ -60,13 +72,17 @@ }, "CMapManager::LoadMap": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::LoadMap"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::LoadMap" + ], "note": "Loads Setting.txt, initial height tile and .msenv from 40250 packs for reachable Warp chain; outdoor scene lifecycle remains TODO.", "test": "project/python_game_render_test.gd" }, "CMapManager::IsMapReady": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::IsMapReady"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::IsMapReady" + ], "note": "Reports pack-backed transition map readiness instead of CMapOutdoor::IsReady.", "test": "project/python_game_render_test.gd" }, @@ -81,7 +97,9 @@ }, "CMapManager::GetHeight": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::GetHeight"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::GetHeight" + ], "note": "Uses the 40250 terrain triangle interpolation on packed height.raw; placed-object height remains TODO.", "test": "project/python_game_render_test.gd" }, @@ -93,13 +111,17 @@ }, "CMapManager::BeginEnvironment": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::BeginEnvironment"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::BeginEnvironment" + ], "note": "Applies .msenv material/background light through RecordingDevice; fog and full CMapOutdoor environment remain TODO.", "test": "project/python_game_render_test.gd" }, "CMapManager::EndEnvironment": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::EndEnvironment"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::EndEnvironment" + ], "note": "Restores recorded lighting state; full environment state restoration remains TODO.", "test": "project/python_game_render_test.gd" }, @@ -158,7 +180,12 @@ "status": "TODO" }, "CMapManager::isAttrOn": { - "status": "TODO" + "status": "ADAPTED", + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::isAttrOn" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "CMapOutdoor::isAttrOn + CTerrain::isAttrOn verbatim over native attr.atr tiles (Setting.txt map size instead of the CTerrain list)." }, "CMapManager::GetAttr": { "status": "TODO" @@ -177,7 +204,9 @@ }, "CMapManager::__LoadMapInfoVector": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::__LoadMapInfoVector"], + "impl": [ + "extension/src/platform/pending/GameLib/MapManager.cpp:CMapManager::__LoadMapInfoVector" + ], "note": "Reads packed AtlasInfo bounds with 40250 terrain tile size; parser is transitional.", "test": "project/python_game_render_test.gd" } diff --git a/audit/port-map/UserInterface/PythonBackground.cpp.json b/audit/port-map/UserInterface/PythonBackground.cpp.json index acd3723a..c062e164 100644 --- a/audit/port-map/UserInterface/PythonBackground.cpp.json +++ b/audit/port-map/UserInterface/PythonBackground.cpp.json @@ -48,7 +48,9 @@ }, "CPythonBackground::Initialize": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Initialize"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Initialize" + ], "note": "40250 locale AtlasInfo setup, backed by transitional MapManager loader.", "test": "project/python_game_render_test.gd" }, @@ -56,26 +58,45 @@ "status": "TODO" }, "CPythonBackground::GetPickingPoint": { - "status": "TODO" + "status": "ADAPTED", + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GetPickingPoint" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "40250 body over the cursor ray; CMapOutdoor::GetPickingPoint forwarded to GetPickingPointWithRay." }, "CPythonBackground::GetPickingPointWithRay": { - "status": "TODO" + "status": "ADAPTED", + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GetPickingPointWithRay" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "Terrain half only: CMapOutdoor object pick (FGetPickingPoint over area objects) needs the unported CMapOutdoor." }, "CPythonBackground::GetPickingPointWithRayOnlyTerrain": { - "status": "TODO" + "status": "ADAPTED", + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GetPickingPointWithRayOnlyTerrain" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "CMapOutdoor::GetPickingPointWithRayOnlyTerrain/__PickTerrainHeight verbatim over the native CMapManager height; PR_FLOAT_TO_INT as floor." }, "CPythonBackground::GetLightDirection": { "status": "TODO" }, "CPythonBackground::Destroy": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Destroy"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Destroy" + ], "note": "Native map state cleanup; SnowEnvironment destruction remains TODO.", "test": "project/python_game_render_test.gd" }, "CPythonBackground::Create": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Create"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Create" + ], "note": "Calls transition MapManager::Create; property and snow initialization remain TODO.", "test": "project/python_game_render_test.gd" }, @@ -104,7 +125,12 @@ "status": "TODO" }, "CPythonBackground::CheckAdvancing": { - "status": "TODO" + "status": "ADAPTED", + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::CheckAdvancing" + ], + "test": "extension/tests/port_login_flow_test.cpp", + "note": "40250 body and collision checkers verbatim, kept in the pending unit; no map objects are registered with CCullingManager until CMapOutdoor is ported." }, "CPythonBackground::RenderSky": { "status": "TODO" @@ -132,25 +158,33 @@ }, "CPythonBackground::SetCharacterDirLight": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::SetCharacterDirLight"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::SetCharacterDirLight" + ], "note": "Statement-equivalent 40250 function, kept in pending translation unit until the whole unit is ported; reachable RenderGame draws carry native light0.", "test": "project/python_game_render_test.gd" }, "CPythonBackground::SetBackgroundDirLight": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::SetBackgroundDirLight"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::SetBackgroundDirLight" + ], "note": "Statement-equivalent 40250 light setter, still in transitional pending translation unit.", "test": "project/python_game_render_test.gd" }, "CPythonBackground::GlobalPositionToLocalPosition": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GlobalPositionToLocalPosition"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GlobalPositionToLocalPosition" + ], "note": "40250 base subtraction, kept in transitional pending translation unit.", "test": "project/python_game_render_test.gd" }, "CPythonBackground::LocalPositionToGlobalPosition": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::LocalPositionToGlobalPosition"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::LocalPositionToGlobalPosition" + ], "note": "40250 base addition, kept in transitional pending translation unit.", "test": "project/python_game_render_test.gd" }, @@ -162,7 +196,9 @@ }, "CPythonBackground::Warp": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Warp"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::Warp" + ], "note": "Native AtlasInfo map selection and local coordinates reach pack-backed map; minimap, dungeon portals, target effects and full CMapOutdoor lifecycle remain TODO.", "test": "project/python_game_render_test.gd" }, @@ -174,7 +210,9 @@ }, "CPythonBackground::GetWarpMapName": { "status": "ADAPTED", - "impl": ["extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GetWarpMapName"], + "impl": [ + "extension/src/platform/pending/UserInterface/PythonBackground.cpp:CPythonBackground::GetWarpMapName" + ], "note": "Returns current native Warp map name; still in pending translation unit.", "test": "project/python_game_render_test.gd" }, diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index 71cfac4c..80bd40e5 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -429,7 +429,8 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 4. **2V3 — 本地移动**:加入 `PythonPlayer`、`PythonPlayerInput*`、`PythonPlayerEventHandler`、 `InstanceBaseMovement`、`ActorInstance{Motion,Event,Position,Rotation,CollisionDetection}` 的实际依赖闭包。验收为输入经过新路径 改变位置/朝向、发送与 40250 相同的移动封包,并在离线边界测试和真服 smoke 中通过。接通时删除对应 legacy 玩法逻辑。 - 2V3-a(2026-09-23):`PythonPlayer`、`PythonPlayerInput`/`InputKeyboard`/`InputMouse`、`PythonPlayerEventHandler`、`PythonPlayerSkill`、`PythonPlayerModule`、`GameType` 原样移植(port-map 标 PORTED),`GameplayModules.cpp` 的 `player` 桩删除,`initPlayer()` 按 40250 顺序在 `initchrmgr()` 后调用;`pending/UserInterface/{PythonPlayer,GameType}.cpp` 替身删除,其余被调方(`CPythonSkill`、`CItemData`、`CItemManager`、`CPythonItem` 等)进 `pending`。64 位修正:`playerGetEmotionIconImage` 用 `Py_BuildPointer`,`PyDict_Next` 用 `Py_ssize_t`。根运动:Granny 运行时的 `GrannyUpdateModelMatrix` 原为直通,按 TrackGroups[0] 的 `AccumulationFlags`(AccumulationExtracted)/`LoopTranslation`(libgr2 新读出,战士 run = (0,-300,0),同 .msa `Accumulation`)按控制器本地时钟推进与权重累加位移(Delta × ModelMatrix);VDA 抽取未实现。`port.login_flow` 按 DIK_UP 经 `game.GameWindow` → `player.SetSingleDIKKeyState` 行走 >100 cm,假服解析 CG 包,收到 `CG_MOVE` FUNC_MOVE 后 FUNC_WAIT 且坐标变化;`port.login_live` 2026-09-23 对 192.168.21.203 走 132 cm 通过。待办:鼠标点地移动/攻击、`CheckAdvancing` 与地图属性替身、legacy 移动逻辑随默认路由切到 `port` 删除。 + 2V3-a(2026-09-23):`PythonPlayer`、`PythonPlayerInput`/`InputKeyboard`/`InputMouse`、`PythonPlayerEventHandler`、`PythonPlayerSkill`、`PythonPlayerModule`、`GameType` 原样移植(port-map 标 PORTED),`GameplayModules.cpp` 的 `player` 桩删除,`initPlayer()` 按 40250 顺序在 `initchrmgr()` 后调用;`pending/UserInterface/{PythonPlayer,GameType}.cpp` 替身删除,其余被调方(`CPythonSkill`、`CItemData`、`CItemManager`、`CPythonItem` 等)进 `pending`。64 位修正:`playerGetEmotionIconImage` 用 `Py_BuildPointer`,`PyDict_Next` 用 `Py_ssize_t`。根运动:Granny 运行时的 `GrannyUpdateModelMatrix` 原为直通,按 TrackGroups[0] 的 `AccumulationFlags`(AccumulationExtracted)/`LoopTranslation`(libgr2 新读出,战士 run = (0,-300,0),同 .msa `Accumulation`)按控制器本地时钟推进与权重累加位移(Delta × ModelMatrix);VDA 抽取未实现。`port.login_flow` 按 DIK_UP 经 `game.GameWindow` → `player.SetSingleDIKKeyState` 行走 >100 cm,假服解析 CG 包,收到 `CG_MOVE` FUNC_MOVE 后 FUNC_WAIT 且坐标变化;`port.login_live` 2026-09-23 对 192.168.21.203 走 132 cm 通过。(2V3-a 的待办见 2V3-b。) + 2V3-b(2026-09-23):鼠标点地移动。`CScreen::SetCursorPosition`(GrpScreen.cpp:487)与 `CPythonGraphic::SetCursorPosition` 原样实现,`RenderGame` 每帧用光标建拾取射线;`CPythonBackground::GetPickingPoint`/`GetPickingPointWithRay`/`GetPickingPointWithRayOnlyTerrain` 按 `CMapOutdoor::__PickTerrainHeight` 原样沿射线步进,地形高度取 native `CMapManager::GetHeight`(`PR_FLOAT_TO_INT` 的 x87 取整按 floor),物件拾取随 `CMapOutdoor` 移植再接。`CMapManager::isAttrOn` 按 `CMapOutdoor::isAttrOn`+`CTerrain::isAttrOn` 读包内 `//attr.atr`(阻挡/水/禁 PK),`CheckAdvancing` 及两个碰撞检查器原样(地图物件未注册到 `CCullingManager` 前不会阻挡)。以上在 pending/platform 层,port-map 标 ADAPTED。`port.login_flow` 在按键行走后左键点击角色下方地面:`game.GameWindow.OnMouseLeftButtonDown` → `player.SetMouseState` → 角色走到拾取点(离线 334 cm),假服收到 FUNC_MOVE … FUNC_WAIT;另断言角色所在格不阻挡、所在地形块有阻挡格(离线 17814 格)。`port.login_live` 2026-09-23 对 192.168.21.203 通过(按键 133 cm、点地 296 cm、11406 阻挡格),`python_game_render_test.sh` 离线通过。`client_main` 的 `MT_TEST_MODE=python_ui` 加上 `python_3d_surface.gd`,新路径可手动登录进游戏走动。待办:默认路由切到 `port`(当前 `client_main` 默认仍走 GDScript `AppFlow`)后整体删除 legacy;攻击/拾取物品等 P1 战斗交互属批次 2 之后。 每个切片都必须同时跑 legacy/port 双路回归;只有 2V3 稳定并把默认路由切到 `port` 后,才能删除总开关和剩余 legacy 宿主。 diff --git a/extension/src/platform/EterLib/GrpScreen.cpp b/extension/src/platform/EterLib/GrpScreen.cpp index b3596e04..ddd98b24 100644 --- a/extension/src/platform/EterLib/GrpScreen.cpp +++ b/extension/src/platform/EterLib/GrpScreen.cpp @@ -244,9 +244,35 @@ auto CScreen::SetClearStencil(DWORD) -> void MT_PLATFORM_STUB(); } -auto CScreen::SetCursorPosition(int, int, int, int) -> void +// 40250 GrpScreen.cpp:487, verbatim: the pick ray through the cursor. +void CScreen::SetCursorPosition(int x, int y, int hres, int vres) { - MT_PLATFORM_STUB(); + D3DXVECTOR3 v; + v.x = -(((2.0f * x) / hres) - 1) / ms_matProj._11; + v.y = (((2.0f * y) / vres) - 1) / ms_matProj._22; + v.z = 1.0f; + + D3DXMATRIX matViewInverse=ms_matInverseView; + //D3DXMatrixInverse(&matViewInverse, NULL, &ms_matView); + + ms_vtPickRayDir.x = v.x * matViewInverse._11 + + v.y * matViewInverse._21 + + v.z * matViewInverse._31; + + ms_vtPickRayDir.y = v.x * matViewInverse._12 + + v.y * matViewInverse._22 + + v.z * matViewInverse._32; + + ms_vtPickRayDir.z = v.x * matViewInverse._13 + + v.y * matViewInverse._23 + + v.z * matViewInverse._33; + + ms_vtPickRayOrig.x = matViewInverse._41; + ms_vtPickRayOrig.y = matViewInverse._42; + ms_vtPickRayOrig.z = matViewInverse._43; + + ms_Ray.SetStartPoint(ms_vtPickRayOrig); + ms_Ray.SetDirection(-ms_vtPickRayDir, 51200.0f); } auto CScreen::GetCursorPosition(float *, float *, float *) -> bool diff --git a/extension/src/platform/EterPythonLib/PythonGraphic.cpp b/extension/src/platform/EterPythonLib/PythonGraphic.cpp index d7297fb5..51a589f5 100644 --- a/extension/src/platform/EterPythonLib/PythonGraphic.cpp +++ b/extension/src/platform/EterPythonLib/PythonGraphic.cpp @@ -36,7 +36,11 @@ long CPythonGraphic::GenerateColor(float r, float g, float b, float a) { return void CPythonGraphic::RenderUpButton(float, float, float, float) { MT_PLATFORM_STUB(); } void CPythonGraphic::RenderDownButton(float, float, float, float) { MT_PLATFORM_STUB(); } bool CPythonGraphic::SaveScreenShot(const char*) { MT_PLATFORM_STUB(); return false; } -void CPythonGraphic::SetCursorPosition(int, int) { MT_PLATFORM_STUB(); } +// 40250 PythonGraphic.cpp:52. +void CPythonGraphic::SetCursorPosition(int x, int y) +{ + CScreen::SetCursorPosition(x, y, ms_iWidth, ms_iHeight); +} DWORD CPythonGraphic::GetAvailableMemory() { MT_PLATFORM_STUB(); return 0; } void CPythonGraphic::SetGamma(float) { MT_PLATFORM_STUB(); } diff --git a/extension/src/platform/pending/GameLib/MapManager.cpp b/extension/src/platform/pending/GameLib/MapManager.cpp index 7fc77bfb..4ac59be4 100644 --- a/extension/src/platform/pending/GameLib/MapManager.cpp +++ b/extension/src/platform/pending/GameLib/MapManager.cpp @@ -27,6 +27,7 @@ struct NativeMapState { fmt::MapSetting setting; std::string name; std::map, fmt::HeightMap> heights; + std::map, std::vector> attrs; // attr.atr payload, empty when unreadable bool ready = false; }; std::map s_native_maps; @@ -61,6 +62,28 @@ bool load_height_tile(NativeMapState &state, int tx, int ty) state.heights.emplace(key, std::move(map)); return true; } + +// attr.atr: {u16 magic=2634, u16 w=256, u16 h=256} + 256*256 bytes (40250 CTerrainImpl::LoadAttrMap). +const std::vector *attr_tile(NativeMapState &state, int tx, int ty) +{ + if (tx < 0 || ty < 0 || tx >= state.setting.map_size_x || ty >= state.setting.map_size_y) + return nullptr; + const auto key = std::make_pair(tx, ty); + auto found = state.attrs.find(key); + if (found == state.attrs.end()) + { + char tile[16]; + snprintf(tile, sizeof(tile), "%03d%03d", tx, ty); + std::string bytes; + std::vector attr; + const size_t payload = size_t(CTerrainImpl::ATTRMAP_XSIZE) * CTerrainImpl::ATTRMAP_YSIZE; + if (pack_bytes(state.name + "/" + tile + "/attr.atr", bytes) && bytes.size() == 6 + payload && + (static_cast(bytes[0]) | (static_cast(bytes[1]) << 8)) == 2634) + attr.assign(bytes.begin() + 6, bytes.end()); + found = state.attrs.emplace(key, std::move(attr)).first; + } + return found->second.empty() ? nullptr : &found->second; +} } auto CMapManager::GetHeight(float fx, float fy) -> float @@ -194,16 +217,56 @@ auto CMapManager::GetShadowMapColor(float, float) -> DWORD return mt_platform_stub_return(); } -auto CMapManager::isAttrOn(float, float, BYTE) -> bool +// 40250 MapManager.cpp:506 → CMapOutdoor::isAttrOn (MapOutdoor.cpp:1202). +bool CMapManager::isAttrOn(float fX, float fY, BYTE byAttr) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (!IsMapReady()) + return false; + + // PORT: PR_FLOAT_TO_INT is x87 fistp (round to nearest) corrected down, i.e. floor. + return isAttrOn(int(std::floor(fX)), int(std::floor(fY)), byAttr); } -auto CMapManager::isAttrOn(int, int, BYTE) -> bool +// 40250 MapManager.cpp:524 → CMapOutdoor::isAttrOn (MapOutdoor.cpp:1220) → CTerrain::isAttrOn (AreaTerrain.cpp:487). +// PORT: CMapOutdoor/CTerrain are not ported; the terrain count and the attribute map come from the native map +// state (Setting.txt MapSize, //attr.atr) instead of the loaded CTerrain list. +bool CMapManager::isAttrOn(int iX, int iY, BYTE byAttr) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (!IsMapReady()) + return false; + auto state = s_native_maps.find(this); + if (state == s_native_maps.end() || !state->second.ready) + return false; + const int countX = state->second.setting.map_size_x, countY = state->second.setting.map_size_y; + if (iX < 0 || iY < 0 || iX > countX * CTerrainImpl::TERRAIN_XSIZE || iY > countY * CTerrainImpl::TERRAIN_YSIZE) + return false; + + WORD wTerrainCoordX, wTerrainCoordY; + wTerrainCoordX = iX / CTerrainImpl::TERRAIN_XSIZE; + wTerrainCoordY = iY / CTerrainImpl::TERRAIN_YSIZE; + + const std::vector *attr = attr_tile(state->second, wTerrainCoordX, wTerrainCoordY); + if (!attr) + return false; + + WORD wLocalX, wLocalY; + wLocalX = (iX - wTerrainCoordX * CTerrainImpl::TERRAIN_XSIZE) / (CTerrainImpl::HALF_CELLSCALE); + wLocalY = (iY - wTerrainCoordY * CTerrainImpl::TERRAIN_YSIZE) / (CTerrainImpl::HALF_CELLSCALE); + + if (wLocalX >= CTerrainImpl::ATTRMAP_XSIZE || wLocalY >= CTerrainImpl::ATTRMAP_YSIZE) + return false; + + BYTE byMapAttr = (*attr)[wLocalY * CTerrainImpl::ATTRMAP_XSIZE + wLocalX]; + + if ( byAttr < 16 ) + return (byMapAttr & byAttr) ? true : false; + else + { + if ( byAttr/16 == byMapAttr/16) + return true; + else + return false; + } } auto CMapManager::BeginEnvironment() -> void diff --git a/extension/src/platform/pending/UserInterface/PythonBackground.cpp b/extension/src/platform/pending/UserInterface/PythonBackground.cpp index 26acbf6c..c5a0def9 100644 --- a/extension/src/platform/pending/UserInterface/PythonBackground.cpp +++ b/extension/src/platform/pending/UserInterface/PythonBackground.cpp @@ -3,6 +3,7 @@ // (docs/PORT-PLAN.md). #include "UserInterface/StdAfx.h" #include "UserInterface/PythonBackground.h" +#include "UserInterface/InstanceBase.h" #include "EterLib/StateManager.h" #include "../../PlatformStub.h" @@ -107,16 +108,143 @@ CPythonBackground::~CPythonBackground() MT_PLATFORM_STUB(); } -auto CPythonBackground::CheckAdvancing(CInstanceBase *) -> bool +// 40250 PythonBackground.cpp:547-608, verbatim. No map objects are registered with CCullingManager until +// CMapOutdoor is ported, so the checkers see no opponent and advancing is never blocked. +struct CollisionChecker { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + bool isBlocked; + CInstanceBase* pInstance; + CollisionChecker(CInstanceBase* pInstance) : pInstance(pInstance), isBlocked(false) {} + void operator () (CGraphicObjectInstance* pOpponent) + { + if (isBlocked) + return; + + if (!pOpponent) + return; + + if (pInstance->IsBlockObject(*pOpponent)) + isBlocked=true; + } +}; + +struct CollisionAdjustChecker +{ + bool isBlocked; + CInstanceBase* pInstance; + CollisionAdjustChecker(CInstanceBase* pInstance) : pInstance(pInstance), isBlocked(false) {} + void operator () (CGraphicObjectInstance* pOpponent) + { + if (!pOpponent) + return; + + if (pInstance->AvoidObject(*pOpponent)) + isBlocked=true; + } +}; + +bool CPythonBackground::CheckAdvancing(CInstanceBase * pInstance) +{ + if (!IsMapReady()) + return true; + + Vector3d center; + float radius; + pInstance->GetGraphicThingInstanceRef().GetBoundingSphere(center,radius); + + CCullingManager & rkCullingMgr = CCullingManager::Instance(); + + CollisionAdjustChecker kCollisionAdjustChecker(pInstance); + rkCullingMgr.ForInRange(center, radius, &kCollisionAdjustChecker); + if (kCollisionAdjustChecker.isBlocked) + { + CollisionChecker kCollisionChecker(pInstance); + rkCullingMgr.ForInRange(center, radius, &kCollisionChecker); + if (kCollisionChecker.isBlocked) + { + pInstance->BlockMovement(); + return true; + } + else + { + pInstance->NEW_MoveToDestPixelPositionDirection(pInstance->NEW_GetDstPixelPositionRef()); + } + return false; + } + return false; } -auto CPythonBackground::GetPickingPointWithRayOnlyTerrain(const CRay &, D3DXVECTOR3 *) -> bool +namespace { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); +// 40250 CMapOutdoor::__PickTerrainHeight (MapOutdoor.cpp:448), marching the ray against the terrain height. +// PORT: CMapOutdoor is not ported; the terrain is the native CMapManager height map, and GetTerrainNum/ +// GetTerrainPointer become "the map is ready" (GetHeight reads the height tile under the point). +bool PickTerrainHeight(CMapManager& rkMap, float& fPos, const D3DXVECTOR3& v3Start, const D3DXVECTOR3& v3End, float fStep, + float fRayRange, float fLimitRange, D3DXVECTOR3* pv3Pick) +{ + D3DXVECTOR3 v3CurPos; + + float fRayRangeInv=1.0f/fRayRange; + while (fPos < fRayRange && fPos= v3CurPos.z) + { + *pv3Pick = v3CurPos; + return true; + } + else + { + fMultiplier = fMAX(1.0f, 0.01f * ( v3CurPos.z - fMapHeight ) ); + } + } + fPos += fStep * fMultiplier; + } + + return false; +} +} + +// 40250 CMapOutdoor::GetPickingPointWithRayOnlyTerrain (MapOutdoor.cpp:549). +bool CPythonBackground::GetPickingPointWithRayOnlyTerrain(const CRay & rRay, D3DXVECTOR3 * v3IntersectPt) +{ + bool bTerrainPick = false; + D3DXVECTOR3 v3TerrainPick; + + D3DXVECTOR3 v3Start, v3End, v3Dir; + float fRayRange; + rRay.GetStartPoint(&v3Start); + rRay.GetDirection(&v3Dir, &fRayRange); + rRay.GetEndPoint(&v3End); + + float fPos = 0.0f; + bTerrainPick=true; + if (!PickTerrainHeight(*this, fPos, v3Start, v3End, 5.0f, fRayRange, 5000.0f, &v3TerrainPick)) + if (!PickTerrainHeight(*this, fPos, v3Start, v3End, 10.0f, fRayRange, 10000.0f, &v3TerrainPick)) + if (!PickTerrainHeight(*this, fPos, v3Start, v3End, 100.0f, fRayRange, 100000.0f, &v3TerrainPick)) + bTerrainPick=false; + + if (bTerrainPick) + { + *v3IntersectPt = v3TerrainPick; + return true; + } + + return false; +} + +// 40250 CMapOutdoor::GetPickingPointWithRay (MapOutdoor.cpp:484). +// PORT: the object pick (FGetPickingPoint over CCullingManager) needs CMapOutdoor's area objects; only the +// terrain half runs until the map is ported. +bool CPythonBackground::GetPickingPointWithRay(const CRay & rRay, D3DXVECTOR3 * v3IntersectPt) +{ + return GetPickingPointWithRayOnlyTerrain(rRay, v3IntersectPt); } // 40250 PythonBackground.cpp:173 (CPythonApplication::RenderGame's SetPerspective); m_pkMap stays null @@ -207,8 +335,8 @@ void CPythonBackground::Initialize() CMapManager::Initialize(); } -auto CPythonBackground::GetPickingPoint(D3DXVECTOR3 *) -> bool +// 40250 PythonBackground.cpp:263 → CMapOutdoor::GetPickingPoint (MapOutdoor.cpp:443): the cursor ray. +bool CPythonBackground::GetPickingPoint(D3DXVECTOR3 * v3IntersectPt) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return GetPickingPointWithRay(ms_Ray, v3IntersectPt); } diff --git a/extension/tests/port_login_flow_test.cpp b/extension/tests/port_login_flow_test.cpp index 479f72f8..7afbdceb 100644 --- a/extension/tests/port_login_flow_test.cpp +++ b/extension/tests/port_login_flow_test.cpp @@ -48,6 +48,7 @@ #include "UserInterface/StdAfx.h" #include "UserInterface/PythonCharacterManager.h" #include "UserInterface/InstanceBase.h" +#include "UserInterface/PythonBackground.h" #include "EterGrnLib/StdAfx.h" #include "EterGrnLib/LODController.h" #include "EterGrnLib/ModelInstance.h" @@ -413,6 +414,54 @@ int main(int argc, char** argv) for (const auto& e : m) std::printf("port_login_flow_test: %s\n", e.c_str()); } + + // The terrain attribute map (attr.atr) under the actor: CInstanceBase's block/water/ban-PK checks read it + // through CPythonBackground::isAttrOn. The actor stands on a free cell; its 256x256 tile has blocked cells. + if (main_instance) + { + TPixelPosition here = {}; + main_instance->NEW_GetPixelPosition(&here); + CPythonBackground& bg = CPythonBackground::Instance(); + CHECK(!bg.isAttrOn(here.x, here.y, CTerrainImpl::ATTRIBUTE_BLOCK)); + const int tx = int(here.x) / CTerrainImpl::TERRAIN_XSIZE * CTerrainImpl::TERRAIN_XSIZE; + const int ty = int(here.y) / CTerrainImpl::TERRAIN_YSIZE * CTerrainImpl::TERRAIN_YSIZE; + int blocked = 0; + for (int y = 0; y < CTerrainImpl::TERRAIN_YSIZE; y += CTerrainImpl::HALF_CELLSCALE) + for (int x = 0; x < CTerrainImpl::TERRAIN_XSIZE; x += CTerrainImpl::HALF_CELLSCALE) + blocked += bg.isAttrOn(tx + x, ty + y, CTerrainImpl::ATTRIBUTE_BLOCK) ? 1 : 0; + std::printf("port_login_flow_test: %d blocked attribute cells in the actor's terrain\n", blocked); + CHECK(blocked > 0); + } + + // 8. (批次 2V3) Click-to-move: a left click on the ground below the actor reaches game.GameWindow's + // OnMouseLeftButtonDown → player.SetMouseState(MBT_LEFT, MBS_PRESS); CPythonPlayer picks the ground + // through the cursor ray (CScreen::SetCursorPosition → CPythonBackground::GetPickingPoint) and the + // actor walks to it, sending CG_MOVE FUNC_MOVE, then FUNC_WAIT on arrival. + const size_t moves_before = server ? server->Events().size() : 0; + if (main_instance) + main_instance->NEW_GetPixelPosition(&start); + PythonBoot::UIMouseButton(1, true, 400, 520); + pump_until(0.1, [] { return false; }); + PythonBoot::UIMouseButton(1, false, 400, 520); + CHECK(main_instance && pump_until(3, [&] { return walked() > 100.0f; })); + CHECK(main_instance && pump_until(5, [&] { return !main_instance->IsWalking(); })); + std::printf("port_login_flow_test: click walked %.0f cm\n", main_instance ? walked() : 0.0f); + if (server) + { + auto click_moves = [&] { + std::vector out; + const auto events = server->Events(); + for (size_t i = moves_before; i < events.size(); ++i) + if (events[i].rfind("game2:move ", 0) == 0) + out.push_back(events[i]); + return out; + }; + CHECK(pump_until(2, [&] { auto m = click_moves(); return m.size() >= 2 && m.back().rfind("game2:move 0 ", 0) == 0; })); + const auto m = click_moves(); + CHECK(!m.empty() && m.front().rfind("game2:move 1 0 ", 0) == 0); + for (const auto& e : m) + std::printf("port_login_flow_test: %s\n", e.c_str()); + } } if (server) { diff --git a/project/client_main.gd b/project/client_main.gd index 97c2241b..c98078ba 100644 --- a/project/client_main.gd +++ b/project/client_main.gd @@ -36,12 +36,18 @@ func _ready() -> void: get_tree().quit(1 if fail else 0) return # MT_TEST_MODE=python_ui:40250 system.py 的界面直接画在 Godot Canvas 上(PORT-PLAN 2V0-f), - # Logo → 登录阶段,可用鼠标键盘操作;app.Exit() 退出进程。 + # Logo → 登录 → 选人 → 进游戏,RenderGame 的 3D 绘制由 python_3d_surface.gd 画出(2V2-e), + # 方向键/鼠标点地走动走 40250 原路径(2V3);app.Exit() 退出进程。 if OS.get_environment("MT_TEST_MODE") == "python_ui": AssetPack.ensure() var surface: Control = preload("res://python_ui_surface.gd").new() surface.name = "PythonUISurface" add_child(surface) + # The UI surface runs CPythonApplication::Process in its _process; the 3D surface reads that + # frame's draws after it, so it comes second in the tree. + var world: Node3D = preload("res://python_3d_surface.gd").new() + world.name = "Python3DSurface" + add_child(world) var err: String = surface.run_app() if err != "": printerr("python_ui: " + err)