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