Fix overlapping OS triangle cursor inside #play-stage per D2gfx.dll #10029 (Closes #718)

This commit is contained in:
troytt 2026-10-03 11:28:57 +00:00
parent 33b3448fb0
commit 290f433104
4 changed files with 49 additions and 1 deletions

View File

@ -231,6 +231,12 @@
background: #000;
box-shadow: 0 0 42px rgba(0, 0, 0, 0.95), 0 0 0 1px #2e251b;
overflow: hidden;
cursor: none;
}
#play-stage,
#play-stage * {
cursor: none !important;
}
#bnet-shell {
@ -241,6 +247,7 @@
align-items: center;
justify-content: center;
background: #000;
cursor: none;
}
#bnet-canvas {
@ -251,6 +258,7 @@
aspect-ratio: 4 / 3;
display: block;
image-rendering: pixelated;
cursor: none;
}
#view {
@ -262,6 +270,7 @@
background: #080706;
z-index: 5;
image-rendering: pixelated;
cursor: none;
}
#d2-hud-canvas {
@ -273,6 +282,7 @@
z-index: 10;
pointer-events: none;
image-rendering: pixelated;
cursor: none;
}
#play-disconnect-banner {

View File

@ -321,6 +321,9 @@ export class FrontendRenderer {
constructor(canvas: HTMLCanvasElement, state: BnetFlowState) {
this.canvas = canvas
if (this.canvas && this.canvas.style) {
this.canvas.style.cursor = 'none'
}
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Failed to acquire 2D context for bnet-canvas')
ctx.imageSmoothingEnabled = false

View File

@ -148,6 +148,12 @@ export async function startPlayApp(options?: {
throw new Error('Missing required DOM elements in play.html')
}
for (const el of [stageEl, bnetShell, bnetCanvas, viewCanvas, hudCanvas]) {
if (el.style) {
el.style.cursor = 'none'
}
}
const bootProgress = new BootProgressTracker({ stageEl }).install()
const settingsStore = getGlobalSettingsStore()
@ -350,7 +356,7 @@ export async function startPlayApp(options?: {
chatInputEl.placeholder = 'Say...'
chatInputEl.hidden = true
chatInputEl.style.cssText =
'position:absolute;left:24px;bottom:64px;width:360px;max-width:calc(100% - 48px);box-sizing:border-box;z-index:25;background:rgba(10,8,6,0.92);border:1px solid #c8a858;color:#f2e6c8;padding:4px 8px;font:13px "D2Formal436",serif;outline:none;'
'position:absolute;left:24px;bottom:64px;width:360px;max-width:calc(100% - 48px);box-sizing:border-box;z-index:25;background:rgba(10,8,6,0.92);border:1px solid #c8a858;color:#f2e6c8;padding:4px 8px;font:13px "D2Formal436",serif;outline:none;cursor:none;'
stageEl.appendChild(chatInputEl)
const toggleInGameChatInput = (): void => {

View File

@ -32,6 +32,8 @@ import {
} from '../src/common/items/cursor-meta.ts'
import { BAKED_UI_MANIFEST } from '../src/common/items/baked-ui-meta.ts'
import { HudManager, type HudCommandSink } from '../src/client/ui/hud-manager.ts'
import { FrontendRenderer } from '../src/client/frontend/frontend-renderer.ts'
import { createInitialFlowState } from '../src/client/frontend/flow-state.ts'
import { openGeneratedVendorShop } from './helpers/vendor-shop-fixture.ts'
import { VENDOR_BUTTON_TABLE_X } from '../src/client/ui/world-panels.ts'
import { seedDemoHud } from './fixtures/hud-demo-state.ts'
@ -522,6 +524,33 @@ describe('Diablo II v1.13c Dynamic Cursor System (Issue #148 & #491)', () => {
expect(hud.state.cursorTradeMode).toBeNull()
expect(hud.state.cursorAnimation).toBe('protate')
})
it('hides the native OS triangle cursor across #play-stage, #view, #bnet-canvas, and #d2-hud-canvas per D2gfx.dll #10029 (0x6fa88290) (Issue #718)', () => {
// 1. FrontendRenderer hides native OS cursor on #bnet-canvas (D2Win.dll 0x6f8f7800 -> D2gfx.#10029)
const bnetCanvas = {
width: 800,
height: 600,
style: { cursor: 'default' },
getContext: () => ({ imageSmoothingEnabled: true }),
} as unknown as HTMLCanvasElement
const fe = new FrontendRenderer(bnetCanvas, createInitialFlowState())
expect(fe.canvas.style.cursor).toBe('none')
// 2. HudManager hides native OS cursor on #d2-hud-canvas (D2Client.dll 0x6fac63d0 -> D2gfx.#10029)
const { mockCanvas: hudCanvas } = createTestHud()
expect(hudCanvas.style.cursor).toBe('none')
// 3. play.html enforces cursor: none on #play-stage and all child canvases (#bnet-shell, #bnet-canvas, #view, #d2-hud-canvas)
// so pointer-events: none on #d2-hud-canvas over the game world never exposes the OS cursor on #view
const playHtml = readFileSync(join(process.cwd(), 'play.html'), 'utf8')
expect(playHtml).toMatch(/#play-stage\s*\{[^}]*cursor:\s*none;/s)
expect(playHtml).toMatch(/#play-stage,\s*#play-stage\s*\*\s*\{[^}]*cursor:\s*none\s*!important;/s)
expect(playHtml).toMatch(/#bnet-shell\s*\{[^}]*cursor:\s*none;/s)
expect(playHtml).toMatch(/#bnet-canvas\s*\{[^}]*cursor:\s*none;/s)
expect(playHtml).toMatch(/#view\s*\{[^}]*cursor:\s*none;/s)
expect(playHtml).toMatch(/#d2-hud-canvas\s*\{[^}]*cursor:\s*none;/s)
})
})
})