UR-12: a small platform-host interface
A proposal for three optional Module hooks that let a platform host render a native cursor. This is not a modification interface, and it exposes no game state.
This request doesn't correct a defect either. It proposes a small interface.
The Emscripten client already has a platform layer — on Windows it talks to Win32, on the web to the browser. A native host is a third platform, and today it has no supported way to do platform work.
Fix: high effort, relative to everything else in this section — the change touches the platform layer's cursor path and needs a decision about the interface shape. Medium blast radius: the interface is optional, so nothing changes for hosts that don't use it, but the risk sits in the cursor path, which every player uses.
This is not a modification interface, a plugin API, or an add-on system. It exposes no game state, no packets, no account data, and no gameplay operation, and it can't change what the client does. It carries presentation information out of the client, and platform capability in.
An official way to read party or agent data would be a different product decision, and we don't raise it here.
The problem this solves
Our host renders a native macOS cursor. That removes one frame of latency and keeps the pointer correct while the game thread is busy. To do it, the host has to know which cursor the client wants to show — information the client holds in memory, which we currently read through a table of memory addresses.
That table is the least stable thing we own. Every client build moves it, and when we can't re-measure it, the feature stops until we ship an update. The same applies to any future platform work: a native file dialog, native text input, native window state.
The proposal
Emscripten modules already take host callbacks through the Module object. We
propose three optional entries. When the host doesn't supply an entry, the
client keeps its current behaviour exactly.
1. Cursor notification
// Optional. The client calls this when the active cursor changes.
Module.platformCursorChanged = function (cursor) {
// cursor.kind : "hidden" | "standard" | "custom"
// cursor.id : a stable small integer for a standard cursor
// cursor.width : pixels, for a custom cursor
// cursor.height : pixels, for a custom cursor
// cursor.pixels : Uint8Array, RGBA, width * height * 4
// cursor.hotspotX, cursor.hotspotY : pixels
};The client calls this whenever it changes the cursor, and the host sets the native cursor. Without the function defined, the client draws the cursor as it does today.
This single hook replaces twelve memory addresses in our tables.
2. Cursor query
// Optional. The host may ask for the current cursor at any time.
Module.platformGetCursor(); // returns the same object as aboveUseful after a window focus change, when the host has to restore the cursor without a change event.
3. A capability declaration
// Optional. The host declares what it can do.
Module.platformCapabilities = {
nativeCursor: true,
nativeFileDialog: false,
};The client reads this once at startup and can then skip work the host takes over — for the cursor, its own cursor draw.
Why a callback and not an export
We considered an exported function that the host polls each frame. A callback is better for both sides: the client knows when the cursor changes and a poll doesn't, it costs nothing while nothing changes, and it needs no memory addresses at all.
Acceptance criteria
- With no
Module.platformCursorChangeddefined, the client behaves exactly as it does today. No extra call occurs. - With the function defined, the client calls it once per cursor change, and not more.
- The call carries enough information to draw the cursor without a read of client memory.
- A hidden cursor produces one call with
kind: "hidden". - The call does not occur inside a frame-critical path, or it costs less than 0.1 ms.
platformGetCursor()returns the current cursor at any time after startup.
What this replaces on our side
Our host keeps a table of memory addresses for each certified client build. Twelve of those entries describe the cursor: the active art pointer, the software model, the show count, the colour buffer, and the texture-chain offsets. With this interface all twelve go away, our companion component reads nothing for the cursor feature, and the most fragile part of the project disappears with it.
Priority
Last on purpose — every defect in this section matters more than this proposal. And if the full interface is too much, the cursor notification alone (item 1) gives most of the value; items 2 and 3 can wait.