[Parity][ui/cursor] 游戏画面内同时显示系统三角鼠标与游戏手指光标(双重光标重合),未按 1.13c D2gfx #10029 隐藏系统硬件光标 #718

Closed
opened 2026-10-03 07:22:22 +00:00 by troytt · 0 comments
Owner

问题描述

在 play.html 游戏画面内(包括前置主菜单、Battle.net 登录、选人/建角、大厅界面,以及进入游戏后的世界场景),鼠标位置同时显示两个重合的光标:

  1. 操作系统/浏览器的默认系统三角箭头光标(cursor: auto / default);
  2. 游戏 Canvas 绘制的 Diablo II 铁手(Gauntlet)手指光标(protate.dc6 / ohand.dc6 / orotate.dc6 / buysell.dc6)。

需要严格对齐原版 1.13c 行为:在游戏画面(#play-stage 客户区)内隐藏系统三角光标,只保留游戏手指光标(或手持物品/买卖修鉴图标)。


原版 Diablo II v1.13c 实现机制(二进制反汇编核查)

在原版 v1.13c 中,游戏客户区全流程只显示软件绘制的 DC6 手指光标,通过 D2gfx.dll、D2Win.dll 与 D2Client.dll 三层协同隐藏 Windows 系统硬件光标:

1. D2gfx.dll:窗口创建与系统光标显隐控制(0x6fa88070、0x6fa880a0、0x6fa88290 [#10029]、0x6fa885a0 [#10072])

  • 全局状态标志:0x6fa90cf8(gbCursorVisible,初值为 1)。
  • 隐藏系统光标 WINDOW_HideCursor (0x6fa880a0):
    6fa880a0: mov eax, ds:0x6fa90cf8   ; gbCursorVisible
    6fa880a5: test eax, eax
    6fa880a7: je 0x6fa880c3            ; 已隐藏则直接返回
    6fa880aa: mov esi, [0x6fa8d170]    ; USER32.ShowCursor
    6fa880b0: push 0                   ; do {
    6fa880b2: call esi                 ;   count = ShowCursor(FALSE);
    6fa880b4: test eax, eax            ; } while (count >= 0);
    6fa880b6: jge 0x6fa880b0
    6fa880b8: mov [0x6fa90cf8], 0      ; gbCursorVisible = 0
    
  • 恢复系统光标 WINDOW_ShowCursor (0x6fa88070):
    当 gbCursorVisible == 0 时,循环调用 ShowCursor(TRUE) 直至内部计数 >= 0(0x6fa88080..0x6fa88086),并将 0x6fa90cf8 置回 1。
  • 窗口创建立即隐藏 (WINDOW_Create @ 0x6fa885a0 / #10072):
    CreateWindowExA(0x6fa887e8)、ShowWindow、UpdateWindow、SetFocus 执行完毕后,在 0x6fa8883b 立即调用 call 0x6fa880a0(WINDOW_HideCursor)隐藏系统箭头光标。
  • 导出接口 D2GFX_ShowCursor(BOOL bHide) (0x6fa88290 / Ordinal #10029):
    窗口模式下([0x6fa9125c] != 0),若参数 bHide != 0 则调用 0x6fa880a0 隐藏系统光标;若 bHide == 0 则调用 0x6fa88070 显示系统光标。

2. D2Win.dll:前置菜单与战网大厅界面的光标控制(0x6f8f7800、0x6f8f7020)

  • 客户区鼠标移动 D2WIN_OnMouseMove (0x6f8f7800):
    6f8f7806: movzx esi, word ptr [eax+0xc]  ; mouseX
    6f8f780b: movzx edi, word ptr [eax+0xe]  ; mouseY
    6f8f780f: push 1
    6f8f7811: call 0x6f8e77e6                ; D2gfx.#10029 -> WINDOW_HideCursor()
    
    每次鼠标在菜单/大厅客户区内移动时,强制确保系统光标处于隐藏状态(push 1),仅在帧末由 D2Win 软件渲染手指光标。
  • 非客户区鼠标移动 D2WIN_OnNcMouseMove (0x6f8f7020):
    仅当鼠标移出客户区(如窗口标题栏,[esi+0x8] == 2)时,执行 push 0; call 0x6f8e77e6 恢复系统光标。

3. D2Client.dll:游戏内世界与 HUD 的光标控制(0x6fac63d0、0x6fac60a0)

  • 游戏内鼠标移动 D2CLIENT_OnMouseMove (0x6fac63d0):
    更新 gnMouseX(0x6fbcb828)与 gnMouseY(0x6fbcb824)后,在 0x6fac6454 执行:
    6fac6454: push 1
    6fac6456: call 0x6fabd1c2                ; D2gfx.#10029 -> WINDOW_HideCursor()
    
    随后更新空闲计时器 0x6fbcb860 = GetTickCount() 并驱动 0x6fba8558 光标状态机,在画面最上层仅绘制游戏内手指光标或手持物品。
  • 移出客户区 D2CLIENT_OnNcMouseMove (0x6fac60a0):
    仅在鼠标移出客户区([esi+0x8] == 2)时调用 push 0; call 0x6fabd1c2 恢复系统光标。

当前 diablo2-web 根因分析

  1. 游戏内世界区域(#view + #d2-hud-canvas)穿透导致 cursor: none 失效:

    • src/client/ui/hud-manager.ts:798-800 仅在 this.hudCanvas(#d2-hud-canvas)上设置了 this.hudCanvas.style.cursor = "none"。
    • 但在 play.html:264 及 src/client/ui/hud-manager.ts:2545-2554 中,当鼠标位于游戏世界场景(未悬停在底栏/面板上,且未持物、未处于买卖/鉴定模式)时,this.hudCanvas.style.pointerEvents 被动态设为 "none":
      if (this.hudCanvas.style) {
        this.hudCanvas.style.pointerEvents = intercept ? "auto" : "none"
      }
      
    • 根据浏览器 CSS 命中测试规范,pointer-events: none 的元素不参与鼠标命中测试,其 cursor: none 样式也会被浏览器完全忽略;命中测试直接穿透到底层的 #view(WebGL2 世界画布,play.html:246-255)和 #play-stage(play.html:214-224)。
    • 而 #view 与 #play-stage 均未设置 cursor: none,浏览器直接回退为默认系统三角光标,与 #d2-hud-canvas 上 renderCursor() 绘制的游戏手指光标重叠。
  2. 前置主菜单与战网大厅(#bnet-shell + #bnet-canvas)完全未隐藏系统光标:

    • src/client/frontend/frontend-renderer.ts:1645 每帧在 #bnet-canvas 上绘制游戏手指光标(this.cursor.render(ctx, this.cursorAtlas, nowMs))。
    • 但 FrontendRenderer 构造函数(src/client/frontend/frontend-renderer.ts:322-330)与 play.html:226-244(#bnet-shell、#bnet-canvas)均未设置 cursor: none,导致整个前置菜单、登录、选人、建角及大厅界面全程显示系统三角光标与游戏手指双重光标。

修复方向与验收标准

  1. 在游戏客户区容器及所有画布层统一隐藏系统光标:
    • 在 play.html 中为 #play-stage 及其子元素(#bnet-shell、#bnet-canvas、#view、#d2-hud-canvas 等)设置 cursor: none。
    • 在 src/client/frontend/frontend-renderer.ts 构造函数中对 this.canvas.style 设置 cursor = "none"。
    • 在 src/client/main.ts 初始化时对 stageEl、bnetShell、bnetCanvas、viewCanvas、hudCanvas 显式确保 cursor: none,不受 hudCanvas.style.pointerEvents 在 "auto" 与 "none" 之间切换的影响。
  2. 非客户区(顶部工具栏 #d2-toolbar-container 与右侧抓包面板 #d2-inspector-container)保持系统光标:
    • 鼠标移出 #play-stage 进入网页工具栏或抓包器时正常显示系统光标,严格对应原版窗口模式 WM_NCMOUSEMOVE 的客户区/非客户区边界行为。
  3. 验收标准:
    • 在主菜单、Battle.net 登录、角色选择、角色创建、大厅、游戏世界行走/战斗、打开/关闭各 HUD 面板、手持物品、NPC 交易/修理/鉴定等所有状态下,#play-stage 内只显示游戏自身的 DC6 手指/物品光标,绝不出现系统三角鼠标光标。
## 问题描述 在 `play.html` 游戏画面内(包括前置主菜单、Battle.net 登录、选人/建角、大厅界面,以及进入游戏后的世界场景),鼠标位置同时显示**两个重合的光标**: 1. 操作系统/浏览器的默认**系统三角箭头光标**(`cursor: auto / default`); 2. 游戏 Canvas 绘制的 **Diablo II 铁手(Gauntlet)手指光标**(`protate.dc6` / `ohand.dc6` / `orotate.dc6` / `buysell.dc6`)。 需要严格对齐原版 1.13c 行为:在游戏画面(`#play-stage` 客户区)内隐藏系统三角光标,**只保留游戏手指光标**(或手持物品/买卖修鉴图标)。 --- ## 原版 Diablo II v1.13c 实现机制(二进制反汇编核查) 在原版 v1.13c 中,游戏客户区全流程只显示软件绘制的 DC6 手指光标,通过 `D2gfx.dll`、`D2Win.dll` 与 `D2Client.dll` 三层协同隐藏 Windows 系统硬件光标: ### 1. `D2gfx.dll`:窗口创建与系统光标显隐控制(`0x6fa88070`、`0x6fa880a0`、`0x6fa88290 [#10029]`、`0x6fa885a0 [#10072]`) - **全局状态标志**:`0x6fa90cf8`(`gbCursorVisible`,初值为 `1`)。 - **隐藏系统光标 `WINDOW_HideCursor` (`0x6fa880a0`)**: ```asm 6fa880a0: mov eax, ds:0x6fa90cf8 ; gbCursorVisible 6fa880a5: test eax, eax 6fa880a7: je 0x6fa880c3 ; 已隐藏则直接返回 6fa880aa: mov esi, [0x6fa8d170] ; USER32.ShowCursor 6fa880b0: push 0 ; do { 6fa880b2: call esi ; count = ShowCursor(FALSE); 6fa880b4: test eax, eax ; } while (count >= 0); 6fa880b6: jge 0x6fa880b0 6fa880b8: mov [0x6fa90cf8], 0 ; gbCursorVisible = 0 ``` - **恢复系统光标 `WINDOW_ShowCursor` (`0x6fa88070`)**: 当 `gbCursorVisible == 0` 时,循环调用 `ShowCursor(TRUE)` 直至内部计数 `>= 0`(`0x6fa88080..0x6fa88086`),并将 `0x6fa90cf8` 置回 `1`。 - **窗口创建立即隐藏 (`WINDOW_Create` @ `0x6fa885a0` / `#10072`)**: `CreateWindowExA`(`0x6fa887e8`)、`ShowWindow`、`UpdateWindow`、`SetFocus` 执行完毕后,在 `0x6fa8883b` 立即调用 `call 0x6fa880a0`(`WINDOW_HideCursor`)隐藏系统箭头光标。 - **导出接口 `D2GFX_ShowCursor(BOOL bHide)` (`0x6fa88290` / Ordinal `#10029`)**: 窗口模式下(`[0x6fa9125c] != 0`),若参数 `bHide != 0` 则调用 `0x6fa880a0` 隐藏系统光标;若 `bHide == 0` 则调用 `0x6fa88070` 显示系统光标。 ### 2. `D2Win.dll`:前置菜单与战网大厅界面的光标控制(`0x6f8f7800`、`0x6f8f7020`) - **客户区鼠标移动 `D2WIN_OnMouseMove` (`0x6f8f7800`)**: ```asm 6f8f7806: movzx esi, word ptr [eax+0xc] ; mouseX 6f8f780b: movzx edi, word ptr [eax+0xe] ; mouseY 6f8f780f: push 1 6f8f7811: call 0x6f8e77e6 ; D2gfx.#10029 -> WINDOW_HideCursor() ``` 每次鼠标在菜单/大厅客户区内移动时,强制确保系统光标处于隐藏状态(`push 1`),仅在帧末由 `D2Win` 软件渲染手指光标。 - **非客户区鼠标移动 `D2WIN_OnNcMouseMove` (`0x6f8f7020`)**: 仅当鼠标移出客户区(如窗口标题栏,`[esi+0x8] == 2`)时,执行 `push 0; call 0x6f8e77e6` 恢复系统光标。 ### 3. `D2Client.dll`:游戏内世界与 HUD 的光标控制(`0x6fac63d0`、`0x6fac60a0`) - **游戏内鼠标移动 `D2CLIENT_OnMouseMove` (`0x6fac63d0`)**: 更新 `gnMouseX`(`0x6fbcb828`)与 `gnMouseY`(`0x6fbcb824`)后,在 `0x6fac6454` 执行: ```asm 6fac6454: push 1 6fac6456: call 0x6fabd1c2 ; D2gfx.#10029 -> WINDOW_HideCursor() ``` 随后更新空闲计时器 `0x6fbcb860 = GetTickCount()` 并驱动 `0x6fba8558` 光标状态机,在画面最上层仅绘制游戏内手指光标或手持物品。 - **移出客户区 `D2CLIENT_OnNcMouseMove` (`0x6fac60a0`)**: 仅在鼠标移出客户区(`[esi+0x8] == 2`)时调用 `push 0; call 0x6fabd1c2` 恢复系统光标。 --- ## 当前 `diablo2-web` 根因分析 1. **游戏内世界区域(`#view` + `#d2-hud-canvas`)穿透导致 `cursor: none` 失效**: - `src/client/ui/hud-manager.ts:798-800` 仅在 `this.hudCanvas`(`#d2-hud-canvas`)上设置了 `this.hudCanvas.style.cursor = "none"`。 - 但在 `play.html:264` 及 `src/client/ui/hud-manager.ts:2545-2554` 中,当鼠标位于游戏世界场景(未悬停在底栏/面板上,且未持物、未处于买卖/鉴定模式)时,`this.hudCanvas.style.pointerEvents` 被动态设为 `"none"`: ```ts if (this.hudCanvas.style) { this.hudCanvas.style.pointerEvents = intercept ? "auto" : "none" } ``` - 根据浏览器 CSS 命中测试规范,`pointer-events: none` 的元素不参与鼠标命中测试,其 `cursor: none` 样式也会被浏览器完全忽略;命中测试直接穿透到底层的 `#view`(WebGL2 世界画布,`play.html:246-255`)和 `#play-stage`(`play.html:214-224`)。 - 而 `#view` 与 `#play-stage` 均未设置 `cursor: none`,浏览器直接回退为默认系统三角光标,与 `#d2-hud-canvas` 上 `renderCursor()` 绘制的游戏手指光标重叠。 2. **前置主菜单与战网大厅(`#bnet-shell` + `#bnet-canvas`)完全未隐藏系统光标**: - `src/client/frontend/frontend-renderer.ts:1645` 每帧在 `#bnet-canvas` 上绘制游戏手指光标(`this.cursor.render(ctx, this.cursorAtlas, nowMs)`)。 - 但 `FrontendRenderer` 构造函数(`src/client/frontend/frontend-renderer.ts:322-330`)与 `play.html:226-244`(`#bnet-shell`、`#bnet-canvas`)均未设置 `cursor: none`,导致整个前置菜单、登录、选人、建角及大厅界面全程显示系统三角光标与游戏手指双重光标。 --- ## 修复方向与验收标准 1. **在游戏客户区容器及所有画布层统一隐藏系统光标**: - 在 `play.html` 中为 `#play-stage` 及其子元素(`#bnet-shell`、`#bnet-canvas`、`#view`、`#d2-hud-canvas` 等)设置 `cursor: none`。 - 在 `src/client/frontend/frontend-renderer.ts` 构造函数中对 `this.canvas.style` 设置 `cursor = "none"`。 - 在 `src/client/main.ts` 初始化时对 `stageEl`、`bnetShell`、`bnetCanvas`、`viewCanvas`、`hudCanvas` 显式确保 `cursor: none`,不受 `hudCanvas.style.pointerEvents` 在 `"auto"` 与 `"none"` 之间切换的影响。 2. **非客户区(顶部工具栏 `#d2-toolbar-container` 与右侧抓包面板 `#d2-inspector-container`)保持系统光标**: - 鼠标移出 `#play-stage` 进入网页工具栏或抓包器时正常显示系统光标,严格对应原版窗口模式 `WM_NCMOUSEMOVE` 的客户区/非客户区边界行为。 3. **验收标准**: - 在主菜单、Battle.net 登录、角色选择、角色创建、大厅、游戏世界行走/战斗、打开/关闭各 HUD 面板、手持物品、NPC 交易/修理/鉴定等所有状态下,`#play-stage` 内只显示游戏自身的 DC6 手指/物品光标,绝不出现系统三角鼠标光标。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#718
No description provided.