From 339115fd71d351a2be7f49f0378b0485e8c7334b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ptaszek?= Date: Mon, 14 Sep 2026 11:56:26 +0200 Subject: [PATCH 1/4] Lua: AllowQuit callin, so a game can hold a window close request (#3359) * Lua: AllowQuit callin, so a game can hold a window close request Closing the window (close button, Alt+F4) set globalQuit directly, with no way for a game to ask about unsaved work first. Add an unsynced control callin AllowQuit, asked on SDL_WINDOWEVENT_CLOSE and SDL_QUIT. Any handle returning false keeps the game running (logged); handles without the callin allow, so nothing changes for games that don't use it. Not asked while still loading, since events are pumped from the load thread there. Spring.Quit and /quitforce are unchanged and never ask. * Add changelog entry --- doc/pr-changelogs/3359.md | 1 + rts/Lua/LuaHandle.cpp | 26 ++++++++++++++++++++++++++ rts/Lua/LuaHandle.h | 1 + rts/System/EventClient.cpp | 1 + rts/System/EventClient.h | 1 + rts/System/EventHandler.cpp | 6 ++++++ rts/System/EventHandler.h | 1 + rts/System/Events.def | 1 + rts/System/SpringApp.cpp | 24 ++++++++++++++++++++++-- 9 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 doc/pr-changelogs/3359.md 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/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/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/SpringApp.cpp b/rts/System/SpringApp.cpp index fd0a928910b..7f20c1697e3 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,7 +1247,7 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) } break; case SDL_WINDOWEVENT_CLOSE: { - gu->globalQuit = true; + RequestQuit(); } break; }; } break; @@ -1240,7 +1260,7 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) sound->DeviceChanged(event.adevice.which); } break; case SDL_QUIT: { - gu->globalQuit = true; + RequestQuit(); } break; case SDL_TEXTEDITING: { if (activeController != nullptr) From 0abbf7d7317e99f4b2afcc76454f1b5aa09998c0 Mon Sep 17 00:00:00 2001 From: Vandomas Date: Mon, 14 Sep 2026 13:44:41 +0300 Subject: [PATCH 2/4] Fix the audio output being closed when a device is added (#3340) --- rts/System/Sound/ISound.h | 2 +- rts/System/Sound/Null/NullSound.h | 2 +- rts/System/Sound/OpenAL/Sound.cpp | 8 ++++++-- rts/System/Sound/OpenAL/Sound.h | 2 +- rts/System/SpringApp.cpp | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) 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 7f20c1697e3..5c9f5abc6c8 100644 --- a/rts/System/SpringApp.cpp +++ b/rts/System/SpringApp.cpp @@ -1253,11 +1253,11 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) } 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: { RequestQuit(); From ab6d35ee664e8651fe045fcb11535f23e3f688dd Mon Sep 17 00:00:00 2001 From: Vandomas Date: Mon, 14 Sep 2026 13:52:33 +0300 Subject: [PATCH 3/4] Clear the Lua shader error log for each gl.CreateShader (#3339) --- rts/Lua/LuaShaders.cpp | 2 ++ 1 file changed, 2 insertions(+) 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); From e117c05a78222bf26b4984128c50535af38a28b0 Mon Sep 17 00:00:00 2001 From: Floris Date: Mon, 14 Sep 2026 13:55:58 +0200 Subject: [PATCH 4/4] after camera panning via middlemouse toggling: position cursor in the center of the screen (#3186) --- rts/Game/UI/MouseHandler.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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(); + } }