Skip to content

Integration

Paste this into your own Panorama script. Edit the UMM_ constants and the body of applySetting; the rest is boilerplate you never touch.

(function () {
var UMM_CHANNEL = "ClientUI_FireOutput";
var UMM_PROTOCOL = 1;
var UMM_ID = "my_mod"; // stable, unique; namespaces your saved settings
var UMM_NAME = "My Mod"; // optional tab title; defaults to the id
var UMM_SETTINGS = [
{ id: "enabled", type: "toggle", label: "Enabled", default: true },
{ id: "opacity", type: "slider", label: "Opacity",
min: 20, max: 100, step: 5, default: 90, unit: "%" }
];
// Your one code path. Runs for every setting at boot and on every change.
function applySetting(key, value) {
if (key === "enabled") myPanel.visible = value;
if (key === "opacity") myPanel.style.opacity = (value / 100).toFixed(2);
}
function ummAnnounce() {
try {
$.DispatchEvent(UMM_CHANNEL, JSON.stringify({
umm: UMM_PROTOCOL, t: "register",
id: UMM_ID, name: UMM_NAME, settings: UMM_SETTINGS
}));
} catch (e) {}
}
function onUmmBus(payload) {
if (typeof payload !== "string" || payload.indexOf('"umm"') === -1) return;
var 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);
}
function registerSettings() {
try { $.RegisterForUnhandledEvent(UMM_CHANNEL, onUmmBus); } catch (e) {}
// Apply declared defaults now, so the mod works even without UMM installed.
for (var i = 0; i < UMM_SETTINGS.length; i++) {
var s = UMM_SETTINGS[i];
if (s.type === "group") continue; // a header, no value
if (s.type === "checks") { // one independent boolean per option
var opts = s.options || [];
for (var k = 0; k < opts.length; k++) applySetting(opts[k].id, opts[k]["default"]);
continue;
}
applySetting(s.id, s["default"]);
}
ummAnnounce();
}
registerSettings();
})();

That is the whole integration. There is nothing to bundle, nothing to require and no version to track.

Everything is UMM_-prefixed on purpose. You are pasting into a file that already has code in it, and the prefix keeps the snippet from colliding with what is already there and makes it greppable later.

The worked example walks through buffs-log’s copy of it line by line, using the same identifiers.

  • UMM_ID is required, unique, and permanent. It namespaces your saved settings, so changing it orphans every value your users have saved.
  • UMM_NAME is optional and defaults to the id. It is the tab title.
  • applySetting(key, value) is your only code path. It fires once per setting at boot, again if UMM has a saved value for it, and on every change afterwards. Keep it idempotent: the boot sequence can call it twice for one setting, first with the default, then with the saved value.
  • The default loop skips group and descends into checks. Neither carries a value of its own, so a loop without those guards breaks the moment you add a divider. See the settings reference.
  • Persistence is UMM’s job. A change applies immediately and holds for the session; the player clicks Save Settings in the window to keep it across restarts. Restored values reach you as ordinary set messages.
  • Reset needs no code. The reset button sends your mod a set message carrying the default value, identical to a change made by hand.

Mods live in separate VPKs and separate Panorama panel contexts, and exactly one channel crosses contexts: ClientUI_FireOutput. It is engine-declared and carries a string payload. Arbitrary event names are not dispatchable, which is why every community framework piggybacks on this one event.

Two consequences shape the snippet:

  • Namespacing lives inside the payload ({"umm":1,...}), because the channel itself is shared with everyone.
  • Handlers must not throw on foreign traffic. Hence the cheap indexOf('"umm"') check before JSON.parse, and the try/catch around it. Other mods are broadcasting on this channel and your handler sees all of it.

Load order between UMM and your mod is undefined, so both directions are covered: a mod that boots later announces itself, and UMM broadcasts hello on boot so mods that booted earlier announce themselves again.

Three messages cross the boundary between UMM and your mod. Each is a JSON string on ClientUI_FireOutput carrying umm: 1 as its version marker.

{umm:1, t:"hello"} // UMM -> mods, "re-announce"
{umm:1, t:"register", id, name, settings, values} // mod -> UMM, full manifest
{umm:1, t:"set", id, key, value} // UMM -> mod, value changed

values is the one field the snippet leaves out. Send it alongside your manifest if your mod already holds values it wants UMM to adopt; see value precedence for how it ranks against stored values and your declared defaults.

Deadlock Mod Manager shows a mod’s requirements on its install page, and it takes them from the GameBanana submission - not from anything in your VPK. If you want players to see UMM listed there, add it to your mod page’s requirements yourself.

  1. Open your mod on GameBanana and click Edit, then the Technical tab.
  2. Scroll down to Requirements and add an entry.
  3. Name it Universal Mod Manager and set the URL to https://gamebanana.com/mods/693642.
  4. Click the gear next to the entry and pick Required or Recommended, then set the one below it to Enabled.
  5. Save the submission. Deadlock Mod Manager picks the change up the next time it refreshes your mod’s page.

Use the GameBanana page URL, not the docs site or a direct download link. That URL is what the manager resolves back to a mod it can install.