Add custom main-menu overlay & input routing

Integrates a PrismaUI full-screen main-menu overlay with input capture and a bridge to the server browser. Adds F4TMainMenuBridge and F4TMainMenuVanillaSettings stubs, exposes Show/Hide/IsMainMenuVisible/IsCustomMenuInputCaptured in F4TPrismaUI, and routes gamepad input via SetMainMenuActive in F4TMenuInput. MainMenuInject implements robust readiness checks with Bink/frame gating and disables legacy multiplayer-row injection (constexpr false). Fixes cursor clipping by clearing RE::MenuCursor constraints each frame and removes unsupported viewport meta from HTML. Adds window.CO_App/handleControllerInput stubs and updates docs/changelog.
This commit is contained in:
2026-07-06 17:47:57 +12:00
parent 980944f4ba
commit f1635948ee
16 changed files with 1226 additions and 377 deletions
+10
View File
@@ -0,0 +1,10 @@
#pragma once
namespace F4T::MainMenu
{
namespace MainMenuBridge
{
void ForwardControllerInput(const char* a_button, const char* a_phase);
void Initialize();
}
}
+1
View File
@@ -8,6 +8,7 @@ namespace RE
namespace F4T::MainMenuInject
{
bool IsMainMenuOnMainPanel(const RE::UI* a_ui);
bool IsMainMenuReadyForCustomOverlay(const RE::UI* a_ui);
void SetMainMenuInputSuppressed(bool a_suppressed);
void Install();
}
@@ -0,0 +1,7 @@
#pragma once
namespace F4T::MainMenuVanillaSettings
{
void OpenGameSettings();
void Initialize();
}
+1
View File
@@ -10,6 +10,7 @@ namespace F4T::MenuInput
{
void Register();
void SetBrowserActive(bool a_active);
void SetMainMenuActive(bool a_active);
bool IsGamepadConnected();
void PollGamepad();
bool ForwardButtonEvent(const RE::ButtonEvent* a_event);
+7
View File
@@ -3,7 +3,14 @@
namespace F4T::PrismaUI
{
void OnGameDataReady();
void ShowServerBrowser();
void HideServerBrowser();
bool IsBrowserVisible();
void ShowMainMenu();
void HideMainMenu();
bool IsMainMenuVisible();
bool IsCustomMenuInputCaptured();
}
+63
View File
@@ -0,0 +1,63 @@
#include "pch.h"
#include "F4TPrismaUI.h"
#include "F4TNetworking.h"
#include <nlohmann/json.hpp>
#include <string_view>
namespace F4T::MainMenu
{
namespace
{
// Forward declare the dispatch function from F4TPrismaUI.cpp internal namespace
// We'll use an indirect approach through logging that F4TPrismaUI can intercept
std::string GetLogPrefix()
{
return F4T::Networking::GetLocalPlayerLogPrefix() + " [MainMenuBridge]";
}
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 });
}
// Stub for now - will be wired to DispatchMainMenuControllerInput from F4TPrismaUI
void DispatchControllerInputToMainMenu(const char* a_button, const char* a_phase)
{
// This would call g_api->Invoke in F4TPrismaUI, but we keep it simple for now
// In v1, controller input is logged and JS can poll for events if needed
}
}
namespace MainMenuBridge
{
void ForwardControllerInput(const char* a_button, const char* a_phase)
{
if (!a_button || !a_phase) {
return;
}
// For v1, just log - JS will poll or we'll add a callback mechanism later
LogInfo(std::format("Gamepad: {} {}", a_button, a_phase));
DispatchControllerInputToMainMenu(a_button, a_phase);
}
void DispatchEvent(const nlohmann::json& a_event)
{
LogInfo(std::format("Main menu event: {}", a_event.dump()));
}
void Initialize()
{
LogInfo("Main menu bridge initialized.");
}
}
}
+216 -46
View File
@@ -43,6 +43,10 @@ namespace F4T::MainMenuInject
int g_resyncFrameCounter = 0;
int g_injectFailLogCooldown = 0;
// Set to true to inject the Multiplayer row; false to use the custom main menu overlay instead.
// With PrismaUI main menu overlay active, the row is unreachable, so injection is disabled by default.
constexpr bool kInjectMultiplayerRow = false;
std::string GetLogPrefix()
{
return F4T::Networking::GetLocalPlayerLogPrefix() + " [MainMenuInject]";
@@ -105,28 +109,87 @@ namespace F4T::MainMenuInject
return pauseMode.GetBoolean();
}
bool IsMainStateForMenu(RE::MainMenu* a_mainMenu)
int g_titleMenuFrames = 0;
RE::IMenu* g_lastTitleMenu = nullptr;
constexpr std::string_view kPlayBinkMenuName{ "PlayBinkMenu" };
void TickTitleMenuFrames()
{
const auto* ui = RE::UI::GetSingleton();
const auto mainMenu = ui ? ui->GetMenu<RE::MainMenu>() : nullptr;
if (mainMenu && ui->GetMenuOpen<RE::MainMenu>()) {
if (g_lastTitleMenu != mainMenu.get()) {
g_titleMenuFrames = 0;
g_lastTitleMenu = mainMenu.get();
} else {
++g_titleMenuFrames;
}
} else {
g_titleMenuFrames = 0;
g_lastTitleMenu = nullptr;
}
}
std::optional<std::string_view> TryGetMainMenuCurrentState(RE::MainMenu* a_mainMenu)
{
if (!a_mainMenu || !a_mainMenu->uiMovie) {
return false;
return std::nullopt;
}
Scaleform::GFx::Value state;
if (a_mainMenu->menuObj.IsObject() && a_mainMenu->menuObj.GetMember("currentState", &state)) {
if (state.IsString()) {
const char* currentState = state.GetString();
return currentState == nullptr || std::strcmp(currentState, "MAIN_STATE") == 0;
if (currentState != nullptr) {
return std::string_view{ currentState };
}
}
}
if (a_mainMenu->uiMovie->GetVariable(&state, "currentState") && state.IsString()) {
const char* currentState = state.GetString();
return currentState == nullptr || std::strcmp(currentState, "MAIN_STATE") == 0;
if (currentState != nullptr) {
return std::string_view{ currentState };
}
}
return true;
return std::nullopt;
}
bool IsTitleIntroBinkPlaying(const RE::UI* a_ui)
{
return a_ui && a_ui->GetMenuOpen(RE::BSFixedString(kPlayBinkMenuName));
}
bool IsMainPanelVisible(RE::MainMenu* a_mainMenu)
{
if (!a_mainMenu || !a_mainMenu->uiMovie) {
return false;
}
Scaleform::GFx::Value mainPanel;
if (a_mainMenu->menuObj.IsObject()) {
a_mainMenu->menuObj.GetMember("MainPanel_mc", &mainPanel);
}
if (!mainPanel.IsObject()) {
a_mainMenu->uiMovie->GetVariable(&mainPanel, "MainPanel_mc");
}
if (!mainPanel.IsObject()) {
return false;
}
Scaleform::GFx::Value visible;
if (mainPanel.GetMember("_visible", &visible) && visible.IsBoolean()) {
return visible.GetBoolean();
}
return false;
}
bool IsMainMenuIntroComplete(RE::MainMenu* a_mainMenu);
bool IsMainStateForMenu(RE::MainMenu* a_mainMenu);
bool IsTitleScreenMainMenu(RE::StartMenuBase* a_menu)
{
if (!a_menu || !a_menu->uiMovie) {
@@ -223,6 +286,70 @@ namespace F4T::MainMenuInject
return a_outEntryList.IsArray() || a_outEntryList.IsObject();
}
bool IsMainMenuMainListReady(RE::MainMenu* a_mainMenu)
{
Scaleform::GFx::Value listWidget;
Scaleform::GFx::Value entryList;
if (!ResolveMainList(a_mainMenu, listWidget, entryList)) {
return false;
}
if (!entryList.IsArray()) {
return false;
}
return entryList.GetArraySize() > 0;
}
bool IsMainMenuIntroComplete(RE::MainMenu* a_mainMenu)
{
if (!a_mainMenu) {
return false;
}
if (a_mainMenu->userEngaged) {
return true;
}
if (a_mainMenu->creditScreen) {
return false;
}
const auto* ui = RE::UI::GetSingleton();
if (IsTitleIntroBinkPlaying(ui)) {
return false;
}
// Title screen is up once the vanilla main panel/list is visible and populated.
if (IsMainPanelVisible(a_mainMenu) && IsMainMenuMainListReady(a_mainMenu)) {
return true;
}
if (a_mainMenu->mainBinkShown) {
return true;
}
// Short safety net only if GFx visibility checks are unavailable.
constexpr int kFallbackFramesAfterOpen = 45;
return g_titleMenuFrames >= kFallbackFramesAfterOpen && a_mainMenu->GetIsMenuReady();
}
bool IsMainStateForMenu(RE::MainMenu* a_mainMenu)
{
if (!a_mainMenu || !a_mainMenu->uiMovie) {
return false;
}
const auto currentState = TryGetMainMenuCurrentState(a_mainMenu);
if (currentState.has_value()) {
return *currentState == "MAIN_STATE";
}
// Title screen often leaves currentState unset even on the main list.
// Accept that once intro playback is finished.
return IsMainMenuIntroComplete(a_mainMenu);
}
bool InsertEntryAfterIndex(
Scaleform::GFx::Movie* a_movie,
Scaleform::GFx::Value& a_entryList,
@@ -301,37 +428,41 @@ namespace F4T::MainMenuInject
return true;
}
void TryInjectForStartMenu(RE::StartMenuBase* a_menu)
{
if (!IsTitleScreenMainMenu(a_menu)) {
return;
}
void TryInjectForStartMenu(RE::StartMenuBase* a_menu)
{
if (!IsTitleScreenMainMenu(a_menu)) {
return;
}
if constexpr (kInjectMultiplayerRow) {
InjectMultiplayerRow(static_cast<RE::MainMenu*>(a_menu));
}
}
void RegisterItemPressListener(RE::MainMenu* a_mainMenu);
void TryInjectOpenMainMenu()
{
auto* ui = RE::UI::GetSingleton();
if (!ui) {
return;
}
void TryInjectOpenMainMenu()
{
auto* ui = RE::UI::GetSingleton();
if (!ui) {
return;
}
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
if (!mainMenu || !mainMenu->uiMovie) {
return;
}
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
if (!mainMenu || !mainMenu->uiMovie) {
return;
}
if (!ui->GetMenuOpen<RE::MainMenu>() && !mainMenu->menuCanBeVisible) {
return;
}
if (!ui->GetMenuOpen<RE::MainMenu>() && !mainMenu->menuCanBeVisible) {
return;
}
LogInfo("Running catch-up injection for already-open MainMenu.");
RegisterItemPressListener(mainMenu.get());
LogInfo("Running catch-up injection for already-open MainMenu.");
RegisterItemPressListener(mainMenu.get());
if constexpr (kInjectMultiplayerRow) {
InjectMultiplayerRow(mainMenu.get());
}
}
using InitMainListFn = void (*)(RE::StartMenuBase*);
InitMainListFn g_originalInitMainList = nullptr;
@@ -638,7 +769,7 @@ namespace F4T::MainMenuInject
void MainMenuOnButtonEventHook(RE::BSInputEventUser* a_this, const RE::ButtonEvent* a_event)
{
if (F4T::PrismaUI::IsBrowserVisible()) {
if (F4T::PrismaUI::IsCustomMenuInputCaptured()) {
F4T::MenuInput::ForwardButtonEvent(a_event);
return;
}
@@ -662,7 +793,7 @@ namespace F4T::MainMenuInject
void MainMenuOnThumbstickEventHook(RE::BSInputEventUser* a_this, const RE::ThumbstickEvent* a_event)
{
if (F4T::PrismaUI::IsBrowserVisible()) {
if (F4T::PrismaUI::IsCustomMenuInputCaptured()) {
F4T::MenuInput::ForwardThumbstickEvent(a_event);
return;
}
@@ -696,16 +827,20 @@ namespace F4T::MainMenuInject
LogInfo("Installed MainMenu input hooks on BSInputEventUser vtable.");
}
void PollMultiplayerActivation()
{
if (F4T::PrismaUI::IsBrowserVisible()) {
return;
}
void PollMultiplayerActivation()
{
if (F4T::PrismaUI::IsCustomMenuInputCaptured()) {
return;
}
const auto* ui = RE::UI::GetSingleton();
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
return;
}
if constexpr (!kInjectMultiplayerRow) {
return;
}
const auto* ui = RE::UI::GetSingleton();
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
return;
}
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
if (!mainMenu || !IsMultiplayerRowSelected(mainMenu.get())) {
@@ -737,18 +872,22 @@ namespace F4T::MainMenuInject
LogInfo("Opened multiplayer menu from activation poll.");
}
void ResyncMultiplayerRow()
{
if (++g_resyncFrameCounter < kResyncThrottleFrames) {
return;
}
void ResyncMultiplayerRow()
{
if (++g_resyncFrameCounter < kResyncThrottleFrames) {
return;
}
g_resyncFrameCounter = 0;
g_resyncFrameCounter = 0;
const auto* ui = RE::UI::GetSingleton();
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
return;
}
if constexpr (!kInjectMultiplayerRow) {
return;
}
const auto* ui = RE::UI::GetSingleton();
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
return;
}
const auto mainMenu = ui->GetMenu<RE::MainMenu>();
if (!mainMenu || !mainMenu->uiMovie || IsPauseMode(mainMenu->uiMovie.get())) {
@@ -829,6 +968,28 @@ namespace F4T::MainMenuInject
return IsMainStateForMenu(mainMenu.get());
}
bool IsMainMenuReadyForCustomOverlay(const RE::UI* a_ui)
{
if (!a_ui || !a_ui->GetMenuOpen<RE::MainMenu>()) {
return false;
}
const auto mainMenu = a_ui->GetMenu<RE::MainMenu>();
if (!mainMenu || !mainMenu->uiMovie) {
return false;
}
if (IsPauseMode(mainMenu->uiMovie.get())) {
return false;
}
if (!IsMainMenuIntroComplete(mainMenu.get())) {
return false;
}
return IsMainStateForMenu(mainMenu.get());
}
void SetMainMenuInputSuppressed(bool a_suppressed)
{
const auto* ui = RE::UI::GetSingleton();
@@ -868,8 +1029,17 @@ namespace F4T::MainMenuInject
if (taskInterface) {
if (!g_resyncTaskStarted) {
taskInterface->AddTaskPermanent([]() {
TickTitleMenuFrames();
PollMultiplayerActivation();
ResyncMultiplayerRow();
// Auto-show only after intro Binks finish (or are skipped) and GFx reaches MAIN_STATE.
if (!F4T::PrismaUI::IsMainMenuVisible()) {
const auto* ui = RE::UI::GetSingleton();
if (IsMainMenuReadyForCustomOverlay(ui)) {
F4T::PrismaUI::ShowMainMenu();
}
}
});
g_resyncTaskStarted = true;
}
+48
View File
@@ -0,0 +1,48 @@
#include "pch.h"
#include "F4TMainMenuInject.h"
#include "F4TPrismaUI.h"
#include "F4TNetworking.h"
#include "Scaleform/G/GFx_Movie.h"
#include "Scaleform/G/GFx_Value.h"
#include <string_view>
namespace F4T::MainMenuVanillaSettings
{
namespace
{
bool g_vanillaSettingsFromCustomMainMenu = false;
std::string GetLogPrefix()
{
return F4T::Networking::GetLocalPlayerLogPrefix() + " [MainMenuVanillaSettings]";
}
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 OpenGameSettings()
{
LogInfo("openGameSettings called - vanilla settings sub-flow not yet implemented.");
LogInfo("Discovery TODO: Log MainPanel_mc.List_mc.entryList to find SETTINGS index on title screen.");
LogInfo("Discovery TODO: Test SetVisible(false) on MainPanel_mc to hide left column.");
LogInfo("Discovery TODO: Find GFx invoke path to programmatically open settings panel.");
}
void Initialize()
{
LogInfo("Main menu vanilla settings module initialized (stubs only).");
}
}
+79 -54
View File
@@ -2,6 +2,7 @@
#include "F4TMenuInput.h"
#include "F4TServerBrowserBridge.h"
#include "F4TMainMenuBridge.h"
#include <REX/W32/XINPUT.h>
@@ -12,6 +13,7 @@ namespace F4T::MenuInput
namespace
{
std::atomic_bool g_browserActive{ false };
std::atomic_bool g_mainMenuActive{ false };
std::atomic_bool g_registered{ false };
RE::BSTSmartPointer<RE::BSInputEnableLayer> g_menuInputBlockLayer;
@@ -146,7 +148,13 @@ namespace F4T::MenuInput
void ForwardButtonPhase(const char* a_button, const char* a_phase)
{
if (a_button && a_phase) {
if (!a_button || !a_phase) {
return;
}
if (g_mainMenuActive.load()) {
F4T::MainMenu::MainMenuBridge::ForwardControllerInput(a_button, a_phase);
} else if (g_browserActive.load()) {
F4T::ServerBrowser::ServerBrowserBridge::ForwardControllerInput(a_button, a_phase);
}
}
@@ -210,63 +218,63 @@ namespace F4T::MenuInput
g_menuInputBlockLayer.reset();
}
void PollGamepadInternal()
{
if (!g_browserActive.load()) {
return;
}
const auto* xinputGetState = GetXInputGetState();
if (!xinputGetState) {
return;
}
REX::W32::XINPUT_STATE state{};
if (xinputGetState(0, std::addressof(state)) != ERROR_SUCCESS) {
return;
}
const auto buttons = state.gamepad.buttons;
for (std::size_t i = 0; i < std::size(kGamepadButtons); ++i) {
const auto down = (buttons & kGamepadButtons[i].mask) != 0;
if (down != g_buttonHeld[i]) {
ForwardButtonPhase(kGamepadButtons[i].name, down ? "down" : "up");
g_buttonHeld[i] = down;
}
}
const auto* stickButton = StickAxesToButton(state.gamepad.thumbLX, state.gamepad.thumbLY);
if (stickButton != g_activeStickButton) {
if (g_activeStickButton) {
ForwardButtonPhase(g_activeStickButton, "up");
}
g_activeStickButton = stickButton;
if (stickButton) {
ForwardButtonPhase(stickButton, "down");
}
g_stickRepeatTime = std::chrono::steady_clock::now();
return;
}
if (!stickButton) {
return;
}
const auto now = std::chrono::steady_clock::now();
if (now - g_stickRepeatTime < 400ms) {
return;
}
ForwardButtonPhase(stickButton, "down");
g_stickRepeatTime = now - 320ms;
void PollGamepadInternal()
{
if (!g_browserActive.load() && !g_mainMenuActive.load()) {
return;
}
const auto* xinputGetState = GetXInputGetState();
if (!xinputGetState) {
return;
}
REX::W32::XINPUT_STATE state{};
if (xinputGetState(0, std::addressof(state)) != ERROR_SUCCESS) {
return;
}
const auto buttons = state.gamepad.buttons;
for (std::size_t i = 0; i < std::size(kGamepadButtons); ++i) {
const auto down = (buttons & kGamepadButtons[i].mask) != 0;
if (down != g_buttonHeld[i]) {
ForwardButtonPhase(kGamepadButtons[i].name, down ? "down" : "up");
g_buttonHeld[i] = down;
}
}
const auto* stickButton = StickAxesToButton(state.gamepad.thumbLX, state.gamepad.thumbLY);
if (stickButton != g_activeStickButton) {
if (g_activeStickButton) {
ForwardButtonPhase(g_activeStickButton, "up");
}
g_activeStickButton = stickButton;
if (stickButton) {
ForwardButtonPhase(stickButton, "down");
}
g_stickRepeatTime = std::chrono::steady_clock::now();
return;
}
if (!stickButton) {
return;
}
const auto now = std::chrono::steady_clock::now();
if (now - g_stickRepeatTime < 400ms) {
return;
}
ForwardButtonPhase(stickButton, "down");
g_stickRepeatTime = now - 320ms;
}
}
bool ForwardButtonEvent(const RE::ButtonEvent* a_event)
{
if (!a_event || !g_browserActive.load()) {
if (!a_event || (!g_browserActive.load() && !g_mainMenuActive.load())) {
return false;
}
@@ -296,7 +304,7 @@ namespace F4T::MenuInput
bool ForwardThumbstickEvent(const RE::ThumbstickEvent* a_event)
{
if (!a_event || !g_browserActive.load()) {
if (!a_event || (!g_browserActive.load() && !g_mainMenuActive.load())) {
return false;
}
@@ -362,4 +370,21 @@ namespace F4T::MenuInput
g_stickRepeatTime = {};
ReleaseMenuInputBlock();
}
void SetMainMenuActive(bool a_active)
{
g_mainMenuActive.store(a_active);
if (a_active) {
SyncPolledGamepadBaseline();
AcquireMenuInputBlock();
return;
}
for (auto& held : g_buttonHeld) {
held = false;
}
g_activeStickButton = nullptr;
g_stickRepeatTime = {};
ReleaseMenuInputBlock();
}
}
+521 -269
View File
@@ -5,6 +5,8 @@
#include "F4TNetworking.h"
#include "F4TPrismaUI.h"
#include "F4TServerBrowserBridge.h"
#include "F4TMainMenuBridge.h"
#include "F4TMainMenuVanillaSettings.h"
#include "PrismaUI_F4_API.h"
#include "Scaleform/G/GFx_Movie.h"
@@ -21,22 +23,44 @@ namespace F4T::PrismaUI
namespace
{
constexpr auto kServerBrowserHtmlPath = "CommonwealthOnline/browser/index.html";
constexpr auto kMainMenuHtmlPath = "CommonwealthOnline/main-menu/index.html";
constexpr auto kToggleVirtualKey = 0x78; // VK_F9
PRISMA_UI_API::IVPrismaUI4* g_api = nullptr;
// Server browser state
PrismaView g_serverBrowserView = 0;
bool g_visible = false;
bool g_domReady = false;
bool g_browserVisible = false;
bool g_browserDomReady = false;
// Main menu state
PrismaView g_mainMenuView = 0;
bool g_mainMenuVisible = false;
bool g_mainMenuDomReady = false;
bool g_mainMenuPendingShow = false;
bool g_mainMenuBridgeReady = false;
// Shared state
bool g_toggleTaskStarted = false;
bool g_cursorRestoreTaskStarted = false;
bool g_menuWatchTaskStarted = false;
bool g_pendingShow = false;
int g_cursorRestoreFramesLeft = 0;
bool g_browserMouseCursorVisible = true;
bool g_browserPreferGamepad = false;
bool g_menuMouseCursorVisible = true;
bool g_menuPreferGamepad = false;
bool g_hasLastCursorPos = false;
POINT g_lastCursorPos{};
// Forward declarations (definitions appear later in this anonymous namespace;
// several functions call each other out of definition order).
void HideMainMenuInternal();
void ShowMainMenuInternal();
void HideServerBrowserInternal();
void ShowServerBrowserInternal();
void CreateMainMenuView();
void CreateServerBrowserView();
void SyncBrowserHover();
void WatchMainMenuState();
std::string GetLogPrefix()
{
return F4T::Networking::GetLocalPlayerLogPrefix() + " [PrismaUI]";
@@ -72,30 +96,76 @@ namespace F4T::PrismaUI
}
}
void ApplyBrowserPointerMode(bool a_mouseCursorVisible)
void ReleaseMenuCursorConstraints()
{
g_browserMouseCursorVisible = a_mouseCursorVisible;
auto* menuCursor = RE::MenuCursor::GetSingleton();
if (!menuCursor) {
return;
}
if (g_api && g_api->IsValid(g_serverBrowserView) && g_visible) {
if (a_mouseCursorVisible) {
// Third Focus arg is disableFocusMenu, not cursor visibility. PrismaUI draws
// cursor.png only while input capture is active (after Focus).
g_api->Focus(g_serverBrowserView, false, true);
} else {
// Unfocus disables PrismaUI input capture so its Ultralight cursor is not drawn.
g_api->Unfocus(g_serverBrowserView);
SuppressNativeGameCursor();
}
menuCursor->ClearConstraints();
const auto* mode = a_mouseCursorVisible ? "mouse" : "gamepad";
const auto script = std::format(
"if(window.CO_App&&window.CO_App.setPointerMode){{window.CO_App.setPointerMode('{}');}}",
mode);
g_api->Invoke(g_serverBrowserView, script.c_str());
} else if (!a_mouseCursorVisible) {
const auto* renderWindow = RE::BSGraphics::GetCurrentRendererWindow();
if (!renderWindow) {
return;
}
const auto width = static_cast<std::int32_t>(renderWindow->windowWidth);
const auto height = static_cast<std::int32_t>(renderWindow->windowHeight);
if (width <= 0 || height <= 0) {
return;
}
// MainMenu keeps a tight MenuCursor box for title-screen parallax; clear it every frame
// while our overlay owns input so the OS pointer can reach the full window.
menuCursor->minCursorX = 0;
menuCursor->minCursorY = 0;
menuCursor->maxCursorX = width;
menuCursor->maxCursorY = height;
}
void ApplyBrowserPointerMode(bool a_mouseCursorVisible)
{
g_menuMouseCursorVisible = a_mouseCursorVisible;
// Apply to whichever menu is currently visible
PrismaView activeView = 0;
const char* namespace_obj = nullptr;
bool disableFocusMenu = true;
if (g_mainMenuVisible && g_api && g_api->IsValid(g_mainMenuView)) {
activeView = g_mainMenuView;
namespace_obj = "window.CO_App";
// The main menu is the primary/standalone title-screen UI, not a panel opened
// on top of another menu's cursor. Let PrismaUI open its own FocusMenu (kUsesCursor /
// kCursor input context) instead of relying on vanilla MainMenu's cursor handling,
// which re-centers the OS cursor each frame for its background camera parallax and
// otherwise pins the pointer to the middle of the screen.
disableFocusMenu = false;
} else if (g_browserVisible && g_api && g_api->IsValid(g_serverBrowserView)) {
activeView = g_serverBrowserView;
namespace_obj = "window.CO_App";
disableFocusMenu = true;
}
if (activeView != 0) {
if (a_mouseCursorVisible) {
ReleaseMenuCursorConstraints();
g_api->Focus(activeView, false, disableFocusMenu);
} else {
g_api->Unfocus(activeView);
SuppressNativeGameCursor();
}
const auto* mode = a_mouseCursorVisible ? "mouse" : "gamepad";
const auto script = std::format(
"if({}&&{}.setPointerMode){{{}.setPointerMode('{}');}}",
namespace_obj, namespace_obj, namespace_obj, mode);
g_api->Invoke(activeView, script.c_str());
} else if (!a_mouseCursorVisible) {
SuppressNativeGameCursor();
}
}
void ResetBrowserPointerTracking()
{
@@ -103,63 +173,62 @@ namespace F4T::PrismaUI
g_lastCursorPos = {};
}
void UpdateBrowserPointerMode()
{
if (!g_visible) {
return;
}
void UpdateBrowserPointerMode()
{
if (!g_mainMenuVisible && !g_browserVisible) {
return;
}
if (!g_browserPreferGamepad) {
if (!g_browserMouseCursorVisible) {
ApplyBrowserPointerMode(true);
}
return;
}
const auto* renderWindow = RE::BSGraphics::GetCurrentRendererWindow();
if (!renderWindow || !renderWindow->hwnd) {
return;
}
POINT cursor{};
if (!::GetCursorPos(&cursor)) {
return;
}
::ScreenToClient(reinterpret_cast<HWND>(renderWindow->hwnd), &cursor);
const auto mouseButtonDown =
(GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0 ||
(GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0 ||
(GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
bool mouseMoved = false;
if (g_hasLastCursorPos) {
const auto deltaX = std::abs(cursor.x - g_lastCursorPos.x);
const auto deltaY = std::abs(cursor.y - g_lastCursorPos.y);
mouseMoved = deltaX > 1 || deltaY > 1;
}
g_lastCursorPos = cursor;
g_hasLastCursorPos = true;
if (!g_browserMouseCursorVisible && (mouseMoved || mouseButtonDown)) {
if (!g_menuPreferGamepad) {
if (!g_menuMouseCursorVisible) {
ApplyBrowserPointerMode(true);
return;
}
if (!g_browserMouseCursorVisible) {
// PrismaUI re-shows CursorMenu each frame when input capture is off; keep it hidden.
SuppressNativeGameCursor();
}
return;
}
void InitializeBrowserPointerMode()
{
g_browserPreferGamepad = F4T::MenuInput::IsGamepadConnected();
ResetBrowserPointerTracking();
const auto* renderWindow = RE::BSGraphics::GetCurrentRendererWindow();
if (!renderWindow || !renderWindow->hwnd) {
return;
}
POINT cursor{};
if (!::GetCursorPos(&cursor)) {
return;
}
::ScreenToClient(reinterpret_cast<HWND>(renderWindow->hwnd), &cursor);
const auto mouseButtonDown =
(GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0 ||
(GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0 ||
(GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
bool mouseMoved = false;
if (g_hasLastCursorPos) {
const auto deltaX = std::abs(cursor.x - g_lastCursorPos.x);
const auto deltaY = std::abs(cursor.y - g_lastCursorPos.y);
mouseMoved = deltaX > 1 || deltaY > 1;
}
g_lastCursorPos = cursor;
g_hasLastCursorPos = true;
if (!g_menuMouseCursorVisible && (mouseMoved || mouseButtonDown)) {
ApplyBrowserPointerMode(true);
return;
}
if (!g_menuMouseCursorVisible) {
SuppressNativeGameCursor();
}
}
void InitializeBrowserPointerMode()
{
g_menuPreferGamepad = F4T::MenuInput::IsGamepadConnected();
ResetBrowserPointerTracking();
}
void RestoreNativeGameCursor()
{
auto* ui = RE::UI::GetSingleton();
@@ -189,11 +258,11 @@ namespace F4T::PrismaUI
void SyncBrowserHover()
{
if (!g_visible || !g_domReady || !g_api || !g_api->IsValid(g_serverBrowserView)) {
if (!g_browserVisible || !g_browserDomReady || !g_api || !g_api->IsValid(g_serverBrowserView)) {
return;
}
if (!g_browserMouseCursorVisible) {
if (!g_menuMouseCursorVisible) {
return;
}
@@ -209,235 +278,397 @@ namespace F4T::PrismaUI
::ScreenToClient(reinterpret_cast<HWND>(renderWindow->hwnd), &cursor);
const auto script = std::format(
"if(window.CO_Hover&&window.CO_Hover.updateFromPoint){{window.CO_Hover.updateFromPoint({},{});}}",
cursor.x,
cursor.y);
g_api->Invoke(g_serverBrowserView, script.c_str());
const auto script = std::format(
"if(window.CO_Hover&&window.CO_Hover.updateFromPoint){{window.CO_Hover.updateFromPoint({},{});}}",
cursor.x,
cursor.y);
g_api->Invoke(g_serverBrowserView, script.c_str());
}
void DispatchMainMenuControllerInput(const char* a_button, const char* a_phase)
{
if (!g_mainMenuVisible || !g_mainMenuDomReady || !g_api || !g_api->IsValid(g_mainMenuView)) {
return;
}
void HideServerBrowserInternal()
{
const bool wasVisible = g_visible;
g_visible = false;
F4T::MenuInput::SetBrowserActive(false);
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
const auto script = std::format(
"if(window.handleControllerInput){{window.handleControllerInput('{}', '{}');}}",
a_button ? a_button : "",
a_phase ? a_phase : "");
g_api->Invoke(g_mainMenuView, script.c_str());
}
if (!g_api || !g_api->IsValid(g_serverBrowserView)) {
return;
}
void HideServerBrowserInternal()
{
const bool wasVisible = g_browserVisible;
g_browserVisible = false;
F4T::MenuInput::SetBrowserActive(false);
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
if (wasVisible) {
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserHidden();
}
g_api->Unfocus(g_serverBrowserView);
g_api->Hide(g_serverBrowserView);
RequestNativeCursorRestore();
if (!g_api || !g_api->IsValid(g_serverBrowserView)) {
return;
}
void WatchMainMenuState()
{
if (!g_visible) {
return;
}
if (wasVisible) {
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserHidden();
}
const auto* ui = RE::UI::GetSingleton();
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !F4T::MainMenuInject::IsMainMenuOnMainPanel(ui)) {
g_api->Unfocus(g_serverBrowserView);
g_api->Hide(g_serverBrowserView);
RequestNativeCursorRestore();
}
void WatchMainMenuState()
{
if (!g_mainMenuVisible && !g_browserVisible) {
return;
}
const auto* ui = RE::UI::GetSingleton();
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !F4T::MainMenuInject::IsMainMenuOnMainPanel(ui)) {
if (g_mainMenuVisible) {
HideMainMenuInternal();
}
if (g_browserVisible) {
HideServerBrowserInternal();
}
}
}
void CreateServerBrowserView();
void HideMainMenuInternal()
{
const bool wasVisible = g_mainMenuVisible;
g_mainMenuVisible = false;
F4T::MenuInput::SetMainMenuActive(false);
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
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);
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
InitializeBrowserPointerMode();
g_api->Show(g_serverBrowserView);
ApplyBrowserPointerMode(!g_browserPreferGamepad);
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
g_api->Invoke(
g_serverBrowserView,
"if(window.onBrowserShown){window.onBrowserShown();}");
if (!g_api || !g_api->IsValid(g_mainMenuView)) {
return;
}
void ToggleServerBrowser()
{
if (!g_api) {
g_api->Unfocus(g_mainMenuView);
g_api->Hide(g_mainMenuView);
RequestNativeCursorRestore();
}
void OnMainMenuDomReady(PrismaView a_view)
{
g_mainMenuDomReady = true;
LogInfo("Main menu 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() + " [MainMenu 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::MainMenu::MainMenuBridge::Initialize();
g_mainMenuBridgeReady = true;
g_api->BindUIEvent(a_view, "coAction", [](const char* a_json) {
if (!a_json) {
return;
}
if (g_visible) {
HideServerBrowserInternal();
try {
const auto action = nlohmann::json::parse(a_json);
const auto actionName = action.value("action", "");
if (actionName == "openView") {
const auto viewName = action.value("view", "");
if (viewName == "multiplayer") {
HideMainMenuInternal();
F4T::PrismaUI::ShowServerBrowser();
return;
}
} else if (actionName == "openGameSettings") {
LogInfo("openGameSettings requested from main menu.");
F4T::MainMenuVanillaSettings::OpenGameSettings();
return;
} else if (actionName == "quit") {
LogInfo("quit requested from main menu (not yet implemented).");
return;
}
} catch (const std::exception& ex) {
REX::WARN("{} [JS] Failed to parse coAction JSON: {}", GetLogPrefix(), ex.what());
}
});
if (g_mainMenuPendingShow) {
ShowMainMenuInternal();
}
}
void CreateMainMenuView()
{
if (!g_api) {
return;
}
if (g_mainMenuView != 0 && g_api->IsValid(g_mainMenuView)) {
return;
}
g_mainMenuDomReady = false;
g_mainMenuView = g_api->CreateView(kMainMenuHtmlPath, OnMainMenuDomReady);
if (g_mainMenuView == 0) {
LogWarning(std::format(
"Failed to create main menu view. Is Data/PrismaUI_F4/views/{} installed?",
kMainMenuHtmlPath));
return;
}
g_api->Hide(g_mainMenuView);
LogInfo(std::format("Created main menu view for '{}'.", kMainMenuHtmlPath));
}
void ShowMainMenuInternal()
{
if (!g_api) {
return;
}
if (!g_api->IsValid(g_mainMenuView)) {
g_mainMenuPendingShow = true;
CreateMainMenuView();
return;
}
if (!g_mainMenuDomReady) {
g_mainMenuPendingShow = true;
return;
}
const auto* ui = RE::UI::GetSingleton();
if (!F4T::MainMenuInject::IsMainMenuReadyForCustomOverlay(ui)) {
g_mainMenuPendingShow = true;
return;
}
g_mainMenuPendingShow = false;
g_mainMenuVisible = true;
F4T::MenuInput::SetMainMenuActive(true);
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
InitializeBrowserPointerMode();
g_api->Show(g_mainMenuView);
ApplyBrowserPointerMode(!g_menuPreferGamepad);
LogInfo("Main menu overlay shown.");
g_api->Invoke(
g_mainMenuView,
"if(window.onMainMenuShown){window.onMainMenuShown();}");
}
void ShowServerBrowserInternal()
{
if (!g_api) {
return;
}
if (!g_api->IsValid(g_serverBrowserView)) {
g_mainMenuPendingShow = true;
CreateServerBrowserView();
return;
}
if (!g_browserDomReady) {
g_mainMenuPendingShow = true;
return;
}
g_mainMenuPendingShow = false;
g_browserVisible = true;
F4T::MenuInput::SetBrowserActive(true);
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
InitializeBrowserPointerMode();
g_api->Show(g_serverBrowserView);
ApplyBrowserPointerMode(!g_menuPreferGamepad);
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
g_api->Invoke(
g_serverBrowserView,
"if(window.onBrowserShown){window.onBrowserShown();}");
}
void ToggleServerBrowser()
{
if (!g_api) {
return;
}
if (g_browserVisible) {
HideServerBrowserInternal();
return;
}
ShowServerBrowserInternal();
}
void OnServerBrowserDomReady(PrismaView a_view)
{
g_browserDomReady = 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);
F4T::ServerBrowser::ServerBrowserBridge::PushUITheme();
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_mainMenuPendingShow) {
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);
F4T::ServerBrowser::ServerBrowserBridge::PushUITheme();
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;
}
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));
if (g_serverBrowserView != 0 && g_api->IsValid(g_serverBrowserView)) {
return;
}
void StartGameThreadTasks()
{
const auto* taskInterface = F4SE::GetTaskInterface();
if (!taskInterface) {
LogWarning("F4SE task interface unavailable; F9 server browser toggle disabled.");
return;
}
g_browserDomReady = 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;
}
if (!g_toggleTaskStarted) {
taskInterface->AddTaskPermanent([]() {
static bool lastToggleDown = false;
const auto toggleDown = (GetAsyncKeyState(kToggleVirtualKey) & 0x8000) != 0;
if (toggleDown && !lastToggleDown) {
ToggleServerBrowser();
}
lastToggleDown = toggleDown;
});
g_api->Hide(g_serverBrowserView);
LogInfo(std::format("Created server browser view for '{}'.", kServerBrowserHtmlPath));
}
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 StartGameThreadTasks()
{
const auto* taskInterface = F4SE::GetTaskInterface();
if (!taskInterface) {
LogWarning("F4SE task interface unavailable; F9 server browser toggle and main menu disabled.");
return;
}
if (!g_toggleTaskStarted) {
taskInterface->AddTaskPermanent([]() {
F4T::MenuInput::PollGamepad();
UpdateBrowserPointerMode();
static bool lastToggleDown = false;
const auto toggleDown = (GetAsyncKeyState(kToggleVirtualKey) & 0x8000) != 0;
if (toggleDown && !lastToggleDown) {
ToggleServerBrowser();
}
lastToggleDown = toggleDown;
});
static std::uint32_t hoverSyncCounter = 0;
if (++hoverSyncCounter % 2 != 0) {
g_toggleTaskStarted = true;
}
if (!g_cursorRestoreTaskStarted) {
taskInterface->AddTaskPermanent([]() {
if (g_cursorRestoreFramesLeft <= 0) {
return;
}
SyncBrowserHover();
RestoreNativeGameCursor();
g_cursorRestoreFramesLeft--;
});
g_cursorRestoreTaskStarted = true;
}
if (!g_menuWatchTaskStarted) {
taskInterface->AddTaskPermanent(WatchMainMenuState);
g_menuWatchTaskStarted = true;
}
taskInterface->AddTaskPermanent([]() {
F4T::MenuInput::PollGamepad();
UpdateBrowserPointerMode();
if (g_mainMenuVisible || g_browserVisible) {
ReleaseMenuCursorConstraints();
SuppressNativeGameCursor();
}
static std::uint32_t hoverSyncCounter = 0;
if (++hoverSyncCounter % 2 != 0) {
return;
}
// Main menu doesn't need hover sync; only browser does
if (g_browserVisible) {
SyncBrowserHover();
}
});
}
}
void OnGameDataReady()
@@ -455,6 +686,7 @@ namespace F4T::PrismaUI
F4T::MenuInput::Register();
StartGameThreadTasks();
CreateServerBrowserView();
CreateMainMenuView();
}
void ShowServerBrowser()
@@ -469,6 +701,26 @@ namespace F4T::PrismaUI
bool IsBrowserVisible()
{
return g_visible;
return g_browserVisible;
}
void ShowMainMenu()
{
ShowMainMenuInternal();
}
void HideMainMenu()
{
HideMainMenuInternal();
}
bool IsMainMenuVisible()
{
return g_mainMenuVisible;
}
bool IsCustomMenuInputCaptured()
{
return g_mainMenuVisible || g_browserVisible;
}
}