Add LAN discovery and Local servers UI

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.
This commit is contained in:
2026-06-22 15:53:17 +12:00
parent 7efe407421
commit 9ceb6601ae
18 changed files with 1318 additions and 25 deletions
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace F4T::LanDiscovery
{
struct DiscoveredServer
{
std::string host;
std::uint16_t port{ 7777 };
std::string name;
int players{ 0 };
int maxPlayers{ 16 };
int ping{ 0 };
};
std::vector<DiscoveredServer> Scan(std::string& a_error);
}
+4
View File
@@ -48,8 +48,11 @@ namespace F4T::ServerBrowser
void ResetFilters();
void ApplyFilters();
void SetAllServers(std::vector<ServerEntry> a_servers);
void SetLocalServers(std::vector<ServerEntry> a_servers);
void ToggleFavorite(const std::string& a_serverId);
const ServerEntry* FindServer(const std::string& a_serverId) const;
const ServerEntry* FindLocalServer(const std::string& a_serverId) const;
const std::vector<ServerEntry>& GetLocalServers() const { return m_localServers; }
const std::vector<ServerEntry>& GetFilteredServers() const { return m_filteredServers; }
const BrowserFilters& GetFilters() const { return m_filters; }
std::string GetConnectionStatus() const { return m_connectionStatus; }
@@ -63,6 +66,7 @@ namespace F4T::ServerBrowser
bool PassesFilters(const ServerEntry& a_entry) const;
std::vector<ServerEntry> m_allServers;
std::vector<ServerEntry> m_localServers;
std::vector<ServerEntry> m_filteredServers;
BrowserFilters m_filters;
std::string m_connectionStatus{ "Online" };
+584
View File
@@ -0,0 +1,584 @@
#include "pch.h"
#include "F4TLanDiscovery.h"
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <iphlpapi.h>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <format>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <nlohmann/json.hpp>
namespace
{
constexpr auto kProtocolName = "commonwealth-online";
constexpr auto kDiscoverPort = 7778;
constexpr auto kDefaultGamePort = 7777;
constexpr auto kUdpListenDuration = 3500ms;
constexpr auto kTcpBatchTimeout = 120ms;
constexpr auto kTcpBatchSize = 32;
constexpr auto kMaxHostsPerSubnet = 1024;
bool g_winsockInitialized = false;
struct LanSubnet
{
std::string localIp;
std::uint32_t networkHostOrder{ 0 };
std::uint32_t broadcastHostOrder{ 0 };
};
struct TcpProbe
{
SOCKET socket{ INVALID_SOCKET };
std::string host;
std::uint16_t port{ kDefaultGamePort };
};
bool EnsureWinsockInitialized()
{
if (g_winsockInitialized) {
return true;
}
WSADATA winsockData{};
if (WSAStartup(MAKEWORD(2, 2), std::addressof(winsockData)) != 0) {
return false;
}
g_winsockInitialized = true;
return true;
}
std::optional<std::string> IPv4ToString(const IN_ADDR& a_addr)
{
std::array<char, INET_ADDRSTRLEN> buffer{};
if (!InetNtopA(AF_INET, std::addressof(a_addr), buffer.data(), static_cast<DWORD>(buffer.size()))) {
return std::nullopt;
}
return std::string{ buffer.data() };
}
std::optional<std::uint32_t> IPv4ToHostOrder(std::string_view a_ipv4)
{
in_addr address{};
if (inet_pton(AF_INET, std::string{ a_ipv4 }.c_str(), std::addressof(address)) != 1) {
return std::nullopt;
}
return ntohl(address.S_un.S_addr);
}
std::string HostOrderToIPv4(std::uint32_t a_host)
{
in_addr address{};
address.S_un.S_addr = htonl(a_host);
return IPv4ToString(address).value_or("0.0.0.0");
}
std::vector<LanSubnet> GetLanSubnets()
{
std::vector<LanSubnet> subnets;
std::unordered_set<std::uint32_t> seenNetworks;
ULONG bufferSize = 0;
if (GetAdaptersAddresses(
AF_INET,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
nullptr,
nullptr,
std::addressof(bufferSize)) != ERROR_BUFFER_OVERFLOW) {
return subnets;
}
std::vector<std::byte> buffer(bufferSize);
auto* addresses = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
if (GetAdaptersAddresses(
AF_INET,
GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
nullptr,
addresses,
std::addressof(bufferSize)) != NO_ERROR) {
return subnets;
}
for (auto* adapter = addresses; adapter != nullptr; adapter = adapter->Next) {
if (adapter->OperStatus != IfOperStatusUp) {
continue;
}
for (auto* unicast = adapter->FirstUnicastAddress; unicast != nullptr; unicast = unicast->Next) {
if (unicast->Address.lpSockaddr == nullptr ||
unicast->Address.lpSockaddr->sa_family != AF_INET) {
continue;
}
const auto* sockaddr = reinterpret_cast<sockaddr_in*>(unicast->Address.lpSockaddr);
const auto localIp = IPv4ToString(sockaddr->sin_addr);
if (!localIp || localIp->starts_with("127.") || localIp->starts_with("169.254.")) {
continue;
}
auto prefixLength = static_cast<int>(unicast->OnLinkPrefixLength);
if (prefixLength <= 0 || prefixLength > 32) {
prefixLength = 24;
}
const auto ipHost = ntohl(sockaddr->sin_addr.S_un.S_addr);
const auto mask = prefixLength == 32 ?
0xFFFFFFFFu :
(~0u << (32 - prefixLength));
auto network = ipHost & mask;
auto broadcast = network | ~mask;
if ((broadcast - network) > kMaxHostsPerSubnet) {
network = ipHost & 0xFFFFFF00u;
broadcast = network | 0xFFu;
}
if (!seenNetworks.insert(network).second) {
continue;
}
subnets.push_back(LanSubnet{
.localIp = *localIp,
.networkHostOrder = network,
.broadcastHostOrder = broadcast
});
}
}
return subnets;
}
std::vector<std::uint32_t> BuildHostAddresses(const LanSubnet& a_subnet)
{
std::vector<std::uint32_t> hosts;
const auto selfHost = IPv4ToHostOrder(a_subnet.localIp).value_or(0);
if (a_subnet.broadcastHostOrder <= a_subnet.networkHostOrder + 1) {
return hosts;
}
hosts.reserve(static_cast<std::size_t>(a_subnet.broadcastHostOrder - a_subnet.networkHostOrder - 1));
for (auto host = a_subnet.networkHostOrder + 1; host < a_subnet.broadcastHostOrder; ++host) {
if (host == selfHost) {
continue;
}
hosts.push_back(host);
}
return hosts;
}
bool SetSocketNonBlocking(SOCKET a_socket, bool a_nonBlocking)
{
u_long mode = a_nonBlocking ? 1 : 0;
return ioctlsocket(a_socket, FIONBIO, std::addressof(mode)) == 0;
}
bool SendDiscoverRequest(SOCKET a_socket, const sockaddr_in& a_target)
{
static constexpr auto kRequest =
R"({"type":"discover","protocol":"commonwealth-online","version":1})";
const auto sent = sendto(
a_socket,
kRequest,
static_cast<int>(std::strlen(kRequest)),
0,
reinterpret_cast<const sockaddr*>(std::addressof(a_target)),
sizeof(a_target));
return sent > 0;
}
bool MakeDiscoverTarget(sockaddr_in& a_target, std::string_view a_host)
{
a_target = {};
a_target.sin_family = AF_INET;
a_target.sin_port = htons(kDiscoverPort);
return inet_pton(AF_INET, std::string{ a_host }.c_str(), std::addressof(a_target.sin_addr)) == 1;
}
std::optional<F4T::LanDiscovery::DiscoveredServer> ParseDiscoverResponse(
std::string_view a_json,
std::string_view a_host,
int a_pingMs)
{
auto json = nlohmann::json::parse(a_json, nullptr, false);
if (json.is_discarded() || !json.is_object()) {
return std::nullopt;
}
if (json.value("type", "") != "discoverResponse") {
return std::nullopt;
}
if (json.value("protocol", "") != kProtocolName) {
return std::nullopt;
}
F4T::LanDiscovery::DiscoveredServer server;
server.host = std::string{ a_host };
server.port = static_cast<std::uint16_t>(json.value("port", kDefaultGamePort));
server.name = json.value("name", "");
server.players = json.value("players", 0);
server.maxPlayers = json.value("maxPlayers", 16);
server.ping = a_pingMs;
return server;
}
std::string MakeServerKey(std::string_view a_host, std::uint16_t a_port)
{
return std::format("{}:{}", a_host, a_port);
}
bool TryReadWelcomePacket(SOCKET a_socket, std::string& a_line)
{
std::array<char, 256> chunk{};
a_line.clear();
while (a_line.find('\n') == std::string::npos) {
const auto received = recv(a_socket, chunk.data(), static_cast<int>(chunk.size()), 0);
if (received <= 0) {
return false;
}
a_line.append(chunk.data(), static_cast<std::size_t>(received));
if (a_line.size() > 4096) {
return false;
}
}
return true;
}
bool IsWelcomePacket(std::string_view a_line)
{
const auto end = a_line.find('\n');
const auto payload = a_line.substr(0, end);
auto json = nlohmann::json::parse(payload, nullptr, false);
return !json.is_discarded() && json.is_object() && json.value("type", "") == "welcome";
}
void CloseTcpProbe(TcpProbe& a_probe)
{
if (a_probe.socket != INVALID_SOCKET) {
closesocket(a_probe.socket);
a_probe.socket = INVALID_SOCKET;
}
}
void CollectUdpResponses(
SOCKET a_socket,
std::chrono::steady_clock::time_point a_sendStartedAt,
std::chrono::steady_clock::time_point a_deadline,
std::unordered_map<std::string, F4T::LanDiscovery::DiscoveredServer>& a_discovered)
{
std::array<char, 2048> receiveBuffer{};
while (std::chrono::steady_clock::now() < a_deadline) {
fd_set readSet{};
FD_ZERO(std::addressof(readSet));
FD_SET(a_socket, std::addressof(readSet));
const auto remaining = std::chrono::duration_cast<std::chrono::microseconds>(
a_deadline - std::chrono::steady_clock::now());
if (remaining.count() <= 0) {
break;
}
timeval timeout{};
timeout.tv_sec = static_cast<long>(remaining.count() / 1'000'000);
timeout.tv_usec = static_cast<long>(remaining.count() % 1'000'000);
const auto selectResult = select(0, std::addressof(readSet), nullptr, nullptr, std::addressof(timeout));
if (selectResult <= 0) {
break;
}
sockaddr_in senderAddress{};
int senderAddressSize = sizeof(senderAddress);
const auto received = recvfrom(
a_socket,
receiveBuffer.data(),
static_cast<int>(receiveBuffer.size()),
0,
reinterpret_cast<sockaddr*>(std::addressof(senderAddress)),
std::addressof(senderAddressSize));
if (received <= 0) {
continue;
}
const auto senderHost = IPv4ToString(senderAddress.sin_addr);
if (!senderHost) {
continue;
}
const auto pingMs = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - a_sendStartedAt).count());
const auto response = ParseDiscoverResponse(
std::string_view{ receiveBuffer.data(), static_cast<std::size_t>(received) },
*senderHost,
pingMs);
if (!response) {
continue;
}
a_discovered.emplace(MakeServerKey(response->host, response->port), *response);
}
}
void ScanTcpHosts(
const std::vector<std::uint32_t>& a_hosts,
std::unordered_map<std::string, F4T::LanDiscovery::DiscoveredServer>& a_discovered)
{
for (std::size_t offset = 0; offset < a_hosts.size(); offset += kTcpBatchSize) {
std::vector<TcpProbe> probes;
const auto batchEnd = offset + kTcpBatchSize < a_hosts.size() ? offset + kTcpBatchSize : a_hosts.size();
for (std::size_t index = offset; index < batchEnd; ++index) {
const auto host = HostOrderToIPv4(a_hosts[index]);
const auto key = MakeServerKey(host, kDefaultGamePort);
if (a_discovered.contains(key)) {
continue;
}
const auto probeSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (probeSocket == INVALID_SOCKET) {
continue;
}
if (!SetSocketNonBlocking(probeSocket, true)) {
closesocket(probeSocket);
continue;
}
sockaddr_in serverAddress{};
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(kDefaultGamePort);
if (inet_pton(AF_INET, host.c_str(), std::addressof(serverAddress.sin_addr)) != 1) {
closesocket(probeSocket);
continue;
}
const auto connectResult = connect(
probeSocket,
reinterpret_cast<const sockaddr*>(std::addressof(serverAddress)),
sizeof(serverAddress));
if (connectResult != 0 && WSAGetLastError() != WSAEWOULDBLOCK) {
closesocket(probeSocket);
continue;
}
probes.push_back(TcpProbe{ probeSocket, host, kDefaultGamePort });
}
const auto batchDeadline = std::chrono::steady_clock::now() + kTcpBatchTimeout;
while (!probes.empty() && std::chrono::steady_clock::now() < batchDeadline) {
fd_set writeSet{};
fd_set exceptSet{};
FD_ZERO(std::addressof(writeSet));
FD_ZERO(std::addressof(exceptSet));
SOCKET maxSocket = 0;
for (const auto& probe : probes) {
FD_SET(probe.socket, std::addressof(writeSet));
FD_SET(probe.socket, std::addressof(exceptSet));
if (probe.socket > maxSocket) {
maxSocket = probe.socket;
}
}
const auto remaining = std::chrono::duration_cast<std::chrono::microseconds>(
batchDeadline - std::chrono::steady_clock::now());
timeval timeout{};
timeout.tv_sec = static_cast<long>(remaining.count() / 1'000'000);
timeout.tv_usec = static_cast<long>(remaining.count() % 1'000'000);
const auto selectResult = select(
static_cast<int>(maxSocket + 1),
nullptr,
std::addressof(writeSet),
std::addressof(exceptSet),
std::addressof(timeout));
if (selectResult <= 0) {
break;
}
for (auto probeIt = probes.begin(); probeIt != probes.end();) {
auto& probe = *probeIt;
const bool writable = FD_ISSET(probe.socket, std::addressof(writeSet)) != 0;
const bool failed = FD_ISSET(probe.socket, std::addressof(exceptSet)) != 0;
if (!writable && !failed) {
++probeIt;
continue;
}
bool accepted = false;
if (writable && !failed) {
int socketError = 0;
int socketErrorSize = sizeof(socketError);
if (getsockopt(
probe.socket,
SOL_SOCKET,
SO_ERROR,
reinterpret_cast<char*>(std::addressof(socketError)),
std::addressof(socketErrorSize)) == 0 &&
socketError == 0) {
const auto probeStartedAt = std::chrono::steady_clock::now();
SetSocketNonBlocking(probe.socket, false);
DWORD recvTimeoutMs = static_cast<DWORD>(kTcpBatchTimeout.count());
setsockopt(
probe.socket,
SOL_SOCKET,
SO_RCVTIMEO,
reinterpret_cast<const char*>(std::addressof(recvTimeoutMs)),
sizeof(recvTimeoutMs));
std::string line;
if (TryReadWelcomePacket(probe.socket, line) && IsWelcomePacket(line)) {
F4T::LanDiscovery::DiscoveredServer server;
server.host = probe.host;
server.port = probe.port;
server.ping = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - probeStartedAt).count());
a_discovered.emplace(MakeServerKey(server.host, server.port), std::move(server));
accepted = true;
}
}
}
CloseTcpProbe(probe);
probeIt = probes.erase(probeIt);
if (accepted) {
continue;
}
}
}
for (auto& probe : probes) {
CloseTcpProbe(probe);
}
}
}
}
namespace F4T::LanDiscovery
{
std::vector<DiscoveredServer> Scan(std::string& a_error)
{
a_error.clear();
if (!EnsureWinsockInitialized()) {
a_error = "Could not initialize Winsock for LAN discovery.";
return {};
}
const auto discoverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (discoverSocket == INVALID_SOCKET) {
a_error = "Could not create UDP socket for LAN discovery.";
return {};
}
const BOOL broadcastEnabled = TRUE;
if (setsockopt(
discoverSocket,
SOL_SOCKET,
SO_BROADCAST,
reinterpret_cast<const char*>(std::addressof(broadcastEnabled)),
sizeof(broadcastEnabled)) != 0) {
closesocket(discoverSocket);
a_error = "Could not enable LAN discovery broadcast.";
return {};
}
sockaddr_in bindAddress{};
bindAddress.sin_family = AF_INET;
bindAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
bindAddress.sin_port = 0;
if (bind(discoverSocket, reinterpret_cast<const sockaddr*>(std::addressof(bindAddress)), sizeof(bindAddress)) != 0) {
closesocket(discoverSocket);
a_error = "Could not bind LAN discovery socket.";
return {};
}
if (!SetSocketNonBlocking(discoverSocket, true)) {
closesocket(discoverSocket);
a_error = "Could not configure LAN discovery socket.";
return {};
}
const auto subnets = GetLanSubnets();
std::vector<std::uint32_t> tcpHosts;
std::unordered_set<std::uint32_t> tcpHostSet;
sockaddr_in target{};
const auto sendStartedAt = std::chrono::steady_clock::now();
if (MakeDiscoverTarget(target, "127.0.0.1")) {
SendDiscoverRequest(discoverSocket, target);
}
if (MakeDiscoverTarget(target, "255.255.255.255")) {
SendDiscoverRequest(discoverSocket, target);
}
for (const auto& subnet : subnets) {
const auto broadcastIp = HostOrderToIPv4(subnet.broadcastHostOrder);
if (MakeDiscoverTarget(target, broadcastIp)) {
SendDiscoverRequest(discoverSocket, target);
}
for (const auto host : BuildHostAddresses(subnet)) {
const auto hostIp = HostOrderToIPv4(host);
if (MakeDiscoverTarget(target, hostIp)) {
SendDiscoverRequest(discoverSocket, target);
}
if (tcpHostSet.insert(host).second) {
tcpHosts.push_back(host);
}
}
}
std::unordered_map<std::string, DiscoveredServer> discovered;
const auto udpDeadline = sendStartedAt + kUdpListenDuration;
CollectUdpResponses(discoverSocket, sendStartedAt, udpDeadline, discovered);
closesocket(discoverSocket);
ScanTcpHosts(tcpHosts, discovered);
std::vector<DiscoveredServer> results;
results.reserve(discovered.size());
for (auto& [_, server] : discovered) {
results.push_back(std::move(server));
}
std::ranges::sort(results, [](const DiscoveredServer& a_lhs, const DiscoveredServer& a_rhs) {
if (a_lhs.ping != a_rhs.ping) {
return a_lhs.ping < a_rhs.ping;
}
return a_lhs.host < a_rhs.host;
});
return results;
}
}
+124 -3
View File
@@ -1,15 +1,18 @@
#include "pch.h"
#include "F4TNetworking.h"
#include "F4TLanDiscovery.h"
#include "F4TPrismaUI.h"
#include "F4TServerBrowserBridge.h"
#include "F4TServerBrowserData.h"
#include "F4TSessionLaunch.h"
#include <algorithm>
#include <atomic>
#include <charconv>
#include <format>
#include <string_view>
#include <thread>
namespace F4T::ServerBrowser
{
@@ -18,6 +21,33 @@ namespace F4T::ServerBrowser
PRISMA_UI_API::IVPrismaUI4* g_api = nullptr;
PrismaView g_view = 0;
ServerBrowserData g_data;
std::atomic_bool g_localScanRunning{ false };
ServerEntry DiscoveredServerToEntry(const F4T::LanDiscovery::DiscoveredServer& a_discovered)
{
ServerEntry entry;
entry.id = std::format("local:{}:{}", a_discovered.host, a_discovered.port);
entry.name = a_discovered.name;
entry.host = a_discovered.host;
entry.port = static_cast<int>(a_discovered.port);
entry.region = "Local";
entry.worldspace = "Commonwealth";
entry.mode = "Co-op";
entry.players = a_discovered.players;
entry.maxPlayers = a_discovered.maxPlayers;
entry.ping = a_discovered.ping;
entry.status = entry.players >= entry.maxPlayers ? "Full" : "Open";
return entry;
}
nlohmann::json LocalServersToJson()
{
nlohmann::json servers = nlohmann::json::array();
for (const auto& entry : g_data.GetLocalServers()) {
servers.push_back(entry.ToJson());
}
return servers;
}
std::string GetLogPrefix()
{
@@ -61,6 +91,14 @@ namespace F4T::ServerBrowser
});
}
void PushLocalServersUpdated()
{
DispatchEvent(nlohmann::json{
{ "type", "localServersUpdated" },
{ "servers", LocalServersToJson() }
});
}
nlohmann::json ColorToJson(const RE::NiColor& a_color)
{
const auto toByte = [](float a_channel) {
@@ -164,7 +202,8 @@ namespace F4T::ServerBrowser
PushConnectionStatusEvent();
DispatchEvent(nlohmann::json{
{ "type", "joinSucceeded" },
{ "serverId", a_entry.id }
{ "serverId", a_entry.id },
{ "server", a_entry.ToJson() }
});
DispatchEvent(nlohmann::json{
{ "type", "uiSound" },
@@ -261,6 +300,74 @@ namespace F4T::ServerBrowser
LogInfo(" Server list refreshed (mock data).");
}
void CompleteLocalScan(std::vector<ServerEntry> a_servers, std::string a_error, bool a_success)
{
g_data.SetLocalServers(std::move(a_servers));
PushLocalServersUpdated();
auto finishedEvent = nlohmann::json{
{ "type", "localScanFinished" },
{ "success", a_success }
};
if (!a_error.empty()) {
finishedEvent["error"] = a_error;
}
DispatchEvent(std::move(finishedEvent));
if (a_success) {
LogInfo(std::format(" LAN scan found {} local server(s).", g_data.GetLocalServers().size()));
} else {
LogWarning(std::format(" LAN scan found no servers. {}", a_error));
}
g_localScanRunning.store(false);
}
void ScanLocalServers()
{
if (g_localScanRunning.exchange(true)) {
return;
}
DispatchEvent(nlohmann::json{ { "type", "localScanStarted" } });
std::thread([]() {
std::string error;
const auto discovered = F4T::LanDiscovery::Scan(error);
std::vector<ServerEntry> localServers;
localServers.reserve(discovered.size());
for (const auto& server : discovered) {
localServers.push_back(DiscoveredServerToEntry(server));
}
const bool success = !discovered.empty();
if (!success && error.empty()) {
error = "No Commonwealth Online servers responded on the LAN. "
"Check that the host server is running and Windows Firewall allows UDP 7778 and TCP 7777.";
}
const auto serversCopy = std::move(localServers);
const auto errorCopy = std::move(error);
const auto* taskInterface = F4SE::GetTaskInterface();
if (taskInterface) {
taskInterface->AddTask([serversCopy, errorCopy, success]() {
CompleteLocalScan(
std::vector<ServerEntry>(serversCopy),
errorCopy,
success);
});
return;
}
CompleteLocalScan(
std::vector<ServerEntry>(serversCopy),
errorCopy,
success);
}).detach();
}
}
void ServerBrowserBridge::Initialize(PRISMA_UI_API::IVPrismaUI4* a_api, PrismaView a_view)
@@ -294,6 +401,11 @@ namespace F4T::ServerBrowser
return;
}
if (action == "scanLocalServers") {
ScanLocalServers();
return;
}
if (action == "setFilter") {
auto filters = g_data.GetFilters();
const auto filterName = a_action.value("filterName", "");
@@ -338,12 +450,18 @@ namespace F4T::ServerBrowser
}
if (action == "switchTab") {
if (a_action.value("tabName", "") == "local") {
ScanLocalServers();
}
return;
}
if (action == "joinServer") {
const auto serverId = a_action.value("serverId", "");
const auto* entry = g_data.FindServer(serverId);
if (!entry) {
entry = g_data.FindLocalServer(serverId);
}
if (!entry) {
DispatchEvent(nlohmann::json{
{ "type", "joinFailed" },
@@ -362,17 +480,20 @@ namespace F4T::ServerBrowser
if (action == "openDirectConnect") {
ServerEntry entry;
entry.id = "direct";
entry.name = "Direct Connect";
entry.host = a_action.value("address", "127.0.0.1");
entry.port = 7777;
std::string error;
if (!PopulateJoinEntry(entry, a_action, error)) {
entry.id = "direct";
entry.name = "Direct Connect";
CompleteJoinFailure(entry, error);
return;
}
entry.id = std::format("recent:{}:{}", entry.host, entry.port);
entry.name = "";
if (!JoinServerEntry(entry, error)) {
CompleteJoinFailure(entry, error);
}
+15
View File
@@ -135,6 +135,11 @@ namespace F4T::ServerBrowser
ApplyFilters();
}
void ServerBrowserData::SetLocalServers(std::vector<ServerEntry> a_servers)
{
m_localServers = std::move(a_servers);
}
void ServerBrowserData::ToggleFavorite(const std::string& a_serverId)
{
for (auto& entry : m_allServers) {
@@ -156,6 +161,16 @@ namespace F4T::ServerBrowser
return nullptr;
}
const ServerEntry* ServerBrowserData::FindLocalServer(const std::string& a_serverId) const
{
for (const auto& entry : m_localServers) {
if (entry.id == a_serverId) {
return std::addressof(entry);
}
}
return nullptr;
}
nlohmann::json ServerBrowserData::ToServerListJson() const
{
nlohmann::json servers = nlohmann::json::array();
+1 -1
View File
@@ -29,5 +29,5 @@ target("CommonwealthOnline")
add_includedirs("include")
add_includedirs("../ThirdParty/framework-F4-Conversion/example-f4se-plugin/src")
add_packages("nlohmann_json")
add_syslinks("ws2_32")
add_syslinks("ws2_32", "iphlpapi")
set_pcxxheader("src/pch.h")