Skip to content

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.

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.

function ummAnnounce() {
try {
$.DispatchEvent(UMM_CHANNEL, JSON.stringify({
umm: UMM_PROTOCOL, t: "register",
id: UMM_ID, name: "Sinners & Statues", settings: UMM_SETTINGS
}));
} catch (e) {}
}
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.

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:

  1. Apply defaults before announcing. The mod is then correct with or without UMM installed, and stored values simply overwrite a moment later.
  2. Guard the non-value types. group is skipped entirely; checks descends into options because each option is its own boolean.
  3. applySetting is idempotent. Boot legitimately calls it twice for one setting, first with the default and then with the saved value.

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 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.