Add PrismaUI HTML server browser & integration

Integrate a PrismaUI_F4-based HTML server browser and related tooling. Adds a ThirdParty submodule (framework-F4-Conversion), UI views (CommonwealthOnline browser HTML/JS/CSS), and multiple plugin headers/sources to bridge PrismaUI/Scaleform, handle menu input, and manage server browser data and actions (mock server list, join/connect flow).

Key changes:
- New build/deploy scripts: build-prismaui.bat, build-all.bat, deploy-all.bat, deploy-ui.bat and a PowerShell helper for linking the Ultralight SDK (ULTRALIGHT_SDK_PATH, default F:\\UltralightSDK).
- Plugin additions: F4TPrismaUI, F4TScaleformUI, F4TServerBrowserBridge, F4TServerBrowserData, F4TMenuInput, F4TDebugOverlay and implementations to create/show/hide the HTML view, forward controller input, register Scaleform functions (openManager/closeManager), and push UI events.
- Networking: add ConnectToServer(host,port) and keep ConnectToLocalServer() as wrapper; update connection logs and error handling.
- Main menu SWF/AS updates: updated MainMenu.swf and MainMenu.as to call closeManager() when leaving multiplayer and to integrate open/close hooks.
- Documentation: plugin/setup.md updated with PrismaUI build/deploy instructions.
- Removed Main_ModManager.swf (deleted).

Primary intent: provide an in-game HTML server browser (toggleable with F9 or Scaleform calls), wire up controller input, and add build/deploy workflow for PrismaUI/Ultralight so developers can build and deploy the UI and plugin together. Mock server data is used for now; network join/connect plumbing is provided for local/dev servers.
This commit is contained in:
2026-06-08 14:02:49 +12:00
parent 2ced73f7b9
commit fe620020be
38 changed files with 3565 additions and 15 deletions
@@ -0,0 +1,53 @@
(function () {
"use strict";
window.CO_ControllerPrompts = {
render: function (root, state) {
var el = root.querySelector(".co-prompts");
if (!el) return;
var prompts = CO_ControllerPrompts.getPrompts(state);
el.innerHTML = prompts.map(function (p) {
return (
'<div class="co-prompt">' +
'<span class="co-prompt-key">' + p.key + "</span>" +
"<span>" + p.label + "</span>" +
"</div>"
);
}).join("");
},
getPrompts: function (state) {
if (state.joinOverlay) {
if (state.joinOverlay.error) {
return [{ key: "B", label: "Dismiss" }];
}
return [];
}
if (state.activeTab === "direct-connect") {
return [
{ key: "A", label: "Connect" },
{ key: "B", label: "Back" }
];
}
var base = [
{ key: "A", label: "Join" },
{ key: "X", label: "Refresh" },
{ key: "Y", label: "Favorite" },
{ key: "B", label: "Back" }
];
if (state.focusZone === "tabs") {
return [
{ key: "LB", label: "Prev Tab" },
{ key: "RB", label: "Next Tab" },
{ key: "B", label: "Back" }
];
}
return base;
}
};
})();
@@ -0,0 +1,55 @@
(function () {
"use strict";
window.CO_DirectConnect = {
render: function (root, state) {
var pane = root.querySelector('[data-pane="direct-connect"]');
if (!pane) return;
var address = state.directConnect.address || "127.0.0.1";
var port = state.directConnect.port || "7777";
pane.innerHTML =
'<div class="co-direct-connect">' +
"<h2>Direct Connect</h2>" +
'<div class="co-direct-row">' +
'<label for="co-dc-address">Server Address</label>' +
'<input class="co-filter-input" id="co-dc-address" type="text" value="' + escapeAttr(address) + '">' +
"</div>" +
'<div class="co-direct-row">' +
'<label for="co-dc-port">Port</label>' +
'<input class="co-filter-input" id="co-dc-port" type="text" value="' + escapeAttr(port) + '">' +
"</div>" +
'<button type="button" class="co-btn co-dc-connect">Connect</button>' +
'<p class="co-detail-desc" style="margin-top:16px">Enter a server address to connect directly without browsing the list.</p>' +
"</div>";
},
bind: function (root, onConnect) {
var pane = root.querySelector('[data-pane="direct-connect"]');
if (!pane) return;
pane.addEventListener("click", function (e) {
if (!e.target.closest(".co-dc-connect")) return;
var address = pane.querySelector("#co-dc-address");
var port = pane.querySelector("#co-dc-port");
onConnect(address ? address.value : "", port ? port.value : "");
});
},
getValues: function (root) {
var pane = root.querySelector('[data-pane="direct-connect"]');
if (!pane) return { address: "127.0.0.1", port: "7777" };
var address = pane.querySelector("#co-dc-address");
var port = pane.querySelector("#co-dc-port");
return {
address: address ? address.value : "127.0.0.1",
port: port ? port.value : "7777"
};
}
};
function escapeAttr(value) {
return String(value).replace(/"/g, "&quot;");
}
})();
@@ -0,0 +1,98 @@
(function () {
"use strict";
var FILTER_ROWS = [
{ key: "search", type: "text", label: "Search", placeholder: "Search servers..." },
{ key: "region", type: "select", label: "Region", options: ["Any Region", "Oceania", "North America", "Europe", "Local"] },
{ key: "mode", type: "select", label: "Mode", options: ["Any Mode", "Co-op", "Roleplay", "Survival"] },
{ key: "worldspace", type: "select", label: "Worldspace", options: ["Any Worldspace", "Commonwealth", "Far Harbor", "Nuka-World"] },
{ key: "ping", type: "select", label: "Ping", options: ["Any", "< 50 ms", "< 100 ms", "< 150 ms"] },
{ key: "showFull", type: "checkbox", label: "Show Full" },
{ key: "hasPassword", type: "checkbox", label: "Has Password" },
{ key: "modded", type: "checkbox", label: "Modded" }
];
window.CO_Filters = {
FILTER_ROWS: FILTER_ROWS,
render: function (root, state) {
var el = root.querySelector(".co-filters");
if (!el) return;
var focusedClass = state.focusZone === "filters" ? " focused" : "";
var html = FILTER_ROWS.map(function (row, index) {
var rowFocused = state.focusZone === "filters" && state.selectedFilterIndex === index ? " focused" : "";
if (row.type === "text") {
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<input class="co-filter-input" type="text" data-filter="' + row.key + '" placeholder="' + row.placeholder + '" value="' + escapeAttr(state.filters[row.key] || "") + '">' +
"</div>"
);
}
if (row.type === "select") {
var opts = row.options.map(function (opt) {
var sel = (state.filters[row.key] || row.options[0]) === opt ? " selected" : "";
return '<option value="' + escapeAttr(opt) + '"' + sel + ">" + opt + "</option>";
}).join("");
return (
'<div class="co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<div class="co-filter-label">' + row.label + '</div>' +
'<select class="co-filter-select" data-filter="' + row.key + '">' + opts + "</select>" +
"</div>"
);
}
var checked = state.filters[row.key] ? " checked" : "";
return (
'<label class="co-checkbox-row co-filter-row' + rowFocused + '" data-filter-index="' + index + '">' +
'<span class="co-checkbox' + checked + '" data-filter="' + row.key + '"></span>' +
row.label +
"</label>"
);
}).join("");
html += '<button type="button" class="co-btn co-reset-btn">Reset Filters</button>';
el.className = "co-filters" + focusedClass;
el.innerHTML = html;
},
bind: function (root, onFilterChange, onReset) {
var el = root.querySelector(".co-filters");
if (!el) return;
el.addEventListener("change", function (e) {
var target = e.target;
if (!target.dataset.filter) return;
onFilterChange(target.dataset.filter, target.type === "checkbox" ? target.checked : target.value);
});
el.addEventListener("input", function (e) {
var target = e.target;
if (target.classList.contains("co-filter-input")) {
onFilterChange(target.dataset.filter, target.value);
}
});
el.addEventListener("click", function (e) {
if (e.target.closest(".co-reset-btn")) {
onReset();
return;
}
var row = e.target.closest(".co-checkbox-row");
if (row) {
var box = row.querySelector(".co-checkbox");
if (box) {
onFilterChange(box.dataset.filter, !box.classList.contains("checked"));
}
}
});
}
};
function escapeAttr(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;");
}
})();
@@ -0,0 +1,100 @@
window.CO_MOCK_SERVERS = [
{
id: "nz-1",
name: "Commonwealth NZ #1",
region: "Oceania",
players: 6,
maxPlayers: 12,
ping: 32,
worldspace: "Commonwealth",
mode: "Co-op",
status: "Open",
modded: true,
passworded: false,
favorite: true,
description: "Relaxed co-op server for explorers and builders. No PvP. Help each other rebuild the Commonwealth.",
rules: ["No griefing or stealing", "Respect build sites", "Mic optional"]
},
{
id: "vault111",
name: "Vault 111 Co-op",
region: "North America",
players: 3,
maxPlayers: 8,
ping: 48,
worldspace: "Commonwealth",
mode: "Co-op",
status: "Open",
modded: false,
passworded: false,
favorite: false,
description: "Story-friendly co-op. Stay in sync with main quest progression.",
rules: ["No skipping major quest beats without group consent"]
},
{
id: "dc-rp",
name: "DC Wasteland RP",
region: "North America",
players: 10,
maxPlayers: 10,
ping: 72,
worldspace: "Commonwealth",
mode: "Roleplay",
status: "Full",
modded: true,
passworded: true,
favorite: false,
description: "Light roleplay server set around Diamond City trade routes.",
rules: ["Stay in character in voice", "No combat logging"]
},
{
id: "far-harbor",
name: "Far Harbor Expedition",
region: "Europe",
players: 4,
maxPlayers: 6,
ping: 118,
worldspace: "Far Harbor",
mode: "Co-op",
status: "Open",
modded: true,
passworded: false,
favorite: true,
description: "DLC-focused co-op run through Far Harbor content.",
rules: ["DLC content required", "Spoiler tags in chat for newcomers"]
},
{
id: "local-relay",
name: "Local Relay (Dev)",
region: "Local",
players: 0,
maxPlayers: 16,
ping: 1,
worldspace: "Commonwealth",
mode: "Co-op",
status: "Open",
modded: false,
passworded: false,
favorite: false,
host: "127.0.0.1",
port: 7777,
description: "Python relay used by the Commonwealth Online prototype. Start server/server.py before joining.",
rules: ["Development server only"]
},
{
id: "survival-hard",
name: "Survival Hardcore",
region: "Europe",
players: 2,
maxPlayers: 4,
ping: 95,
worldspace: "Commonwealth",
mode: "Survival",
status: "Open",
modded: true,
passworded: false,
favorite: false,
description: "Hardcore survival with limited respawns and shared resources.",
rules: ["Permadeath on disconnect during combat", "Shared stash rules apply"]
}
];
@@ -0,0 +1,60 @@
(function () {
"use strict";
window.CO_ServerDetails = {
render: function (root, state) {
var el = root.querySelector(".co-details");
if (!el) return;
el.className = "co-details" + (state.focusZone === "details" ? " focused" : "");
var server = state.servers[state.selectedServerIndex];
if (!server) {
el.innerHTML = '<div class="co-details-title">Server Details</div><div class="co-empty">Select a server</div>';
return;
}
var rulesHtml = "";
if (server.rules && server.rules.length) {
rulesHtml = "<ul class=\"co-detail-rules\">" + server.rules.map(function (r) {
return "<li>" + escapeHtml(r) + "</li>";
}).join("") + "</ul>";
}
var previewHtml = server.previewImage
? '<img src="' + escapeAttr(server.previewImage) + '" alt="Preview">'
: "Preview Unavailable";
el.innerHTML =
'<div class="co-details-title">Server Details</div>' +
'<div class="co-preview">' + previewHtml + "</div>" +
'<div class="co-detail-name">' + escapeHtml(server.name) + "</div>" +
detailRow("Region", server.region) +
detailRow("Players", server.players + "/" + server.maxPlayers) +
detailRow("Ping", server.ping + " ms") +
detailRow("Worldspace", server.worldspace) +
detailRow("Mode", server.mode) +
detailRow("Status", server.status) +
detailRow("Modded", server.modded ? "Yes" : "No") +
detailRow("Password", server.passworded ? "Required" : "None") +
'<div class="co-detail-desc">' + escapeHtml(server.description || "") + "</div>" +
rulesHtml +
'<div class="co-detail-desc" style="margin-top:8px;color:var(--co-text-dim)">Mod list: coming soon</div>';
}
};
function detailRow(label, value) {
return '<div class="co-detail-row"><span>' + label + '</span><span>' + escapeHtml(String(value)) + "</span></div>";
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function escapeAttr(value) {
return String(value).replace(/"/g, "&quot;");
}
})();
@@ -0,0 +1,80 @@
(function () {
"use strict";
window.CO_ServerList = {
pingClass: function (ping) {
if (ping <= 50) return "co-ping-good";
if (ping <= 100) return "co-ping-mid";
return "co-ping-bad";
},
render: function (root, state) {
var wrap = root.querySelector(".co-server-list-wrap");
var list = root.querySelector(".co-server-list");
if (!wrap || !list) return;
wrap.className = "co-server-list-wrap" + (state.focusZone === "server-list" ? " focused" : "");
if (!state.servers.length) {
list.innerHTML = '<div class="co-empty">No servers found</div>';
return;
}
list.innerHTML = state.servers.map(function (server, index) {
var selected = index === state.selectedServerIndex ? " selected" : "";
var fav = server.favorite ? " on" : "";
var pingCls = CO_ServerList.pingClass(server.ping);
return (
'<div class="co-server-row' + selected + '" data-index="' + index + '" data-id="' + escapeAttr(server.id) + '">' +
'<span class="co-fav' + fav + '">' + (server.favorite ? "\u2605" : "\u2606") + "</span>" +
"<span>" + escapeHtml(server.name) + "</span>" +
"<span>" + server.players + "/" + server.maxPlayers + "</span>" +
'<span class="co-ping ' + pingCls + '">' + server.ping + " ms</span>" +
"<span>" + escapeHtml(server.worldspace) + "</span>" +
"<span>" + escapeHtml(server.mode) + "</span>" +
"<span>" + escapeHtml(server.status) + "</span>" +
"</div>"
);
}).join("");
},
bind: function (root, onSelect) {
var list = root.querySelector(".co-server-list");
if (!list) return;
list.addEventListener("click", function (e) {
var row = e.target.closest(".co-server-row");
if (!row) return;
onSelect(parseInt(row.getAttribute("data-index"), 10));
});
list.addEventListener("dblclick", function (e) {
var row = e.target.closest(".co-server-row");
if (!row) return;
onSelect(parseInt(row.getAttribute("data-index"), 10));
if (window.CO_App && window.CO_App.joinSelected) {
window.CO_App.joinSelected();
}
});
},
scrollToSelected: function (root, index) {
var list = root.querySelector(".co-server-list");
var row = list && list.querySelector('.co-server-row[data-index="' + index + '"]');
if (row) {
row.scrollIntoView({ block: "nearest" });
}
}
};
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function escapeAttr(value) {
return String(value).replace(/"/g, "&quot;");
}
})();
@@ -0,0 +1,42 @@
(function () {
"use strict";
var TAB_ORDER = ["direct-connect", "server-browser", "favorites", "recent"];
window.CO_Tabs = {
TAB_ORDER: TAB_ORDER,
render: function (root, state) {
var tabsEl = root.querySelector(".co-tabs");
if (!tabsEl) return;
var labels = {
"direct-connect": "Direct Connect",
"server-browser": "Server Browser",
favorites: "Favorites",
recent: "Recent"
};
tabsEl.innerHTML = TAB_ORDER.map(function (tabId, index) {
var active = state.activeTab === tabId ? " active" : "";
var focused = state.focusZone === "tabs" && state.selectedTabIndex === index ? " focused" : "";
return (
'<button type="button" class="co-tab' + active + focused + '" data-tab="' + tabId + '" data-index="' + index + '">' +
labels[tabId] +
"</button>"
);
}).join("");
},
bind: function (root, onTabClick) {
var tabsEl = root.querySelector(".co-tabs");
if (!tabsEl) return;
tabsEl.addEventListener("click", function (e) {
var btn = e.target.closest(".co-tab");
if (!btn) return;
onTabClick(btn.getAttribute("data-tab"));
});
}
};
})();