diff --git a/.gitignore b/.gitignore index a6c680c1..7be55504 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ compile_commands.json # compare.py render output /test/compare-shots/ /test/compare-report.json + +# Written by 40250 uitaskbar.py (SaveMouseButtonSettings) into the working directory at runtime. +/project/mouse.cfg diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index 80b6f322..642a8775 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -425,7 +425,7 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 | 2V2-d | `InstanceBase` + `ActorInstance` + `CGraphicThingInstance` adapter:`GC_CHARACTER_ADD` 创建主角实例(msm 模型 + 待机动作) | 完成。d.1:`ActorInstance*.cpp`(Attach/Battle/Blend/CollisionDetection/Event/Fly/Motion/MotionEvent/Position/Render/Rotation/Sync/WeaponTrace)、`InstanceBase*.cpp`(Battle/Effect/Event/Motion/Transform)、`GameUtil`、`FlyTarget`、`PhysicsObject`、`GameEventManager`、`WeaponTrace`、`MapUtil`、`lineintersect_utils` 原样移植,`pending/GameLib/{ActorInstance,FlyTarget,PhysicsObject,MapUtil}.cpp`、`pending/UserInterface/InstanceBase.cpp` 替身删除,`ActorInstance.cpp`/`InstanceBase.cpp` 随主角创建进入运行时(port-map 标 PORTED);`CGameEventManager` 与 `CPythonSystem`(最后一个成员)按 `CPythonApplication` 成员顺序在 `PythonBoot` 创建;`D3D8Types.h` 补全 `D3DTA`/`D3DTEXTUREOP`/`D3DCMPFUNC`/`D3DBLEND`/`D3DCULL`。假服 `CHARACTER_ADD` 的 `bType` 改为 `CHAR_TYPE_PC`(6,原 0 使 `RecvCharacterAppendPacket` 不记主角、`CHAR_ADDITIONAL_INFO` 报错)。2V2-c 的偶发 Loading 头 7 根因:`TPlayerSkill::tNextRead` 是 `time_t`,40250 用 `_USE_32BIT_TIME_T`,LP64 下 `TPacketGCSkillLevelNew` 变成 2551 字节(线上 1531),`CInputDB::PlayerLoad` 在 `PointsPacket` 后发的 `SKILL_LEVEL`(76)读多吞掉后续包;`Packet.h` 改 `LONG` 并 `static_assert` 1531,假服 Loading 阶段补发 `SKILL_LEVEL` 回归。`port.login_flow` 断言主角实例存在且种族 0、无 `TPacketGCCharacterAdditionalInfo` 报错;`port.login_live` 2026-09-23 对 192.168.21.203 连跑 8 次通过,剩余 SYSERR 只有 gr2 `NOT SUPPORT FILE`。d.2:Granny SDK 2.11 运行时在 libgr2 上实现(`platform/EterGrnLib/GrannyRuntime.cpp`,`shim/sdk/granny.h` 按 SDK 头声明所用类型与函数:文件/段、按类型定义的顶点转换含 half、材质贴图按 Usage 查找、网格绑定、变形器、控制器时钟/循环/缓入缓出、加权 SRT 采样与复合矩阵),`EterGrnLib/*.cpp`(LODController/Material/Mesh/Model/ModelInstance*/Motion/Thing/ThingInstance/Util)与 `EterLib/{GrpObjectInstance,GrpCollisionObject,CollisionData}.cpp` 原样移植,对应 platform 骨架与 pending 替身删除(port-map 标 PORTED);`Resource.cpp` 注册 gr2 工厂(mdatr 等 `CAttributeData` 资源化时再注册);VB/IB 为 CPU 缓冲(`platform/EterLib/CpuBuffer.h`),渲染 adapter 属 2V2-e。`app.UpdateGame`/`RenderGame`(40250 `PythonApplication.cpp:321/168`)接入,成员经单例访问(平台没有 `CPythonApplication` 对象),`CPythonGraphic`(含 `CCullingManager`)按成员顺序进 `GameSingletons`;`GetMousePosition` 读 host 光标。`port.login_flow` 断言主角 PART_MAIN 模型有网格、材质漫反射贴图已加载、WAIT 动作使骨骼矩阵逐帧变化;离线与 `port.login_live`(192.168.21.203)2026-09-23 通过 | | 2V2-e | Godot 显示主角(渲染 adapter),离线 + 真服 smoke:进游戏看到可辨认的静止角色 | 完成。e.1 完成:3D 绘制命令通道。`EterLib/StateManager.cpp`、`EterLib/GrpBase.cpp` 原样移植(platform 骨架删除,port-map 标 PORTED);`CGraphicDevice::Create` 建平台 `IDirect3DDevice8`(`platform/EterLib/RecordingDevice.cpp`:记住 transform/渲染状态/贴图阶段/材质/灯光/FVF/流/索引,按 FVF 解码顶点、展开 strip/fan,每次 Draw* 记成一条 `Render3DDraw`,`RenderCommands3D.h`),默认 IB/PDT VB 为 CPU 缓冲;`D3DXMath` 补 `PerspectiveFovRH`/`OrthoRH`/`OrthoOffCenterRH` 与 `ID3DXMatrixStack`。相机:`PythonApplicationCamera.cpp` 原样移植 `__UpdateCamera`/`SetCamera`/`SetCenterPosition`/`GetCenterPosition` 等(成员放文件内静态结构,平台没有 `CPythonApplication` 对象),`Process` 按 40250 顺序调 `__UpdateCamera`→`OnCameraUpdate`→`OnUIUpdate`→`Begin`/`SetInterfaceRenderState`/`OnUIRender`/`End`;`CScreen::Begin/End/Set*Operation`、`CPythonGraphic::Set{Game,Interface}RenderState/PushState/PopState/GetOrthoDepth`、`CCullingManager::Process` 的 view/proj 更新(视锥剔除等 SphereLib)原样;`pending` 补 `CPythonPlayer::NEW_GetMainActorPosition/NEW_GetMainActorPtr/NEW_FindActorPtr/SetMainCharacterIndex`(事件处理器留待 `PythonPlayerEventHandler`)、`CMapManager::IsMapReady`。`port.login_flow` 断言某帧有贴图、带法线/UV 的三角形绘制,投影为 `SetPerspective` 的 RH 透视(_34=-1),且顶点全部落在屏幕内(相机对准主角);离线与 `port.login_live`(192.168.21.203)2026-09-23 通过。e.2 完成:Godot 消费端 `project/python_3d_surface.gd`(`Metin2PythonHost.render3d_draws` 绑定):每条透视绘制成一个 `MeshInstance3D`,变换为 world·view(D3D 行向量矩阵的行即基列),`Camera3D` 在原点,fov/near/far 从 `SetPerspective` 投影反推;材质按贴图/混合/alpha test/剔除/光照状态缓存,贴图只从 40250 包取(`UiAssets.load_pack_tex`,包名保留 `d:` 盘符)。`CPythonBackground::GetFarClip` 原样移植(`m_pkMap` 为空 → 50000),`CMapManager` 构造原样(`m_pkMap=NULL`)。PORT 替身:地图未移植前没有环境数据(`LoadMap`→`SetEnvironmentData`→`SetCharacterDirLight`),D3D 会把受光网格画成全黑;「开光照、无灯、环境光与自发光为 0」的绘制按无光照贴图显示并计入 `unlit_stand_in_count`,随环境移植删除。`script/python_game_render_test.sh`(离线起 `port_fake_login_server`,`--live` 读 `MT_LIVE_*`)跑 `python_game_render_test.gd`:system.py 登录→选人→进游戏,4 条绘制(脸、Object05、身体、头发)全部带贴图,截图中心有角色;2026-09-23 离线通过。e.3 完成:实服冒烟 `MT_LIVE_*` 指向 192.168.21.203,`--live` 进游戏后主角与周围狼群带正确贴图、HUD 在上(246 条绘制、10 张贴图);修 `python_3d_surface.gd` 材质缓存键把 bool 交给 `%d`(格式化失败使所有绘制共用第一个材质即脸部贴图),测试加「每个贴图名一张贴图」断言;2026-09-23 离线与实服通过 | 2V2-e.4(2026-09-23):Python 3D 表面接上 40250 包内真实 A1 地图。`Metin2World` 的高度图、tile/splat、地表 DDS、`.msenv` 和 `AtlasInfo.txt` 经 `pack://` 读取;相机逆矩阵选当前 3×3 区块,地表随 D3D view 进入同一相机空间。角色材质不再使用 `unlit_stand_in_count` 的无光照替身,方向光取地图 `.msenv` 的 Character 参数,地形由同一环境的 Background 参数照明。离线截图能看到脚下石砖/沙地,9/9 区块带 splat,角色受光;`python_game_render_test.sh` 检查真实地图、光和地表像素差异。`CMapManager::LoadMap`、`CPythonBackground::SetCharacterDirLight` 的 native 链仍待逐单元移植;当前平台适配将地形一次性对齐到角色初始海拔(native `GetHeight` 尚为替身),所以不把这一步记作该 native 单元的 PORTED。地图内流式更新已接,跨图 warp 的 3D 场景替换留后续地图生命周期批次。 - 2V2-e.5(2026-09-23):原生 `CPythonNetworkStream::Warp` → `CPythonBackground::Warp` → `CMapManager::LoadMap/GetHeight` 已在运行路径接上包内 AtlasInfo、Setting.txt、height.raw 和 `.msenv`。地形高度使用 40250 的三角插值;`RenderGame` 调 `SetCharacterDirLight` 后录制绘制的 light0 来自该环境。Godot 地表按 native map 名和地图本地坐标放置,移除初始海拔平移,换图时重建地表;离线角色脚底与地表高度差约 0.1 cm,4/4 绘制带 light0。地图后端仍是过渡适配:`CMapOutdoor`、放置物件高度/属性、雾与完整环境状态、迷你地图/传送门等未移植,port-map 保持 ADAPTED/TODO,不宣称整个 native 地图单元 PORTED。修正 `EterBase/Utils` 的 MIN/MAX/fMIN/fMAX 系列返回零桩:地图就绪后相机 `Update` 会调用 fMIN/fMAX,否则距离被压成 0、view 矩阵变 NaN。离线测试增加有限矩阵、native map 一致性、脚底高度和 light0 检查;实服凭据当前环境缺失,换图和实服截图尚待 live 验证。 + 2V2-e.5(2026-09-23):原生 `CPythonNetworkStream::Warp` → `CPythonBackground::Warp` → `CMapManager::LoadMap/GetHeight` 已在运行路径接上包内 AtlasInfo、Setting.txt、height.raw 和 `.msenv`。地形高度使用 40250 的三角插值;`RenderGame` 调 `SetCharacterDirLight` 后录制绘制的 light0 来自该环境。Godot 地表按 native map 名和地图本地坐标放置,移除初始海拔平移,换图时重建地表;离线角色脚底与地表高度差约 0.1 cm,4/4 绘制带 light0。地图后端仍是过渡适配:`CMapOutdoor`、放置物件高度/属性、雾与完整环境状态、迷你地图/传送门等未移植,port-map 保持 ADAPTED/TODO,不宣称整个 native 地图单元 PORTED。修正 `EterBase/Utils` 的 MIN/MAX/fMIN/fMAX 系列返回零桩:地图就绪后相机 `Update` 会调用 fMIN/fMAX,否则距离被压成 0、view 矩阵变 NaN。离线测试增加有限矩阵、native map 一致性、脚底高度和 light0 检查。2026-09-23 实服(192.168.21.203,metin2_map_c1,244 条绘制、142 个 actor)验证时发现两处 Metin2World 旧单位问题:`.msenv` 深度雾(120–520,按米写)和地形 patch 的 `visibility_range_end`(3500,按米写)在本表面的 cm 场景里分别在约 5 m、35 m 处把角色身后的地图洗成天色/剔除,怪物看起来悬空。修正:Python 3D 表面关掉旧雾(雾应来自录制的 `D3DRS_FOG*`,`SetEnvironmentData` 移植前保持关闭),`terrain_patches=1` 整块出网格只做视锥剔除。测试新增“每个 actor 站在地图上”(按 world 矩阵分组取最低顶点,实服最差 19 cm)与“角色身后地图可见”(画面上部纹理对比);离线与实服通过。换图仍待 live 验证。 4. **2V3 — 本地移动**:加入 `PythonPlayer`、`PythonPlayerInput*`、`PythonPlayerEventHandler`、 `InstanceBaseMovement`、`ActorInstance{Motion,Event,Position,Rotation,CollisionDetection}` 的实际依赖闭包。验收为输入经过新路径 改变位置/朝向、发送与 40250 相同的移动封包,并在离线边界测试和真服 smoke 中通过。接通时删除对应 legacy 玩法逻辑。 diff --git a/project/python_3d_surface.gd b/project/python_3d_surface.gd index d0335382..861af016 100644 --- a/project/python_3d_surface.gd +++ b/project/python_3d_surface.gd @@ -141,6 +141,9 @@ func _load_terrain(draw: Dictionary) -> void: terrain.set("assets_root", "pack://") terrain.set("map_path", map_path) terrain.set("load_radius_tiles", 1) + # One mesh per 256 m tile: split patches carry a visibility_range_end meant for metres, which in + # this cm scene culled every patch beyond 35 m (the map behind the actor vanished). + terrain.set("terrain_patches", 1) terrain.set("objects_enabled", false) terrain.set("water_enabled", false) terrain.call("set_collision_enabled", false) @@ -173,6 +176,12 @@ func _load_terrain(draw: Dictionary) -> void: # WorldEnv from .msenv replaces the black environment created before map load. if terrain_report.get("env_ok", false): environment.environment = null + # Its depth fog is in legacy metres, but this scene is in cm, so it fogged everything past + # ~5 m and hid the terrain behind the actor. Fog belongs to the recorded D3DRS_FOG* state, + # which stays off until CPythonBackground::SetEnvironmentData is ported. + var map_environment := terrain.get_node_or_null("WorldEnv") as WorldEnvironment + if map_environment and map_environment.environment: + map_environment.environment.fog_enabled = false func _map_for_position(global_cm: Vector2) -> String: if _atlas.is_empty(): diff --git a/project/python_game_render_test.gd b/project/python_game_render_test.gd index 30c6eab9..472f1150 100644 --- a/project/python_game_render_test.gd +++ b/project/python_game_render_test.gd @@ -154,6 +154,25 @@ func run() -> void: if p.z < actor_foot_cm: actor_foot_cm = p.z foot_xy = Vector2(p.x, -p.y) + # Every actor, not only the main one, stands on the map. An actor's parts (body, head, hair, teeth) + # share its world matrix; the lowest vertex over those parts meets sample_height. + var actor_low := {} + for draw in draws: + var draw_xform: Transform3D = world.d3d_transform(draw["world"]) + var key := str(draw["world"]) + for vertex in draw["positions"]: + var p: Vector3 = draw_xform * vertex + if not actor_low.has(key) or p.z < actor_low[key].z: + actor_low[key] = p + if world.terrain: + var worst_actor_gap_cm := 0.0 + for low in actor_low.values(): + var ground := float(world.terrain.call("sample_height", low.x / 100.0, -low.y / 100.0)) * 100.0 + worst_actor_gap_cm = maxf(worst_actor_gap_cm, absf(low.z - ground)) + report["actors"] = actor_low.size() + report["worst_actor_gap_cm"] = worst_actor_gap_cm + _check(worst_actor_gap_cm < 50.0, "every actor stands on the map (%d actors, worst gap %.1f cm)" + % [actor_low.size(), worst_actor_gap_cm]) report["native_lit_draws"] = native_lit_draws report["view_finite"] = view_finite _check(view_finite, "native camera view stays finite") @@ -242,5 +261,16 @@ func run() -> void: textured_pairs += 1 report["ground_texture_pairs"] = textured_pairs _check(textured_pairs > 20, "textured ground visible in screenshot (%d local contrasts)" % textured_pairs) + # The camera looks 27 degrees down, so the map behind the actor fills the top of the picture + # too (legacy metre-unit fog once washed it out to flat sky colour). + var far_pairs := 0 + for y in range(game_image.get_height() / 10, game_image.get_height() * 3 / 10, 13): + for x in range(30, game_image.get_width() - 35, 17): + var left := game_image.get_pixel(x, y).get_luminance() + var right := game_image.get_pixel(x + 5, y).get_luminance() + if absf(left - right) > 0.04: + far_pairs += 1 + report["far_ground_texture_pairs"] = far_pairs + _check(far_pairs > 20, "map behind the actor visible (%d local contrasts)" % far_pairs) ui.visible = true await _finish(output, report)