Web views
For interfaces richer than Render2D widgets — menus, inventories, scoreboards with real layout — the client can open embedded browser views and drive them with HTML, CSS, and page JavaScript. The Web global manages the views; a small event bridge connects the page to your client script, and from there the normal client↔server events reach the server.
Shipping and serving pages
Section titled “Shipping and serving pages”Put your pages inside the resource’s client directory; they are streamed to players with the resource and served locally through the internal origin:
fw://resources/<resource-name>/<path-inside-resource>const viewId = Web.createView("fw://resources/my-gamemode/client/ui/index.html", { width: 640, height: 420, x: 60, y: 60, zIndex: 10, visible: true, focus: true,});createView returns a numeric view id used by every other call. Views are windowed: position and size are pixels (Web.getScreenSize() for the full screen), resizeView and setViewPosition move them live, and multiple views stack by zIndex.
The focus gate
Section titled “The focus gate”A focused view owns the mouse and keyboard and releases the game controls; hiding or unfocusing it hands control back. This is the game/UI input boundary in one flag:
Web.focusView(viewId, false); // keep it drawn, return input to the gameWeb.hideView(viewId); // hide (also drops focus)Web.showView(viewId);Web.isViewVisible(viewId);An unfocused, visible view is a passive overlay — a HUD panel that still renders live data while the player drives. Remember that key binds do not fire while a focused view owns input.
Page ↔ script events
Section titled “Page ↔ script events”The page calls a global callEvent(name, payloadJson); your script listens with Web.on. Your script calls Web.emit; the page receives a CustomEvent on window with the payload in detail:
Web.on(viewId, "ui:ready", (payload) => { // JSON payloads arrive parsed. Web.emit(viewId, "m2o:init", { resource: "my-gamemode" });});
Web.on(viewId, "ui:close", () => { Web.destroyView(viewId);});window.addEventListener("m2o:init", (e) => { console.log("view initialized by", e.detail.resource);});
document.querySelector("#close").addEventListener("click", () => { callEvent("ui:close", "{}");});Reaching the server
Section titled “Reaching the server”The full loop is page → client script → server and back — the page never talks to the server directly, and the client script is the place to keep the vocabulary narrow:
// Page buttons relay up…Web.on(viewId, "ui:action", (payload) => { Events.emitServer("mygm:ui", payload || {});});
// …and server replies land back in the panel.Events.on("mygm:uiResult", (data) => { if (viewId >= 0) Web.emit(viewId, "ui:result", data);});Events.onClient("mygm:ui", (player, data) => { if (data.action === "heal") { player.restoreHealth(); player.emit("mygm:uiResult", JSON.stringify({ message: "Healed!" })); }});The server-side handler follows the usual trust rules: whitelist the actions, take the player from the handler argument, validate everything else.
Navigation and diagnostics
Section titled “Navigation and diagnostics”Web.loadURL(viewId, url) navigates an existing view. Views are origin-locked to their owning resource: a page navigating cross-origin, to an invalid URL, or to a filtered host is refused, and a BrowserResourceBlockedEvent reports why (cross-origin, invalid-url, host-filter, foreign-event).
A family of browser lifecycle payloads — view created, loading started/ready/failed, navigation, popups, cursor and focus changes, and page console messages (BrowserConsoleMessageEvent, with the message, source, line, and severity) — is documented alongside Web in the client reference; the console messages are the first place to look when a page misbehaves.
Cleanup
Section titled “Cleanup”Destroy views on resourceStop — the framework would collect them anyway, but an explicit teardown also stops your timers:
Events.on("resourceStop", (resourceName) => { if (resourceName !== "my-gamemode") return; clearInterval(tickTimer); if (viewId >= 0) Web.destroyView(viewId);});