Adds LAN server discovery and a Local tab in the multiplayer UI so clients can find relays on the same LAN. Introduces a new F4T::LanDiscovery module (plugin/include/F4TLanDiscovery.h, plugin/src/F4TLanDiscovery.cpp) that probes LAN hosts via UDP discover (port 7778) and TCP welcome fallback (port 7777). Server-side support includes a UDP responder (server/lan_discovery.py) and integration in server_core.py to start/stop discovery. Plugin changes (F4TServerBrowserBridge/Data) add background scanning, dispatch results to the game thread, new local-server storage and events (scanLocalServers, localServersUpdated, localScanStarted/localScanFinished), and small join/recent handling updates. UI changes (ui/.../app.js and components) add localServers state, scanning UX, Recent persistence (localStorage), and join logic for local/direct entries. Docs updated (docs/protocol.md, docs/dev-log.md, server/README.md) and build script links iphlpapi. Also removes two unused exported sprite PNGs.
150 lines
5.4 KiB
JavaScript
150 lines
5.4 KiB
JavaScript
(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";
|
|
},
|
|
|
|
isLocalHost: function (host) {
|
|
if (!host) return false;
|
|
return /^(127\.0\.0\.1|localhost|::1)$/i.test(String(host).trim());
|
|
},
|
|
|
|
displayName: function (server) {
|
|
if (!server) return "";
|
|
var host = server.host || server.address || "";
|
|
var port = server.port > 0 ? server.port : 7777;
|
|
var name = String(server.name || "").trim();
|
|
var isLocal = server.region === "Local" || CO_ServerList.isLocalHost(host);
|
|
var isDirect = server.id === "direct" || name === "Direct Connect";
|
|
if (!name || isLocal || isDirect) {
|
|
return (host || "127.0.0.1") + ":" + port;
|
|
}
|
|
return name;
|
|
},
|
|
|
|
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" : "");
|
|
var isRecent = state.activeTab === "recent";
|
|
var isLocal = state.activeTab === "local";
|
|
|
|
if (!state.servers.length) {
|
|
var emptyText = "No servers found";
|
|
if (isRecent) emptyText = "No recent servers";
|
|
else if (isLocal) {
|
|
emptyText = state.isScanningLocal ?
|
|
"Scanning local network..." :
|
|
"No local servers found. Check the host PC is running server.py and firewall allows UDP 7778 / TCP 7777.";
|
|
}
|
|
list.innerHTML = '<div class="co-empty">' + emptyText + "</div>";
|
|
CO_ServerList.updateScrollIndicator(wrap, list);
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = state.servers.map(function (server, index) {
|
|
var selected = index === state.selectedServerIndex ? " selected" : "";
|
|
var label = CO_ServerList.displayName(server);
|
|
|
|
if (isRecent) {
|
|
return (
|
|
'<div class="co-server-row' + selected + '" data-index="' + index + '" data-id="' + escapeAttr(server.id || "") + '">' +
|
|
'<div class="co-server-row-left">' +
|
|
'<span class="co-server-row-name">' + escapeHtml(label) + "</span>" +
|
|
"</div>" +
|
|
"</div>"
|
|
);
|
|
}
|
|
|
|
if (isLocal) {
|
|
var pingCls = CO_ServerList.pingClass(server.ping || 0);
|
|
return (
|
|
'<div class="co-server-row' + selected + '" data-index="' + index + '" data-id="' + escapeAttr(server.id || "") + '">' +
|
|
'<div class="co-server-row-left">' +
|
|
'<span class="co-server-row-name">' + escapeHtml(label) + "</span>" +
|
|
"</div>" +
|
|
'<div class="co-server-row-right">' +
|
|
"<span>" + (server.players || 0) + "/" + (server.maxPlayers || 16) + "</span>" +
|
|
'<span class="co-ping ' + pingCls + '">' + (server.ping || 0) + " ms</span>" +
|
|
"</div>" +
|
|
"</div>"
|
|
);
|
|
}
|
|
|
|
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) + '">' +
|
|
'<div class="co-server-row-left">' +
|
|
'<span class="co-fav' + fav + '">' + (server.favorite ? "\u2605" : "\u2606") + "</span>" +
|
|
'<span class="co-server-row-name">' + escapeHtml(label) + "</span>" +
|
|
"</div>" +
|
|
'<div class="co-server-row-right">' +
|
|
"<span>" + server.players + "/" + server.maxPlayers + "</span>" +
|
|
'<span class="co-ping ' + pingCls + '">' + server.ping + " ms</span>" +
|
|
"</div>" +
|
|
"</div>"
|
|
);
|
|
}).join("");
|
|
|
|
CO_ServerList.updateScrollIndicator(wrap, list);
|
|
},
|
|
|
|
updateScrollIndicator: function (wrap, list) {
|
|
var indicator = wrap && wrap.querySelector(".co-scroll-indicator");
|
|
if (!indicator || !list) return;
|
|
var hasOverflow = list.scrollHeight > list.clientHeight + 2;
|
|
indicator.classList.toggle("hidden", !hasOverflow);
|
|
},
|
|
|
|
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" });
|
|
}
|
|
var wrap = root.querySelector(".co-server-list-wrap");
|
|
if (wrap && list) {
|
|
CO_ServerList.updateScrollIndicator(wrap, list);
|
|
}
|
|
}
|
|
};
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return String(value).replace(/"/g, """);
|
|
}
|
|
})();
|