(function () { "use strict"; var root = document.getElementById("coRoot"); var panel = document.getElementById("coPanel"); var joinOverlay = document.getElementById("coJoinOverlay"); var joinMessage = document.getElementById("coJoinMessage"); var FOCUS_ZONES = ["tabs", "filters", "server-list"]; var REPEAT_INITIAL_MS = 400; var REPEAT_RATE_MS = 80; var repeatTimer = null; var repeatAction = null; var state = { activeTab: "server-browser", focusZone: "server-list", selectedTabIndex: 1, selectedServerIndex: 0, selectedFilterIndex: 0, filters: { search: "", region: "Any Region", mode: "Any Mode", worldspace: "Any Worldspace", ping: "Any", showFull: false, hasPassword: false, modded: false }, allServers: [], servers: [], favorites: [], recent: [], connectionStatus: "Online", isRefreshing: false, directConnect: { address: "127.0.0.1", port: "7777" }, joinOverlay: null }; function sendAction(action, payload) { var msg = JSON.stringify(Object.assign({ action: action }, payload || {})); if (typeof window.coAction === "function") { window.coAction(msg); return; } console.warn("[CO] coAction bridge unavailable:", action); handleActionLocal(action, payload || {}); } function handleActionLocal(action, payload) { switch (action) { case "refreshServerList": refreshLocalMock(); break; case "setFilter": state.filters[payload.filterName] = payload.value; applyLocalFilters(); render(); break; case "resetFilters": resetLocalFilters(); break; case "toggleFavorite": toggleFavoriteLocal(payload.serverId); break; case "joinServer": joinServerLocal(payload.serverId); break; case "openDirectConnect": joinDirectLocal(payload.address, payload.port); break; case "switchTab": switchTab(payload.tabName); break; case "closeBrowser": if (typeof window.closeBrowser === "function") window.closeBrowser(); break; default: break; } } function refreshLocalMock() { state.isRefreshing = true; render(); setTimeout(function () { state.allServers = (window.CO_MOCK_SERVERS || []).slice(); applyLocalFilters(); state.isRefreshing = false; render(); }, 600); } function resetLocalFilters() { state.filters = { search: "", region: "Any Region", mode: "Any Mode", worldspace: "Any Worldspace", ping: "Any", showFull: false, hasPassword: false, modded: false }; sendAction("resetFilters", {}); applyLocalFilters(); render(); } function applyLocalFilters() { var list = state.allServers.slice(); var f = state.filters; if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function (s) { return s.name.toLowerCase().indexOf(q) >= 0; }); } if (f.region !== "Any Region") { list = list.filter(function (s) { return s.region === f.region; }); } if (f.mode !== "Any Mode") { list = list.filter(function (s) { return s.mode === f.mode; }); } if (f.worldspace !== "Any Worldspace") { list = list.filter(function (s) { return s.worldspace === f.worldspace; }); } if (f.ping === "< 50 ms") list = list.filter(function (s) { return s.ping < 50; }); else if (f.ping === "< 100 ms") list = list.filter(function (s) { return s.ping < 100; }); else if (f.ping === "< 150 ms") list = list.filter(function (s) { return s.ping < 150; }); if (!f.showFull) list = list.filter(function (s) { return s.status !== "Full"; }); if (f.hasPassword) list = list.filter(function (s) { return s.passworded; }); if (f.modded) list = list.filter(function (s) { return s.modded; }); state.servers = list; state.favorites = state.allServers.filter(function (s) { return s.favorite; }); if (state.selectedServerIndex >= state.servers.length) { state.selectedServerIndex = Math.max(0, state.servers.length - 1); } updateServerCount(); } function updateServerCount() { var countEl = document.getElementById("coServerCount"); var titleEl = document.getElementById("coSubpanelTitle"); var count = state.servers.length; if (countEl) { countEl.textContent = count + " Found"; } if (titleEl && state.activeTab === "server-browser") { titleEl.textContent = count === 1 ? "1 Server" : count + " Servers"; } } function toggleFavoriteLocal(serverId) { var server = findServerById(serverId); if (!server) return; server.favorite = !server.favorite; sendAction("toggleFavorite", { serverId: serverId }); applyLocalFilters(); render(); } function findServerById(id) { for (var i = 0; i < state.allServers.length; i++) { if (state.allServers[i].id === id) return state.allServers[i]; } return null; } function getSelectedServer() { return getActiveList()[state.selectedServerIndex] || null; } function getActiveList() { if (state.activeTab === "favorites") return state.favorites; if (state.activeTab === "recent") return state.recent; return state.servers; } function joinServerLocal(serverId) { var server = findServerById(serverId); if (!server) return; showJoinOverlay("Connecting to " + server.name + "..."); sendAction("joinServer", { serverId: serverId }); } function joinDirectLocal(address, port) { showJoinOverlay("Connecting to " + address + ":" + port + "..."); sendAction("openDirectConnect", { address: address, port: port }); } function showJoinOverlay(message, isError) { state.joinOverlay = { message: message, error: !!isError }; joinOverlay.classList.remove("hidden"); if (isError) joinOverlay.classList.add("co-error"); else joinOverlay.classList.remove("co-error"); joinMessage.textContent = message; var spinner = joinOverlay.querySelector(".co-spinner"); if (spinner) spinner.style.display = isError ? "none" : "block"; render(); } function hideJoinOverlay() { state.joinOverlay = null; joinOverlay.classList.add("hidden"); render(); } function switchTab(tabName) { state.activeTab = tabName; state.selectedTabIndex = CO_Tabs.TAB_ORDER.indexOf(tabName); if (state.selectedTabIndex < 0) state.selectedTabIndex = 0; if (tabName === "server-browser") state.focusZone = "server-list"; else if (tabName === "direct-connect") state.focusZone = "tabs"; sendAction("switchTab", { tabName: tabName }); render(); } function render() { if (!root) return; panel.classList.toggle("co-loading", state.isRefreshing); CO_Tabs.render(root, state); CO_ControllerPrompts.render(root, state); var statusEl = document.getElementById("coConnectionStatus"); if (statusEl) { statusEl.textContent = String(state.connectionStatus || "Online").toUpperCase(); } document.querySelectorAll(".co-tab-pane").forEach(function (pane) { pane.classList.toggle("active", pane.getAttribute("data-pane") === state.activeTab); }); if (state.activeTab === "direct-connect") { CO_DirectConnect.render(root, state); } else { var listServers = state.servers; if (state.activeTab === "favorites") listServers = state.favorites; if (state.activeTab === "recent") listServers = state.recent; var listState = Object.assign({}, state, { servers: listServers }); if (state.activeTab === "favorites" || state.activeTab === "recent") { listState.focusZone = state.focusZone === "filters" ? "server-list" : state.focusZone; } if (state.activeTab === "server-browser") { CO_Filters.render(root, listState); } if (state.activeTab !== "direct-connect") { CO_ServerDetails.render(root, listState); } var activePane = root.querySelector('.co-tab-pane[data-pane="' + state.activeTab + '"]'); if (activePane) { var listEl = activePane.querySelector(".co-server-list"); if (listEl) { var tempRoot = { querySelector: function (sel) { if (sel === ".co-server-list-wrap") return activePane.querySelector(".co-server-list-wrap"); if (sel === ".co-server-list") return listEl; return root.querySelector(sel); }}; CO_ServerList.render(tempRoot, listState); } } } updateServerCount(); } function bindUi() { CO_Tabs.bind(root, function (tabName) { switchTab(tabName); }); CO_Filters.bind(root, function (name, value) { state.filters[name] = value; sendAction("setFilter", { filterName: name, value: value }); applyLocalFilters(); render(); }, resetLocalFilters); root.addEventListener("click", function (e) { var row = e.target.closest(".co-tab-pane.active .co-server-row"); if (!row) return; state.selectedServerIndex = parseInt(row.getAttribute("data-index"), 10); render(); }); root.addEventListener("dblclick", function (e) { var row = e.target.closest(".co-tab-pane.active .co-server-row"); if (!row) return; state.selectedServerIndex = parseInt(row.getAttribute("data-index"), 10); joinSelected(); }); CO_DirectConnect.bind(root, function (address, port) { state.directConnect = { address: address, port: port }; joinDirectLocal(address, port); }); document.addEventListener("keydown", onKeyDown); } function onKeyDown(e) { if (state.joinOverlay && state.joinOverlay.error && e.key === "Escape") { hideJoinOverlay(); return; } if (state.activeTab === "direct-connect") return; handleControllerButton(mapKeyToButton(e.key), "down"); } function mapKeyToButton(key) { var map = { ArrowUp: "dpad_up", ArrowDown: "dpad_down", ArrowLeft: "dpad_left", ArrowRight: "dpad_right", Enter: "a", Escape: "b", " ": "a", r: "x", f: "y", q: "lb", e: "rb" }; return map[key] || null; } function handleControllerButton(button, phase) { if (!button || phase !== "down") return; if (state.joinOverlay) { if (button === "b") hideJoinOverlay(); return; } if (button === "lb" || button === "rb") { var idx = state.selectedTabIndex; idx = button === "lb" ? idx - 1 : idx + 1; if (idx < 0) idx = CO_Tabs.TAB_ORDER.length - 1; if (idx >= CO_Tabs.TAB_ORDER.length) idx = 0; switchTab(CO_Tabs.TAB_ORDER[idx]); return; } if (button === "b") { sendAction("closeBrowser", {}); return; } if (state.activeTab === "direct-connect") { if (button === "a") { var vals = CO_DirectConnect.getValues(root); joinDirectLocal(vals.address, vals.port); } return; } if (button === "a") { joinSelected(); return; } if (button === "x") { sendAction("refreshServerList", {}); return; } if (button === "y") { var sel = getSelectedServer(); if (sel) toggleFavoriteLocal(sel.id); return; } navigateFocus(button); } function joinSelected() { var sel = getSelectedServer(); if (!sel) return; joinServerLocal(sel.id); } function nextFocusZone(fromIdx, direction) { var zones = FOCUS_ZONES.slice(); if (state.activeTab !== "server-browser") { zones = zones.filter(function (z) { return z !== "filters"; }); } var current = zones.indexOf(state.focusZone); if (current < 0) current = fromIdx; var next = current + direction; if (next < 0 || next >= zones.length) return null; return zones[next]; } function navigateFocus(button) { var list = getActiveList(); if (button === "dpad_left") { var prevZone = nextFocusZone(FOCUS_ZONES.indexOf(state.focusZone), -1); if (prevZone) { state.focusZone = prevZone; render(); } return; } if (button === "dpad_right") { if (state.activeTab === "direct-connect") return; var nextZone = nextFocusZone(FOCUS_ZONES.indexOf(state.focusZone), 1); if (nextZone) { state.focusZone = nextZone; render(); } return; } if (state.focusZone === "tabs") { if (button === "dpad_up" || button === "dpad_down") { var dir = button === "dpad_up" ? -1 : 1; state.selectedTabIndex = (state.selectedTabIndex + dir + CO_Tabs.TAB_ORDER.length) % CO_Tabs.TAB_ORDER.length; switchTab(CO_Tabs.TAB_ORDER[state.selectedTabIndex]); } return; } if (state.focusZone === "filters") { var maxFilter = CO_Filters.FILTER_ROWS.length; if (button === "dpad_up") state.selectedFilterIndex = Math.max(0, state.selectedFilterIndex - 1); if (button === "dpad_down") state.selectedFilterIndex = Math.min(maxFilter, state.selectedFilterIndex + 1); if (button === "a" && state.selectedFilterIndex === maxFilter) resetLocalFilters(); render(); return; } if (state.focusZone === "server-list") { if (button === "dpad_up") state.selectedServerIndex = Math.max(0, state.selectedServerIndex - 1); if (button === "dpad_down") state.selectedServerIndex = Math.min(list.length - 1, state.selectedServerIndex + 1); render(); CO_ServerList.scrollToSelected(root, state.selectedServerIndex); return; } } function startRepeat(action) { stopRepeat(); repeatAction = action; repeatTimer = setTimeout(function tick() { if (repeatAction) repeatAction(); repeatTimer = setTimeout(tick, REPEAT_RATE_MS); }, REPEAT_INITIAL_MS); } function stopRepeat() { if (repeatTimer) clearTimeout(repeatTimer); repeatTimer = null; repeatAction = null; } window.dispatchCoEvent = function (event) { if (!event || !event.type) return; switch (event.type) { case "serverListUpdated": state.allServers = event.servers || []; applyLocalFilters(); break; case "serverListRefreshStarted": state.isRefreshing = true; break; case "serverListRefreshFinished": state.isRefreshing = false; if (!event.success && event.error) { console.warn("[CO] Refresh failed:", event.error); } break; case "joinStarted": showJoinOverlay("Connecting..."); break; case "joinFailed": showJoinOverlay(event.reason || "Connection failed", true); break; case "joinSucceeded": hideJoinOverlay(); addRecent(event.serverId); break; case "connectionStatusChanged": state.connectionStatus = event.status || "Online"; break; case "uiTheme": if (typeof window.applyCoTheme === "function") { window.applyCoTheme(event); } break; case "menuOpened": break; case "menuClosed": hideJoinOverlay(); break; case "controllerInput": if (event.phase === "down") { handleControllerButton(event.button, "down"); if (event.button === "dpad_up" || event.button === "dpad_down") { startRepeat(function () { handleControllerButton(event.button, "down"); }); } } else if (event.phase === "up") { stopRepeat(); } break; case "uiSound": // TODO: Wire to Fallout 4 menu sound via C++ when audio hooks are available. break; default: break; } render(); }; function addRecent(serverId) { var server = findServerById(serverId); if (!server) return; state.recent = state.recent.filter(function (s) { return s.id !== serverId; }); state.recent.unshift(server); if (state.recent.length > 8) state.recent.pop(); } window.setConnectionStatus = function (connected, logPrefix) { state.connectionStatus = connected ? "Connected" : "Online"; render(); }; window.onBrowserShown = function () { dispatchCoEvent({ type: "menuOpened" }); render(); }; window.coAction = window.coAction || function () { console.warn("[CO] coAction bridge not ready"); }; window.CO_App = { joinSelected: joinSelected, getState: function () { return state; } }; state.allServers = (window.CO_MOCK_SERVERS || []).slice(); applyLocalFilters(); bindUi(); render(); console.log("[CO] Server browser loaded"); })();