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,10 @@
|
||||
#pragma once
|
||||
|
||||
// Future developer/debug overlay (ImGui or PrismaUI inspector panel).
|
||||
// Player-facing UI lives in ui/views/CommonwealthOnline/browser/.
|
||||
namespace F4T::DebugOverlay
|
||||
{
|
||||
inline void Initialize() {}
|
||||
inline void Shutdown() {}
|
||||
inline void Draw() {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace F4T::MenuInput
|
||||
{
|
||||
void Register();
|
||||
void SetBrowserActive(bool a_active);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace F4T::Networking
|
||||
{
|
||||
std::string GetLocalPlayerLogPrefix();
|
||||
bool ConnectToServer(const char* a_host, std::uint16_t a_port);
|
||||
bool ConnectToLocalServer();
|
||||
void DisconnectFromLocalServer();
|
||||
bool IsConnectedToServer();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
namespace F4T::PrismaUI
|
||||
{
|
||||
void OnGameDataReady();
|
||||
void ShowServerBrowser();
|
||||
void HideServerBrowser();
|
||||
bool IsBrowserVisible();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace F4T::ScaleformUI
|
||||
{
|
||||
void RegisterPlugin();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "PrismaUI_F4_API.h"
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace F4T::ServerBrowser
|
||||
{
|
||||
class ServerBrowserBridge
|
||||
{
|
||||
public:
|
||||
static void Initialize(PRISMA_UI_API::IVPrismaUI4* a_api, PrismaView a_view);
|
||||
static void OnBrowserShown();
|
||||
static void OnBrowserHidden();
|
||||
static void HandleAction(const nlohmann::json& a_action);
|
||||
static void ForwardControllerInput(const char* a_button, const char* a_phase);
|
||||
static void PushConnectionStatus();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace F4T::ServerBrowser
|
||||
{
|
||||
struct ServerEntry
|
||||
{
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string region;
|
||||
std::string worldspace;
|
||||
std::string mode;
|
||||
std::string status;
|
||||
std::string description;
|
||||
std::string host;
|
||||
int port{ 0 };
|
||||
int players{ 0 };
|
||||
int maxPlayers{ 0 };
|
||||
int ping{ 0 };
|
||||
bool modded{ false };
|
||||
bool passworded{ false };
|
||||
bool favorite{ false };
|
||||
|
||||
nlohmann::json ToJson() const;
|
||||
static ServerEntry FromJson(const nlohmann::json& a_json);
|
||||
};
|
||||
|
||||
struct BrowserFilters
|
||||
{
|
||||
std::string search;
|
||||
std::string region{ "Any Region" };
|
||||
std::string mode{ "Any Mode" };
|
||||
std::string worldspace{ "Any Worldspace" };
|
||||
std::string ping{ "Any" };
|
||||
bool showFull{ false };
|
||||
bool hasPassword{ false };
|
||||
bool modded{ false };
|
||||
};
|
||||
|
||||
class ServerBrowserData
|
||||
{
|
||||
public:
|
||||
void SetFilters(const BrowserFilters& a_filters);
|
||||
void ResetFilters();
|
||||
void ApplyFilters();
|
||||
void SetAllServers(std::vector<ServerEntry> a_servers);
|
||||
void ToggleFavorite(const std::string& a_serverId);
|
||||
const ServerEntry* FindServer(const std::string& a_serverId) const;
|
||||
const std::vector<ServerEntry>& GetFilteredServers() const { return m_filteredServers; }
|
||||
const BrowserFilters& GetFilters() const { return m_filters; }
|
||||
std::string GetConnectionStatus() const { return m_connectionStatus; }
|
||||
void SetConnectionStatus(std::string a_status) { m_connectionStatus = std::move(a_status); }
|
||||
bool IsRefreshing() const { return m_isRefreshing; }
|
||||
void SetRefreshing(bool a_refreshing) { m_isRefreshing = a_refreshing; }
|
||||
nlohmann::json ToServerListJson() const;
|
||||
static std::vector<ServerEntry> CreateMockServers();
|
||||
|
||||
private:
|
||||
bool PassesFilters(const ServerEntry& a_entry) const;
|
||||
|
||||
std::vector<ServerEntry> m_allServers;
|
||||
std::vector<ServerEntry> m_filteredServers;
|
||||
BrowserFilters m_filters;
|
||||
std::string m_connectionStatus{ "Online" };
|
||||
bool m_isRefreshing{ false };
|
||||
};
|
||||
}
|
||||
@@ -51,6 +51,56 @@ The plugin does not currently include:
|
||||
* Combat sync
|
||||
* Quest sync
|
||||
|
||||
## PrismaUI HTML Server Browser
|
||||
|
||||
Commonwealth Online renders an HTML server browser through [PrismaUI_F4](https://github.com/NomadsReach/framework-F4-Conversion), vendored as:
|
||||
|
||||
```text
|
||||
ThirdParty/framework-F4-Conversion
|
||||
```
|
||||
|
||||
Ultralight SDK setup:
|
||||
|
||||
* Default SDK path: `F:\UltralightSDK`
|
||||
* Override with environment variable: `ULTRALIGHT_SDK_PATH`
|
||||
|
||||
Build PrismaUI_F4:
|
||||
|
||||
```bat
|
||||
build-prismaui.bat
|
||||
```
|
||||
|
||||
Build and deploy everything:
|
||||
|
||||
```bat
|
||||
build.bat
|
||||
build-prismaui.bat
|
||||
deploy-all.bat
|
||||
```
|
||||
|
||||
`deploy-all.bat` defaults to:
|
||||
|
||||
```text
|
||||
D:\SteamLibrary\steamapps\common\Fallout 4\Data
|
||||
```
|
||||
|
||||
It installs:
|
||||
|
||||
* `Data/F4SE/Plugins/PrismaUI_F4.dll`
|
||||
* `Data/F4SE/Plugins/CommonwealthOnline.dll`
|
||||
* `Data/PrismaUI_F4/libs/*.dll`
|
||||
* `Data/PrismaUI_F4/resources/*`
|
||||
* `Data/PrismaUI_F4/views/CommonwealthOnline/browser/index.html`
|
||||
|
||||
In-game controls:
|
||||
|
||||
* Main Menu > Multiplayer opens the server browser
|
||||
* Press **F9** to toggle the browser
|
||||
* Use the server list to browse, filter, and join servers
|
||||
* **Local Relay (Dev)** connects to `127.0.0.1:7777` — start `server/server.py` first
|
||||
|
||||
If PrismaUI_F4 is missing, the plugin still loads and networking continues to work; only the HTML UI is skipped.
|
||||
|
||||
## Required Tools
|
||||
|
||||
The plugin setup is expected to require:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TMenuInput.h"
|
||||
#include "F4TServerBrowserBridge.h"
|
||||
|
||||
#include <F4SE/InputMap.h>
|
||||
|
||||
namespace F4T::MenuInput
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::atomic_bool g_browserActive{ false };
|
||||
std::atomic_bool g_sinkRegistered{ false };
|
||||
|
||||
const char* GamepadButtonName(std::uint32_t a_keyCode)
|
||||
{
|
||||
switch (a_keyCode) {
|
||||
case F4SE::InputMap::kGamepadButtonOffset_DPAD_UP:
|
||||
return "dpad_up";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_DPAD_DOWN:
|
||||
return "dpad_down";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_DPAD_LEFT:
|
||||
return "dpad_left";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_DPAD_RIGHT:
|
||||
return "dpad_right";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_A:
|
||||
return "a";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_B:
|
||||
return "b";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_X:
|
||||
return "x";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_Y:
|
||||
return "y";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_LEFT_SHOULDER:
|
||||
return "lb";
|
||||
case F4SE::InputMap::kGamepadButtonOffset_RIGHT_SHOULDER:
|
||||
return "rb";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
class GamepadInputHandler :
|
||||
public RE::BSInputEventUser
|
||||
{
|
||||
public:
|
||||
static GamepadInputHandler* GetSingleton()
|
||||
{
|
||||
static GamepadInputHandler instance;
|
||||
return std::addressof(instance);
|
||||
}
|
||||
|
||||
bool ShouldHandleEvent(const RE::InputEvent* a_event) override
|
||||
{
|
||||
if (!g_browserActive.load() || !a_event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return a_event->eventType == RE::INPUT_EVENT_TYPE::kButton &&
|
||||
a_event->device == RE::INPUT_DEVICE::kGamepad;
|
||||
}
|
||||
|
||||
void OnButtonEvent(const RE::ButtonEvent* a_event) override
|
||||
{
|
||||
if (!a_event || !g_browserActive.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto keyCode = static_cast<std::uint32_t>(a_event->GetBSButtonCode());
|
||||
const auto* buttonName = GamepadButtonName(keyCode);
|
||||
if (!buttonName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* phase = nullptr;
|
||||
if (a_event->QJustPressed()) {
|
||||
phase = "down";
|
||||
} else if (a_event->QReleased()) {
|
||||
phase = "up";
|
||||
}
|
||||
|
||||
if (phase) {
|
||||
F4T::ServerBrowser::ServerBrowserBridge::ForwardControllerInput(buttonName, phase);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void Register()
|
||||
{
|
||||
if (g_sinkRegistered.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* menuControls = RE::MenuControls::GetSingleton();
|
||||
if (menuControls) {
|
||||
menuControls->handlers.push_back(GamepadInputHandler::GetSingleton());
|
||||
REX::INFO("[MenuInput] Registered gamepad handler for server browser.");
|
||||
} else {
|
||||
REX::WARN("[MenuInput] MenuControls unavailable; gamepad navigation disabled.");
|
||||
g_sinkRegistered.store(false);
|
||||
}
|
||||
}
|
||||
|
||||
void SetBrowserActive(bool a_active)
|
||||
{
|
||||
g_browserActive.store(a_active);
|
||||
}
|
||||
}
|
||||
@@ -596,8 +596,12 @@ namespace F4T::Networking
|
||||
return "[LocalPlayerId=" + std::to_string(*assignedPlayerId) + "]";
|
||||
}
|
||||
|
||||
bool ConnectToLocalServer()
|
||||
bool ConnectToServer(const char* a_host, std::uint16_t a_port)
|
||||
{
|
||||
if (!a_host || a_host[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsConnectedToServer()) {
|
||||
return true;
|
||||
}
|
||||
@@ -610,27 +614,25 @@ namespace F4T::Networking
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep this first networking milestone local-only so it can prove the
|
||||
// packet path without exposing any gameplay state to a remote server.
|
||||
const auto localSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (localSocket == INVALID_SOCKET) {
|
||||
LogWarningWithLocalPlayerPrefix("Could not create socket for Commonwealth Online local server connection.");
|
||||
LogWarningWithLocalPlayerPrefix("Could not create socket for Commonwealth Online server connection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetNonBlocking(localSocket)) {
|
||||
closesocket(localSocket);
|
||||
LogWarningWithLocalPlayerPrefix(
|
||||
"Could not configure non-blocking socket for Commonwealth Online local server connection.");
|
||||
"Could not configure non-blocking socket for Commonwealth Online server connection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_in serverAddress{};
|
||||
serverAddress.sin_family = AF_INET;
|
||||
serverAddress.sin_port = htons(kLocalServerPort);
|
||||
if (inet_pton(AF_INET, kLocalServerHost, std::addressof(serverAddress.sin_addr)) != 1) {
|
||||
serverAddress.sin_port = htons(a_port);
|
||||
if (inet_pton(AF_INET, a_host, std::addressof(serverAddress.sin_addr)) != 1) {
|
||||
closesocket(localSocket);
|
||||
LogWarningWithLocalPlayerPrefix("Could not parse Commonwealth Online local server address.");
|
||||
LogWarningWithLocalPlayerPrefix(std::format("Could not parse Commonwealth Online server address '{}'.", a_host));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -641,12 +643,10 @@ namespace F4T::Networking
|
||||
|
||||
if (!connected) {
|
||||
closesocket(localSocket);
|
||||
// The server is a developer tool, not a runtime requirement. A failed
|
||||
// connection should leave Fallout 4 running and simply disable packet sends.
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Could not connect to Commonwealth Online local server at {}:{}",
|
||||
kLocalServerHost,
|
||||
kLocalServerPort));
|
||||
"Could not connect to Commonwealth Online server at {}:{}",
|
||||
a_host,
|
||||
a_port));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -662,10 +662,15 @@ namespace F4T::Networking
|
||||
StartReceiveThread();
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(
|
||||
std::format("Connected to Commonwealth Online local server at {}:{}.", kLocalServerHost, kLocalServerPort));
|
||||
std::format("Connected to Commonwealth Online server at {}:{}.", a_host, a_port));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConnectToLocalServer()
|
||||
{
|
||||
return ConnectToServer(kLocalServerHost, kLocalServerPort);
|
||||
}
|
||||
|
||||
void DisconnectFromLocalServer()
|
||||
{
|
||||
StopReceiveThread();
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TMenuInput.h"
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TPrismaUI.h"
|
||||
#include "F4TServerBrowserBridge.h"
|
||||
#include "PrismaUI_F4_API.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string_view>
|
||||
|
||||
namespace F4T::PrismaUI
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr auto kServerBrowserHtmlPath = "CommonwealthOnline/browser/index.html";
|
||||
constexpr auto kToggleVirtualKey = 0x78; // VK_F9
|
||||
|
||||
PRISMA_UI_API::IVPrismaUI4* g_api = nullptr;
|
||||
PrismaView g_serverBrowserView = 0;
|
||||
bool g_visible = false;
|
||||
bool g_domReady = false;
|
||||
bool g_toggleTaskStarted = false;
|
||||
bool g_cursorRestoreTaskStarted = false;
|
||||
bool g_menuWatchTaskStarted = false;
|
||||
bool g_pendingShow = false;
|
||||
int g_cursorRestoreFramesLeft = 0;
|
||||
|
||||
std::string GetLogPrefix()
|
||||
{
|
||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " [PrismaUI]";
|
||||
}
|
||||
|
||||
void LogInfo(std::string_view a_message)
|
||||
{
|
||||
const auto message = GetLogPrefix() + " " + std::string(a_message);
|
||||
REX::INFO(std::string_view{ message });
|
||||
}
|
||||
|
||||
void LogWarning(std::string_view a_message)
|
||||
{
|
||||
const auto message = GetLogPrefix() + " " + std::string(a_message);
|
||||
REX::WARN(std::string_view{ message });
|
||||
}
|
||||
|
||||
void RestoreNativeGameCursor()
|
||||
{
|
||||
auto* ui = RE::UI::GetSingleton();
|
||||
if (!ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto cursorMenu = ui->GetMenu<RE::CursorMenu>()) {
|
||||
if (cursorMenu->uiMovie) {
|
||||
cursorMenu->uiMovie->SetVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
ui->RefreshCursor();
|
||||
|
||||
if (ui->GetMenuOpen<RE::MainMenu>()) {
|
||||
if (auto* menuCursor = RE::MenuCursor::GetSingleton()) {
|
||||
menuCursor->RegisterCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RequestNativeCursorRestore()
|
||||
{
|
||||
g_cursorRestoreFramesLeft = 4;
|
||||
}
|
||||
|
||||
void HideServerBrowserInternal()
|
||||
{
|
||||
if (!g_visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_api || !g_api->IsValid(g_serverBrowserView)) {
|
||||
g_visible = false;
|
||||
F4T::MenuInput::SetBrowserActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
g_visible = false;
|
||||
F4T::MenuInput::SetBrowserActive(false);
|
||||
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserHidden();
|
||||
g_api->Unfocus(g_serverBrowserView);
|
||||
g_api->Hide(g_serverBrowserView);
|
||||
RequestNativeCursorRestore();
|
||||
}
|
||||
|
||||
void WatchMainMenuState()
|
||||
{
|
||||
if (!g_visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* ui = RE::UI::GetSingleton();
|
||||
if (!ui || !ui->GetMenuOpen<RE::MainMenu>()) {
|
||||
HideServerBrowserInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateServerBrowserView();
|
||||
|
||||
void ShowServerBrowserInternal()
|
||||
{
|
||||
if (!g_api) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_api->IsValid(g_serverBrowserView)) {
|
||||
g_pendingShow = true;
|
||||
CreateServerBrowserView();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_domReady) {
|
||||
g_pendingShow = true;
|
||||
return;
|
||||
}
|
||||
|
||||
g_pendingShow = false;
|
||||
g_visible = true;
|
||||
F4T::MenuInput::SetBrowserActive(true);
|
||||
g_api->Show(g_serverBrowserView);
|
||||
g_api->Focus(g_serverBrowserView, false, false);
|
||||
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
|
||||
g_api->Invoke(
|
||||
g_serverBrowserView,
|
||||
"if(window.onBrowserShown){window.onBrowserShown();}");
|
||||
}
|
||||
|
||||
void ToggleServerBrowser()
|
||||
{
|
||||
if (!g_api) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_visible) {
|
||||
HideServerBrowserInternal();
|
||||
return;
|
||||
}
|
||||
|
||||
ShowServerBrowserInternal();
|
||||
}
|
||||
|
||||
void OnServerBrowserDomReady(PrismaView a_view)
|
||||
{
|
||||
g_domReady = true;
|
||||
LogInfo("Server browser DOM ready.");
|
||||
|
||||
g_api->RegisterConsoleCallback(
|
||||
a_view,
|
||||
[](PrismaView, PRISMA_UI_API::ConsoleMessageLevel a_level, const char* a_message) {
|
||||
if (!a_message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto prefix = GetLogPrefix() + " [JS]";
|
||||
switch (a_level) {
|
||||
case PRISMA_UI_API::ConsoleMessageLevel::Error:
|
||||
REX::CRITICAL("{} {}", prefix, a_message);
|
||||
break;
|
||||
case PRISMA_UI_API::ConsoleMessageLevel::Warning:
|
||||
REX::WARN("{} {}", prefix, a_message);
|
||||
break;
|
||||
default:
|
||||
REX::INFO("{} {}", prefix, a_message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
F4T::ServerBrowser::ServerBrowserBridge::Initialize(g_api, a_view);
|
||||
|
||||
g_api->BindUIEvent(a_view, "coAction", [](const char* a_json) {
|
||||
if (!a_json) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto action = nlohmann::json::parse(a_json);
|
||||
const auto actionName = action.value("action", "");
|
||||
if (actionName == "closeBrowser") {
|
||||
HideServerBrowserInternal();
|
||||
return;
|
||||
}
|
||||
F4T::ServerBrowser::ServerBrowserBridge::HandleAction(action);
|
||||
} catch (const std::exception& ex) {
|
||||
REX::WARN("{} [JS] Failed to parse coAction JSON: {}", GetLogPrefix(), ex.what());
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy bridge stubs for backward compatibility during transition.
|
||||
g_api->BindUIEvent(a_view, "connectServer", [](const char*) {
|
||||
if (!F4T::Networking::IsConnectedToServer()) {
|
||||
F4T::Networking::ConnectToLocalServer();
|
||||
}
|
||||
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
|
||||
});
|
||||
|
||||
g_api->BindUIEvent(a_view, "disconnectServer", [](const char*) {
|
||||
F4T::Networking::DisconnectFromLocalServer();
|
||||
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
|
||||
});
|
||||
|
||||
g_api->BindUIEvent(a_view, "closeBrowser", [](const char*) {
|
||||
HideServerBrowserInternal();
|
||||
});
|
||||
|
||||
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
|
||||
|
||||
if (g_pendingShow) {
|
||||
ShowServerBrowserInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateServerBrowserView()
|
||||
{
|
||||
if (!g_api) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_serverBrowserView != 0 && g_api->IsValid(g_serverBrowserView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_domReady = false;
|
||||
g_serverBrowserView = g_api->CreateView(kServerBrowserHtmlPath, OnServerBrowserDomReady);
|
||||
if (g_serverBrowserView == 0) {
|
||||
LogWarning(std::format(
|
||||
"Failed to create server browser view. Is Data/PrismaUI_F4/views/{} installed?",
|
||||
kServerBrowserHtmlPath));
|
||||
return;
|
||||
}
|
||||
|
||||
g_api->Hide(g_serverBrowserView);
|
||||
LogInfo(std::format("Created server browser view for '{}'.", kServerBrowserHtmlPath));
|
||||
}
|
||||
|
||||
void StartGameThreadTasks()
|
||||
{
|
||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||
if (!taskInterface) {
|
||||
LogWarning("F4SE task interface unavailable; F9 server browser toggle disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_toggleTaskStarted) {
|
||||
taskInterface->AddTaskPermanent([]() {
|
||||
static bool lastToggleDown = false;
|
||||
const auto toggleDown = (GetAsyncKeyState(kToggleVirtualKey) & 0x8000) != 0;
|
||||
if (toggleDown && !lastToggleDown) {
|
||||
ToggleServerBrowser();
|
||||
}
|
||||
lastToggleDown = toggleDown;
|
||||
});
|
||||
|
||||
g_toggleTaskStarted = true;
|
||||
}
|
||||
|
||||
if (!g_cursorRestoreTaskStarted) {
|
||||
taskInterface->AddTaskPermanent([]() {
|
||||
if (g_cursorRestoreFramesLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreNativeGameCursor();
|
||||
g_cursorRestoreFramesLeft--;
|
||||
});
|
||||
|
||||
g_cursorRestoreTaskStarted = true;
|
||||
}
|
||||
|
||||
if (!g_menuWatchTaskStarted) {
|
||||
taskInterface->AddTaskPermanent(WatchMainMenuState);
|
||||
g_menuWatchTaskStarted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnGameDataReady()
|
||||
{
|
||||
g_api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI4>();
|
||||
if (!g_api) {
|
||||
LogWarning(
|
||||
"PrismaUI_F4 API not found. Install PrismaUI_F4.dll and its Ultralight libs before the HTML server browser can render.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogInfo("PrismaUI_F4 API loaded.");
|
||||
F4T::MenuInput::Register();
|
||||
StartGameThreadTasks();
|
||||
CreateServerBrowserView();
|
||||
}
|
||||
|
||||
void ShowServerBrowser()
|
||||
{
|
||||
ShowServerBrowserInternal();
|
||||
}
|
||||
|
||||
void HideServerBrowser()
|
||||
{
|
||||
HideServerBrowserInternal();
|
||||
}
|
||||
|
||||
bool IsBrowserVisible()
|
||||
{
|
||||
return g_visible;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TPrismaUI.h"
|
||||
#include "F4TScaleformUI.h"
|
||||
|
||||
#include "Scaleform/G/GFx_FunctionHandler.h"
|
||||
#include "Scaleform/G/GFx_Movie.h"
|
||||
#include "Scaleform/G/GFx_Value.h"
|
||||
|
||||
namespace F4T::ScaleformUI
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class OpenManagerHandler :
|
||||
public Scaleform::GFx::FunctionHandler
|
||||
{
|
||||
public:
|
||||
void Call(const Params& a_params) override
|
||||
{
|
||||
(void)a_params;
|
||||
|
||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||
if (taskInterface) {
|
||||
taskInterface->AddTask([]() {
|
||||
F4T::PrismaUI::ShowServerBrowser();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
F4T::PrismaUI::ShowServerBrowser();
|
||||
}
|
||||
};
|
||||
|
||||
class CloseManagerHandler :
|
||||
public Scaleform::GFx::FunctionHandler
|
||||
{
|
||||
public:
|
||||
void Call(const Params& a_params) override
|
||||
{
|
||||
(void)a_params;
|
||||
|
||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||
if (taskInterface) {
|
||||
taskInterface->AddTask([]() {
|
||||
F4T::PrismaUI::HideServerBrowser();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
F4T::PrismaUI::HideServerBrowser();
|
||||
}
|
||||
};
|
||||
|
||||
OpenManagerHandler g_openManagerHandler;
|
||||
CloseManagerHandler g_closeManagerHandler;
|
||||
|
||||
bool F4SEAPI RegisterCommonwealthOnlineScaleform(
|
||||
Scaleform::GFx::Movie* a_view,
|
||||
Scaleform::GFx::Value* a_plugin)
|
||||
{
|
||||
if (!a_view || !a_plugin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Scaleform::GFx::Value openManager;
|
||||
a_view->CreateFunction(&openManager, &g_openManagerHandler);
|
||||
if (!a_plugin->SetMember("openManager", openManager)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Scaleform::GFx::Value closeManager;
|
||||
a_view->CreateFunction(&closeManager, &g_closeManagerHandler);
|
||||
return a_plugin->SetMember("closeManager", closeManager);
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterPlugin()
|
||||
{
|
||||
const auto* scaleform = F4SE::GetScaleformInterface();
|
||||
if (!scaleform) {
|
||||
REX::WARN("{} [Scaleform] Scaleform interface unavailable; Main Menu Multiplayer button will not open the server browser.",
|
||||
F4T::Networking::GetLocalPlayerLogPrefix());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scaleform->Register("commonwealthOnline", RegisterCommonwealthOnlineScaleform)) {
|
||||
REX::WARN("{} [Scaleform] Failed to register commonwealthOnline Scaleform API.",
|
||||
F4T::Networking::GetLocalPlayerLogPrefix());
|
||||
return;
|
||||
}
|
||||
|
||||
REX::INFO("{} [Scaleform] Registered root.f4se.plugins.commonwealthOnline openManager/closeManager.",
|
||||
F4T::Networking::GetLocalPlayerLogPrefix());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TServerBrowserBridge.h"
|
||||
#include "F4TServerBrowserData.h"
|
||||
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
|
||||
namespace F4T::ServerBrowser
|
||||
{
|
||||
namespace
|
||||
{
|
||||
PRISMA_UI_API::IVPrismaUI4* g_api = nullptr;
|
||||
PrismaView g_view = 0;
|
||||
ServerBrowserData g_data;
|
||||
|
||||
std::string GetLogPrefix()
|
||||
{
|
||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " [BrowserBridge]";
|
||||
}
|
||||
|
||||
void LogInfo(std::string_view a_message)
|
||||
{
|
||||
REX::INFO("{}{}", GetLogPrefix(), a_message);
|
||||
}
|
||||
|
||||
void LogWarning(std::string_view a_message)
|
||||
{
|
||||
REX::WARN("{}{}", GetLogPrefix(), a_message);
|
||||
}
|
||||
|
||||
void DispatchEvent(const nlohmann::json& a_event)
|
||||
{
|
||||
if (!g_api || g_view == 0 || !g_api->IsValid(g_view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto script = std::format("if(window.dispatchCoEvent){{window.dispatchCoEvent({});}}", a_event.dump());
|
||||
g_api->Invoke(g_view, script.c_str());
|
||||
}
|
||||
|
||||
void PushServerListUpdated()
|
||||
{
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "serverListUpdated" },
|
||||
{ "servers", g_data.ToServerListJson() }
|
||||
});
|
||||
}
|
||||
|
||||
void PushConnectionStatusEvent()
|
||||
{
|
||||
const auto connected = F4T::Networking::IsConnectedToServer();
|
||||
g_data.SetConnectionStatus(connected ? "Connected" : "Online");
|
||||
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "connectionStatusChanged" },
|
||||
{ "status", g_data.GetConnectionStatus() },
|
||||
{ "serverCount", g_data.GetFilteredServers().size() }
|
||||
});
|
||||
|
||||
if (g_api && g_view != 0 && g_api->IsValid(g_view)) {
|
||||
const auto prefix = F4T::Networking::GetLocalPlayerLogPrefix();
|
||||
const auto script = std::format(
|
||||
"if(window.setConnectionStatus){{window.setConnectionStatus({},{});}}",
|
||||
connected ? "true" : "false",
|
||||
nlohmann::json(prefix).dump());
|
||||
g_api->Invoke(g_view, script.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool JoinServerEntry(const ServerEntry& a_entry, std::string& a_error)
|
||||
{
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "joinStarted" },
|
||||
{ "serverId", a_entry.id }
|
||||
});
|
||||
|
||||
if (a_entry.passworded) {
|
||||
a_error = "Password required — password prompt not yet implemented.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto host = a_entry.host.empty() ? "127.0.0.1" : a_entry.host;
|
||||
const auto port = static_cast<std::uint16_t>(a_entry.port > 0 ? a_entry.port : 7777);
|
||||
|
||||
if (!F4T::Networking::ConnectToServer(host.c_str(), port)) {
|
||||
if (host == "127.0.0.1" && port == 7777) {
|
||||
a_error = "Could not connect to local relay. Start server/server.py first.";
|
||||
} else {
|
||||
a_error = std::format("Connection failed — server offline or unreachable at {}:{}", host, port);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RefreshServerList()
|
||||
{
|
||||
g_data.SetRefreshing(true);
|
||||
DispatchEvent(nlohmann::json{ { "type", "serverListRefreshStarted" } });
|
||||
|
||||
// TODO M6: Replace mock refresh with master-server / LAN discovery query.
|
||||
g_data.SetAllServers(ServerBrowserData::CreateMockServers());
|
||||
|
||||
g_data.SetRefreshing(false);
|
||||
PushServerListUpdated();
|
||||
PushConnectionStatusEvent();
|
||||
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "serverListRefreshFinished" },
|
||||
{ "success", true }
|
||||
});
|
||||
|
||||
LogInfo(" Server list refreshed (mock data).");
|
||||
}
|
||||
}
|
||||
|
||||
void ServerBrowserBridge::Initialize(PRISMA_UI_API::IVPrismaUI4* a_api, PrismaView a_view)
|
||||
{
|
||||
g_api = a_api;
|
||||
g_view = a_view;
|
||||
g_data.SetAllServers(ServerBrowserData::CreateMockServers());
|
||||
}
|
||||
|
||||
void ServerBrowserBridge::OnBrowserShown()
|
||||
{
|
||||
DispatchEvent(nlohmann::json{ { "type", "menuOpened" } });
|
||||
RefreshServerList();
|
||||
}
|
||||
|
||||
void ServerBrowserBridge::OnBrowserHidden()
|
||||
{
|
||||
DispatchEvent(nlohmann::json{ { "type", "menuClosed" } });
|
||||
}
|
||||
|
||||
void ServerBrowserBridge::HandleAction(const nlohmann::json& a_action)
|
||||
{
|
||||
const auto action = a_action.value("action", "");
|
||||
if (action.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "refreshServerList") {
|
||||
RefreshServerList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "setFilter") {
|
||||
auto filters = g_data.GetFilters();
|
||||
const auto filterName = a_action.value("filterName", "");
|
||||
if (filterName == "search") {
|
||||
filters.search = a_action.value("value", "");
|
||||
} else if (filterName == "region") {
|
||||
filters.region = a_action.value("value", "Any Region");
|
||||
} else if (filterName == "mode") {
|
||||
filters.mode = a_action.value("value", "Any Mode");
|
||||
} else if (filterName == "worldspace") {
|
||||
filters.worldspace = a_action.value("value", "Any Worldspace");
|
||||
} else if (filterName == "ping") {
|
||||
filters.ping = a_action.value("value", "Any");
|
||||
} else if (filterName == "showFull" || filterName == "hasPassword" || filterName == "modded") {
|
||||
const auto value = a_action.value("value", false);
|
||||
if (filterName == "showFull") {
|
||||
filters.showFull = value;
|
||||
} else if (filterName == "hasPassword") {
|
||||
filters.hasPassword = value;
|
||||
} else {
|
||||
filters.modded = value;
|
||||
}
|
||||
}
|
||||
g_data.SetFilters(filters);
|
||||
PushServerListUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "resetFilters") {
|
||||
g_data.ResetFilters();
|
||||
PushServerListUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "toggleFavorite") {
|
||||
const auto serverId = a_action.value("serverId", "");
|
||||
if (!serverId.empty()) {
|
||||
g_data.ToggleFavorite(serverId);
|
||||
PushServerListUpdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "switchTab") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "joinServer") {
|
||||
const auto serverId = a_action.value("serverId", "");
|
||||
const auto* entry = g_data.FindServer(serverId);
|
||||
if (!entry) {
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "joinFailed" },
|
||||
{ "serverId", serverId },
|
||||
{ "reason", "Server not found." }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (JoinServerEntry(*entry, error)) {
|
||||
PushConnectionStatusEvent();
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "joinSucceeded" },
|
||||
{ "serverId", serverId }
|
||||
});
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "uiSound" },
|
||||
{ "sound", "joinSuccess" }
|
||||
});
|
||||
LogInfo(std::format(" Joined server '{}'.", entry->name));
|
||||
} else {
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "joinFailed" },
|
||||
{ "serverId", serverId },
|
||||
{ "reason", error }
|
||||
});
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "uiSound" },
|
||||
{ "sound", "joinFailed" }
|
||||
});
|
||||
LogWarning(std::format(" Join failed for '{}': {}", entry->name, error));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "openDirectConnect") {
|
||||
ServerEntry entry;
|
||||
entry.id = "direct";
|
||||
entry.name = "Direct Connect";
|
||||
entry.host = a_action.value("address", "127.0.0.1");
|
||||
entry.port = a_action.value("port", 7777);
|
||||
|
||||
std::string error;
|
||||
if (JoinServerEntry(entry, error)) {
|
||||
PushConnectionStatusEvent();
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "joinSucceeded" },
|
||||
{ "serverId", "direct" }
|
||||
});
|
||||
} else {
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "joinFailed" },
|
||||
{ "serverId", "direct" },
|
||||
{ "reason", error }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "closeBrowser") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ServerBrowserBridge::ForwardControllerInput(const char* a_button, const char* a_phase)
|
||||
{
|
||||
if (!a_button || !a_phase) {
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchEvent(nlohmann::json{
|
||||
{ "type", "controllerInput" },
|
||||
{ "button", a_button },
|
||||
{ "phase", a_phase }
|
||||
});
|
||||
}
|
||||
|
||||
void ServerBrowserBridge::PushConnectionStatus()
|
||||
{
|
||||
PushConnectionStatusEvent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TServerBrowserData.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace F4T::ServerBrowser
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool ContainsIgnoreCase(std::string_view a_haystack, std::string_view a_needle)
|
||||
{
|
||||
if (a_needle.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string haystackLower(a_haystack);
|
||||
std::string needleLower(a_needle);
|
||||
std::ranges::transform(haystackLower, haystackLower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::ranges::transform(needleLower, needleLower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return haystackLower.find(needleLower) != std::string::npos;
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json ServerEntry::ToJson() const
|
||||
{
|
||||
auto json = nlohmann::json{
|
||||
{ "id", id },
|
||||
{ "name", name },
|
||||
{ "region", region },
|
||||
{ "players", players },
|
||||
{ "maxPlayers", maxPlayers },
|
||||
{ "ping", ping },
|
||||
{ "worldspace", worldspace },
|
||||
{ "mode", mode },
|
||||
{ "status", status },
|
||||
{ "modded", modded },
|
||||
{ "passworded", passworded },
|
||||
{ "favorite", favorite },
|
||||
{ "description", description }
|
||||
};
|
||||
|
||||
if (!host.empty()) {
|
||||
json["host"] = host;
|
||||
}
|
||||
if (port > 0) {
|
||||
json["port"] = port;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
ServerEntry ServerEntry::FromJson(const nlohmann::json& a_json)
|
||||
{
|
||||
ServerEntry entry;
|
||||
entry.id = a_json.value("id", "");
|
||||
entry.name = a_json.value("name", "");
|
||||
entry.region = a_json.value("region", "");
|
||||
entry.worldspace = a_json.value("worldspace", "");
|
||||
entry.mode = a_json.value("mode", "");
|
||||
entry.status = a_json.value("status", "");
|
||||
entry.description = a_json.value("description", "");
|
||||
entry.host = a_json.value("host", "");
|
||||
entry.port = a_json.value("port", 0);
|
||||
entry.players = a_json.value("players", 0);
|
||||
entry.maxPlayers = a_json.value("maxPlayers", 0);
|
||||
entry.ping = a_json.value("ping", 0);
|
||||
entry.modded = a_json.value("modded", false);
|
||||
entry.passworded = a_json.value("passworded", false);
|
||||
entry.favorite = a_json.value("favorite", false);
|
||||
return entry;
|
||||
}
|
||||
|
||||
void ServerBrowserData::SetFilters(const BrowserFilters& a_filters)
|
||||
{
|
||||
m_filters = a_filters;
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
void ServerBrowserData::ResetFilters()
|
||||
{
|
||||
m_filters = BrowserFilters{};
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
bool ServerBrowserData::PassesFilters(const ServerEntry& a_entry) const
|
||||
{
|
||||
if (!m_filters.search.empty() && !ContainsIgnoreCase(a_entry.name, m_filters.search)) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.region != "Any Region" && a_entry.region != m_filters.region) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.mode != "Any Mode" && a_entry.mode != m_filters.mode) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.worldspace != "Any Worldspace" && a_entry.worldspace != m_filters.worldspace) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.ping == "< 50 ms" && a_entry.ping >= 50) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.ping == "< 100 ms" && a_entry.ping >= 100) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.ping == "< 150 ms" && a_entry.ping >= 150) {
|
||||
return false;
|
||||
}
|
||||
if (!m_filters.showFull && a_entry.status == "Full") {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.hasPassword && !a_entry.passworded) {
|
||||
return false;
|
||||
}
|
||||
if (m_filters.modded && !a_entry.modded) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ServerBrowserData::ApplyFilters()
|
||||
{
|
||||
m_filteredServers.clear();
|
||||
for (const auto& entry : m_allServers) {
|
||||
if (PassesFilters(entry)) {
|
||||
m_filteredServers.push_back(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServerBrowserData::SetAllServers(std::vector<ServerEntry> a_servers)
|
||||
{
|
||||
m_allServers = std::move(a_servers);
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
void ServerBrowserData::ToggleFavorite(const std::string& a_serverId)
|
||||
{
|
||||
for (auto& entry : m_allServers) {
|
||||
if (entry.id == a_serverId) {
|
||||
entry.favorite = !entry.favorite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
const ServerEntry* ServerBrowserData::FindServer(const std::string& a_serverId) const
|
||||
{
|
||||
for (const auto& entry : m_allServers) {
|
||||
if (entry.id == a_serverId) {
|
||||
return std::addressof(entry);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nlohmann::json ServerBrowserData::ToServerListJson() const
|
||||
{
|
||||
nlohmann::json servers = nlohmann::json::array();
|
||||
for (const auto& entry : m_filteredServers) {
|
||||
servers.push_back(entry.ToJson());
|
||||
}
|
||||
return servers;
|
||||
}
|
||||
|
||||
std::vector<ServerEntry> ServerBrowserData::CreateMockServers()
|
||||
{
|
||||
return {
|
||||
ServerEntry{
|
||||
.id = "nz-1",
|
||||
.name = "Commonwealth NZ #1",
|
||||
.region = "Oceania",
|
||||
.worldspace = "Commonwealth",
|
||||
.mode = "Co-op",
|
||||
.status = "Open",
|
||||
.description = "Relaxed co-op server for explorers and builders. No PvP.",
|
||||
.players = 6,
|
||||
.maxPlayers = 12,
|
||||
.ping = 32,
|
||||
.modded = true,
|
||||
.favorite = true },
|
||||
ServerEntry{
|
||||
.id = "vault111",
|
||||
.name = "Vault 111 Co-op",
|
||||
.region = "North America",
|
||||
.worldspace = "Commonwealth",
|
||||
.mode = "Co-op",
|
||||
.status = "Open",
|
||||
.description = "Story-friendly co-op.",
|
||||
.players = 3,
|
||||
.maxPlayers = 8,
|
||||
.ping = 48,
|
||||
.modded = false },
|
||||
ServerEntry{
|
||||
.id = "dc-rp",
|
||||
.name = "DC Wasteland RP",
|
||||
.region = "North America",
|
||||
.worldspace = "Commonwealth",
|
||||
.mode = "Roleplay",
|
||||
.status = "Full",
|
||||
.description = "Light roleplay around Diamond City.",
|
||||
.players = 10,
|
||||
.maxPlayers = 10,
|
||||
.ping = 72,
|
||||
.modded = true,
|
||||
.passworded = true },
|
||||
ServerEntry{
|
||||
.id = "far-harbor",
|
||||
.name = "Far Harbor Expedition",
|
||||
.region = "Europe",
|
||||
.worldspace = "Far Harbor",
|
||||
.mode = "Co-op",
|
||||
.status = "Open",
|
||||
.description = "DLC-focused co-op through Far Harbor.",
|
||||
.players = 4,
|
||||
.maxPlayers = 6,
|
||||
.ping = 118,
|
||||
.modded = true,
|
||||
.favorite = true },
|
||||
ServerEntry{
|
||||
.id = "local-relay",
|
||||
.name = "Local Relay (Dev)",
|
||||
.region = "Local",
|
||||
.worldspace = "Commonwealth",
|
||||
.mode = "Co-op",
|
||||
.status = "Open",
|
||||
.description = "Python relay prototype. Start server/server.py before joining.",
|
||||
.host = "127.0.0.1",
|
||||
.port = 7777,
|
||||
.players = 0,
|
||||
.maxPlayers = 16,
|
||||
.ping = 1,
|
||||
.modded = false },
|
||||
ServerEntry{
|
||||
.id = "survival-hard",
|
||||
.name = "Survival Hardcore",
|
||||
.region = "Europe",
|
||||
.worldspace = "Commonwealth",
|
||||
.mode = "Survival",
|
||||
.status = "Open",
|
||||
.description = "Hardcore survival with limited respawns.",
|
||||
.players = 2,
|
||||
.maxPlayers = 4,
|
||||
.ping = 95,
|
||||
.modded = true }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TLocalAnimationGraphDebug.h"
|
||||
#include "F4TPrismaUI.h"
|
||||
#include "F4TScaleformUI.h"
|
||||
#include "F4TProxyActorController.h"
|
||||
#include "F4TProxyAnimationSync.h"
|
||||
#include "F4TProxyPuppet.h"
|
||||
@@ -636,6 +638,9 @@ namespace
|
||||
}
|
||||
|
||||
switch (a_msg->type) {
|
||||
case F4SE::MessagingInterface::kGameDataReady:
|
||||
F4T::PrismaUI::OnGameDataReady();
|
||||
break;
|
||||
case F4SE::MessagingInterface::kNewGame:
|
||||
case F4SE::MessagingInterface::kPostLoadGame:
|
||||
case F4SE::MessagingInterface::kGameLoaded:
|
||||
@@ -652,6 +657,7 @@ namespace
|
||||
F4SE_PLUGIN_LOAD(const F4SE::LoadInterface* a_f4se)
|
||||
{
|
||||
F4SE::Init(a_f4se);
|
||||
F4T::ScaleformUI::RegisterPlugin();
|
||||
|
||||
LogInfoWithLocalPlayerPrefix("Commonwealth Online plugin loaded successfully.");
|
||||
// TODO: Consider separate per-instance log files once launcher/profile support exists.
|
||||
|
||||
@@ -27,6 +27,7 @@ target("CommonwealthOnline")
|
||||
add_headerfiles("include/**.h")
|
||||
add_includedirs("src")
|
||||
add_includedirs("include")
|
||||
add_includedirs("../ThirdParty/framework-F4-Conversion/example-f4se-plugin/src")
|
||||
add_packages("nlohmann_json")
|
||||
add_syslinks("ws2_32")
|
||||
set_pcxxheader("src/pch.h")
|
||||
|
||||
Reference in New Issue
Block a user