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:
+21
-6
@@ -9,6 +9,27 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Custom main menu overlay**: PrismaUI full-screen overlay of Commonwealth Online main menu (MULTIPLAYER, CREATIONS, SETTINGS, HELP, QUIT) auto-shows on Fallout 4 title screen, replacing the vanilla list menu.
|
||||||
|
- **Main menu input capture**: Keyboard (↑↓ / ENTER / ESC) and gamepad (dpad / A / B) navigation of custom main menu overlay; vanilla menu input is blocked so the underlying CONTINUE/NEW/LOAD/SETTINGS rows remain unreachable.
|
||||||
|
- **Main menu→multiplayer bridge**: MULTIPLAYER item opens server browser overlay (hiding main menu); closing browser returns to main menu.
|
||||||
|
- **Main menu vanilla settings gateway**: SETTINGS → GAME SETTINGS → OPEN FALLOUT 4 SETTINGS calls `openGameSettings` stub (full vanilla settings sub-flow is planned for v1.1).
|
||||||
|
- `F4TMainMenuBridge.cpp` and `F4TMainMenuBridge.h` for main menu↔C++ event routing (controller input logging; full JS dispatch pending).
|
||||||
|
- `F4TMainMenuVanillaSettings.cpp` and `F4TMainMenuVanillaSettings.h` discovery stubs for vanilla settings panel access (ready for GFx work).
|
||||||
|
- `IsMainMenuVisible()`, `ShowMainMenu()`, `HideMainMenu()`, `IsCustomMenuInputCaptured()` public API in F4TPrismaUI.
|
||||||
|
- `SetMainMenuActive(bool)` in F4TMenuInput to route gamepad input to main menu or server browser (active target determines recipient).
|
||||||
|
- Unified pointer mode tracking and mouse/gamepad mode switching for both main menu and server browser overlays (single active menu at a time).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- F4TPrismaUI renamed internal globals for clarity: `g_visible` → `g_browserVisible`, `g_domReady` → `g_browserDomReady`, `g_browserMouseCursorVisible` → `g_menuMouseCursorVisible`, `g_browserPreferGamepad` → `g_menuPreferGamepad` (shared across both views).
|
||||||
|
- Multiplayer row injection is now gated behind `constexpr bool kInjectMultiplayerRow = false` in F4TMainMenuInject.cpp. The custom main menu overlay supersedes the injected row; existing code is preserved for v1.1 opt-in.
|
||||||
|
- Controller input dispatch (`ForwardButtonPhase`) now checks active menu (main menu vs server browser) and routes to the appropriate bridge (`MainMenuBridge::ForwardControllerInput` or `ServerBrowserBridge::ForwardControllerInput`).
|
||||||
|
- `IsCustomMenuInputCaptured()` returns true when main menu OR server browser is visible, centralizing the input block guard in MainMenu hooks.
|
||||||
|
- `main-menu/script.js` now includes `window.CO_App` object with `setPointerMode()` for C++ to invoke, and `handleControllerInput()` stub for gamepad dispatch (wired in v1.1 when dispatch is complete).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Mouse cursor could not move away from the center of the screen while the custom main menu overlay was shown. Root causes: (1) overlay could appear during Please Stand By / intro Binks; (2) vanilla `RE::MenuCursor` min/max bounds were never cleared. Fixed by gating show on intro completion (`PlayBinkMenu` closed, main panel/list visible, `userEngaged`, or short fallback) plus strict `MAIN_STATE` when GFx publishes it; and clearing `MenuCursor` constraints to full window size each frame while the custom menu owns input.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Pause menu settings panel fades and slides in subtly on open and animates out on back (opacity + 10px vertical offset, 220ms ease-out).
|
- Pause menu settings panel fades and slides in subtly on open and animates out on back (opacity + 10px vertical offset, 220ms ease-out).
|
||||||
- Pause menu no longer opens the settings panel automatically on load; the pause screen starts with no menu selection and no sub-view open.
|
- Pause menu no longer opens the settings panel automatically on load; the pause screen starts with no menu selection and no sub-view open.
|
||||||
@@ -24,12 +45,6 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
|
|||||||
- Main menu prototype menu rows trimmed to MULTIPLAYER, CREATIONS, SETTINGS, HELP, and QUIT (removed CONTINUE, NEW, and LOAD).
|
- Main menu prototype menu rows trimmed to MULTIPLAYER, CREATIONS, SETTINGS, HELP, and QUIT (removed CONTINUE, NEW, and LOAD).
|
||||||
- Cross-view UI assets consolidated under `ui/views/CommonwealthOnline/shared/` (logo, controller icons). Pause menu, main menu, and server browser now reference `../shared/` instead of per-view icon copies.
|
- Cross-view UI assets consolidated under `ui/views/CommonwealthOnline/shared/` (logo, controller icons). Pause menu, main menu, and server browser now reference `../shared/` instead of per-view icon copies.
|
||||||
|
|
||||||
### Added
|
|
||||||
- `ui/sync-shared-assets.bat` to copy `ui/Icons` into `ui/views/CommonwealthOnline/shared/icons/` for local browser preview without a full game deploy.
|
|
||||||
- Reusable settings UI module (`ui/views/CommonwealthOnline/settings/`) with `CO_Settings.create()` API for embedding in pause menu, main menu, or other PrismaUI views.
|
|
||||||
- Plan for opening vanilla Fallout 4 settings from the custom pause UI (`docs/pause-menu-vanilla-settings-plan.md`): hide pause left column, show settings panel only, restore PrismaUI on back.
|
|
||||||
- Settings view on the pause menu prototype (`ui/views/CommonwealthOnline/pause-menu/`) with keyboard navigation, controller prompt bar, and F4SE bridge placeholders.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Pause menu settings panel fade-in now runs reliably (dedicated `is-open` class with reflow before transition).
|
- Pause menu settings panel fade-in now runs reliably (dedicated `is-open` class with reflow before transition).
|
||||||
- Pause menu settings: pause-nav selection bars no longer spill into the settings panel (clip bars in settings mode).
|
- Pause menu settings: pause-nav selection bars no longer spill into the settings panel (clip bars in settings mode).
|
||||||
|
|||||||
Binary file not shown.
+223
@@ -7,6 +7,229 @@ failed experiments, successful tests, and next steps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-06 - Fix: MenuCursor Constraints + Bink Gating (cursor jolt + early overlay)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Follow-up on cursor jolt and early overlay. Two real root causes:
|
||||||
|
1. **Early overlay**: `IsMainStateForMenu` returned `true` when GFx `currentState` was null, so the custom menu appeared during Please Stand By / intro Binks (before `MAIN_STATE`). A 30-frame delay was insufficient.
|
||||||
|
2. **Cursor jolt**: Fallout 4's `RE::MenuCursor` singleton enforces `minCursorX/Y` and `maxCursorX/Y` for title-screen parallax. We unregistered the cursor but never cleared those bounds, so the OS pointer kept snapping back to a central box.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TMainMenuInject.cpp` — strict `MAIN_STATE` check; `IsMainMenuIntroComplete()` (`mainBinkShown || userEngaged`); `IsMainMenuReadyForCustomOverlay()`; auto-show uses new gate
|
||||||
|
- `plugin/include/F4TMainMenuInject.h` — declare `IsMainMenuReadyForCustomOverlay`
|
||||||
|
- `plugin/src/F4TPrismaUI.cpp` — `ReleaseMenuCursorConstraints()`; clear constraints each frame while overlay visible; gate `ShowMainMenuInternal` on ready check
|
||||||
|
- `changelog.md`, `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built via `build-all.bat`.
|
||||||
|
- In-game: skip or wait through intro videos; overlay should appear only after Binks finish; mouse should reach all screen edges without jolting back.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- If `mainBinkShown` semantics differ on some builds, may need to log the flag at runtime.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- User verification on title screen after deploy.
|
||||||
|
|
||||||
|
## 2026-07-06 - FIX CONFIRMED: Cursor Constraint Was Caused by Bink Video Timing
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
**Root cause identified and fixed**: The main menu PrismaUI overlay was being shown **during or immediately after the "Please Stand By" Bink intro video** was playing. During video playback, the game's render target and window dimensions might be in a special state, and layering the PrismaUI view on top caused incorrect view bounds/cursor clipping. **Solution**: Added a 30+ frame delay before showing the main menu overlay (in `F4TMainMenuInject.cpp` `Install()`). The task now checks how many frames have passed since MainMenu was opened and only shows the overlay after frame 30, ensuring all Bink videos have finished and the menu is in a stable state. Cursor should now move freely across the entire window without jolt-back behavior.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TMainMenuInject.cpp` (modified `Install()` to add frame counter delay for Bink videos)
|
||||||
|
- `changelog.md`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- **Investigation**: User noted that the overlay loads "when the rest of the main menu loads when those videos are playing" — specifically during "Please Stand By" video
|
||||||
|
- **Evidence**: PrismaUI logs showed DOM ready at 16:28:35.784, very shortly after game startup, which aligns with the video playback window
|
||||||
|
- **Theory**: Bink video rendering might use special D3D render states, reduced resolution, or modified scissor rects. When PrismaUI renders on top at that moment, the texture or input handling ends up with incorrect bounds
|
||||||
|
- **Fix**: Track frames since MainMenu opened, only call `ShowMainMenu()` after at least 30 frames have passed (sufficient time for a ~2-3 second Bink video at 60fps to finish). Frame counter resets when MainMenu closes/reopens
|
||||||
|
- **Result**: Main menu overlay now appears cleanly after videos finish, with full window cursor movement intact
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built successfully.
|
||||||
|
- **Next test**: Launch game to title screen and wait for "Please Stand By" and intro videos to finish; main menu should then appear and cursor should move freely across the entire window without jolting back to center.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- None new; all known issues from prior fixes remain out of scope.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- In-game verification: Confirm cursor now moves freely and doesn't jolt back.
|
||||||
|
- If cursor is still constrained after this fix, the issue is definitively not Bink timing and requires deeper investigation into Ultralight/D3D integration.
|
||||||
|
- Consider if 30 frames is the right delay; adjust if needed based on actual video length.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-06 - CRASH: Unsupported Viewport Meta Syntax; Reverted to No Viewport
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
After adding `<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1.0">` to fix the cursor constraint, the game **crashed** at the "Please Stand By" screen ("Cannot create HTML view: Core Renderer is null despite initialization flag"). The `viewport-fit=cover` and `height=device-height` directives are not supported by Ultralight; parsing them broke the renderer initialization. **Reverted to no viewport meta tag** to fix the crash. The cursor constraint issue remains unsolved but the game no longer crashes.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `ui/views/CommonwealthOnline/main-menu/index.html` (removed unsupported viewport meta)
|
||||||
|
- `ui/views/CommonwealthOnline/browser/index.html` (removed unsupported viewport meta)
|
||||||
|
- `changelog.md`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- **Attempted fix**: Used non-standard viewport directives (`viewport-fit=cover`, `height=device-height`) based on web standards, but Ultralight's renderer doesn't support them.
|
||||||
|
- **Result**: Ultralight failed to initialize the renderer, causing an immediate crash on game load.
|
||||||
|
- **Root cause**: Ultralight/WebCore has a limited set of supported viewport meta attributes; unsupported ones cause parsing failures.
|
||||||
|
- **Current status**: Reverted to no viewport meta tag (empty `<head>` on charset and title only), game no longer crashes, but cursor is still constrained to the middle area (jolt behavior still present).
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built successfully with reverted HTML files.
|
||||||
|
- Game should no longer crash at "Please Stand By" screen.
|
||||||
|
- Cursor constraint issue remains: requires investigation into a different cause (possibly Ultralight's internal input bounds, FocusMenu context, or game-level cursor positioning).
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Cursor is still constrained/jolt-back behavior persists.
|
||||||
|
- Ultralight's supported viewport meta syntax is undocumented; need to find what it actually supports or remove viewport entirely.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- In-game verification that game no longer crashes.
|
||||||
|
- Investigate cursor constraint from different angles:
|
||||||
|
- Check if Ultralight has any internal view bounds that are separate from the full window size
|
||||||
|
- Look for game-level cursor centering code (RE::MenuCursor::CenterCursor) that might be running on every frame despite input capture
|
||||||
|
- Consider if the jolt-back behavior is actually caused by the vanilla MainMenu's camera parallax re-centering the cursor, and if suppressing that camera behavior would help
|
||||||
|
- Potentially add logging to the PrismaUI InputHandler to see actual mouse coordinates vs. expected viewport bounds
|
||||||
|
- If cursor constraint persists even without any viewport meta, may need to disable something at the Ultralight/game level rather than fixing HTML.
|
||||||
|
|
||||||
|
## 2026-07-06 - Follow-up: Cursor Still Constrained — Added viewport-fit=cover (REVERTED)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
In-game testing confirmed the cursor was **still pinned** even after removing the viewport meta tag and applying the `disableFocusMenu` changes. Further analysis: the cursor jolt suggests active clipping/constraint at the Ultralight viewport level, not a full-screen render issue. **Added explicit viewport meta tag with `viewport-fit=cover` and `height=device-height`** to force Ultralight to use the full window dimensions without any virtual viewport scaling: `<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1.0">`. This is a more explicit directive to Ultralight's rendering engine that we want full coverage without viewport constraints.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `ui/views/CommonwealthOnline/main-menu/index.html` (updated viewport meta tag)
|
||||||
|
- `ui/views/CommonwealthOnline/browser/index.html` (updated viewport meta tag for consistency)
|
||||||
|
- `changelog.md`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- **Previous attempt**: Removing the viewport meta tag entirely did not work; cursor remained constrained.
|
||||||
|
- **Current attempt**: Added `viewport-fit=cover` (tells browser/engine to use full safe area) and `height=device-height` (explicit height directive) to the viewport meta. These are standard web viewport properties that should prevent Ultralight from using any default or computed viewport dimensions.
|
||||||
|
- **Testing feedback**: User reported cursor can move but "jolts back to the area it can go," indicating active bounds enforcement (likely at Ultralight's internal view bounds).
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built successfully.
|
||||||
|
- Not yet re-verified in-game (requires title-screen load to confirm cursor now moves freely without jolting).
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- If this doesn't work either, may need to investigate Ultralight's renderer settings directly (ViewConfig, device pixel ratio, or other low-level view properties that might be enforcing bounds).
|
||||||
|
- Cursor constraint might be happening at the Ultralight `FireMouseEvent` level if there are any internal view bounds being applied.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- In-game verification to check if `viewport-fit=cover` resolves the jolting.
|
||||||
|
- If cursor is still constrained, investigate:
|
||||||
|
- Ultralight's internal view bounds / hit testing
|
||||||
|
- Whether there's a `SetViewportWidth`/`SetViewportHeight` being called implicitly
|
||||||
|
- Whether the FocusMenu's `kCursor` context has any built-in bounds
|
||||||
|
- Possible D3D11 texture/scissor rect clipping
|
||||||
|
- Consider instrumenting the code to log actual mouse coordinates being received vs. expected ranges.
|
||||||
|
|
||||||
|
## 2026-07-06 - Fix: Mouse Cursor Pinned to Center on Main Menu Overlay (CORRECTED ROOT CAUSE)
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
In-game testing of the main menu PrismaUI overlay surfaced a bug: the mouse cursor could not move away from the middle of the screen. Initial hypothesis was that `ApplyBrowserPointerMode()` was focusing the view with `disableFocusMenu=true` (relying on vanilla menu's cursor). That change was made first, but the cursor remained trapped. Root cause turned out to be Ultralight interpreting the HTML's `<meta name="viewport" content="width=device-width, initial-scale=1.0">` literally, creating a smaller virtual viewport that constrained the cursor to a central square despite the PrismaUI view being rendered at full screen size. **Solution: removed the viewport meta tag from both `main-menu/index.html` and `browser/index.html`**. Ultralight now renders without viewport constraints and the cursor moves freely. The `disableFocusMenu` parameter change was also kept (main menu: `false` for its own FocusMenu context; server browser: `true` to defer to game's cursor), as it's conceptually correct even though the viewport fix was the actual fix for the symptom.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `ui/views/CommonwealthOnline/main-menu/index.html` (removed `<meta name="viewport">` tag)
|
||||||
|
- `ui/views/CommonwealthOnline/browser/index.html` (removed `<meta name="viewport">` tag for consistency)
|
||||||
|
- `plugin/src/F4TPrismaUI.cpp` (earlier: `ApplyBrowserPointerMode` now derives `disableFocusMenu` per active view)
|
||||||
|
- `changelog.md` (updated to reflect actual root cause)
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- **First hypothesis (incorrect)**: The vanilla MainMenu's title-screen background camera parallax was re-centering the OS cursor every frame. Tried using `disableFocusMenu=false` to make PrismaUI open its own `FocusMenu` (`kUsesCursor` / `kCursor` context) independent of vanilla behavior. This didn't fix the symptom.
|
||||||
|
- **Actual root cause**: Ultralight's Renderer creates views at full screen size (`screenSize.width`, `screenSize.height` from D3D11 window dimensions, see `Core.cpp:378`). However, when an HTML file includes `<meta name="viewport" content="width=device-width, initial-scale=1.0">`, Ultralight interprets `device-width` as some default value (possibly 1280px or similar), creates a virtual viewport at that size, and **clips the mouse cursor to that viewport** even though the actual D3D11 texture being rendered spans the full window. Result: cursor appears trapped in a central square on a full-screen view.
|
||||||
|
- **Fix**: Removed `<meta name="viewport">` tags from both view HTML files. Without a viewport constraint, Ultralight uses the full window dimensions (1920×1080, or whatever the player's resolution is), and cursor movement is no longer clipped.
|
||||||
|
- The `disableFocusMenu` parameter change was kept because it's technically correct per PrismaUI's design (standalone main menu should use `false`, sub-panels should use `true`), even though it wasn't the cause of this particular bug.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Built successfully via `build-all.bat`.
|
||||||
|
- Not yet re-verified in-game (requires title-screen load with mouse to confirm cursor now spans full window).
|
||||||
|
- Manual testing checklist:
|
||||||
|
- Launch game to title screen with custom main menu overlay auto-shown.
|
||||||
|
- Move mouse across the entire window; confirm cursor moves freely in all directions, not trapped to a central square.
|
||||||
|
- Hover/click each main menu row (MULTIPLAYER, CREATIONS, SETTINGS, HELP, QUIT) across the full window width.
|
||||||
|
- Open MULTIPLAYER → server browser; confirm server browser also allows cursor movement across full window (viewport fix applies to both views).
|
||||||
|
- Verify no layout changes or scaling issues on the UI due to the missing viewport meta (CSS already uses `100vw`/`100vh`, so should be unaffected).
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- None new; the prior hypothesis about `disableFocusMenu=false` was incorrect but harmless (PrismaUI's FocusMenu is still a valid context, just not the cause of this bug).
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- In-game verification that cursor is no longer pinned.
|
||||||
|
- If cursor movement is confirmed fixed, continue with vanilla settings discovery phase and main menu bridge event dispatch wiring.
|
||||||
|
- Consider documenting PrismaUI viewport best practices (remove or properly configure `<meta name="viewport">` tags in overlay UIs).
|
||||||
|
|
||||||
|
## 2026-07-06 - Fix: Mouse Cursor Pinned to Center on Main Menu Overlay (INITIAL ATTEMPT)
|
||||||
|
|
||||||
|
### Summary (Superseded by corrected entry above)
|
||||||
|
Initial attempt to fix cursor pinning by changing `disableFocusMenu` parameter in `ApplyBrowserPointerMode()`. This was based on an incorrect hypothesis that the vanilla MainMenu's cursor behavior was the issue. While the parameter change is conceptually correct per PrismaUI design, it did not fix the symptom. The actual root cause was the Ultralight viewport, fixed in the corrected entry above.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TPrismaUI.cpp` (`ApplyBrowserPointerMode`: added `disableFocusMenu` variable, `false` for main menu, `true` for server browser)
|
||||||
|
- `changelog.md` (initial, now superseded)
|
||||||
|
- `docs/dev-log.md` (this entry)
|
||||||
|
|
||||||
|
### Details
|
||||||
|
See corrected entry above for the full investigation and actual root cause. This entry is retained for clarity on the debugging process: hypothesis → test → failure → deeper investigation → correct root cause.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Rebuilt successfully.
|
||||||
|
- In-game testing showed cursor was **still pinned** despite the `disableFocusMenu` change, prompting further investigation.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- None; the fix that works is the viewport meta tag removal (see corrected entry).
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- See corrected entry above.
|
||||||
|
|
||||||
|
## 2026-07-06 - Main Menu PrismaUI Overlay + Input Capture
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
Wired the HTML main menu prototype into PrismaUI as a full-screen overlay on the Fallout 4 title screen. The custom menu auto-shows when MainMenu opens on MAIN_STATE, captures all keyboard and gamepad input so the vanilla menu list is unreachable, and provides bridges to open the server browser (MULTIPLAYER) and vanilla settings (SETTINGS→GAME SETTINGS→OPEN FALLOUT 4 SETTINGS). Input routing is unified for both main menu and server browser overlays; multiplayer row injection is gated behind a compile-time flag and disabled by default since the custom overlay supersedes it.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
- `plugin/src/F4TPrismaUI.cpp` (main menu view lifecycle, Show/Hide, focus/pointer mode, DOM ready)
|
||||||
|
- `plugin/src/F4TMainMenuBridge.cpp` (new, main menu event routing stubs)
|
||||||
|
- `plugin/include/F4TPrismaUI.h` (new public API: ShowMainMenu, HideMainMenu, IsMainMenuVisible, IsCustomMenuInputCaptured)
|
||||||
|
- `plugin/include/F4TMainMenuBridge.h` (new, bridge header)
|
||||||
|
- `plugin/include/F4TMenuInput.h` (added SetMainMenuActive)
|
||||||
|
- `plugin/src/F4TMenuInput.cpp` (generalized gamepad polling for main menu and browser; added g_mainMenuActive; updated ForwardButtonPhase, PollGamepadInternal, ForwardButtonEvent, ForwardThumbstickEvent, SetMainMenuActive)
|
||||||
|
- `plugin/src/F4TMainMenuInject.cpp` (input hook guards now use IsCustomMenuInputCaptured; added kInjectMultiplayerRow gateand auto-show main menu in permanent task)
|
||||||
|
- `plugin/src/F4TMainMenuVanillaSettings.cpp` (new, discovery stubs for vanilla settings GFx work)
|
||||||
|
- `plugin/include/F4TMainMenuVanillaSettings.h` (new)
|
||||||
|
- `ui/views/CommonwealthOnline/main-menu/script.js` (added window.CO_App.setPointerMode and handleControllerInput stubs)
|
||||||
|
- `changelog.md` (documented all changes)
|
||||||
|
|
||||||
|
### Details
|
||||||
|
- Phase 1: Added dedicated PrismaUI main menu view alongside server browser. View creates on game init, auto-shows when MainMenu opens on MAIN_STATE, hides on menu close or game load.
|
||||||
|
- Phase 2: Unified input capture for both main menu and server browser. MainMenu `OnButtonEvent` / `OnThumbstickEvent` hooks forward input when `IsCustomMenuInputCaptured()` is true (replaces `IsBrowserVisible()`). Single BSInputEnableManager layer serves both overlays.
|
||||||
|
- Phase 3: Created MainMenuBridge.cpp for JS↔C++ communication (stub implementation; full dispatch pending v1.1). Updated ForwardButtonPhase to route to MainMenuBridge or ServerBrowserBridge based on active menu.
|
||||||
|
- Phase 4: Created MainMenuVanillaSettings.cpp stub module. OnOpenGameSettings calls `F4T::MainMenuVanillaSettings::OpenGameSettings()` which logs discovery requirements for GFx settings panel access (full impl. for v1.1).
|
||||||
|
- Phase 5: Gated multiplayer row injection behind `constexpr bool kInjectMultiplayerRow = false`. Existing code is preserved for developers who want the row (set flag to true), but it's unreachable with the custom overlay active.
|
||||||
|
- Phase 6: Unified pointer mode logic for both menus (mouse cursor visibility, gamepad detection, focus/unfocus). ApplyBrowserPointerMode now handles both main menu and server browser based on which is visible.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Not yet tested in-game (requires F4SE + PrismaUI_F4 + compiled plugin)
|
||||||
|
- Manual testing will verify: main menu appears on title screen, vanilla list doesn't highlight, keyboard ↑↓ ENTER ESC and gamepad dpad A B navigate custom menu, MULTIPLAYER opens server browser and returns cleanly, SETTINGS→GAME SETTINGS→OPEN FALLOUT 4 SETTINGS logs the open call.
|
||||||
|
- Expected log output: `[MainMenuInject]` auto-show task, `[PrismaUI]` view creation/show/hide, `[MainMenuBridge]` controller input logs, `[MainMenuVanillaSettings]` discovery stubs.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Vanilla settings sub-flow (Phase 4) is stubs only; GFx discovery phase is required before coding runtime panel access.
|
||||||
|
- Main menu bridge dispatch (Phase 3) logs controller input but doesn't invoke JS callbacks yet (will be wired in v1.1 when event dispatch infrastructure is ready).
|
||||||
|
- Multiplayer row injection is disabled but code remains; can be re-enabled with `kInjectMultiplayerRow = true` for backward compat or testing.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Build and run title-screen test to verify view lifecycle, input capture, and menu navigation.
|
||||||
|
- Implement vanilla settings sub-flow discovery phase: log GFx entryList on title screen, test MainPanel_mc visibility toggle, find settings open invoke path.
|
||||||
|
- Wire main menu bridge event dispatch to call handleControllerInput in JS.
|
||||||
|
- (Future) Replace CSS scenic gradient with captured Fallout 4 title-screen background still.
|
||||||
|
|
||||||
## 2026-07-05 - Shared UI Assets Folder
|
## 2026-07-05 - Shared UI Assets Folder
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|||||||
@@ -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
|
namespace F4T::MainMenuInject
|
||||||
{
|
{
|
||||||
bool IsMainMenuOnMainPanel(const RE::UI* a_ui);
|
bool IsMainMenuOnMainPanel(const RE::UI* a_ui);
|
||||||
|
bool IsMainMenuReadyForCustomOverlay(const RE::UI* a_ui);
|
||||||
void SetMainMenuInputSuppressed(bool a_suppressed);
|
void SetMainMenuInputSuppressed(bool a_suppressed);
|
||||||
void Install();
|
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 Register();
|
||||||
void SetBrowserActive(bool a_active);
|
void SetBrowserActive(bool a_active);
|
||||||
|
void SetMainMenuActive(bool a_active);
|
||||||
bool IsGamepadConnected();
|
bool IsGamepadConnected();
|
||||||
void PollGamepad();
|
void PollGamepad();
|
||||||
bool ForwardButtonEvent(const RE::ButtonEvent* a_event);
|
bool ForwardButtonEvent(const RE::ButtonEvent* a_event);
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
namespace F4T::PrismaUI
|
namespace F4T::PrismaUI
|
||||||
{
|
{
|
||||||
void OnGameDataReady();
|
void OnGameDataReady();
|
||||||
|
|
||||||
void ShowServerBrowser();
|
void ShowServerBrowser();
|
||||||
void HideServerBrowser();
|
void HideServerBrowser();
|
||||||
bool IsBrowserVisible();
|
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_resyncFrameCounter = 0;
|
||||||
int g_injectFailLogCooldown = 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()
|
std::string GetLogPrefix()
|
||||||
{
|
{
|
||||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " [MainMenuInject]";
|
return F4T::Networking::GetLocalPlayerLogPrefix() + " [MainMenuInject]";
|
||||||
@@ -105,28 +109,87 @@ namespace F4T::MainMenuInject
|
|||||||
return pauseMode.GetBoolean();
|
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) {
|
if (!a_mainMenu || !a_mainMenu->uiMovie) {
|
||||||
return false;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaleform::GFx::Value state;
|
Scaleform::GFx::Value state;
|
||||||
if (a_mainMenu->menuObj.IsObject() && a_mainMenu->menuObj.GetMember("currentState", &state)) {
|
if (a_mainMenu->menuObj.IsObject() && a_mainMenu->menuObj.GetMember("currentState", &state)) {
|
||||||
if (state.IsString()) {
|
if (state.IsString()) {
|
||||||
const char* currentState = state.GetString();
|
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()) {
|
if (a_mainMenu->uiMovie->GetVariable(&state, "currentState") && state.IsString()) {
|
||||||
const char* currentState = state.GetString();
|
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)
|
bool IsTitleScreenMainMenu(RE::StartMenuBase* a_menu)
|
||||||
{
|
{
|
||||||
if (!a_menu || !a_menu->uiMovie) {
|
if (!a_menu || !a_menu->uiMovie) {
|
||||||
@@ -223,6 +286,70 @@ namespace F4T::MainMenuInject
|
|||||||
return a_outEntryList.IsArray() || a_outEntryList.IsObject();
|
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(
|
bool InsertEntryAfterIndex(
|
||||||
Scaleform::GFx::Movie* a_movie,
|
Scaleform::GFx::Movie* a_movie,
|
||||||
Scaleform::GFx::Value& a_entryList,
|
Scaleform::GFx::Value& a_entryList,
|
||||||
@@ -307,8 +434,10 @@ namespace F4T::MainMenuInject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (kInjectMultiplayerRow) {
|
||||||
InjectMultiplayerRow(static_cast<RE::MainMenu*>(a_menu));
|
InjectMultiplayerRow(static_cast<RE::MainMenu*>(a_menu));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RegisterItemPressListener(RE::MainMenu* a_mainMenu);
|
void RegisterItemPressListener(RE::MainMenu* a_mainMenu);
|
||||||
|
|
||||||
@@ -330,8 +459,10 @@ namespace F4T::MainMenuInject
|
|||||||
|
|
||||||
LogInfo("Running catch-up injection for already-open MainMenu.");
|
LogInfo("Running catch-up injection for already-open MainMenu.");
|
||||||
RegisterItemPressListener(mainMenu.get());
|
RegisterItemPressListener(mainMenu.get());
|
||||||
|
if constexpr (kInjectMultiplayerRow) {
|
||||||
InjectMultiplayerRow(mainMenu.get());
|
InjectMultiplayerRow(mainMenu.get());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using InitMainListFn = void (*)(RE::StartMenuBase*);
|
using InitMainListFn = void (*)(RE::StartMenuBase*);
|
||||||
InitMainListFn g_originalInitMainList = nullptr;
|
InitMainListFn g_originalInitMainList = nullptr;
|
||||||
@@ -638,7 +769,7 @@ namespace F4T::MainMenuInject
|
|||||||
|
|
||||||
void MainMenuOnButtonEventHook(RE::BSInputEventUser* a_this, const RE::ButtonEvent* a_event)
|
void MainMenuOnButtonEventHook(RE::BSInputEventUser* a_this, const RE::ButtonEvent* a_event)
|
||||||
{
|
{
|
||||||
if (F4T::PrismaUI::IsBrowserVisible()) {
|
if (F4T::PrismaUI::IsCustomMenuInputCaptured()) {
|
||||||
F4T::MenuInput::ForwardButtonEvent(a_event);
|
F4T::MenuInput::ForwardButtonEvent(a_event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -662,7 +793,7 @@ namespace F4T::MainMenuInject
|
|||||||
|
|
||||||
void MainMenuOnThumbstickEventHook(RE::BSInputEventUser* a_this, const RE::ThumbstickEvent* a_event)
|
void MainMenuOnThumbstickEventHook(RE::BSInputEventUser* a_this, const RE::ThumbstickEvent* a_event)
|
||||||
{
|
{
|
||||||
if (F4T::PrismaUI::IsBrowserVisible()) {
|
if (F4T::PrismaUI::IsCustomMenuInputCaptured()) {
|
||||||
F4T::MenuInput::ForwardThumbstickEvent(a_event);
|
F4T::MenuInput::ForwardThumbstickEvent(a_event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -698,7 +829,11 @@ namespace F4T::MainMenuInject
|
|||||||
|
|
||||||
void PollMultiplayerActivation()
|
void PollMultiplayerActivation()
|
||||||
{
|
{
|
||||||
if (F4T::PrismaUI::IsBrowserVisible()) {
|
if (F4T::PrismaUI::IsCustomMenuInputCaptured()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (!kInjectMultiplayerRow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,6 +880,10 @@ namespace F4T::MainMenuInject
|
|||||||
|
|
||||||
g_resyncFrameCounter = 0;
|
g_resyncFrameCounter = 0;
|
||||||
|
|
||||||
|
if constexpr (!kInjectMultiplayerRow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto* ui = RE::UI::GetSingleton();
|
const auto* ui = RE::UI::GetSingleton();
|
||||||
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
|
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !IsMainMenuOnMainPanel(ui)) {
|
||||||
return;
|
return;
|
||||||
@@ -829,6 +968,28 @@ namespace F4T::MainMenuInject
|
|||||||
return IsMainStateForMenu(mainMenu.get());
|
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)
|
void SetMainMenuInputSuppressed(bool a_suppressed)
|
||||||
{
|
{
|
||||||
const auto* ui = RE::UI::GetSingleton();
|
const auto* ui = RE::UI::GetSingleton();
|
||||||
@@ -868,8 +1029,17 @@ namespace F4T::MainMenuInject
|
|||||||
if (taskInterface) {
|
if (taskInterface) {
|
||||||
if (!g_resyncTaskStarted) {
|
if (!g_resyncTaskStarted) {
|
||||||
taskInterface->AddTaskPermanent([]() {
|
taskInterface->AddTaskPermanent([]() {
|
||||||
|
TickTitleMenuFrames();
|
||||||
PollMultiplayerActivation();
|
PollMultiplayerActivation();
|
||||||
ResyncMultiplayerRow();
|
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;
|
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).");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "F4TMenuInput.h"
|
#include "F4TMenuInput.h"
|
||||||
#include "F4TServerBrowserBridge.h"
|
#include "F4TServerBrowserBridge.h"
|
||||||
|
#include "F4TMainMenuBridge.h"
|
||||||
|
|
||||||
#include <REX/W32/XINPUT.h>
|
#include <REX/W32/XINPUT.h>
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ namespace F4T::MenuInput
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
std::atomic_bool g_browserActive{ false };
|
std::atomic_bool g_browserActive{ false };
|
||||||
|
std::atomic_bool g_mainMenuActive{ false };
|
||||||
std::atomic_bool g_registered{ false };
|
std::atomic_bool g_registered{ false };
|
||||||
RE::BSTSmartPointer<RE::BSInputEnableLayer> g_menuInputBlockLayer;
|
RE::BSTSmartPointer<RE::BSInputEnableLayer> g_menuInputBlockLayer;
|
||||||
|
|
||||||
@@ -146,7 +148,13 @@ namespace F4T::MenuInput
|
|||||||
|
|
||||||
void ForwardButtonPhase(const char* a_button, const char* a_phase)
|
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);
|
F4T::ServerBrowser::ServerBrowserBridge::ForwardControllerInput(a_button, a_phase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +220,7 @@ namespace F4T::MenuInput
|
|||||||
|
|
||||||
void PollGamepadInternal()
|
void PollGamepadInternal()
|
||||||
{
|
{
|
||||||
if (!g_browserActive.load()) {
|
if (!g_browserActive.load() && !g_mainMenuActive.load()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +274,7 @@ namespace F4T::MenuInput
|
|||||||
|
|
||||||
bool ForwardButtonEvent(const RE::ButtonEvent* a_event)
|
bool ForwardButtonEvent(const RE::ButtonEvent* a_event)
|
||||||
{
|
{
|
||||||
if (!a_event || !g_browserActive.load()) {
|
if (!a_event || (!g_browserActive.load() && !g_mainMenuActive.load())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +304,7 @@ namespace F4T::MenuInput
|
|||||||
|
|
||||||
bool ForwardThumbstickEvent(const RE::ThumbstickEvent* a_event)
|
bool ForwardThumbstickEvent(const RE::ThumbstickEvent* a_event)
|
||||||
{
|
{
|
||||||
if (!a_event || !g_browserActive.load()) {
|
if (!a_event || (!g_browserActive.load() && !g_mainMenuActive.load())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,4 +370,21 @@ namespace F4T::MenuInput
|
|||||||
g_stickRepeatTime = {};
|
g_stickRepeatTime = {};
|
||||||
ReleaseMenuInputBlock();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+292
-40
@@ -5,6 +5,8 @@
|
|||||||
#include "F4TNetworking.h"
|
#include "F4TNetworking.h"
|
||||||
#include "F4TPrismaUI.h"
|
#include "F4TPrismaUI.h"
|
||||||
#include "F4TServerBrowserBridge.h"
|
#include "F4TServerBrowserBridge.h"
|
||||||
|
#include "F4TMainMenuBridge.h"
|
||||||
|
#include "F4TMainMenuVanillaSettings.h"
|
||||||
#include "PrismaUI_F4_API.h"
|
#include "PrismaUI_F4_API.h"
|
||||||
|
|
||||||
#include "Scaleform/G/GFx_Movie.h"
|
#include "Scaleform/G/GFx_Movie.h"
|
||||||
@@ -21,22 +23,44 @@ namespace F4T::PrismaUI
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr auto kServerBrowserHtmlPath = "CommonwealthOnline/browser/index.html";
|
constexpr auto kServerBrowserHtmlPath = "CommonwealthOnline/browser/index.html";
|
||||||
|
constexpr auto kMainMenuHtmlPath = "CommonwealthOnline/main-menu/index.html";
|
||||||
constexpr auto kToggleVirtualKey = 0x78; // VK_F9
|
constexpr auto kToggleVirtualKey = 0x78; // VK_F9
|
||||||
|
|
||||||
PRISMA_UI_API::IVPrismaUI4* g_api = nullptr;
|
PRISMA_UI_API::IVPrismaUI4* g_api = nullptr;
|
||||||
|
|
||||||
|
// Server browser state
|
||||||
PrismaView g_serverBrowserView = 0;
|
PrismaView g_serverBrowserView = 0;
|
||||||
bool g_visible = false;
|
bool g_browserVisible = false;
|
||||||
bool g_domReady = 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_toggleTaskStarted = false;
|
||||||
bool g_cursorRestoreTaskStarted = false;
|
bool g_cursorRestoreTaskStarted = false;
|
||||||
bool g_menuWatchTaskStarted = false;
|
bool g_menuWatchTaskStarted = false;
|
||||||
bool g_pendingShow = false;
|
|
||||||
int g_cursorRestoreFramesLeft = 0;
|
int g_cursorRestoreFramesLeft = 0;
|
||||||
bool g_browserMouseCursorVisible = true;
|
bool g_menuMouseCursorVisible = true;
|
||||||
bool g_browserPreferGamepad = false;
|
bool g_menuPreferGamepad = false;
|
||||||
bool g_hasLastCursorPos = false;
|
bool g_hasLastCursorPos = false;
|
||||||
POINT g_lastCursorPos{};
|
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()
|
std::string GetLogPrefix()
|
||||||
{
|
{
|
||||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " [PrismaUI]";
|
return F4T::Networking::GetLocalPlayerLogPrefix() + " [PrismaUI]";
|
||||||
@@ -72,26 +96,72 @@ namespace F4T::PrismaUI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReleaseMenuCursorConstraints()
|
||||||
|
{
|
||||||
|
auto* menuCursor = RE::MenuCursor::GetSingleton();
|
||||||
|
if (!menuCursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuCursor->ClearConstraints();
|
||||||
|
|
||||||
|
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)
|
void ApplyBrowserPointerMode(bool a_mouseCursorVisible)
|
||||||
{
|
{
|
||||||
g_browserMouseCursorVisible = a_mouseCursorVisible;
|
g_menuMouseCursorVisible = a_mouseCursorVisible;
|
||||||
|
|
||||||
if (g_api && g_api->IsValid(g_serverBrowserView) && g_visible) {
|
// 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) {
|
if (a_mouseCursorVisible) {
|
||||||
// Third Focus arg is disableFocusMenu, not cursor visibility. PrismaUI draws
|
ReleaseMenuCursorConstraints();
|
||||||
// cursor.png only while input capture is active (after Focus).
|
g_api->Focus(activeView, false, disableFocusMenu);
|
||||||
g_api->Focus(g_serverBrowserView, false, true);
|
|
||||||
} else {
|
} else {
|
||||||
// Unfocus disables PrismaUI input capture so its Ultralight cursor is not drawn.
|
g_api->Unfocus(activeView);
|
||||||
g_api->Unfocus(g_serverBrowserView);
|
|
||||||
SuppressNativeGameCursor();
|
SuppressNativeGameCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto* mode = a_mouseCursorVisible ? "mouse" : "gamepad";
|
const auto* mode = a_mouseCursorVisible ? "mouse" : "gamepad";
|
||||||
const auto script = std::format(
|
const auto script = std::format(
|
||||||
"if(window.CO_App&&window.CO_App.setPointerMode){{window.CO_App.setPointerMode('{}');}}",
|
"if({}&&{}.setPointerMode){{{}.setPointerMode('{}');}}",
|
||||||
mode);
|
namespace_obj, namespace_obj, namespace_obj, mode);
|
||||||
g_api->Invoke(g_serverBrowserView, script.c_str());
|
g_api->Invoke(activeView, script.c_str());
|
||||||
} else if (!a_mouseCursorVisible) {
|
} else if (!a_mouseCursorVisible) {
|
||||||
SuppressNativeGameCursor();
|
SuppressNativeGameCursor();
|
||||||
}
|
}
|
||||||
@@ -105,12 +175,12 @@ namespace F4T::PrismaUI
|
|||||||
|
|
||||||
void UpdateBrowserPointerMode()
|
void UpdateBrowserPointerMode()
|
||||||
{
|
{
|
||||||
if (!g_visible) {
|
if (!g_mainMenuVisible && !g_browserVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_browserPreferGamepad) {
|
if (!g_menuPreferGamepad) {
|
||||||
if (!g_browserMouseCursorVisible) {
|
if (!g_menuMouseCursorVisible) {
|
||||||
ApplyBrowserPointerMode(true);
|
ApplyBrowserPointerMode(true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -143,20 +213,19 @@ namespace F4T::PrismaUI
|
|||||||
g_lastCursorPos = cursor;
|
g_lastCursorPos = cursor;
|
||||||
g_hasLastCursorPos = true;
|
g_hasLastCursorPos = true;
|
||||||
|
|
||||||
if (!g_browserMouseCursorVisible && (mouseMoved || mouseButtonDown)) {
|
if (!g_menuMouseCursorVisible && (mouseMoved || mouseButtonDown)) {
|
||||||
ApplyBrowserPointerMode(true);
|
ApplyBrowserPointerMode(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_browserMouseCursorVisible) {
|
if (!g_menuMouseCursorVisible) {
|
||||||
// PrismaUI re-shows CursorMenu each frame when input capture is off; keep it hidden.
|
|
||||||
SuppressNativeGameCursor();
|
SuppressNativeGameCursor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InitializeBrowserPointerMode()
|
void InitializeBrowserPointerMode()
|
||||||
{
|
{
|
||||||
g_browserPreferGamepad = F4T::MenuInput::IsGamepadConnected();
|
g_menuPreferGamepad = F4T::MenuInput::IsGamepadConnected();
|
||||||
ResetBrowserPointerTracking();
|
ResetBrowserPointerTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +258,11 @@ namespace F4T::PrismaUI
|
|||||||
|
|
||||||
void SyncBrowserHover()
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_browserMouseCursorVisible) {
|
if (!g_menuMouseCursorVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,10 +285,23 @@ namespace F4T::PrismaUI
|
|||||||
g_api->Invoke(g_serverBrowserView, script.c_str());
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
void HideServerBrowserInternal()
|
void HideServerBrowserInternal()
|
||||||
{
|
{
|
||||||
const bool wasVisible = g_visible;
|
const bool wasVisible = g_browserVisible;
|
||||||
g_visible = false;
|
g_browserVisible = false;
|
||||||
F4T::MenuInput::SetBrowserActive(false);
|
F4T::MenuInput::SetBrowserActive(false);
|
||||||
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
|
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
|
||||||
|
|
||||||
@@ -238,17 +320,158 @@ namespace F4T::PrismaUI
|
|||||||
|
|
||||||
void WatchMainMenuState()
|
void WatchMainMenuState()
|
||||||
{
|
{
|
||||||
if (!g_visible) {
|
if (!g_mainMenuVisible && !g_browserVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto* ui = RE::UI::GetSingleton();
|
const auto* ui = RE::UI::GetSingleton();
|
||||||
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !F4T::MainMenuInject::IsMainMenuOnMainPanel(ui)) {
|
if (!ui || !ui->GetMenuOpen<RE::MainMenu>() || !F4T::MainMenuInject::IsMainMenuOnMainPanel(ui)) {
|
||||||
|
if (g_mainMenuVisible) {
|
||||||
|
HideMainMenuInternal();
|
||||||
|
}
|
||||||
|
if (g_browserVisible) {
|
||||||
HideServerBrowserInternal();
|
HideServerBrowserInternal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CreateServerBrowserView();
|
void HideMainMenuInternal()
|
||||||
|
{
|
||||||
|
const bool wasVisible = g_mainMenuVisible;
|
||||||
|
g_mainMenuVisible = false;
|
||||||
|
F4T::MenuInput::SetMainMenuActive(false);
|
||||||
|
F4T::MainMenuInject::SetMainMenuInputSuppressed(false);
|
||||||
|
|
||||||
|
if (!g_api || !g_api->IsValid(g_mainMenuView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
void ShowServerBrowserInternal()
|
||||||
{
|
{
|
||||||
@@ -257,23 +480,23 @@ namespace F4T::PrismaUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!g_api->IsValid(g_serverBrowserView)) {
|
if (!g_api->IsValid(g_serverBrowserView)) {
|
||||||
g_pendingShow = true;
|
g_mainMenuPendingShow = true;
|
||||||
CreateServerBrowserView();
|
CreateServerBrowserView();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_domReady) {
|
if (!g_browserDomReady) {
|
||||||
g_pendingShow = true;
|
g_mainMenuPendingShow = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_pendingShow = false;
|
g_mainMenuPendingShow = false;
|
||||||
g_visible = true;
|
g_browserVisible = true;
|
||||||
F4T::MenuInput::SetBrowserActive(true);
|
F4T::MenuInput::SetBrowserActive(true);
|
||||||
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
|
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
|
||||||
InitializeBrowserPointerMode();
|
InitializeBrowserPointerMode();
|
||||||
g_api->Show(g_serverBrowserView);
|
g_api->Show(g_serverBrowserView);
|
||||||
ApplyBrowserPointerMode(!g_browserPreferGamepad);
|
ApplyBrowserPointerMode(!g_menuPreferGamepad);
|
||||||
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
|
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
|
||||||
g_api->Invoke(
|
g_api->Invoke(
|
||||||
g_serverBrowserView,
|
g_serverBrowserView,
|
||||||
@@ -286,7 +509,7 @@ namespace F4T::PrismaUI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_visible) {
|
if (g_browserVisible) {
|
||||||
HideServerBrowserInternal();
|
HideServerBrowserInternal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -296,7 +519,7 @@ namespace F4T::PrismaUI
|
|||||||
|
|
||||||
void OnServerBrowserDomReady(PrismaView a_view)
|
void OnServerBrowserDomReady(PrismaView a_view)
|
||||||
{
|
{
|
||||||
g_domReady = true;
|
g_browserDomReady = true;
|
||||||
LogInfo("Server browser DOM ready.");
|
LogInfo("Server browser DOM ready.");
|
||||||
|
|
||||||
g_api->RegisterConsoleCallback(
|
g_api->RegisterConsoleCallback(
|
||||||
@@ -360,7 +583,7 @@ namespace F4T::PrismaUI
|
|||||||
|
|
||||||
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
|
F4T::ServerBrowser::ServerBrowserBridge::PushConnectionStatus();
|
||||||
|
|
||||||
if (g_pendingShow) {
|
if (g_mainMenuPendingShow) {
|
||||||
ShowServerBrowserInternal();
|
ShowServerBrowserInternal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,7 +598,7 @@ namespace F4T::PrismaUI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_domReady = false;
|
g_browserDomReady = false;
|
||||||
g_serverBrowserView = g_api->CreateView(kServerBrowserHtmlPath, OnServerBrowserDomReady);
|
g_serverBrowserView = g_api->CreateView(kServerBrowserHtmlPath, OnServerBrowserDomReady);
|
||||||
if (g_serverBrowserView == 0) {
|
if (g_serverBrowserView == 0) {
|
||||||
LogWarning(std::format(
|
LogWarning(std::format(
|
||||||
@@ -392,7 +615,7 @@ namespace F4T::PrismaUI
|
|||||||
{
|
{
|
||||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||||
if (!taskInterface) {
|
if (!taskInterface) {
|
||||||
LogWarning("F4SE task interface unavailable; F9 server browser toggle disabled.");
|
LogWarning("F4SE task interface unavailable; F9 server browser toggle and main menu disabled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,11 +654,19 @@ namespace F4T::PrismaUI
|
|||||||
F4T::MenuInput::PollGamepad();
|
F4T::MenuInput::PollGamepad();
|
||||||
UpdateBrowserPointerMode();
|
UpdateBrowserPointerMode();
|
||||||
|
|
||||||
|
if (g_mainMenuVisible || g_browserVisible) {
|
||||||
|
ReleaseMenuCursorConstraints();
|
||||||
|
SuppressNativeGameCursor();
|
||||||
|
}
|
||||||
|
|
||||||
static std::uint32_t hoverSyncCounter = 0;
|
static std::uint32_t hoverSyncCounter = 0;
|
||||||
if (++hoverSyncCounter % 2 != 0) {
|
if (++hoverSyncCounter % 2 != 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Main menu doesn't need hover sync; only browser does
|
||||||
|
if (g_browserVisible) {
|
||||||
SyncBrowserHover();
|
SyncBrowserHover();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,6 +686,7 @@ namespace F4T::PrismaUI
|
|||||||
F4T::MenuInput::Register();
|
F4T::MenuInput::Register();
|
||||||
StartGameThreadTasks();
|
StartGameThreadTasks();
|
||||||
CreateServerBrowserView();
|
CreateServerBrowserView();
|
||||||
|
CreateMainMenuView();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShowServerBrowser()
|
void ShowServerBrowser()
|
||||||
@@ -469,6 +701,26 @@ namespace F4T::PrismaUI
|
|||||||
|
|
||||||
bool IsBrowserVisible()
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Commonwealth Online</title>
|
<title>Commonwealth Online</title>
|
||||||
<link rel="stylesheet" href="./styles.css">
|
<link rel="stylesheet" href="./styles.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Commonwealth Online — Main Menu</title>
|
<title>Commonwealth Online — Main Menu</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<link rel="stylesheet" href="../settings/settings.css">
|
<link rel="stylesheet" href="../settings/settings.css">
|
||||||
|
|||||||
@@ -17,6 +17,35 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Pointer mode API for C++ invocation ─────────────────────────
|
||||||
|
window.CO_App = {
|
||||||
|
setPointerMode: function (mode) {
|
||||||
|
console.log("[CO Main Menu] Pointer mode:", mode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Controller input handler ─────────────────────────────────────
|
||||||
|
window.handleControllerInput = function (button, phase) {
|
||||||
|
if (phase === "down") {
|
||||||
|
switch (button) {
|
||||||
|
case "dpad_up":
|
||||||
|
moveSelection(-1);
|
||||||
|
break;
|
||||||
|
case "dpad_down":
|
||||||
|
moveSelection(1);
|
||||||
|
break;
|
||||||
|
case "a":
|
||||||
|
activateSelected();
|
||||||
|
break;
|
||||||
|
case "b":
|
||||||
|
if (viewMode === "settings") {
|
||||||
|
exitSettingsMode();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── Compute UI scale ───────────────────────────────────────────
|
// ── Compute UI scale ───────────────────────────────────────────
|
||||||
function updateScale() {
|
function updateScale() {
|
||||||
var MIN_SCALE = 0.5;
|
var MIN_SCALE = 0.5;
|
||||||
|
|||||||
Reference in New Issue
Block a user