diff --git a/doc/pr-changelogs/3359.md b/doc/pr-changelogs/3359.md new file mode 100644 index 00000000000..5e9c5b2c4ac --- /dev/null +++ b/doc/pr-changelogs/3359.md @@ -0,0 +1 @@ +* added the unsynced `AllowQuit` callin; returning false keeps the game running when the window close button or Alt+F4 is used (`Spring.Quit` and `/quitforce` still quit unconditionally) diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 7c14c9ed582..c2af62eabde 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -883,15 +883,21 @@ void CMouseHandler::HideMouse() void CMouseHandler::ToggleMiddleClickScroll() { RECOIL_DETAILED_TRACY_ZONE; - if (locked) { - ShowMouse(); - } else { - HideMouse(); - } - locked = !locked; mmbScroll = !mmbScroll; ignoreMove = mmbScroll; + + if (locked) { + HideMouse(); + } else { + ShowMouse(); + + const int2 cursorPos = GetViewMouseCenter(); + lastx = cursorPos.x; + lasty = cursorPos.y; + mouseInput->SetWarpPos(cursorPos); + UpdateCursorCameraDir(); + } } diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index ea2ee64ec82..5e887903ba8 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -3824,6 +3824,32 @@ bool CLuaHandle::CommandNotify(const Command& cmd) } +/*** Called when the window is about to be closed (close button, Alt+F4, or a quit request from the OS). + * + * Every unsynced handle is asked and one `false` is enough to keep the game running. `Spring.Quit` and `/quitforce` do not ask, so a game that vetoed can still quit later and the user can always force an exit. One click can trigger this twice, once per SDL event. Not asked while the game is still loading, so LuaIntro never receives it. + * + * @function Callins:AllowQuit + * @return boolean allow false keeps the game running + */ +bool CLuaHandle::AllowQuit() +{ + RECOIL_DETAILED_TRACY_ZONE; + LUA_CALL_IN_CHECK(L, true); + luaL_checkstack(L, 2, __func__); + static const LuaHashString cmdStr(__func__); + + if (!cmdStr.GetGlobalFunc(L)) + return true; + + if (!RunCallIn(L, cmdStr, 0, 1)) + return true; + + const bool retval = luaL_optboolean(L, -1, true); + lua_pop(L, 1); + return retval; +} + + /*** Called when text is entered into the console (e.g. `Spring.Echo`). * * @function Callins:AddConsoleLine diff --git a/rts/Lua/LuaHandle.h b/rts/Lua/LuaHandle.h index caaa5258dfb..e016400b469 100644 --- a/rts/Lua/LuaHandle.h +++ b/rts/Lua/LuaHandle.h @@ -226,6 +226,7 @@ class CLuaHandle : public CEventClient void MiniMapGeometryChanged(const int2 newPos, const int2 newDim, const int2 oldPos, const int2 oldDim) override; bool CommandNotify(const Command& cmd) override; + bool AllowQuit() override; bool AddConsoleLine(const std::string& msg, const std::string& section, int level) override; diff --git a/rts/Lua/LuaShaders.cpp b/rts/Lua/LuaShaders.cpp index acaeda87712..e7fdcdf58b4 100644 --- a/rts/Lua/LuaShaders.cpp +++ b/rts/Lua/LuaShaders.cpp @@ -671,6 +671,8 @@ int LuaShaders::CreateShader(lua_State* L) if (!graphicSrcEmpty && !computeSrcEmpty) return 0; + CLuaHandle::GetActiveShaders(L).errorLog.clear(); + bool success; const GLuint vertObj = CompileObject(L, shdrDefs, vertSrcs, GL_VERTEX_SHADER, success); diff --git a/rts/System/EventClient.cpp b/rts/System/EventClient.cpp index 849ad9d0fce..6ab7a3c5827 100644 --- a/rts/System/EventClient.cpp +++ b/rts/System/EventClient.cpp @@ -88,6 +88,7 @@ void CEventClient::MiniMapRotationChanged(const float newRot, const float oldRot void CEventClient::MiniMapStateChanged(const bool isMinimized, const bool isMaximized, const bool isSlaved) {} void CEventClient::MiniMapGeometryChanged(const int2 newPos, const int2 newDim, const int2 oldPos, const int2 oldDim) {} bool CEventClient::CommandNotify(const Command& cmd) { return false; } +bool CEventClient::AllowQuit() { return true; } bool CEventClient::AddConsoleLine(const std::string& msg, const std::string& section, int level) { return false; } diff --git a/rts/System/EventClient.h b/rts/System/EventClient.h index 36061d76252..122ca951235 100644 --- a/rts/System/EventClient.h +++ b/rts/System/EventClient.h @@ -318,6 +318,7 @@ class CEventClient virtual void MiniMapStateChanged(const bool isMinimized, const bool isMaximized, const bool isSlaved); virtual void MiniMapGeometryChanged(const int2 newPos, const int2 newDim, const int2 oldPos, const int2 oldDim); virtual bool CommandNotify(const Command& cmd); + virtual bool AllowQuit(); virtual bool AddConsoleLine(const std::string& msg, const std::string& section, int level); diff --git a/rts/System/EventHandler.cpp b/rts/System/EventHandler.cpp index 7f8e96eb4ac..d58a650e366 100644 --- a/rts/System/EventHandler.cpp +++ b/rts/System/EventHandler.cpp @@ -817,6 +817,12 @@ bool CEventHandler::CommandNotify(const Command& cmd) return ControlReverseIterateDefTrue(listCommandNotify, &CEventClient::CommandNotify, cmd); } +bool CEventHandler::AllowQuit() +{ + ZoneScoped; + return ControlIterateDefTrue(listAllowQuit, &CEventClient::AllowQuit); +} + void CEventHandler::KeyBindingsChanged() { ZoneScoped; diff --git a/rts/System/EventHandler.h b/rts/System/EventHandler.h index 1525855ef1b..8f90360009f 100644 --- a/rts/System/EventHandler.h +++ b/rts/System/EventHandler.h @@ -250,6 +250,7 @@ class CEventHandler void MiniMapStateChanged(const bool isMinimized, const bool isMaximized, const bool isSlaved); void MiniMapGeometryChanged(const int2 newPos, const int2 newDim, const int2 oldPos, const int2 oldDim); bool CommandNotify(const Command& cmd); + bool AllowQuit(); bool AddConsoleLine(const std::string& msg, const std::string& section, int level); diff --git a/rts/System/Events.def b/rts/System/Events.def index 5add5b1700f..ee93b4585d8 100644 --- a/rts/System/Events.def +++ b/rts/System/Events.def @@ -130,6 +130,7 @@ SETUP_EVENT(MiniMapStateChanged, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(MiniMapGeometryChanged, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(CommandNotify, MANAGED_BIT | UNSYNCED_BIT | CONTROL_BIT) + SETUP_EVENT(AllowQuit, MANAGED_BIT | UNSYNCED_BIT | CONTROL_BIT) SETUP_EVENT(AddConsoleLine, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(GroupChanged, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(GameSetup, MANAGED_BIT | UNSYNCED_BIT | CONTROL_BIT) diff --git a/rts/System/Sound/ISound.h b/rts/System/Sound/ISound.h index 6638a4469f1..4dfa2cb08d0 100644 --- a/rts/System/Sound/ISound.h +++ b/rts/System/Sound/ISound.h @@ -55,7 +55,7 @@ class ISound { virtual bool Mute() = 0; virtual bool IsMuted() const = 0; - virtual void DeviceChanged(uint32_t sdlDeviceIndex) = 0; + virtual void DeviceChanged(uint32_t sdlDeviceIndex, bool added) = 0; ///change current output device static bool ChangeOutput(bool forceNullSound = false); diff --git a/rts/System/Sound/Null/NullSound.h b/rts/System/Sound/Null/NullSound.h index 86bafe416fc..26dd2219a94 100644 --- a/rts/System/Sound/Null/NullSound.h +++ b/rts/System/Sound/Null/NullSound.h @@ -35,7 +35,7 @@ class NullSound : public ISound bool Mute() override { return true; } bool IsMuted() const override { return true; } - void DeviceChanged(uint32_t sdlDeviceIndex) override {} + void DeviceChanged(uint32_t sdlDeviceIndex, bool added) override {} void Iconified(bool state) override {} diff --git a/rts/System/Sound/OpenAL/Sound.cpp b/rts/System/Sound/OpenAL/Sound.cpp index 832f506bfcc..a4947bae62b 100644 --- a/rts/System/Sound/OpenAL/Sound.cpp +++ b/rts/System/Sound/OpenAL/Sound.cpp @@ -345,11 +345,15 @@ bool CSound::Mute() return mute; } -void CSound::DeviceChanged(uint32_t sdlDeviceIndex) +void CSound::DeviceChanged(uint32_t sdlDeviceIndex, bool added) { // handles SDL_AUDIODEVICEREMOVED and SDL_AUDIODEVICEADDED - if (!hasAlcSoftLoopBack || sdlDeviceIndex != sdlDeviceID) + if (!hasAlcSoftLoopBack) + return; + + // sdlDeviceIndex is an instance id for REMOVED events but a device index for ADDED ones + if (added ? (sdlDeviceID != 0) : (sdlDeviceIndex != sdlDeviceID)) return; LOG("[Sound::%s] SDL failed to handle device change, reopening", __func__); diff --git a/rts/System/Sound/OpenAL/Sound.h b/rts/System/Sound/OpenAL/Sound.h index 9d02f938d1c..ac98853c326 100644 --- a/rts/System/Sound/OpenAL/Sound.h +++ b/rts/System/Sound/OpenAL/Sound.h @@ -57,7 +57,7 @@ class CSound : public ISound bool Mute() override; bool IsMuted() const override { return mute; } - void DeviceChanged(uint32_t sdlDeviceIndex) override; + void DeviceChanged(uint32_t sdlDeviceIndex, bool added) override; void Iconified(bool state) override; diff --git a/rts/System/SpringApp.cpp b/rts/System/SpringApp.cpp index fd0a928910b..5c9f5abc6c8 100644 --- a/rts/System/SpringApp.cpp +++ b/rts/System/SpringApp.cpp @@ -1072,6 +1072,26 @@ void SpringApp::Kill(bool fromRun) } +// close button, Alt+F4, SDL_QUIT; quitforce and Spring.Quit set globalQuit directly and skip this +static void RequestQuit() +{ + if (gu->globalQuit) + return; + + // don't call into Lua while loading, events are pumped from the load thread and handles are still registering + if (game != nullptr && !game->IsDoneLoading()) { + gu->globalQuit = true; + return; + } + + if (!eventHandler.AllowQuit()) { + LOG("[SpringApp::%s] quit vetoed by Lua (AllowQuit), use /quitforce to override", __func__); + return; + } + + gu->globalQuit = true; +} + bool SpringApp::MainEventHandler(const SDL_Event& event) { switch (event.type) { @@ -1227,20 +1247,20 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) } break; case SDL_WINDOWEVENT_CLOSE: { - gu->globalQuit = true; + RequestQuit(); } break; }; } break; case SDL_AUDIODEVICEREMOVED: { LOG("[SpringApp::%s][SDL_AUDIODEVICEREMOVED][1] type=%u, which=%u, iscapture=%u", __func__, event.adevice.type, event.adevice.which, static_cast(event.adevice.iscapture)); - sound->DeviceChanged(event.adevice.which); + sound->DeviceChanged(event.adevice.which, false); } break; case SDL_AUDIODEVICEADDED: { LOG("[SpringApp::%s][SDL_AUDIODEVICEADDED][1] type=%u, which=%u, iscapture=%u", __func__, event.adevice.type, event.adevice.which, static_cast(event.adevice.iscapture)); - sound->DeviceChanged(event.adevice.which); + sound->DeviceChanged(event.adevice.which, true); } break; case SDL_QUIT: { - gu->globalQuit = true; + RequestQuit(); } break; case SDL_TEXTEDITING: { if (activeController != nullptr)