diff --git a/client/src/ChallengeMode.lua b/client/src/ChallengeMode.lua
index 680a3ba7..a1cc347d 100644
--- a/client/src/ChallengeMode.lua
+++ b/client/src/ChallengeMode.lua
@@ -196,8 +196,8 @@ function ChallengeMode:onMatchEnded(match)
-- so always record the result, even if it may have been an abort
local gameTime = 0
local stackEngine = match.stacks[1].engine
- if stackEngine ~= nil and stackEngine.game_stopwatch then
- gameTime = stackEngine.game_stopwatch
+ if stackEngine ~= nil and stackEngine.stopWatch then
+ gameTime = stackEngine.stopWatch
end
self:recordStageResult(winners, gameTime)
diff --git a/client/src/ChallengeModePlayer.lua b/client/src/ChallengeModePlayer.lua
index 7f2993e9..9305e528 100644
--- a/client/src/ChallengeModePlayer.lua
+++ b/client/src/ChallengeModePlayer.lua
@@ -11,7 +11,6 @@ local ChallengeModePlayerStack = require("client.src.ChallengeModePlayerStack")
---@class ChallengeModePlayer : MatchParticipant
---@field usedCharacterIds string[] array of character ids that have already been used during the life time of the player
---@field settings ChallengeModePlayerSettings
-
local ChallengeModePlayer = class(
function(self, playerNumber)
self.name = "Challenger"
diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua
index ba8c0c29..8da9f2f0 100644
--- a/client/src/ClientMatch.lua
+++ b/client/src/ClientMatch.lua
@@ -539,8 +539,8 @@ function ClientMatch:drawTimer()
-- Draw the timer for time attack
local frames = 0
local stack = self.stacks[1]
- if stack ~= nil and stack.engine.game_stopwatch ~= nil and tonumber(stack.engine.game_stopwatch) ~= nil then
- frames = stack.engine.game_stopwatch
+ if stack ~= nil and stack.engine.stopWatch ~= nil and tonumber(stack.engine.stopWatch) ~= nil then
+ frames = stack.engine.stopWatch
end
if self.engine.timeLimit then
diff --git a/client/src/MatchParticipant.lua b/client/src/MatchParticipant.lua
index a52d39ba..a388dc02 100644
--- a/client/src/MatchParticipant.lua
+++ b/client/src/MatchParticipant.lua
@@ -28,6 +28,7 @@ local ModController = require("client.src.mods.ModController")
---@field ready boolean if the participant is ready to start the game (wants to and actually is)
---@field human boolean if the participant is a human
---@field isLocal boolean if the participant is controlled by a local player
+---@field playerNumber integer the (external) id for the player within the room; used to assign server messages to the correct player when spectating
---@field stack ClientStack?
-- a match participant represents the minimum spec for a what constitutes a "player" in a battleRoom / match
diff --git a/client/src/PlayerStack.lua b/client/src/PlayerStack.lua
index 81f41022..bdc05b6c 100644
--- a/client/src/PlayerStack.lua
+++ b/client/src/PlayerStack.lua
@@ -253,7 +253,6 @@ function PlayerStack:onRollback(engine)
-- to fool Match without having to wrap everything into getters
self.clock = engine.clock
- self.game_stopwatch = engine.game_stopwatch
--prof.push("rollback copy analytics")
self.analytic:rollbackToFrame(self.clock)
diff --git a/client/src/PuzzleSet.lua b/client/src/PuzzleSet.lua
index b6a3d85e..fea69e13 100644
--- a/client/src/PuzzleSet.lua
+++ b/client/src/PuzzleSet.lua
@@ -174,8 +174,8 @@ function PuzzleSet.loadV1(setName, puzzleSetData)
for _, puzzleData in pairs(puzzleSetData) do
if type(puzzleData) == "table" and #puzzleData >= 2 and type(puzzleData[1]) == "string" and type(puzzleData[2]) == "number" then
local args = {
- puzzleType = "moves",
- startTiming = "countdown",
+ puzzleType = Puzzle.PUZZLE_TYPES.moves,
+ startTiming = Puzzle.START_TIMINGS.countdown,
moves = puzzleData[2],
stack = puzzleData[1]
}
@@ -198,7 +198,7 @@ function PuzzleSet.loadV2(puzzleSetData)
for _, puzzleData in pairs(puzzleSetData[PuzzleSet.PUZZLE_SET_PROPERTY.PUZZLES]) do
local args = {
puzzleType = puzzleData[Puzzle.PUZZLE_PROPERTY.TYPE],
- startTiming = puzzleData["Do Countdown"] and "countdown" or "immediately",
+ startTiming = puzzleData["Do Countdown"] and Puzzle.START_TIMINGS.countdown or Puzzle.START_TIMINGS.immediately,
moves = puzzleData[Puzzle.PUZZLE_PROPERTY.MOVES],
stack = puzzleData[Puzzle.PUZZLE_PROPERTY.STACK],
stopTime = puzzleData[Puzzle.PUZZLE_PROPERTY.STOP],
@@ -263,9 +263,10 @@ function PuzzleSet.loadV3(puzzleSetData)
local puzzleSet = PuzzleSet(puzzleSetName, puzzleSetDescription, {}, {})
for _, puzzleData in pairs(puzzleSetData[PuzzleSet.PUZZLE_SET_PROPERTY.PUZZLES] or {}) do
+ ---@type string
+ local puzzleTypeValue = puzzleData[Puzzle.PUZZLE_PROPERTY.TYPE]
local args = {
- puzzleType = puzzleData[Puzzle.PUZZLE_PROPERTY.TYPE],
- startTiming = puzzleData[Puzzle.PUZZLE_PROPERTY.START_TIMING],
+ puzzleType = string.lower(puzzleTypeValue),
moves = puzzleData[Puzzle.PUZZLE_PROPERTY.MOVES],
stack = puzzleData[Puzzle.PUZZLE_PROPERTY.STACK],
stopTime = puzzleData[Puzzle.PUZZLE_PROPERTY.STOP],
@@ -276,10 +277,18 @@ function PuzzleSet.loadV3(puzzleSetData)
helpDescription = puzzleData[Puzzle.PUZZLE_PROPERTY.HELP_DESCRIPTION]
}
if puzzleData[Puzzle.PUZZLE_PROPERTY.CURSOR_START_LEFT] then
- args.cursorStartLeft = {
- row = puzzleData[Puzzle.PUZZLE_PROPERTY.CURSOR_START_LEFT][Puzzle.CURSOR_PROPERTY.ROW],
- column = puzzleData[Puzzle.PUZZLE_PROPERTY.CURSOR_START_LEFT][Puzzle.CURSOR_PROPERTY.COLUMN]
- }
+ args.cursorStartLeft = {row = puzzleData[Puzzle.PUZZLE_PROPERTY.CURSOR_START_LEFT].Row, column = puzzleData[Puzzle.PUZZLE_PROPERTY.CURSOR_START_LEFT].Column}
+ end
+ if puzzleData[Puzzle.PUZZLE_PROPERTY.START_TIMING] then
+ if puzzleData[Puzzle.PUZZLE_PROPERTY.START_TIMING] == "First Swap" then
+ args.startTiming = Puzzle.START_TIMINGS.firstSwap
+ elseif puzzleData[Puzzle.PUZZLE_PROPERTY.START_TIMING] == "First Input" then
+ args.startTiming = Puzzle.START_TIMINGS.firstInput
+ elseif puzzleData[Puzzle.PUZZLE_PROPERTY.START_TIMING] == "Countdown" then
+ args.startTiming = Puzzle.START_TIMINGS.countdown
+ elseif puzzleData[Puzzle.PUZZLE_PROPERTY.START_TIMING] == "Immediately" then
+ args.startTiming = Puzzle.START_TIMINGS.immediately
+ end
end
local puzzle = Puzzle(args)
diff --git a/client/src/debug/DebugSettings.lua b/client/src/debug/DebugSettings.lua
index 43b02a2d..b2254b46 100644
--- a/client/src/debug/DebugSettings.lua
+++ b/client/src/debug/DebugSettings.lua
@@ -356,7 +356,9 @@ function DebugSettings.setShowRuntimeGraph(value)
DebugSettings.set("showRuntimeGraph", value)
end
--- Sets the VS frames behind value
+--- Sets the VS frames behind value
+--- Careful with setting this too high when using this with replays: if stack 1 is the losing one it can happen that they never receive the garbage that topped them out
+--- because the second stack does not simulate far enough to send it, causing the replay to get stuck
---@param value number
function DebugSettings.setVSFramesBehind(value)
DebugSettings.set("vsFramesBehind", value)
diff --git a/client/src/graphics/ChallengeModeTimeSplitsUIElement.lua b/client/src/graphics/ChallengeModeTimeSplitsUIElement.lua
index ce1b68ab..577fed4a 100644
--- a/client/src/graphics/ChallengeModeTimeSplitsUIElement.lua
+++ b/client/src/graphics/ChallengeModeTimeSplitsUIElement.lua
@@ -25,7 +25,7 @@ function ChallengeModeTimeSplitsUIElement:drawTimeSplits()
local currentStageTime = time
local isCurrentStage = (self.currentStageIndex and i == self.currentStageIndex)
if isCurrentStage and self.challengeMode.match and not self.challengeMode.match.ended then
- currentStageTime = currentStageTime + (self.challengeMode.match.stacks[1].engine.game_stopwatch or 0)
+ currentStageTime = currentStageTime + (self.challengeMode.match.stacks[1].engine.stopWatch or 0)
end
totalTime = totalTime + currentStageTime
diff --git a/client/src/scenes/PortraitGame.lua b/client/src/scenes/PortraitGame.lua
index d95886cb..52e1a849 100644
--- a/client/src/scenes/PortraitGame.lua
+++ b/client/src/scenes/PortraitGame.lua
@@ -18,8 +18,8 @@ PortraitGame.name = "PortraitGame"
local function getTimer(match)
local frames = 0
local stack = match.stacks[1]
- if stack ~= nil and stack.engine.game_stopwatch ~= nil and tonumber(stack.engine.game_stopwatch) ~= nil then
- frames = stack.engine.game_stopwatch
+ if stack ~= nil and stack.engine.stopWatch ~= nil and tonumber(stack.engine.stopWatch) ~= nil then
+ frames = stack.engine.stopWatch
end
if match.engine.timeLimit then
diff --git a/client/tests/PuzzleSetTests.lua b/client/tests/PuzzleSetTests.lua
index 0ecd35e6..3dce3e90 100644
--- a/client/tests/PuzzleSetTests.lua
+++ b/client/tests/PuzzleSetTests.lua
@@ -200,6 +200,25 @@ function PuzzleSetTests.testExactJSONFormatting()
assert(prettified == expected, "JSON formatting should match exactly")
end
+function PuzzleSetTests.testLoadV3WithoutStartTiming()
+ -- Test that loading a v3 puzzle without StartTiming works
+ local puzzleSetData = {
+ ["Set Name"] = "Test Set",
+ ["Description"] = "Test description",
+ ["Puzzles"] = {
+ {
+ ["Puzzle Type"] = "moves",
+ ["Moves"] = 5,
+ ["Stack"] = "1254216999999952"
+ }
+ }
+ }
+
+ local result = PuzzleSet.loadV3(puzzleSetData)
+ assert(result ~= nil, "Should have loaded puzzle set")
+ assert(#result.puzzles == 1, "Should have one puzzle")
+end
+
-- Run the tests
PuzzleSetTests.updatePuzzleValid()
PuzzleSetTests.generateSaveDataValid()
@@ -208,5 +227,6 @@ PuzzleSetTests.generateSaveDataWithOptionalFields()
PuzzleSetTests.testJSONValidityRequirement1()
PuzzleSetTests.testUnchangedPuzzlePreservationRequirement3()
PuzzleSetTests.testExactJSONFormatting()
+PuzzleSetTests.testLoadV3WithoutStartTiming()
return PuzzleSetTests
\ No newline at end of file
diff --git a/common/compatibility/LegacyPanelSource.lua b/common/compatibility/LegacyPanelSource.lua
index 0a0d6d05..596bacde 100644
--- a/common/compatibility/LegacyPanelSource.lua
+++ b/common/compatibility/LegacyPanelSource.lua
@@ -197,7 +197,7 @@ function LegacyPanelSource:clone(stack)
return source
end
-function LegacyPanelSource:saveForRollback(frame)
+function LegacyPanelSource:saveForRollback(clock)
local copy = self.rollbackBuffer:getOldest()
if not copy then
@@ -209,11 +209,11 @@ function LegacyPanelSource:saveForRollback(frame)
copy.panelGenCount = self.panelGenCount
copy.garbageGenCount = self.garbageGenCount
- self.rollbackBuffer:saveCopy(frame, copy)
+ self.rollbackBuffer:saveCopy(clock, copy)
end
-function LegacyPanelSource:rollbackToFrame(frame)
- local copy = self.rollbackBuffer:rollbackToFrame(frame)
+function LegacyPanelSource:rollbackToFrame(clock)
+ local copy = self.rollbackBuffer:rollbackToFrame(clock)
if not copy then
error("Could not rollback LegacyPanelSource")
@@ -225,8 +225,8 @@ function LegacyPanelSource:rollbackToFrame(frame)
self.garbageGenCount = copy.garbageGenCount
end
-function LegacyPanelSource:rewindToFrame(frame)
- self:rollbackToFrame(frame)
+function LegacyPanelSource:rewindToFrame(clock)
+ self:rollbackToFrame(clock)
end
return LegacyPanelSource
\ No newline at end of file
diff --git a/common/engine/AttackEngine.lua b/common/engine/AttackEngine.lua
index 35f740aa..778acc06 100644
--- a/common/engine/AttackEngine.lua
+++ b/common/engine/AttackEngine.lua
@@ -30,7 +30,7 @@ AttackPattern =
---@field treatMetalAsCombo boolean whether the metal garbage is treated the same as combo garbage (aka they can mix)
---@field attackPatterns AttackPattern[] The array of AttackPattern objects this engine will run through.
---@field attackSettings table The format for serializing AttackPattern information
----@field stopwatch integer The stopwatch to control the continuity of the sending process
+---@field stopWatch integer The stopWatch to control the continuity of the sending process
---@field outgoingGarbage GarbageQueue The garbage queue attacks are added to
local AttackEngine = class(
function(self, attackSettings, garbageQueue)
@@ -50,7 +50,7 @@ local AttackEngine = class(
self:addAttackPatternsFromTable(attackSettings.attackPatterns)
self.attackSettings = attackSettings
- self.stopwatch = 0
+ self.stopWatch = 0
self.outgoingGarbage = garbageQueue
-- to ensure correct behaviour according to the pattern definition
@@ -85,7 +85,7 @@ end
-- Adds an attack pattern that happens repeatedly on a timer.
---@param width integer? the width of the garbage block in columns
---@param height integer? the height of the garbage block in rows
----@param start integer the stopwatch frame these attacks should start being sent
+---@param start integer the stopWatch frame these attacks should start being sent
---@param metal boolean? if this is a metal block
---@param chain boolean? if this is a chain attack
function AttackEngine.addAttackPattern(self, width, height, start, metal, chain)
@@ -94,7 +94,7 @@ function AttackEngine.addAttackPattern(self, width, height, start, metal, chain)
self.attackPatterns[#self.attackPatterns + 1] = attackPattern
end
----@param chainEnd integer the stopwatch frame the ongoing chain is being finalized
+---@param chainEnd integer the stopWatch frame the ongoing chain is being finalized
function AttackEngine.addEndChainPattern(self, chainEnd)
local attackPattern = AttackPattern(0, 0, self.delayBeforeStart + chainEnd, false, false, true)
self.attackPatterns[#self.attackPatterns + 1] = attackPattern
@@ -117,21 +117,21 @@ function AttackEngine.run(self)
-- that the recipient is stalling acceptance so we shouldn't push more inside
if self.disableQueueLimit or self.outgoingGarbage.transitTimers:len() <= 6 then
for i = 1, #self.attackPatterns do
- if self.stopwatch >= self.attackPatterns[i].startTime then
- local difference = self.stopwatch - self.attackPatterns[i].startTime
+ if self.stopWatch >= self.attackPatterns[i].startTime then
+ local difference = self.stopWatch - self.attackPatterns[i].startTime
local remainder = difference % totalAttackTimeBeforeRepeat
if remainder == 0 then
if self.attackPatterns[i].endsChain then
if not self.outgoingGarbage.currentChain then
break
end
- self.outgoingGarbage:finalizeCurrentChain(self.stopwatch)
+ self.outgoingGarbage:finalizeCurrentChain(self.stopWatch)
else
local garbage = self.attackPatterns[i].garbage
if garbage.isChain then
- self.outgoingGarbage:addChainLink(self.stopwatch, math.random(1, 11), math.random(1, 6))
+ self.outgoingGarbage:addChainLink(self.stopWatch, math.random(1, 11), math.random(1, 6))
else
- garbage.frameEarned = self.stopwatch
+ garbage.frameEarned = self.stopWatch
-- we need a coordinate for the origin of the attack animation
garbage.rowEarned = math.random(1, 11)
garbage.colEarned = math.random(1, 6)
@@ -147,21 +147,21 @@ function AttackEngine.run(self)
end
end
- self.stopwatch = self.stopwatch + 1
+ self.stopWatch = self.stopWatch + 1
end
-function AttackEngine:saveForRollback(frame)
- self.outgoingGarbage:saveForRollback(frame)
+function AttackEngine:saveForRollback(stopWatch)
+ self.outgoingGarbage:saveForRollback(stopWatch)
end
-function AttackEngine:rollbackToFrame(frame)
- self.outgoingGarbage:rollbackToFrame(frame)
- self.stopwatch = frame
+function AttackEngine:rollbackToFrame(stopWatch)
+ self.outgoingGarbage:rollbackToFrame(stopWatch)
+ self.stopWatch = stopWatch
end
-function AttackEngine:rewindToFrame(frame)
- self.outgoingGarbage:rewindToFrame(frame)
- self.stopwatch = frame
+function AttackEngine:rewindToFrame(stopWatch)
+ self.outgoingGarbage:rewindToFrame(stopWatch)
+ self.stopWatch = stopWatch
end
return AttackEngine
\ No newline at end of file
diff --git a/common/engine/BaseStack.lua b/common/engine/BaseStack.lua
index a1fb5648..23f5627d 100644
--- a/common/engine/BaseStack.lua
+++ b/common/engine/BaseStack.lua
@@ -9,11 +9,11 @@ local MatchRules = require("common.data.MatchRules")
---@field is_local boolean effectively if the Stack is receiving its inputs via local input
---@field framesBehindArray integer[] Records how far behind the stack was at each match clock time
---@field framesBehind integer How far behind the stack is at the current Match clock time
----@field clock integer how many times run has been called
----@field game_stopwatch integer how many times the simulation has run
----@field game_stopwatch_running boolean if the stack is simulating during runs
+---@field clock integer how many times run has been called; this is equivalent to how many inputs have been processed;
This is the chief timer to measure synchronicity and the driver of rollback and inputs
+---@field stopWatch integer how many times the game physics have run; unlike a clock and just like a stopWatch this frame timer only runs when the simulation is running
+---@field stopWatchIsRunning boolean if the stack is running the game physics during runs
---@field game_over_clock integer What the clock time was when the Stack went game over
----@field do_countdown boolean if the stack is performing a countdown at the start of the match
+---@field do_countdown boolean if the stack is currently performing a countdown / will perform a countdown at the start of the match;
this is state, the value will change at the end of countdown
---@field countdown_timer boolean? ephemeral timer used for tracking countdown progress at the start of the game
---@field outgoingGarbage GarbageQueue
---@field incomingGarbage GarbageQueue
@@ -61,8 +61,8 @@ function(self, args)
self.framesBehindArray = {}
self.framesBehind = 0
self.clock = 0
- self.game_stopwatch = 0
- self.game_stopwatch_running = true
+ self.stopWatch = 0
+ self.stopWatchIsRunning = true
self.game_over_clock = -1 -- the exact clock frame the stack lost, -1 while alive
Signal.turnIntoEmitter(self)
self:createSignal("gameOver")
@@ -116,7 +116,7 @@ end
---@param doCountdown boolean
function BaseStack:setCountdown(doCountdown)
self.do_countdown = doCountdown
- self.game_stopwatch_running = not self.do_countdown
+ self.stopWatchIsRunning = not self.do_countdown
end
---@param maxRunsPerFrame integer
@@ -161,15 +161,15 @@ function BaseStack:saveForRollback()
error("did not implement saveForRollback")
end
----@param frame integer the frame to rollback to if possible
+---@param clock integer the frame to rollback to if possible
---@return boolean success if rolling back succeeded
-function BaseStack:rollbackToFrame(frame)
+function BaseStack:rollbackToFrame(clock)
error("did not implement rollbackToFrame")
end
----@param frame integer the frame to rewind to if possible
+---@param clock integer the frame to rewind to if possible
---@return boolean success if rewinding succeeded
-function BaseStack:rewindToFrame(frame)
+function BaseStack:rewindToFrame(clock)
error("did not implement rewindToFrame")
end
diff --git a/common/engine/GarbageQueue.lua b/common/engine/GarbageQueue.lua
index b688c5b6..9f85b2a7 100644
--- a/common/engine/GarbageQueue.lua
+++ b/common/engine/GarbageQueue.lua
@@ -201,13 +201,13 @@ function GarbageQueue:saveForRollback(frame)
self.rollbackBuffer:saveCopy(frame, copy)
end
----@param frame integer
-function GarbageQueue:rollbackToFrame(frame)
- assert(self.rollbackBuffer, "Attempted to rollback garbage queue to frame " .. frame .. " but no rollback buffer has been kept")
+---@param stopWatch integer
+function GarbageQueue:rollbackToFrame(stopWatch)
+ assert(self.rollbackBuffer, "Attempted to rollback garbage queue to frame " .. stopWatch .. " but no rollback buffer has been kept")
- local copy = self.rollbackBuffer:rollbackToFrame(frame)
+ local copy = self.rollbackBuffer:rollbackToFrame(stopWatch)
- assert(copy, "Attempted to rollback garbage queue to frame " .. frame .. " but no rollback copy was available")
+ assert(copy, "Attempted to rollback garbage queue to frame " .. stopWatch .. " but no rollback copy was available")
self.stagedGarbage = copy.stagedGarbage
self.currentChain = copy.currentChain
@@ -220,20 +220,20 @@ function GarbageQueue:rollbackToFrame(frame)
-- this may not universally work for multiplayer with more than 2 players
for i = self.transitTimers.last, self.transitTimers.first, -1 do
local transitFrame = self.transitTimers[i]
- if transitFrame >= frame + GARBAGE_DELAY_LAND_TIME then
+ if transitFrame >= stopWatch + GARBAGE_DELAY_LAND_TIME then
self.garbageInTransit[transitFrame] = nil
self.transitTimers.last = self.transitTimers.last - 1
end
end
end
----@param frame integer
-function GarbageQueue:rewindToFrame(frame)
- assert(self.rollbackBuffer, "Attempted to rewind garbage queue to frame " .. frame .. " but no rollback buffer has been kept")
+---@param stopWatch integer
+function GarbageQueue:rewindToFrame(stopWatch)
+ assert(self.rollbackBuffer, "Attempted to rewind garbage queue to frame " .. stopWatch .. " but no rollback buffer has been kept")
- local copy = self.rollbackBuffer:rollbackToFrame(frame)
+ local copy = self.rollbackBuffer:rollbackToFrame(stopWatch)
- assert(copy, "Attempted to rewind garbage queue to frame " .. frame .. " but no rollback copy was available")
+ assert(copy, "Attempted to rewind garbage queue to frame " .. stopWatch .. " but no rollback copy was available")
self.stagedGarbage = copy.stagedGarbage
self.currentChain = copy.currentChain
diff --git a/common/engine/GeneratorSource.lua b/common/engine/GeneratorSource.lua
index 060eef55..e88b3604 100644
--- a/common/engine/GeneratorSource.lua
+++ b/common/engine/GeneratorSource.lua
@@ -222,7 +222,7 @@ function GeneratorSource:clone(stack)
return source
end
-function GeneratorSource:saveForRollback(frame)
+function GeneratorSource:saveForRollback(clock)
local copy = self.rollbackBuffer:getOldest()
if not copy then
@@ -236,11 +236,11 @@ function GeneratorSource:saveForRollback(frame)
copy.adjacentAccepted = self.panelGenerator.adjacentAccepted
copy.adjacentDenied = self.panelGenerator.adjacentDenied
- self.rollbackBuffer:saveCopy(frame, copy)
+ self.rollbackBuffer:saveCopy(clock, copy)
end
-function GeneratorSource:rollbackToFrame(frame)
- local copy = self.rollbackBuffer:rollbackToFrame(frame)
+function GeneratorSource:rollbackToFrame(clock)
+ local copy = self.rollbackBuffer:rollbackToFrame(clock)
if not copy then
error("Could not rollback GeneratorSource")
@@ -254,8 +254,8 @@ function GeneratorSource:rollbackToFrame(frame)
self.panelGenerator.adjacentDenied = copy.adjacentDenied
end
-function GeneratorSource:rewindToFrame(frame)
- self:rollbackToFrame(frame)
+function GeneratorSource:rewindToFrame(clock)
+ self:rollbackToFrame(clock)
end
return GeneratorSource
\ No newline at end of file
diff --git a/common/engine/Health.lua b/common/engine/Health.lua
index 7728929e..710b90d7 100644
--- a/common/engine/Health.lua
+++ b/common/engine/Health.lua
@@ -121,10 +121,10 @@ function Health:saveRollbackCopy()
end
end
-function Health:rollbackToFrame(frame)
- local copy = self.rollbackCopies[frame]
+function Health:rollbackToFrame(clock)
+ local copy = self.rollbackCopies[clock]
- for i = frame + 1, self.clock do
+ for i = clock + 1, self.clock do
self.rollbackCopyPool:push(self.rollbackCopies[i])
self.rollbackCopies[i] = nil
end
@@ -133,7 +133,7 @@ function Health:rollbackToFrame(frame)
self.currentLines = copy.currentLines
self.framesToppedOutToLose = copy.framesToppedOutToLose
self.lastWasFourCombo = copy.lastWasFourCombo
- self.clock = frame
+ self.clock = clock
end
---@return HealthSettings
diff --git a/common/engine/Match.lua b/common/engine/Match.lua
index cb7433fb..1a7d6acb 100644
--- a/common/engine/Match.lua
+++ b/common/engine/Match.lua
@@ -297,18 +297,18 @@ function Match:pushGarbageTo(stack)
for _, st in ipairs(self.garbageSources[stack]) do
local oldestTransitTime = st:getOldestFinishedGarbageTransitTime()
if oldestTransitTime and ((not st.outgoingGarbage.illegalStuffIsAllowed) or (#stack.incomingGarbage.stagedGarbage < 72)) then
- if stack.game_stopwatch > oldestTransitTime then
+ if stack.stopWatch > oldestTransitTime then
-- recipient went past the frame it was supposed to receive the garbage -> rollback to that frame
-- hypothetically, IF the receiving stack's garbage target was different than the sender forcing the rollback here
-- it may be necessary to perform extra steps to ensure the recipient of the stack getting rolled back is getting correct garbage
-- which may even include another rollback
- if not self:rollbackToFrame(stack, oldestTransitTime) and not stack.incomingGarbage.illegalStuffIsAllowed then
+ if not self:rollbackToStopWatch(stack, oldestTransitTime) and not stack.incomingGarbage.illegalStuffIsAllowed then
-- if we can't rollback, it's a desync
self.desyncError = true
self:abort()
end
end
- local garbageDelivery = st:getReadyGarbageAt(stack.game_stopwatch)
+ local garbageDelivery = st:getReadyGarbageAt(stack.stopWatch)
if garbageDelivery then
--logger.debug("Pushing garbage delivery to incoming garbage queue: " .. table_to_string(garbageDelivery))
stack:receiveGarbage(garbageDelivery)
@@ -327,7 +327,7 @@ function Match:shouldSaveRollback(stack)
for senderIndex, targetList in ipairs(self.garbageTargets) do
for _, target in ipairs(targetList) do
if target == stack then
- if self.stacks[senderIndex].game_stopwatch + GARBAGE_DELAY_LAND_TIME <= stack.game_stopwatch then
+ if self.stacks[senderIndex].stopWatch + GARBAGE_DELAY_LAND_TIME <= stack.stopWatch then
return true
end
end
@@ -338,12 +338,20 @@ function Match:shouldSaveRollback(stack)
end
end
+-- attempt to rollback the specified stack to the specified stopWatch
+---@param stack BaseStack
+---@param stopWatch integer
+---@return boolean success
+function Match:rollbackToStopWatch(stack, stopWatch)
+ return self:rollbackToFrame(stack, stopWatch + (stack.clock - stack.stopWatch))
+end
+
-- attempt to rollback the specified stack to the specified frame
---@param stack BaseStack
----@param frame integer
+---@param clock integer
---@return boolean success
-function Match:rollbackToFrame(stack, frame)
- if stack:rollbackToFrame(frame) then
+function Match:rollbackToFrame(stack, clock)
+ if stack:rollbackToFrame(clock) then
return true
end
@@ -352,22 +360,21 @@ end
-- rewind is ONLY to be used for replay playback as it relies on all stacks being at the same clock time
-- and also uses slightly different data required only in a both-sides rollback scenario that would never occur for online rollback
----@param frame integer
-function Match:rewindToFrame(frame)
+---@param clock integer
+function Match:rewindToFrame(clock)
-- Bounds check: don't allow rewinding to negative frames
- if frame < 0 then
+ if clock < 0 then
return
end
-
local failed = false
for i, stack in ipairs(self.stacks) do
- if not stack:rewindToFrame(frame) then
+ if not stack:rewindToFrame(clock) then
failed = true
break
end
end
if not failed then
- self.clock = frame
+ self.clock = clock
self.ended = false
end
end
@@ -558,7 +565,7 @@ function Match:hasEnded()
end
if self.timeLimit then
- if tableUtils.trueForAll(self.stacks, function(stack) return stack.game_stopwatch and stack.game_stopwatch >= self.timeLimit end) then
+ if tableUtils.trueForAll(self.stacks, function(stack) return stack.stopWatch and stack.stopWatch >= self.timeLimit end) then
self.ended = true
return true
end
@@ -611,7 +618,7 @@ function Match:shouldRun(stack, runsSoFar)
if not stack:game_ended() then
if self.timeLimit then
-- timeLimit will malfunction with SimulatedStack
- if stack.game_stopwatch and stack.game_stopwatch >= self.timeLimit then
+ if stack.stopWatch and stack.stopWatch >= self.timeLimit then
-- the stack should only run 1 frame beyond the time limit (excluding countdown)
return false
end
@@ -626,7 +633,7 @@ function Match:shouldRun(stack, runsSoFar)
-- In debug mode allow non-local player 2 to fall a certain number of frames behind
if not stack.is_local and self.debug.vsFramesBehind > 0 and tableUtils.indexOf(self.stacks, stack) == 2 then
-- Only stay behind if the game isn't over for the local player (=garbageTarget) yet
- if self.garbageTargets[2][1] and self.garbageTargets[2][1].game_ended and self.garbageTargets[2][1]:game_ended() == false then
+ if self.garbageTargets[2][1] and self.garbageTargets[2][1]:game_ended() == false then
if stack.clock + self.debug.vsFramesBehind >= self.garbageTargets[2][1].clock then
return false
end
diff --git a/common/engine/Puzzle.lua b/common/engine/Puzzle.lua
index 4671939a..65236b97 100644
--- a/common/engine/Puzzle.lua
+++ b/common/engine/Puzzle.lua
@@ -10,8 +10,6 @@ local system = require("client.src.system")
---@field row integer
---@field column integer
----@alias PuzzleType ("moves" | "chain" | "clear")
-
---@class PuzzleArgs
---@field puzzleType PuzzleType
---@field stack string representation of the panel colors, the last character is the bottom right panel
@@ -49,12 +47,12 @@ Puzzle = class(
---@param self Puzzle
---@param puzzleArgs GarbagePuzzleArgs
function(self, puzzleArgs)
- self.puzzleType = puzzleArgs.puzzleType or "moves"
+ self.puzzleType = puzzleArgs.puzzleType or Puzzle.PUZZLE_TYPES.moves
self.cursorStartLeft = puzzleArgs.cursorStartLeft
if puzzleArgs.startTiming then
self.startTiming = puzzleArgs.startTiming
else
- if self.puzzleType == "clear" or self.puzzleType == "chain" then
+ if self.puzzleType == Puzzle.PUZZLE_TYPES.clear or self.puzzleType == Puzzle.PUZZLE_TYPES.chain then
if self.cursorStartLeft then
self.startTiming = Puzzle.START_TIMINGS.firstInput
else
@@ -125,7 +123,8 @@ end
---@alias PuzzleStartTiming "countdown" | "immediately" | "firstInput" | "firstSwap"
Puzzle.START_TIMINGS = { countdown = "countdown", immediately = "immediately", firstInput = "firstInput", firstSwap = "firstSwap" }
-Puzzle.PUZZLE_TYPES = { "moves", "chain", "clear" }
+---@enum PuzzleType
+Puzzle.PUZZLE_TYPES = { moves = "moves", chain = "chain", clear = "clear" }
Puzzle.LEGAL_CHARACTERS = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "[", "]", "{", "}", "=" }
Puzzle.PUZZLE_PROPERTY = {
@@ -201,7 +200,7 @@ end
function Puzzle:fillMissingPanelsInPuzzleString(width, height)
local puzzleString = self.stack
local boardSizeInPanels = width * height
- if self.puzzleType == "clear" then
+ if self.puzzleType == Puzzle.PUZZLE_TYPES.clear then
-- first fill up the currently started row
local fillUpLength = (puzzleString:len() % width)
if fillUpLength > 0 then
@@ -302,12 +301,12 @@ function Puzzle:validate()
errMessage = errMessage .. "\nPuzzlestring contains invalid characters: " .. table.concat(illegalCharacters, ", ")
end
- if not tableUtils.contains(Puzzle.PUZZLE_TYPES, self.puzzleType) then
+ if not Puzzle.PUZZLE_TYPES[self.puzzleType] then
errMessage = errMessage ..
"\nInvalid puzzle type detected, available puzzle types are: " .. table.concat(Puzzle.PUZZLE_TYPES, ", ")
end
- if string.lower(self.puzzleType) == "moves" and (not tonumber(self.moves) or tonumber(self.moves) < 1 ) then
+ if self.puzzleType == Puzzle.PUZZLE_TYPES.moves and (not tonumber(self.moves) or tonumber(self.moves) < 1 ) then
errMessage = errMessage ..
"\nInvalid number of moves detected, expecting a number greater than zero but instead got " .. self.moves
end
@@ -325,6 +324,7 @@ end
-- Helper function to convert a single puzzle to save data format
---@return table
function Puzzle:getSaveData()
+ ---@type table
local puzzleData = {
[Puzzle.PUZZLE_PROPERTY.TYPE] = self.puzzleType,
[Puzzle.PUZZLE_PROPERTY.START_TIMING] = self.startTiming,
@@ -408,7 +408,7 @@ function Puzzle:toGameMode()
mode.matchRules.stackOverConditions[MatchRules.StackOverConditions.SWAPS] = self.moves
end
- if self.puzzleType == "clear" then
+ if self.puzzleType == Puzzle.PUZZLE_TYPES.clear then
mode.matchRules.stackOverConditions[MatchRules.StackOverConditions.HEALTH] = 0
mode.matchRules.stackWinConditions[MatchRules.StackWinConditions.MATCHABLE_GARBAGE_PANELS] = 0
mode.matchRules.stackSetupModifications.stopTime = self.stopTime
@@ -418,10 +418,10 @@ function Puzzle:toGameMode()
allowManualRaise = false,
passiveRaise = false,
}
- if self.puzzleType == "chain" then
+ if self.puzzleType == Puzzle.PUZZLE_TYPES.chain then
mode.matchRules.stackOverConditions[MatchRules.StackOverConditions.CHAIN] = false
mode.matchRules.stackWinConditions[MatchRules.StackWinConditions.MATCHABLE_PANELS] = 0
- elseif self.puzzleType == "moves" then
+ elseif self.puzzleType == Puzzle.PUZZLE_TYPES.moves then
mode.matchRules.stackWinConditions[MatchRules.StackWinConditions.MATCHABLE_PANELS] = 0
end
end
diff --git a/common/engine/PuzzleSource.lua b/common/engine/PuzzleSource.lua
index f361d71f..9d8a2ab2 100644
--- a/common/engine/PuzzleSource.lua
+++ b/common/engine/PuzzleSource.lua
@@ -225,7 +225,7 @@ function PuzzleSource:clone(stack)
return source
end
-function PuzzleSource:saveForRollback(frame)
+function PuzzleSource:saveForRollback(clock)
local copy = self.rollbackBuffer:getOldest()
if not copy then
@@ -238,11 +238,11 @@ function PuzzleSource:saveForRollback(frame)
copy.garbageGenCount = self.garbageGenCount
-- self.panels is not stored under the assumption that panels always get fully consumed on the frame they got created
- self.rollbackBuffer:saveCopy(frame, copy)
+ self.rollbackBuffer:saveCopy(clock, copy)
end
-function PuzzleSource:rollbackToFrame(frame)
- local copy = self.rollbackBuffer:rollbackToFrame(frame)
+function PuzzleSource:rollbackToFrame(clock)
+ local copy = self.rollbackBuffer:rollbackToFrame(clock)
if not copy then
error("Could not rollback PuzzleSource")
@@ -254,8 +254,8 @@ function PuzzleSource:rollbackToFrame(frame)
self.garbageGenCount = copy.garbageGenCount
end
-function PuzzleSource:rewindToFrame(frame)
- self:rollbackToFrame(frame)
+function PuzzleSource:rewindToFrame(clock)
+ self:rollbackToFrame(clock)
end
return PuzzleSource
diff --git a/common/engine/SimulatedStack.lua b/common/engine/SimulatedStack.lua
index 67f4aed3..f5ce9b8e 100644
--- a/common/engine/SimulatedStack.lua
+++ b/common/engine/SimulatedStack.lua
@@ -39,40 +39,50 @@ function SimulatedStack:addHealth(healthSettings)
end
function SimulatedStack:run()
- if self.do_countdown and self.countdown_timer > 0 then
+ if self.stopWatchIsRunning then
+ self:runPhysics()
+ elseif self.do_countdown and self.countdown_timer > 0 then
if self.healthEngine then
self.healthEngine.clock = self.clock
end
if self.clock >= consts.COUNTDOWN_START then
self.countdown_timer = self.countdown_timer - 1
end
- else
- if self.attackEngine then
- self.attackEngine:run()
+ if self.countdown_timer == 0 then
+ self.do_countdown = nil
+ self.stopWatchIsRunning = true
end
+ else
+ error("stopWatch of SimulatedStack is not running but neither is the countdown")
+ end
- self.outgoingGarbage:processStagedGarbageForClock(self.game_stopwatch)
+ self.clock = self.clock + 1
- if self.healthEngine then
- -- perform the equivalent of queued garbage being dropped
- -- except a little quicker than on real stacks
- for i = #self.incomingGarbage.stagedGarbage, 1, -1 do
- self.healthEngine:receiveGarbage(self.clock, self.incomingGarbage:pop())
- end
+ self:emitSignal("finishedRun")
+end
- self.health = self.healthEngine:run()
- end
+function SimulatedStack:runPhysics()
+ if self.attackEngine then
+ self.attackEngine:run()
+ end
+
+ self.outgoingGarbage:processStagedGarbageForClock(self.stopWatch)
- if self.health <= 0 then
- self:setGameOver()
+ if self.healthEngine then
+ -- perform the equivalent of queued garbage being dropped
+ -- except a little quicker than on real stacks
+ for i = #self.incomingGarbage.stagedGarbage, 1, -1 do
+ self.healthEngine:receiveGarbage(self.clock, self.incomingGarbage:pop())
end
- self.game_stopwatch = self.game_stopwatch + 1
+ self.health = self.healthEngine:run()
end
- self.clock = self.clock + 1
+ if self.health <= 0 then
+ self:setGameOver()
+ end
- self:emitSignal("finishedRun")
+ self.stopWatch = self.stopWatch + 1
end
function SimulatedStack:setGameOver()
@@ -121,18 +131,18 @@ function SimulatedStack:saveForRollback()
copy = {}
end
- self.incomingGarbage:saveForRollback(self.game_stopwatch)
+ self.incomingGarbage:saveForRollback(self.stopWatch)
if self.healthEngine then
self.healthEngine:saveRollbackCopy()
end
if self.attackEngine then
- self.attackEngine:saveForRollback(self.game_stopwatch)
+ self.attackEngine:saveForRollback(self.stopWatch)
end
copy.health = self.health
- copy.game_stopwatch = self.game_stopwatch
+ copy.stopWatch = self.stopWatch
copy.game_over_clock = self.game_over_clock
self.rollbackCopies[self.clock] = copy
@@ -144,11 +154,11 @@ function SimulatedStack:saveForRollback()
end
end
-local function internalRollbackToFrame(stack, frame)
- local copy = stack.rollbackCopies[frame]
+local function internalRollbackToFrame(stack, clock)
+ local copy = stack.rollbackCopies[clock]
- if copy and frame < stack.clock then
- for f = frame, stack.clock do
+ if copy and clock < stack.clock then
+ for f = clock, stack.clock do
if stack.rollbackCopies[f] then
stack.rollbackCopyPool:push(stack.rollbackCopies[f])
stack.rollbackCopies[f] = nil
@@ -156,13 +166,13 @@ local function internalRollbackToFrame(stack, frame)
end
if stack.healthEngine then
- stack.healthEngine:rollbackToFrame(frame)
+ stack.healthEngine:rollbackToFrame(clock)
stack.health = stack.healthEngine.framesToppedOutToLose
else
stack.health = copy.health
end
- stack.game_stopwatch = copy.game_stopwatch
+ stack.stopWatch = copy.stopWatch
stack.game_over_clock = copy.game_over_clock
return true
@@ -171,33 +181,33 @@ local function internalRollbackToFrame(stack, frame)
return false
end
-function SimulatedStack:rollbackToFrame(frame)
- if internalRollbackToFrame(self, frame) then
- self.incomingGarbage:rollbackToFrame(self.game_stopwatch)
+function SimulatedStack:rollbackToFrame(clock)
+ if internalRollbackToFrame(self, clock) then
+ self.incomingGarbage:rollbackToFrame(self.stopWatch)
if self.attackEngine then
- self.attackEngine:rollbackToFrame(self.game_stopwatch)
+ self.attackEngine:rollbackToFrame(self.stopWatch)
end
self.lastRollbackFrame = self.clock
- self.clock = frame
+ self.clock = clock
return true
end
return false
end
-function SimulatedStack:rewindToFrame(frame)
- if internalRollbackToFrame(self, frame) then
- self.incomingGarbage:rewindToFrame(self.game_stopwatch)
+function SimulatedStack:rewindToFrame(clock)
+ if internalRollbackToFrame(self, clock) then
+ self.incomingGarbage:rewindToFrame(self.stopWatch)
if self.attackEngine then
- self.attackEngine:rewindToFrame(self.game_stopwatch)
+ self.attackEngine:rewindToFrame(self.stopWatch)
end
- -- we did roll back but we want to stay here
- self.lastRollbackFrame = frame
- self.clock = frame
+ -- we did roll back but we want to stay here
+ self.lastRollbackFrame = clock
+ self.clock = clock
return true
end
diff --git a/common/engine/Stack.lua b/common/engine/Stack.lua
index fa73454c..4b8c4dc4 100644
--- a/common/engine/Stack.lua
+++ b/common/engine/Stack.lua
@@ -110,7 +110,6 @@ local DIRECTION_ROW = {up = 1, down = -1, left = 0, right = 0}
--- panel[i] gets the row where i is the index of the row with 1 being the bottommost row in play (not dimmed) \n
--- panel[i][j] gets the panel at row i where j is the column index counting from left to right starting from 1 \n
--- the update order for panels is bottom to top and left to right as well
----@field game_stopwatch_running boolean set to false if countdown starts
---@field displacement integer This variable indicates how far below the top of the play area the top row of panels actually is. \n
--- This variable being decremented causes the stack to rise. \n
--- During the automatic rising routine, if this variable is 0, it's reset to 15, all the panels are moved up one row, and a new row is generated at the bottom. \n
@@ -149,7 +148,6 @@ local DIRECTION_ROW = {up = 1, down = -1, left = 0, right = 0}
---@field peak_shake_time integer Records the maximum shake time obtained for the current stretch of uninterrupted shake time. \n
--- Any additional shake time gained before shake depletes to 0 will reset shake_time back to this value. Set to 0 when shake_time reaches 0.
---@field warningsTriggered table ancient ancient, probably remove
----@field game_stopwatch integer Clock time minus time that swaps were blocked
---@field rollbackBuffer RollbackBuffer A specialized class to manage memory for rollback data
---@field panelTemplate (Panel | fun(row: integer, column: integer, id: integer?): Panel) A template class based on Panel enriched by tailor made closures containing references to the Stack
---@field swapStallingBackLog table tracks swaps that will incur a health cost for stalling if not swapping would have resulted in health loss
@@ -184,7 +182,7 @@ local Stack = class(
s.inputMethod = args.inputMethod
if s.behaviours.delaySimulationUntil then
- s.game_stopwatch_running = false
+ s.stopWatchIsRunning = false
end
s.swapStallingBackLog = {}
@@ -396,8 +394,8 @@ function Stack:rollbackCopy()
copy.health = self.health
copy.countdown_timer = self.countdown_timer
copy.clock = self.clock
- copy.game_stopwatch = self.game_stopwatch
- copy.game_stopwatch_running = self.game_stopwatch_running
+ copy.stopWatch = self.stopWatch
+ copy.stopWatchIsRunning = self.stopWatchIsRunning
copy.rise_lock = self.rise_lock
copy.top_cur_row = self.top_cur_row
copy.displacement = self.displacement
@@ -442,9 +440,9 @@ function Stack:rollbackCopy()
end
---@param stack Stack
----@param frame integer
-local function internalRollbackToFrame(stack, frame)
- local copy = stack.rollbackBuffer:rollbackToFrame(frame)
+---@param clock integer
+local function internalRollbackToFrame(stack, clock)
+ local copy = stack.rollbackBuffer:rollbackToFrame(clock)
if not copy then
return false
@@ -452,8 +450,8 @@ local function internalRollbackToFrame(stack, frame)
stack.countdown_timer = copy.countdown_timer
stack.clock = copy.clock
- stack.game_stopwatch = copy.game_stopwatch
- stack.game_stopwatch_running = copy.game_stopwatch_running
+ stack.stopWatch = copy.stopWatch
+ stack.stopWatchIsRunning = copy.stopWatchIsRunning
stack.rise_lock = copy.rise_lock
stack.top_cur_row = copy.top_cur_row
stack.displacement = copy.displacement
@@ -520,7 +518,7 @@ local function internalRollbackToFrame(stack, frame)
-- this is for the interpolation of the shake animation only (not a physics relevant field)
local previousData = stack.rollbackBuffer:peekPrevious()
- if previousData and previousData.clock == frame - 1 then
+ if previousData and previousData.clock == clock - 1 then
stack.prev_shake_time = previousData.shake_time
else
-- if this is the oldest rollback frame we don't need to interpolate with previous values
@@ -532,15 +530,15 @@ local function internalRollbackToFrame(stack, frame)
return true
end
----@param frame integer the frame to rollback to if possible
+---@param clock integer the frame to rollback to if possible
---@return boolean success if rolling back succeeded
-function Stack:rollbackToFrame(frame)
+function Stack:rollbackToFrame(clock)
local currentFrame = self.clock
- if internalRollbackToFrame(self, frame) then
- self.incomingGarbage:rollbackToFrame(self.game_stopwatch)
- self.outgoingGarbage:rollbackToFrame(self.game_stopwatch)
- self.panelSource:rollbackToFrame(frame)
+ if internalRollbackToFrame(self, clock) then
+ self.incomingGarbage:rollbackToFrame(self.stopWatch)
+ self.outgoingGarbage:rollbackToFrame(self.stopWatch)
+ self.panelSource:rollbackToFrame(clock)
self.rollbackCount = self.rollbackCount + 1
-- match will try to fast forward this stack to that frame
@@ -552,16 +550,16 @@ function Stack:rollbackToFrame(frame)
return false
end
----@param frame integer the frame to rewind to if possible
+---@param clock integer the frame to rewind to if possible
---@return boolean success if rewinding succeeded
-function Stack:rewindToFrame(frame)
- if internalRollbackToFrame(self, frame) then
- self.incomingGarbage:rewindToFrame(self.game_stopwatch)
- self.outgoingGarbage:rewindToFrame(self.game_stopwatch)
- self.panelSource:rewindToFrame(frame)
+function Stack:rewindToFrame(clock)
+ if internalRollbackToFrame(self, clock) then
+ self.incomingGarbage:rewindToFrame(self.stopWatch)
+ self.outgoingGarbage:rewindToFrame(self.stopWatch)
+ self.panelSource:rewindToFrame(clock)
-- we did roll back but we want to stay here
- self.lastRollbackFrame = frame
+ self.lastRollbackFrame = clock
self:emitSignal("rollbackPerformed", self)
return true
@@ -579,11 +577,11 @@ function Stack:saveForRollback()
self:rollbackCopy()
prof.pop("Stack.rollbackCopy")
prof.push("incomingGarbage:saveForRollback")
- self.incomingGarbage:saveForRollback(self.game_stopwatch)
+ self.incomingGarbage:saveForRollback(self.stopWatch)
prof.pop("incomingGarbage:saveForRollback")
prof.push("outgoingGarbage:saveForRollback")
if self.outgoingGarbage then
- self.outgoingGarbage:saveForRollback(self.game_stopwatch)
+ self.outgoingGarbage:saveForRollback(self.stopWatch)
end
prof.pop("outgoingGarbage:saveForRollback")
self.panelSource:saveForRollback(self.clock)
@@ -774,26 +772,26 @@ function Stack:run()
if self.behaviours.delaySimulationUntil == "countdownEnded" and self.clock <= (consts.COUNTDOWN_START + consts.COUNTDOWN_LENGTH) then
self:runCountdown()
if self.clock == (consts.COUNTDOWN_START + consts.COUNTDOWN_LENGTH) then
- self.game_stopwatch_running = true
+ self.stopWatchIsRunning = true
end
end
--prof.push("Stack:simulate")
- if self.game_stopwatch_running then
- self:simulate()
+ if self.stopWatchIsRunning then
+ self:runPhysics()
else
-- these behaviours need to run "half a frame" on their first one to give the first swap the chance to queue to prevent instant game over on the next one
-- otherwise, if health is 1 and no stop/shake is given and the stack is topped out, passive raise will instakill
if self.behaviours.delaySimulationUntil == "firstInput" then
if self.input_state ~= self:idleInput() then
- self.game_stopwatch_running = true
- -- need to compensate the fact that we increment stopwatch at the end of the frame without having simulated
- self.game_stopwatch = -1
+ self.stopWatchIsRunning = true
+ -- need to compensate the fact that we increment stopWatch at the end of the frame without having simulated
+ self.stopWatch = -1
end
elseif self.behaviours.delaySimulationUntil == "firstSwap" then
if self.swapThisFrame then
- self.game_stopwatch_running = true
- self.game_stopwatch = -1
+ self.stopWatchIsRunning = true
+ self.stopWatch = -1
end
end
end
@@ -815,13 +813,13 @@ function Stack:run()
self:handleManualRaise()
- if self.game_stopwatch_running then
+ if self.stopWatchIsRunning then
prof.push("pop from incoming garbage q")
if self:shouldDropGarbage() then
self:tryDropGarbage()
end
prof.pop("pop from incoming garbage q")
- self.game_stopwatch = self.game_stopwatch + 1
+ self.stopWatch = self.stopWatch + 1
end
self.clock = self.clock + 1
@@ -931,7 +929,7 @@ function Stack:shouldDropGarbage()
end
-- One run of the engine routine.
-function Stack:simulate()
+function Stack:runPhysics()
table.clear(self.garbageLandedThisFrame)
self.wasToppedOut = self:isToppedOut()
@@ -982,14 +980,14 @@ function Stack:simulate()
self.chain_counter = 0
if self.outgoingGarbage then
- logger.debug("Player " .. self.which .. " chain ended at " .. self.game_stopwatch)
- self.outgoingGarbage:finalizeCurrentChain(self.game_stopwatch)
+ logger.debug("Player " .. self.which .. " chain ended at " .. self.stopWatch)
+ self.outgoingGarbage:finalizeCurrentChain(self.stopWatch)
end
end
--prof.pop("chain update")
--prof.push("process staged garbage")
- self.outgoingGarbage:processStagedGarbageForClock(self.game_stopwatch)
+ self.outgoingGarbage:processStagedGarbageForClock(self.stopWatch)
--prof.pop("process staged garbage")
self:removeExtraRows()
@@ -1354,10 +1352,10 @@ end
-- tries to drop a width x height garbage.
-- returns true if garbage was dropped, false otherwise
function Stack:tryDropGarbage()
- logger.debug("trying to drop garbage at frame " .. self.game_stopwatch)
+ logger.debug("trying to drop garbage at frame " .. self.stopWatch)
local garbage = self.incomingGarbage:pop()
- logger.debug(string.format("%d Dropping garbage on stack %d - height %d width %d %s", self.game_stopwatch, self.which, garbage.height, garbage.width, garbage.isMetal and "Metal" or ""))
+ logger.debug(string.format("%d Dropping garbage on stack %d - height %d width %d %s", self.stopWatch, self.which, garbage.height, garbage.width, garbage.isMetal and "Metal" or ""))
self:dropGarbage(garbage.width, garbage.height, garbage.isMetal)
@@ -1455,8 +1453,8 @@ function Stack:getAttackPatternData()
data.attackPatterns = {}
data.extraInfo = {}
data.extraInfo.matchLength = " "
- if self.game_stopwatch > 0 then
- data.extraInfo.matchLength = frames_to_time_string(self.game_stopwatch)
+ if self.stopWatch > 0 then
+ data.extraInfo.matchLength = frames_to_time_string(self.stopWatch)
else
-- there is nothing to export!
return
@@ -1648,7 +1646,7 @@ function Stack:checkGameOver()
-- but also as a negative (accidently killing yourself in non-threatening circumstances)
return true
end
- elseif not self:hasActivePanels() and not self:swapQueued() and self.game_stopwatch_running then
+ elseif not self:hasActivePanels() and not self:swapQueued() and self.stopWatchIsRunning then
if stackOverCondition == MatchRules.StackOverConditions.SWAPS then
if self.swapCount >= value then
return true
@@ -1761,12 +1759,12 @@ function Stack:setCountdown(doCountdown)
self.do_countdown = doCountdown
if doCountdown then
self.behaviours.delaySimulationUntil = "countdownEnded"
- self.game_stopwatch_running = false
+ self.stopWatchIsRunning = false
else
if self.behaviours.delaySimulationUntil == "countdownEnded" then
self.behaviours.delaySimulationUntil = nil
end
- self.game_stopwatch_running = not self.behaviours.delaySimulationUntil
+ self.stopWatchIsRunning = not self.behaviours.delaySimulationUntil
end
end
diff --git a/common/engine/checkMatches.lua b/common/engine/checkMatches.lua
index 18f9e069..248b9560 100644
--- a/common/engine/checkMatches.lua
+++ b/common/engine/checkMatches.lua
@@ -109,10 +109,6 @@ local function canMatch(panel)
end
function Stack:checkMatches()
- if self.do_countdown then
- return
- end
-
prof.push("Stack:checkMatches")
--local reference = self:getMatchingPanels2()
local matchingPanels = self:getMatchingPanels()
@@ -468,7 +464,7 @@ function Stack:getConnectedGarbagePanels2(matchingPanels)
for row = 1, #self.panels do
for col = 1, self.width do
- panel = self.panels[row][col]
+ local panel = self.panels[row][col]
if panel.isGarbage and panel.state == "normal" and not idGarbage[panel.garbageId]
-- we only want to match garbage that is either fully or partially on-screen OR has been on-screen before
-- example: chain garbage several rows high lands in row 12; by visuals/shake it is clear that it is more than 1 row high
@@ -758,14 +754,14 @@ function Stack:convertGarbagePanels(isChain)
end
function Stack:pushGarbage(coordinate, isChain, comboSize, metalCount)
- logger.debug("P" .. self.which .. "@" .. self.game_stopwatch .. ": Pushing garbage for " .. (isChain and "chain" or "combo") .. " with " .. comboSize .. " panels")
+ logger.debug("P" .. self.which .. "@" .. self.stopWatch .. ": Pushing garbage for " .. (isChain and "chain" or "combo") .. " with " .. comboSize .. " panels")
for i = 3, metalCount do
self.outgoingGarbage:push({
width = 6,
height = 1,
isMetal = true,
isChain = false,
- frameEarned = self.game_stopwatch,
+ frameEarned = self.stopWatch,
rowEarned = coordinate.row,
colEarned = coordinate.column
})
@@ -779,7 +775,7 @@ function Stack:pushGarbage(coordinate, isChain, comboSize, metalCount)
height = 1,
isMetal = false,
isChain = false,
- frameEarned = self.game_stopwatch,
+ frameEarned = self.stopWatch,
rowEarned = coordinate.row,
colEarned = coordinate.column
})
@@ -791,7 +787,7 @@ function Stack:pushGarbage(coordinate, isChain, comboSize, metalCount)
-- If we did a combo also, we need to enqueue the attack graphic one row higher cause thats where the chain card will be.
rowOffset = 1
end
- self.outgoingGarbage:addChainLink(self.game_stopwatch, coordinate.column, coordinate.row + rowOffset)
+ self.outgoingGarbage:addChainLink(self.stopWatch, coordinate.column, coordinate.row + rowOffset)
end
end
diff --git a/common/tests/engine/GarbageQueueTestingUtils.lua b/common/tests/engine/GarbageQueueTestingUtils.lua
index c0bca785..343ccfc1 100644
--- a/common/tests/engine/GarbageQueueTestingUtils.lua
+++ b/common/tests/engine/GarbageQueueTestingUtils.lua
@@ -49,6 +49,7 @@ function GarbageQueueTestingUtils.createMatch(stackHealth, attackFile)
return match
end
+---@param match Match
function GarbageQueueTestingUtils.runToFrame(match, frame)
local stack = match.stacks[1]
while stack.clock < frame do
@@ -61,6 +62,7 @@ function GarbageQueueTestingUtils.runToFrame(match, frame)
end
-- clears panels until only "count" rows are left
+---@param stack Stack|SimulatedStack
function GarbageQueueTestingUtils.reduceRowsTo(stack, count)
for row = #stack.panels, count + 1 do
for col = 1, stack.width do
@@ -70,12 +72,14 @@ function GarbageQueueTestingUtils.reduceRowsTo(stack, count)
end
-- fill up panels with non-matching panels until "count" rows are filled
+---@param stack Stack|SimulatedStack
function GarbageQueueTestingUtils.fillRowsTo(stack, count)
+ ---@cast stack Stack
for row = 1, count do
if not stack.panels[row] then
stack.panels[row] = {}
for col = 1, stack.width do
- stack.createPanelAt(row, col)
+ stack:createPanelAt(row, col)
end
end
for col = 1, stack.width do
@@ -84,17 +88,22 @@ function GarbageQueueTestingUtils.fillRowsTo(stack, count)
end
end
+---@param stack Stack|SimulatedStack
function GarbageQueueTestingUtils.simulateActivity(stack)
+ ---@diagnostic disable-next-line: duplicate-set-field
stack.hasActivePanels = function() return true end
end
+---@param stack Stack|SimulatedStack
function GarbageQueueTestingUtils.simulateInactivity(stack)
+ ---@diagnostic disable-next-line: duplicate-set-field
stack.hasActivePanels = function() return false end
end
+---@param stack Stack|SimulatedStack
function GarbageQueueTestingUtils.sendGarbage(stack, width, height, chain, metal, time)
-- -1 cause this will get called after the frame ended instead of during the frame
- local frameEarned = time or stack.game_stopwatch
+ local frameEarned = time or stack.stopWatch
local isChain = chain or false
local isMetal = metal or false
diff --git a/common/tests/engine/StackReplayTests.lua b/common/tests/engine/StackReplayTests.lua
index cbedbafc..9dcb158a 100644
--- a/common/tests/engine/StackReplayTests.lua
+++ b/common/tests/engine/StackReplayTests.lua
@@ -139,7 +139,7 @@ local function basicTimeAttackTest()
assert(match.engineVersion == consts.ENGINE_VERSIONS.TELEGRAPH_COMPATIBLE)
assert(match.timeLimit ~= nil)
assert(match.panelSource.seed == 3490465)
- assert(match.stacks[1].game_stopwatch == 7200)
+ assert(match.stacks[1].stopWatch == 7200)
assert(match.stacks[1].levelData.maxHealth == 1)
assert(match.stacks[1].score == 10353)
assert(tableUtils.count(match.stacks[1].outgoingGarbage.history, function(g) return g.isChain end) == 8)
@@ -373,7 +373,7 @@ local function fallingWhileHoverBeginsDoesNotChain()
end
local function platformTest(waitFrames, useMatchSide)
- local puzzle = Puzzle({puzzleType = "chain", stack = "3000994339949999994999999999999999999999999999999999", stopTime = 60})
+ local puzzle = Puzzle({puzzleType = Puzzle.PUZZLE_TYPES.chain, stack = "3000994339949999994999999999999999999999999999999999", stopTime = 60})
local match = StackReplayTestingUtils.createSinglePlayerMatch(puzzle:toGameMode(), puzzle:toPanelSource(), "controller", LevelPresets.getModern(10))
local stack = match.stacks[1]
---@cast stack Stack
diff --git a/common/tests/engine/StackRollbackReplayTests.lua b/common/tests/engine/StackRollbackReplayTests.lua
index 27068050..a26012a6 100644
--- a/common/tests/engine/StackRollbackReplayTests.lua
+++ b/common/tests/engine/StackRollbackReplayTests.lua
@@ -196,6 +196,18 @@ local function rollbackFromDeath()
assert(stack.game_over_clock == 652)
end
+local function liveDesync()
+ local match = StackReplayTestingUtils:setupReplayWithPath(testReplayFolder .. "v046-2023-01-28-02-39-32-JamBox-L10-vs-Galadic97-L10-Casual-P1wins.txt")
+ match.debug.vsFramesBehind = 120
+
+ StackReplayTestingUtils:fullySimulateMatch(match)
+
+ assert(match.ended and not match.aborted)
+ assert(not match:isIrrecoverablyDesynced())
+ assert(match.stacks[1].rollbackCount == 5)
+ assert(match.gameOverClock == 2039)
+end
+
logger.info("running rollbackFromDeath")
rollbackFromDeath()
@@ -206,4 +218,7 @@ logger.info("running rollbackNotPastAttackTest")
rollbackNotPastAttackTest()
logger.info("running rollbackFullyPastAttack")
-rollbackFullyPastAttack()
\ No newline at end of file
+rollbackFullyPastAttack()
+
+logger.info("running liveDesync1")
+liveDesync()
\ No newline at end of file
diff --git a/common/tests/engine/StackTests.lua b/common/tests/engine/StackTests.lua
index 690a1fab..b64ee7fd 100644
--- a/common/tests/engine/StackTests.lua
+++ b/common/tests/engine/StackTests.lua
@@ -7,7 +7,7 @@ local TestUtils = require("common.tests.TestUtils")
local function puzzleTest()
-- to stop rising
- local puzzle = Puzzle({puzzleType = "moves", moves = 1, stack = "011010"})
+ local puzzle = Puzzle({puzzleType = Puzzle.PUZZLE_TYPES.moves, moves = 1, stack = "011010"})
local match = StackReplayTestingUtils.createSinglePlayerMatch(puzzle:toGameMode(), puzzle:toPanelSource())
local stack = match.stacks[1]
---@cast stack Stack
@@ -27,7 +27,7 @@ end
puzzleTest()
local function clearPuzzleTest()
- local puzzle = Puzzle({puzzleType = "clear", stack = "[============================][====]246260[====]600016514213466313451511124242", stopTime = 60})
+ local puzzle = Puzzle({puzzleType = Puzzle.PUZZLE_TYPES.clear, stack = "[============================][====]246260[====]600016514213466313451511124242", stopTime = 60})
local match = StackReplayTestingUtils.createSinglePlayerMatch(puzzle:toGameMode(), puzzle:toPanelSource())
local stack = match.stacks[1]
---@cast stack Stack
@@ -141,7 +141,7 @@ testShakeFrames()
local function swapStalling1Test1()
- local puzzle = Puzzle({puzzleType = "clear", stack = "[======================][====]246260[====]600016514213461336451511124242"})
+ local puzzle = Puzzle({puzzleType = Puzzle.PUZZLE_TYPES.clear, stack = "[======================][====]246260[====]600016514213461336451511124242"})
local match = StackReplayTestingUtils.createSinglePlayerMatch(puzzle:toGameMode(), puzzle:toPanelSource(), "controller", LevelPresets.getModern(10))
local stack = match.stacks[1]
---@cast stack Stack