Skip to content
M2O Scripting APIScripting reference
GuideClient APIMaintainer authored

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.

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>
client/main.js
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.

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:

client/main.js
Web.focusView(viewId, false); // keep it drawn, return input to the game
Web.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.

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:

client/main.js
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);
});
client/ui/index.html (page script)
window.addEventListener("m2o:init", (e) => {
console.log("view initialized by", e.detail.resource);
});
document.querySelector("#close").addEventListener("click", () => {
callEvent("ui:close", "{}");
});

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:

client/main.js
// 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);
});
server/main.js
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.

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.

Destroy views on resourceStop — the framework would collect them anyway, but an explicit teardown also stops your timers:

client/main.js
Events.on("resourceStop", (resourceName) => {
if (resourceName !== "my-gamemode") return;
clearInterval(tickTimer);
if (viewId >= 0) Web.destroyView(viewId);
});