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:
@@ -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, """);
|
||||
}
|
||||
})();
|
||||
@@ -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, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<");
|
||||
}
|
||||
})();
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return String(value).replace(/"/g, """);
|
||||
}
|
||||
})();
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return String(value).replace(/"/g, """);
|
||||
}
|
||||
})();
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user