Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a delay to the pause feature #6643

Merged
merged 18 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/snippets/features.6643.md
Original file line number Diff line number Diff line change
@@ -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.
Garanas marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 5 additions & 4 deletions engine/User.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 14 additions & 3 deletions lua/ui/game/gamemain.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
100 changes: 100 additions & 0 deletions lua/ui/game/pause.lua
Original file line number Diff line number Diff line change
@@ -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
Garanas marked this conversation as resolved.
Show resolved Hide resolved
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
Comment on lines +65 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this hook is necessary. Shouldn't the nonsensical case be handled by the engine already?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this hook is necessary. Shouldn't the nonsensical case be handled by the engine already?

I'll check this evening.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am not mistaken then the message would be sent and it would override the person who initiated the pause.


local oldSessionResume = _G.SessionResume
---@return 'Accepted' | 'Declined'
_G.SessionResume = function()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hooks won't take effect until the file is imported. The re-assignment will also be re-run every time the file is imported. So I think it's better to just have a function here like function SessionResumeHook() and hook the global from UserInit.lua _G.SessionResume = import("/lua/ui/game/pause.lua").SessionResumeHook.
We already do this for a lot of global user functions except the entire function is written in user init in a do end block to protect the scope (which I believe can be bypassed by debugging functions but I digress).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports are cached, it won't run every time. But it will re-run if you change the file and have /EnableDiskWatch as a program argument.

I can move it out if we feel uncomfortable with it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I forgot about caching.
I still think user init is a better place to change the function because then there would never be a question of when the function is changed.

Copy link
Member Author

@Garanas Garanas Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmhh, I understand. If we do that then it becomes trivial to write a UI mod to remove the delay.

I'm fine either way, the moment someone does that and is reported then that's more data for the moderators to understand the intentions of the user in question.

What are your thoughts @lL1l1 ?

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
13 changes: 9 additions & 4 deletions lua/ui/game/tabs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading