diff --git a/audit/history.jsonl b/audit/history.jsonl index 23c3ea54..53db4269 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -538,3 +538,4 @@ {"time": "2026-09-24T12:00:00Z", "event": "port_round", "unit": "batch 3-a: CPythonSkill + skill module + EterBase/Poly", "ported": ["UserInterface/PythonSkill.cpp", "EterBase/Poly/Base.cpp", "EterBase/Poly/Poly.cpp", "EterBase/Poly/SymTable.cpp", "EterBase/Poly/Symbol.cpp"], "adapted": ["PythonSkill.cpp: icon handle functions (Py_BuildPointer), skillGetGradeData (varargs struct)", "PythonItemModule.cpp: itemGetIconInstance/itemDeleteIconInstance (Py_BuildPointer)"], "divergent": [], "needs_live": [], "tests": ["port.login_flow: skill 1 name/type/cooltime after LoadLocaleData; grade icon + icon instance in game", "port.login_live 192.168.21.203 passes", "ctest 19/19, python_game_render_test PASS", "port_map check 0 errors"], "todo": ["EffectLib", "CMapOutdoor", "default route switch"]} {"time": "2026-09-24T14:00:00Z", "event": "port_round", "unit": "batch 3-b: EffectLib + EterBase/Utils platform bodies", "ported": ["EffectLib/*.cpp (17 units, 251 functions)"], "adapted": ["EffectLib/Type.h: comment line-splice fix", "EterBase/Utils.cpp (platform): 23 functions, 40250 bodies with POSIX swaps", "ScriptLib/Resource.cpp (platform): mde/mdatr resource types"], "divergent": [], "needs_live": [], "tests": ["port.utils: path/split/argv helpers", "port.login_flow: level_up.mse CreateEffect -> alpha-blended levelup_1 particle draws"]} {"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"]} diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index 63398f56..bff2374b 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -97,14 +97,16 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 ### 迁移方式:始终保持可玩 -旧 GDScript 路径和新的 ported 路径在切片接通前共存,由构建/运行开关选择(例如 `MT_PORT_PATH=legacy|port`,默认 -`legacy`)。规则: +旧 GDScript 路径和新的 ported 路径共存,由运行开关 `MT_PORT_PATH=legacy|port` 选择(`project/client_main.gd`)。 +2026-09-24 起默认 `port`;未设开关而缺 `Metin2PythonHost` 或 40250 `Client/pack` 时退回 legacy 并告警,显式 `port` +则直接报错退出;`MT_TEST_MODE=smoke/playable`(AppFlow 真服测试)固定走 legacy,`MT_TEST_MODE=python_ui` 保留为 +`port` 的别名。规则: - 新单元先编译进 `port_logic`,但**只有被新路径的运行时调用到**,才算实现("运行时不可达不算实现"同样适用于新代码); 没有接通前,port-map 状态保持 `TODO`,`note` 写"已照抄,未接通"。 - 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient 或真服 e2e)。 -- 每次接通都同时跑 `legacy` 和 `port` 两套路由的相关回归;共享 extension 的改动不能破坏默认的 legacy 路径。 +- 每次接通都同时跑 `legacy` 和 `port` 两套路由的相关回归;共享 extension 的改动不能破坏 legacy 路径。 - `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V3 完成、默认值切到 `port` 并稳定后整体删除。 ### 批次 2A:基础 @@ -447,8 +449,9 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 批次 3-a CPythonSkill(2026-09-24):`UserInterface/PythonSkill.cpp`(含 skill 模块)与 `EterBase/Poly/`(技能公式解析器)原样移植,删除 pending 替身与 skill 模块桩,`initskill()` 按 40250 顺序在 `initfly()` 之后,实例按 `m_pySkill` 位置进 PythonBoot 的 `GameSingletons`;平台 `PythonApplication.cpp` 里重复的 `SKILL_EFFECT_UPGRADE_ENABLE` 删掉(40250 定义在 PythonSkill.cpp),`LoadLocaleData` 的 skilldesc/skilltable 现在真正载入。改动:技能图标/图标实例句柄用 `Py_BuildPointer`/`PyTuple_GetPointer`(item 模块的 `GetIconInstance/DeleteIconInstance` 同改),`skillGetGradeData` 把非平凡结构传进 varargs(clang 拒绝),改为显式读同样的前 4 字节(脚本不调用)。`port.login_flow`:选角阶段 skill 1 = Three-Way Cut、ACTIVE、冷却 12;进游戏后主动技能只有分等级图标(`GetIconImage` 为 0,与 40250 相同),`GetIconImageNew/GetIconInstanceNew` 可用;真服同样通过。 批次 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 块)。已知未修: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 宿主与开关;修武器挂点。 -每个切片都必须同时跑 legacy/port 双路回归;只有 2V3 稳定并把默认路由切到 `port` 后,才能删除总开关和剩余 legacy 宿主。 +每个切片都必须同时跑 legacy/port 双路回归;默认路由已切到 `port`(2026-09-24);port 路由稳定后删除总开关和剩余 legacy 宿主。 ## 5. 数据源切换:m2dev assets → 40250 diff --git a/project/client_main.gd b/project/client_main.gd index c98078ba..99954e66 100644 --- a/project/client_main.gd +++ b/project/client_main.gd @@ -1,6 +1,9 @@ # client_main —— 打包客户端的入口场景根(main_scene = res://client_main.tscn)。 # -# 只做一件事:起 AppFlow(登录 → 选人 → 进游戏 串场控制器)。 +# 默认走 port 路由(PORT-PLAN §2 总开关):40250 system.py 在内嵌解释器里跑,界面画在 +# python_ui_surface.gd、RenderGame 的 3D 绘制画在 python_3d_surface.gd。 +# MT_PORT_PATH=legacy 回到旧的 GDScript AppFlow(登录 → 选人 → 进游戏 串场控制器); +# MT_TEST_MODE=smoke/playable 是 AppFlow 的真服测试,固定走 legacy。 # 资源目录由 AssetRoot 决定:默认 res://../assets(开发树),打包后设环境变量 # MT_ASSETS 指向真实资源目录即可(见 project/asset_root.gd)。 extends Node @@ -35,29 +38,50 @@ func _ready() -> void: printerr("%d check(s) failed" % fail) get_tree().quit(1 if fail else 0) return - # MT_TEST_MODE=python_ui:40250 system.py 的界面直接画在 Godot Canvas 上(PORT-PLAN 2V0-f), - # 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) - get_tree().quit(1) - return if OS.get_environment("MT_TEST_MODE") == "forest_render": var forest := ForestMapRenderTest.new() forest.name = "ForestMapRenderTest" add_child(forest) return + # port 路由:40250 system.py 的界面直接画在 Godot Canvas 上(PORT-PLAN 2V0-f), + # Logo → 登录 → 选人 → 进游戏,RenderGame 的 3D 绘制由 python_3d_surface.gd 画出(2V2-e), + # 方向键/鼠标点地走动走 40250 原路径(2V3);app.Exit() 退出进程。 + # MT_TEST_MODE=python_ui 是切换默认值之前的入口,保留为 MT_PORT_PATH=port 的别名。 + var mode := OS.get_environment("MT_TEST_MODE") + var route := OS.get_environment("MT_PORT_PATH") + if mode == "python_ui": + route = "port" + elif mode == "smoke" or mode == "playable": + route = "legacy" + if route != "legacy": + if route != "" and route != "port": + push_warning("[client] 未知的 MT_PORT_PATH=%s,按 port 处理" % route) + if _start_port_route(route != ""): + return + _start_legacy_route() + +# Returns false when the port route cannot start and the default (unset MT_PORT_PATH) should fall +# back to legacy; an explicit MT_PORT_PATH=port quits with the error instead. +func _start_port_route(explicit: bool) -> bool: + AssetPack.ensure() + if not explicit and not (ClassDB.class_exists("Metin2PythonHost") and AssetRoot.pack_ready()): + push_warning("[client] port 路由缺 Metin2PythonHost 或 40250 Client/pack(%s),退回 legacy AppFlow" % AssetRoot.client_path()) + return false + 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("port: " + err) + get_tree().quit(1) + return true + +func _start_legacy_route() -> void: # 打包 app 从 Finder/open 启动时没有 shell 环境;40250 服务器使用 classic # DH2/CTR 协议,因此默认固定到 classic,同时保留 MT_PROTOCOL 覆盖能力。 if OS.get_environment("MT_PROTOCOL").is_empty():