diff --git a/changelog/snippets/features.6643.md b/changelog/snippets/features.6643.md new file mode 100644 index 0000000000..034ecc8276 --- /dev/null +++ b/changelog/snippets/features.6643.md @@ -0,0 +1,3 @@ +- (#6643) Introduce a small enforced delay before a paused session can resume + +The delay applies to all players but the player that initiated the pause. The delay is 10 seconds. The delay is not configurable. diff --git a/engine/User.lua b/engine/User.lua index 400b0869b1..262ef69e07 100644 --- a/engine/User.lua +++ b/engine/User.lua @@ -1096,10 +1096,11 @@ end --- Resume the world simulation function SessionResume() end - ---- ----@param client? number | number[] client or clients ----@param message table | number | string + +--- Sends a message to one, more or all other connected clients. This message is sent separately from the simulation. But it is sent in order. +---@overload fun(message: table | number | string) +---@param client? number | number[] # client or clients +---@param message table | number | string # function SessionSendChatMessage(client, message) end diff --git a/lua/ui/game/gamemain.lua b/lua/ui/game/gamemain.lua index ce2b0a6505..1f99cc9b57 100644 --- a/lua/ui/game/gamemain.lua +++ b/lua/ui/game/gamemain.lua @@ -725,9 +725,12 @@ function OnQueueChanged(newQueue) end end --- Called after the Sim has confirmed the game is indeed paused. This will happen --- on everyone's machine in a network game. +--- Called by the engine after the sim confirmed that the game is indeed paused. This is run on all instances that are connected to the lobby. +---@param pausedBy integer # The index of the client in the clients list (that you get via `GetSessionClients`) +---@param timeoutsRemaining number function OnPause(pausedBy, timeoutsRemaining) + import("/lua/ui/game/pause.lua").OnPause(pausedBy, timeoutsRemaining) + PauseSound("World",true) PauseSound("Music",true) PauseVoice("VO",true) @@ -737,11 +740,17 @@ end -- Called after the Sim has confirmed that the game has resumed. local ResumedBy = nil + +--- Transmitted via a Chat command by another user to inform Lua who sent the resume command. +---@param sender string # The name of the player that resumed the game. function SendResumedBy(sender) if not ResumedBy then ResumedBy = sender end end +--- Called by the engine when the simulation is resumed function OnResume() + import("/lua/ui/game/pause.lua").OnResume() + PauseSound("World",false) PauseSound("Music",false) PauseVoice("VO",false) @@ -753,6 +762,8 @@ end -- Called immediately when the user hits the pause button on the machine -- that initiated the pause and other network players won't call this function function OnUserPause(pause) + import("/lua/ui/game/pause.lua").OnUserPause(pause) + local Tabs = import("/lua/ui/game/tabs.lua") local focus = GetArmiesTable().focusArmy if Tabs.CanUserPause() then @@ -770,7 +781,7 @@ function OnUserPause(pause) else SessionSendChatMessage(import('/lua/ui/game/clientutils.lua').GetAll(), { to = 'all', - text = 'Unpaused the game', + text = 'Resumed the game', Chat = true, }) end diff --git a/lua/ui/game/pause.lua b/lua/ui/game/pause.lua new file mode 100644 index 0000000000..edab05d223 --- /dev/null +++ b/lua/ui/game/pause.lua @@ -0,0 +1,100 @@ +--****************************************************************************************************** +--** Copyright (c) 2025 Willem 'Jip' Wijnia +--** +--** Permission is hereby granted, free of charge, to any person obtaining a copy +--** of this software and associated documentation files (the "Software"), to deal +--** in the Software without restriction, including without limitation the rights +--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--** copies of the Software, and to permit persons to whom the Software is +--** furnished to do so, subject to the following conditions: +--** +--** The above copyright notice and this permission notice shall be included in all +--** copies or substantial portions of the Software. +--** +--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +--** SOFTWARE. +--****************************************************************************************************** + +---@type integer +local OnPauseClientIndex = -1 + +---@type number +local OnPauseTimestamp = 0 + +---@type number +local ResumeThreshold = 10 -- seconds + +---@return integer # The index of the client, like the parameter `pausedBy` of OnPause +---@return Client? # The data of the client +local function FindLocalClient() + local allClients = GetSessionClients() + for k = 1, table.getn(allClients) do + local client = allClients[k] + if client["local"] then + return k, client + end + end + + return -1, nil +end + +--- Called from `gamemain.lua` when the simulation pauses for all clients. +---@param pausedBy integer # The index of the client in the clients list (that you get via `GetSessionClients`) +---@param timeoutsRemaining number +function OnPause(pausedBy, timeoutsRemaining) + -- keep track of who paused and when + OnPauseClientIndex = pausedBy + OnPauseTimestamp = GetSystemTimeSeconds() +end + +--- Called by the engine when the simulation resumed for all clients. +function OnResume() + OnPauseClientIndex = -1 + OnPauseTimestamp = 0 +end + +-- Called immediately by the engine on the machine that initiated the pause. This function is called only by the client that is initiating the (un)pause. +function OnUserPause(pause) +end + +local oldSessionRequestPause = _G.SessionRequestPause +_G.SessionRequestPause = function() + -- makes no sense to request a pause on top of a pause + if SessionIsPaused() then + return + end + + oldSessionRequestPause() +end + +local oldSessionResume = _G.SessionResume +---@return 'Accepted' | 'Declined' +_G.SessionResume = function() + local localClientIndex, clientData = FindLocalClient() + local timeDifference = GetSystemTimeSeconds() - OnPauseTimestamp + + -- conditions that allow an immediate resume of the session + if SessionIsReplay() or + not SessionIsMultiplayer() or + OnPauseClientIndex == localClientIndex or -- feature: the person who initiated the pause can resume at any time + timeDifference > ResumeThreshold -- feature: any person can resume after the pause lasted past the threshold + then + SessionSendChatMessage({ SendResumedBy = true }) + oldSessionResume() + return 'Accepted' + else + -- inform other clients + SessionSendChatMessage(import('/lua/ui/game/clientutils.lua').GetAll(), { + to = 'all', + text = string.format('Wants to resume the game but has to wait %d seconds', ResumeThreshold - timeDifference), + Chat = true, + }) + + return 'Declined' + end +end diff --git a/lua/ui/game/tabs.lua b/lua/ui/game/tabs.lua index 8656f8f6c1..d27e2ead85 100644 --- a/lua/ui/game/tabs.lua +++ b/lua/ui/game/tabs.lua @@ -506,16 +506,21 @@ function CommonLogic() end end tab.OnCheck = function(self, checked) - if checked then + if not SessionIsPaused() then if not CanUserPause() then return end + SessionRequestPause() self:SetGlowState(checked) else - SessionSendChatMessage({SendResumedBy=true}) - SessionResume() - self:SetGlowState(checked) + local status = SessionResume() + if status == 'Accepted' then + self:SetGlowState(checked) + else + -- reset the check since the resume was declined + self:SetCheck(not checked, true) + end end end tab.OnClick = function(self, modifiers)