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 4 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
21 changes: 18 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,19 @@ 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 send the resume command.
Garanas marked this conversation as resolved.
Show resolved Hide resolved
---@param sender string # The name of the player that resumed the game.
function SendResumedBy(sender)
import("/lua/ui/game/pause.lua").SendResumedBy(sender)

if not ResumedBy then ResumedBy = sender end
end

--- Called by the engine when the simulation
Garanas marked this conversation as resolved.
Show resolved Hide resolved
function OnResume()
import("/lua/ui/game/pause.lua").OnResume()

PauseSound("World",false)
PauseSound("Music",false)
PauseVoice("VO",false)
Expand All @@ -753,6 +764,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 +783,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 Expand Up @@ -1051,6 +1064,8 @@ end
---@param sender string # username
---@param data table
function ReceiveChat(sender, data)
LOG("ReceiveChat", sender)
reprsl(data)
if data.Identifier then

-- we highly encourage to use the 'Identifier' field to quickly identify the correct function
Expand Down
112 changes: 112 additions & 0 deletions lua/ui/game/pause.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
--******************************************************************************************************
--** 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 PauseThreshold = 10
Garanas marked this conversation as resolved.
Show resolved Hide resolved

---@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 by the engine when the simulation pauses for all clients.
Garanas marked this conversation as resolved.
Show resolved Hide resolved
---@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 ?

LOG("SessionResume")

-- scenario's that are all good
Garanas marked this conversation as resolved.
Show resolved Hide resolved
if SessionIsReplay() or
not SessionIsMultiplayer()
then
oldSessionResume()
return 'Accepted'
end

-- local client initiated the pause, we're all good
Garanas marked this conversation as resolved.
Show resolved Hide resolved
local localClientIndex, clientData = FindLocalClient()
if OnPauseClientIndex == localClientIndex then
LOG("Same client unpauses")
oldSessionResume()
return 'Accepted'
end

-- we waited long enough, we're good
Garanas marked this conversation as resolved.
Show resolved Hide resolved
local timeDifference = GetSystemTimeSeconds() - OnPauseTimestamp
if timeDifference > PauseThreshold then
LOG("Threshold is met to resume")
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', PauseThreshold - timeDifference),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should have LOC tags for text. The time should also be rounded up so you can't tell the player to wait 0 seconds.

Suggested change
text = string.format('Wants to resume the game but has to wait %d seconds', PauseThreshold - timeDifference),
text = LOCF('<LOC pause_0003>Wants to resume the game but has to wait %d seconds', math.ceil(PauseThreshold - timeDifference)),

We already have:

pause_0000="By %s"
pause_0001="Game Resumed"
pause_0002="Game Paused"

Copy link
Member Author

Choose a reason for hiding this comment

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

Chat messages (in the current setup) are not localized. This suggestion would fall in the category of properly supporting system messages, which is what @4z0t is suggesting below too.

I think it is outside of the scope of this pull request. If we would like to support system messages we first would need to do some refactoring in my point of view.

Chat = true,
})

return 'Declined'
end
end
14 changes: 10 additions & 4 deletions lua/ui/game/tabs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -506,16 +506,22 @@ 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
SessionSendChatMessage({SendResumedBy=true})
lL1l1 marked this conversation as resolved.
Show resolved Hide resolved
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