Worked example
buffs-log (“Sinners & Statues”) is the reference integration. It uses the
inline snippet rather than the client library, and it is the code UMM is tested
against.
This is the snippet, in a real mod. Same names, same
structure, so everything below should already look familiar. It differs in four
ways: it declares more settings, it uses const/let rather than var, it
writes the tab title inline instead of keeping a UMM_NAME constant, and it
calls registerSettings() from its own init rather than at the bottom of the
closure.
Declaring the settings
Section titled “Declaring the settings”One flat array, mixing group dividers, a checks row, toggles and sliders.
Abridged here; the shipping array is three groups and seven control rows, which
come to eight stored values because the checks row carries two.
const UMM_SETTINGS = [ { type: "group", label: "General" }, { id: "enabled", type: "toggle", label: "Enabled", default: true, description: "Show the buff tracker panel on the HUD" }, { type: "checks", label: "Visible Panels", description: "Which parts of the panel to show", options: [ { id: "show_counters", label: "Counters", default: true }, { id: "show_breakdown", label: "Stats", default: true } ] }, { type: "group", label: "Position" }, { id: "pos_x", type: "slider", label: "Horizontal Position", min: 0, max: 90, step: 0.5, default: 28.5, unit: "%", description: "Distance from the left edge of the screen" }];The positions are percentages of the screen rather than pixels. Panorama’s
layout pixels are 1080p-normalised and then rescaled by uiscale, so a literal
pixel offset lands somewhere different on every other resolution.
Announcing
Section titled “Announcing”function ummAnnounce() { try { $.DispatchEvent(UMM_CHANNEL, JSON.stringify({ umm: UMM_PROTOCOL, t: "register", id: UMM_ID, name: "Sinners & Statues", settings: UMM_SETTINGS })); } catch (e) {}}Listening
Section titled “Listening”function onUmmBus(payload) { if (typeof payload !== "string" || payload.indexOf("\"umm\"") === -1) return; let msg; try { msg = JSON.parse(payload); } catch (e) { return; } if (!msg || msg.umm !== UMM_PROTOCOL) return; if (msg.t === "hello") ummAnnounce(); else if (msg.t === "set" && msg.id === UMM_ID) applySetting(msg.key, msg.value);}Note the guards, in order: not a string, does not mention umm, does not parse,
wrong protocol version, wrong mod id. Everything else on this channel belongs to
somebody else.
Booting
Section titled “Booting”function registerSettings() { try { $.RegisterForUnhandledEvent(UMM_CHANNEL, onUmmBus); } catch (e) {} // Apply declared defaults now so the panel is right even without UMM. Any // values UMM has stored arrive right after, as a `set` per setting, and // overwrite. applySetting is the one code path, so a boot can call it twice // for a setting (default, then saved), which is fine since it's idempotent. for (let i = 0; i < UMM_SETTINGS.length; i++) { const s = UMM_SETTINGS[i]; if (s.type === "group") continue; // a header, no value if (s.type === "checks") { const opts = s.options || []; for (let k = 0; k < opts.length; k++) applySetting(opts[k].id, opts[k]["default"]); continue; } applySetting(s.id, s["default"]); } ummAnnounce();}Three things generalise from this:
- Apply defaults before announcing. The mod is then correct with or without UMM installed, and stored values simply overwrite a moment later.
- Guard the non-value types.
groupis skipped entirely;checksdescends intooptionsbecause each option is its own boolean. applySettingis idempotent. Boot legitimately calls it twice for one setting, first with the default and then with the saved value.
The same mod, with the client library
Section titled “The same mod, with the client library”Everything above is the inline snippet, which is what buffs-log ships. Here is
the same integration written against the
client library instead. The manifest is untouched, because
both paths take the same one.
var settings = UMM.register({ id: "buffs_log", name: "Sinners & Statues", settings: UMM_SETTINGS, // the identical array declared above onChange: applySetting});applySetting is passed straight through, unchanged. It is the one thing both
paths have in common, and the one thing you always write yourself.
What the library absorbs is everything around it: the UMM_CHANNEL and
UMM_PROTOCOL constants, ummAnnounce, onUmmBus with its five guards, and
registerSettings with its group and checks descent. Three functions and two
constants become one field.
What you take on instead is a vendored file to keep somewhere and an include to order correctly. That trade is the whole decision.
The regression test
Section titled “The regression test”The test that matters for any integration: uninstall UMM, relaunch, and confirm your mod still loads and uses its defaults with no JS errors in the console.