diff --git a/src/client/core/app.cpp b/src/client/core/app.cpp index 5c738e04..84873578 100644 --- a/src/client/core/app.cpp +++ b/src/client/core/app.cpp @@ -409,7 +409,7 @@ void App::OnPacketReceived(const NetworkPacket& packet) QueueOrCreateWorld(id, url, textureName, width, height); } - else if (event.name == CefEvent::Server::CreateWorld2DBrowser && event.args.size() >= 6) { + else if (event.name == CefEvent::Server::CreateWorld2DBrowser && event.args.size() >= 10) { int id = event.args[0].intValue; const std::string& url = event.args[1].stringValue; float worldX = event.args[2].floatValue; @@ -541,6 +541,12 @@ void App::OnPacketReceived(const NetworkPacket& packet) { browser_.ExitGame(); } + else if (event.name == CefEvent::Server::SetEscapeMenuMode && event.args.size() >= 1) + { + int mode = event.args[0].intValue; + + browser_.SetEscapeMenuMode(static_cast(mode)); + } break; } case PacketType::EmitBrowserEvent: diff --git a/src/client/core/browser/escape_menu.cpp b/src/client/core/browser/escape_menu.cpp new file mode 100644 index 00000000..5c121d9e --- /dev/null +++ b/src/client/core/browser/escape_menu.cpp @@ -0,0 +1,213 @@ +#include "escape_menu.hpp" + +#include +#include +#include + +namespace +{ + bool IsPadStartPressed(CPad* pad) + { + if (!pad) + return false; + + return pad->NewState.Start != 0 + || pad->OldState.Start != 0 + || pad->PCTempKeyState.Start != 0 + || pad->PCTempJoyState.Start != 0 + || pad->PCTempMouseState.Start != 0; + } + + void ClearPadStartState(CPad* pad) + { + if (!pad) + return; + + pad->NewState.Start = 0; + pad->OldState.Start = 0; + pad->PCTempKeyState.Start = 0; + pad->PCTempJoyState.Start = 0; + pad->PCTempMouseState.Start = 0; + } +} + +const char* EscapeMenuController::GetModeName(EscapeMenuMode mode) +{ + switch (mode) + { + case EscapeMenuMode::Gta: + return "gta"; + case EscapeMenuMode::Disabled: + return "disabled"; + case EscapeMenuMode::Custom: + return "custom"; + } + + return "unknown"; +} + +EscapeMenuMode EscapeMenuController::NormalizeMode(EscapeMenuMode mode) +{ + switch (mode) + { + case EscapeMenuMode::Gta: + case EscapeMenuMode::Disabled: + case EscapeMenuMode::Custom: + return mode; + } + + return EscapeMenuMode::Gta; +} + +bool EscapeMenuController::ShouldSuppressNativeMenu(EscapeMenuMode mode) +{ + return mode == EscapeMenuMode::Disabled || mode == EscapeMenuMode::Custom; +} + +EscapeMenuMode EscapeMenuController::SetMode(EscapeMenuMode mode) +{ + mode = NormalizeMode(mode); + + const EscapeMenuMode previous = mode_.exchange(mode, std::memory_order_acq_rel); + + if (mode != EscapeMenuMode::Custom) + SetCustomMenuOpen(false); + + if (mode == EscapeMenuMode::Gta) + { + input_down_.store(false, std::memory_order_release); + } + else + { + SuppressNativeMenu(); + } + + return previous; +} + +EscapeMenuMode EscapeMenuController::GetMode() const +{ + return mode_.load(std::memory_order_acquire); +} + +bool EscapeMenuController::IsNativePauseMenuVisible() const +{ + return FrontEndMenuManager.m_bMenuActive && !FrontEndMenuManager.m_bDontDrawFrontEnd; +} + +bool EscapeMenuController::UpdateInput() +{ + const EscapeMenuMode mode = GetMode(); + if (!ShouldSuppressNativeMenu(mode)) + { + input_down_.store(false, std::memory_order_release); + return false; + } + + const bool escape_down = (::GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0; + const bool start_down = IsPadStartPressed(CPad::GetPad(0)) || IsPadStartPressed(CPad::GetPad(1)); + + return ProcessInputState(escape_down || start_down); +} + +bool EscapeMenuController::ConsumeWndProcMessage(UINT message, WPARAM w_param, LPARAM l_param) +{ + if (!ShouldSuppressNativeMenu(GetMode())) + return false; + + if ((message == WM_KEYDOWN || message == WM_KEYUP || message == WM_SYSKEYDOWN || message == WM_SYSKEYUP) + && ((static_cast(w_param) & 0xFF) == VK_ESCAPE)) + { + const bool down = (message == WM_KEYDOWN || message == WM_SYSKEYDOWN); + const bool repeat = down ? (((static_cast(l_param) >> 30) & 1u) != 0u) : false; + + if (!(down && repeat)) + { + ProcessInputState(down); + } + else + { + SuppressNativeMenu(); + } + + return true; + } + + if (message == WM_CHAR && w_param == VK_ESCAPE) + { + SuppressNativeMenu(); + return true; + } + + return false; +} + +void EscapeMenuController::SuppressNativeMenu() +{ + if (!ShouldSuppressNativeMenu(GetMode())) + return; + + ClearPadStartState(CPad::GetPad(0)); + ClearPadStartState(CPad::GetPad(1)); + + FrontEndMenuManager.m_bActivateMenuNextFrame = false; + + if (FrontEndMenuManager.m_bMenuActive) + { + FrontEndMenuManager.m_bMenuActive = false; + FrontEndMenuManager.m_bActivateMenuNextFrame = false; + CTimer::EndUserPause(); + + if (auto* pad = CPad::GetPad(0)) + { + pad->JustOutOfFrontEnd = 1; + } + } +} + +bool EscapeMenuController::IsCustomMenuOpen() const +{ + return custom_menu_open_.load(std::memory_order_acquire); +} + +bool EscapeMenuController::HasPendingCustomMenuVisibilityChange() +{ + return custom_menu_visibility_changed_.exchange(false, std::memory_order_acq_rel); +} + +bool EscapeMenuController::ProcessInputState(bool is_down) +{ + const EscapeMenuMode mode = GetMode(); + + if (mode == EscapeMenuMode::Gta) + { + input_down_.store(is_down, std::memory_order_release); + return false; + } + + const bool was_down = input_down_.exchange(is_down, std::memory_order_acq_rel); + const bool just_pressed = is_down && !was_down; + + SuppressNativeMenu(); + + if (mode == EscapeMenuMode::Custom && just_pressed) + { + ToggleCustomMenu(); + } + + return true; +} + +void EscapeMenuController::SetCustomMenuOpen(bool open) +{ + const bool previous = custom_menu_open_.exchange(open, std::memory_order_acq_rel); + if (previous != open) + { + custom_menu_visibility_changed_.store(true, std::memory_order_release); + } +} + +void EscapeMenuController::ToggleCustomMenu() +{ + SetCustomMenuOpen(!IsCustomMenuOpen()); +} diff --git a/src/client/core/browser/escape_menu.hpp b/src/client/core/browser/escape_menu.hpp new file mode 100644 index 00000000..12aafac0 --- /dev/null +++ b/src/client/core/browser/escape_menu.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include + +// Defines how the native GTA SA ESC/pause menu is handled +enum class EscapeMenuMode : std::uint8_t +{ + Gta = 0, + Disabled = 1, + Custom = 2 +}; + +class EscapeMenuController +{ +public: + static const char* GetModeName(EscapeMenuMode mode); + static EscapeMenuMode NormalizeMode(EscapeMenuMode mode); + static bool ShouldSuppressNativeMenu(EscapeMenuMode mode); + + EscapeMenuMode SetMode(EscapeMenuMode mode); + EscapeMenuMode GetMode() const; + + bool IsNativePauseMenuVisible() const; + bool UpdateInput(); + bool ConsumeWndProcMessage(UINT message, WPARAM w_param, LPARAM l_param); + void SuppressNativeMenu(); + + bool IsCustomMenuOpen() const; + bool HasPendingCustomMenuVisibilityChange(); + +private: + bool ProcessInputState(bool is_down); + void SetCustomMenuOpen(bool open); + void ToggleCustomMenu(); + + std::atomic mode_{ EscapeMenuMode::Gta }; + std::atomic input_down_{ false }; + std::atomic custom_menu_open_{ false }; + std::atomic custom_menu_visibility_changed_{ false }; +}; diff --git a/src/client/core/browser/manager.cpp b/src/client/core/browser/manager.cpp index e17b41da..7c394044 100644 --- a/src/client/core/browser/manager.cpp +++ b/src/client/core/browser/manager.cpp @@ -996,7 +996,7 @@ void BrowserManager::OnPaint(int id, const void* buffer, int width, int height) bool BrowserManager::RenderAll() { - if (!draw_enabled_) + if (ShouldSkipBrowserRendering()) return false; UpdateAudioSpatialization(); @@ -1099,6 +1099,9 @@ bool BrowserManager::RenderAll() void BrowserManager::OnBeforeEntityRender(CEntity* entity) { + if (ShouldSkipBrowserRendering()) + return; + auto it = entityToBrowserId_.find(entity); if (it == entityToBrowserId_.end()) return; @@ -1131,6 +1134,9 @@ BrowserInstance* BrowserManager::GetBrowserInstance(int id) bool BrowserManager::IsAnyBrowserVisible() const { + if (ShouldSkipBrowserRendering()) + return false; + for (const auto& [id, browser] : browsers_) { if (browser && browser->visible) @@ -1286,6 +1292,62 @@ void BrowserManager::ExitGame() ExitProcess(0); } +void BrowserManager::SetEscapeMenuMode(EscapeMenuMode mode) +{ + const EscapeMenuMode previous = escape_menu_.SetMode(mode); + const EscapeMenuMode current = escape_menu_.GetMode(); + + DispatchEscapeMenuEvents(); + + if (previous != current) + { + LOG_INFO("[CEF] Escape menu mode changed: {} -> {}", + EscapeMenuController::GetModeName(previous), + EscapeMenuController::GetModeName(current)); + } +} + +bool BrowserManager::ShouldSkipBrowserRendering() const +{ + return !draw_enabled_ || escape_menu_.IsNativePauseMenuVisible(); +} + +void BrowserManager::DispatchEscapeMenuEvents() +{ + if (escape_menu_.HasPendingCustomMenuVisibilityChange()) + { + EmitCustomEscapeMenuVisibility(); + } +} + +void BrowserManager::EmitCustomEscapeMenuVisibility() +{ + if (!CefCurrentlyOn(TID_UI)) + { + CefPostTask(TID_UI, base::BindOnce(&BrowserManager::EmitCustomEscapeMenuVisibility, base::Unretained(this))); + return; + } + + const bool visible = escape_menu_.IsCustomMenuOpen(); + + for (const auto& [id, instance] : browsers_) + { + if (!instance || !instance->browser || !instance->browser->IsValid()) + continue; + + CefRefPtr frame = instance->browser->GetMainFrame(); + if (!frame || !frame->IsValid()) + continue; + + CefRefPtr msg = CefProcessMessage::Create("emit_event"); + CefRefPtr list = msg->GetArgumentList(); + list->SetString(0, "cef:escape_menu"); + list->SetBool(1, visible); + + frame->SendProcessMessage(PID_RENDERER, msg); + } +} + void BrowserManager::OnDeviceLost() { // Stop CEF rendering during device reset @@ -1341,6 +1403,12 @@ void BrowserManager::OnDeviceReset(IDirect3DDevice9* device) LRESULT BrowserManager::OnWndProcMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (escape_menu_.ConsumeWndProcMessage(msg, wParam, lParam)) + { + DispatchEscapeMenuEvents(); + return true; + } + if (msg == WM_KEYDOWN || msg == WM_KEYUP || msg == WM_SYSKEYDOWN || msg == WM_SYSKEYUP) { if (key_capture_enabled_ && network_.GetState() == ConnectionState::CONNECTED && !network_.IsNonCefServer()) diff --git a/src/client/core/browser/manager.hpp b/src/client/core/browser/manager.hpp index a29bac38..c65898d9 100644 --- a/src/client/core/browser/manager.hpp +++ b/src/client/core/browser/manager.hpp @@ -7,6 +7,7 @@ #include #include "client.hpp" +#include "escape_menu.hpp" #include "player_stats.hpp" #include "include/base/cef_callback.h" #include "include/cef_app.h" @@ -148,6 +149,9 @@ class BrowserManager void ExitGame(); + // Native GTA SA ESC/pause menu handling + void SetEscapeMenuMode(EscapeMenuMode mode); + // Callbacks from BrowserClient void OnBrowserCreated(int id, CefRefPtr browser); void OnBrowserClosed(int id); @@ -187,6 +191,10 @@ class BrowserManager void CreateWorldBrowserInternal(int id, const std::string& url, std::string textureName, float width, float height); void CreateWorld2DBrowserInternal(int id, const std::string& url, float worldX, float worldY, float worldZ, float width, float height, float offsetZ, float pivotX, float pivotY); + bool ShouldSkipBrowserRendering() const; + void DispatchEscapeMenuEvents(); + void EmitCustomEscapeMenuVisibility(); + CEntity* GetEntityFromObjectId(int objectId); private: @@ -220,4 +228,7 @@ class BrowserManager std::bitset<256> key_allowed_{}; std::unordered_map player_stats_poll_; + + // Native GTA SA ESC/pause menu handling + EscapeMenuController escape_menu_; }; \ No newline at end of file diff --git a/src/server/cef.inc b/src/server/cef.inc index 58bbf35f..1a04c990 100644 --- a/src/server/cef.inc +++ b/src/server/cef.inc @@ -100,6 +100,21 @@ enum E_HUD_COMPONENT HUD_COMPONENT_WEAPON }; +/** + * Defines how GTA San Andreas' native ESC/pause menu is handled by the client. + */ +enum E_CEF_ESCAPE_MENU_MODE +{ + // Default GTA behavior: ESC opens the native GTA pause menu. + CEF_ESCAPE_MENU_GTA = 0, + + // ESC input is consumed and the native GTA pause menu is closed if it appears. + CEF_ESCAPE_MENU_DISABLED, + + // ESC input is consumed and every browser receives cef:escape_menu(bool:visible). + CEF_ESCAPE_MENU_CUSTOM +}; + /** * Checks if a player has successfully completed the CEF "handshake". * @@ -411,6 +426,17 @@ native CEF_SetKeyCapture(playerid, bool:enabled); */ native CEF_ExitGame(playerid); +/** + * Sets how GTA San Andreas' native ESC/pause menu should be handled. + * + * In CUSTOM mode, JavaScript can listen with: + * cef.on("cef:escape_menu", (visible) => { ... }); + * + * @param playerid The ID of the player. + * @param mode One of E_CEF_ESCAPE_MENU_MODE. + */ +native CEF_SetEscapeMenuMode(playerid, E_CEF_ESCAPE_MENU_MODE:mode); + /** * CALLBACKS */ diff --git a/src/server/common/api.cpp b/src/server/common/api.cpp index badcb1db..61515eb9 100644 --- a/src/server/common/api.cpp +++ b/src/server/common/api.cpp @@ -309,4 +309,21 @@ void CefApi::ExitGame(int playerid) event.name = CefEvent::Server::ExitGame; plugin_.SendPacketToPlayer(playerid, PacketType::EmitEvent, event); +} + +void CefApi::SetEscapeMenuMode(int playerid, int mode) +{ + if (mode < 0 || mode > 2) + { + LOG_ERROR("SetEscapeMenuMode: invalid mode %d for playerid=%d", mode, playerid); + return; + } + + LOG_DEBUG("SetEscapeMenuMode: playerid=%d, mode=%d", playerid, mode); + + EmitEventPacket event; + event.name = CefEvent::Server::SetEscapeMenuMode; + event.args.emplace_back(mode); + + plugin_.SendPacketToPlayer(playerid, PacketType::EmitEvent, event); } \ No newline at end of file diff --git a/src/server/common/api.hpp b/src/server/common/api.hpp index f6516688..72f9302d 100644 --- a/src/server/common/api.hpp +++ b/src/server/common/api.hpp @@ -49,6 +49,8 @@ class CefApi void EnableKey(int playerid, int key, bool enabled); void ExitGame(int playerid); + + void SetEscapeMenuMode(int playerid, int mode); private: CefPlugin& plugin_; diff --git a/src/server/common/natives.cpp b/src/server/common/natives.cpp index 7b73f30b..a099ac53 100644 --- a/src/server/common/natives.cpp +++ b/src/server/common/natives.cpp @@ -133,4 +133,9 @@ PAWN_NATIVE(Natives, CEF_EnableKey, void(int playerid, int key, bool enabled)) PAWN_NATIVE(Natives, CEF_ExitGame, void(int playerid)) { CefApi::Instance()->ExitGame(playerid); +} + +PAWN_NATIVE(Natives, CEF_SetEscapeMenuMode, void(int playerid, int mode)) +{ + CefApi::Instance()->SetEscapeMenuMode(playerid, mode); } \ No newline at end of file diff --git a/src/server/omp/cef_extension_api.cpp b/src/server/omp/cef_extension_api.cpp index 9c899db0..19935f8e 100644 --- a/src/server/omp/cef_extension_api.cpp +++ b/src/server/omp/cef_extension_api.cpp @@ -232,6 +232,13 @@ class CefExtension : public ICefComponent auto& list = CefHandlerList(); list.erase(std::remove(list.begin(), list.end(), h), list.end()); } + + void setEscapeMenuMode(int playerid, int mode) override + { + auto* api = CefApi::Instance(); + if (api) + api->SetEscapeMenuMode(playerid, mode); + } }; static CefExtension g_cefExtension; diff --git a/src/server/omp/cef_extension_api.hpp b/src/server/omp/cef_extension_api.hpp index c3b131a2..1a768891 100644 --- a/src/server/omp/cef_extension_api.hpp +++ b/src/server/omp/cef_extension_api.hpp @@ -86,6 +86,9 @@ struct ICefComponent : public IExtension // Miscellaneous. virtual void exitGame(int playerid) = 0; + // Native GTA ESC/pause menu. + virtual void setEscapeMenuMode(int playerid, int mode) = 0; + // Event-handler registry. virtual void addEventHandler(ICefEventHandler* handler) = 0; virtual void removeEventHandler(ICefEventHandler* handler) = 0; diff --git a/src/shared/events.hpp b/src/shared/events.hpp index 20f62b51..943ab0b0 100644 --- a/src/shared/events.hpp +++ b/src/shared/events.hpp @@ -34,6 +34,8 @@ namespace CefEvent inline constexpr const char* EnableKey = "EnableKey"; inline constexpr const char* ExitGame = "ExitGame"; + + inline constexpr const char* SetEscapeMenuMode = "SetEscapeMenuMode"; } namespace Client diff --git a/tests/webviews/react/src/App.tsx b/tests/webviews/react/src/App.tsx index 5a7c5c25..bf4340b1 100644 --- a/tests/webviews/react/src/App.tsx +++ b/tests/webviews/react/src/App.tsx @@ -1,10 +1,20 @@ import { useCef } from '@/providers/cef-provider'; +import EscapeMenuExample from './components/escape-menu-example'; import Login from './pages/login'; function App() { const { isReady } = useCef(); - return
{isReady && }
; + return ( +
+ {isReady && ( + <> + + + + )} +
+ ); } export default App; diff --git a/tests/webviews/react/src/components/escape-menu-example.tsx b/tests/webviews/react/src/components/escape-menu-example.tsx new file mode 100644 index 00000000..a0ff5648 --- /dev/null +++ b/tests/webviews/react/src/components/escape-menu-example.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { useCef } from '@/providers/cef-provider'; + +export default function EscapeMenuExample() { + const { on, off } = useCef(); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const handleEscapeMenu = (nextVisible: boolean) => { + setVisible(Boolean(nextVisible)); + }; + + on('cef:escape_menu', handleEscapeMenu); + + return () => { + off('cef:escape_menu', handleEscapeMenu); + }; + }, [on, off]); + + if (!visible) { + return null; + } + + return ( +
+ + + Custom ESC Menu + + + +

The native GTA pause menu is blocked.

+

This overlay is displayed from the JS event:

+ cef:escape_menu(boolean) +

Press ESC again to hide this example menu.

+
+
+
+ ); +}