Skip to Content
Modlock

Layouts

Draw the mod's own layouts and style sheets, and how the game draws interfaces.

<layout src="shop.xml" data={stock} /> loads a layout from the mod's content addon. No other element runs code in the player's game. The game draws every other element from the typed tree in src/wasm/ui/ui.proto, and when the player presses one, the press comes back to its onPress. The host clears an interface when the player leaves or the mod stops.

  • A layout with no content to load draws an empty panel and logs one line in the game's console.
  • The modlock dev preview page draws the layout from the mod's latest build, with its panels, inline styles and style sheets, but runs none of its scripts.
  • A host with its own interface script loads layouts by passing Layouts to the Drawing in panorama/src/draw.ts. AddonLayouts, for example, loads each layout from the addon given in the screen's tree. Modlock's own renderer loads the layouts of the content its session mounted.

Style sheets

sheet('hud.css') adds a sheet from content/styles/ to every interface the mod shows. When two sheets set the same property, the later sheet wins. In TypeScript, import './content/styles/hud.css' builds to the same call, and the build refuses a sheet outside content/styles/. An element takes the sheet's classes through class="board gold". Changing its classes runs the sheet's transitions, and the element's own style wins over a class.

Sheets load where layouts do. Beside each sheet, the build writes a layout that includes only that sheet, and the game loads that layout into the interface's root panel. A host that passes no Layouts draws the interface without them, and a sheet that fails to load leaves it drawn with one log line.

Hand values to a layout

Your mod hands values to a layout in two ways: the data of the layout's own element, and routes. route(player, 'scores', scores) hands its value to every layout your mod shows that player, so several layouts share one value without a parent passing it down. A value travels as JSON, and the layout sees a change only when your mod sends a different value. route(player, name, undefined) removes the route. Go's ui.Route takes the value as JSON text, and an empty one removes it. The layout's script includes the library scripts/modlock/layout.js and reads them:

<scripts>
  <include src="file://{resources}/scripts/modlock/layout.js" />
  <include src="file://{resources}/scripts/{addon}/shop.js" />
</scripts>
Modlock.data((stock) => drawShop(stock))
Modlock.route('scores', (scores) => drawScores(scores))

Each handler runs at once with the current value and again after each change, with undefined while the mod sends none.

When your mod sends a new value, the renderer writes it the next time it draws the screen, to an attribute of the layout's panel: modlock_data for the element's data, or modlock_route_<name> for a route. After a draw that changed any of these, the renderer plays the sound event modlock_data_changed. The game delivers no custom event to a script, but it delivers PlaySoundEffect to every script, and no sound has that name, so none plays. The library hears it, rereads each attribute, and calls the handler of each value that changed.

The layout answers the mod with its own console commands.

The renderer

The game draws interfaces with the renderer in panorama/:

  • The layout panorama/layout/modlock/ui.xml and its script, which a client content package installs as scripts/modlock/ui.js, beside the layout library scripts/modlock/layout.js. The modlock command line in a release has the library built in. go run ./cmd/modlock-library builds it from source into js/built, where the next build of the command line embeds it.
  • The game keeps a client content package, and the layout in it, loaded through matches. The renderer draws over the HUD.
  • modlock dev and modlock play serve the local player's interfaces on 127.0.0.1:4320 (--ui-port, 0 for none). Deadlock's HTML panels load only https pages, so the renderer reads them through a relay page at --ui-relay, https://hyperline.gg by default. The controller answers only that origin's page. The session's package loads the page at /app/relay/bridge.html?port=<port>, and the page reads the screen from that loopback port.
  • Both commands also install the renderer in the game for the session, so its interfaces show with no setup. They write game/citadel/modlock_dev, a package that adds the renderer to the stock news layout. They mount it between // MODLOCK DEV markers in gameinfo.gi, ahead of other mounted content, and remove it when the session ends. The game reads the package at startup, so a game that was already running shows the interfaces only after a restart.
  • A press runs the console command modlockpress <mod> <node> in the player's game. pressCommand in panorama/src/draw.ts builds that command, and a host with its own renderer calls it to press the same way. The server presses the node for the player who sent it, and only when that player's interface shows it as a button.

On this page