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:
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
namespace F4T::MainMenu
|
||||
{
|
||||
namespace MainMenuBridge
|
||||
{
|
||||
void ForwardControllerInput(const char* a_button, const char* a_phase);
|
||||
void Initialize();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
namespace F4T::PrismaUI
|
||||
{
|
||||
void OnGameDataReady();
|
||||
|
||||
void ShowServerBrowser();
|
||||
void HideServerBrowser();
|
||||
bool IsBrowserVisible();
|
||||
|
||||
void ShowMainMenu();
|
||||
void HideMainMenu();
|
||||
bool IsMainMenuVisible();
|
||||
|
||||
bool IsCustomMenuInputCaptured();
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user