Files
gen1recomp/tests/engine/fanfare_speed_bug1952_test.lua

171 lines
5.3 KiB
Lua

-- home/delay.asm:15
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local check, eq = T.check, T.eq
local Data = T.fixtures.fresh()
require("src.render.Font").load(Data)
local Sound = require("src.core.Sound")
local BattleState = require("src.battle.BattleState")
eq(Sound.rate(), 1, "the rate starts at 1")
Sound.setRate(2)
eq(Sound.rate(), 2, "2X sets the one-shot playback rate to 2")
Sound.setRate(100)
eq(Sound.rate(), 4, "a huge speed clamps to FF_PITCH_MAX")
Sound.setRate(nil)
eq(Sound.rate(), 1, "nil falls back to 1")
Sound.setRate(-1)
eq(Sound.rate(), 1, "a negative speed falls back to 1")
Sound.setRate(0)
eq(Sound.rate(), 1, "zero falls back to 1")
Sound.setRate(0 / 0)
eq(Sound.rate(), 1, "NaN falls back to 1")
Sound.setRate(1)
local function stub(dur, pitch, playing)
return {
playing = playing ~= false,
getDuration = function() return dur end,
getPitch = function() return pitch end,
isPlaying = function(self) return self.playing end,
stop = function(self) self.playing = false end,
}
end
eq(Sound.waitFrames(nil), 0, "no source is no wait")
eq(Sound.waitFrames(stub(2, 1)), 122, "a 2s sfx at pitch 1 is 120 frames plus margin")
eq(Sound.waitFrames(stub(2, 2)), 62, "pitched to 2X it takes half as many frames")
eq(Sound.waitFrames(stub(2, 4)), 32, "and a quarter at the 4X clamp")
Sound.setRate(4)
eq(Sound.waitFrames(stub(2, 4)), 122,
"at rate 4 a 4X-pitched jingle still spans the same 120 logic frames")
eq(Sound.waitFrames(stub(2, 1)), 482,
"a source the rate could not pitch costs proportionally more frames")
Sound.setRate(1)
local broken = { getDuration = function() error("no duration") end }
eq(Sound.waitFrames(broken), 180, "a source with no usable duration gets the fallback")
eq(Sound.waitFrames(broken, 90), 90, "and the caller may pick the fallback")
eq(Sound.waitFrames({}), 180, "so does a source with no getDuration at all")
local function gate(src)
local st = setmetatable({
waitingSound = src,
queue = {},
game = { stack = { top = function() return nil end } },
}, BattleState)
return st
end
local stuck = stub(1, 1)
local st = gate(stuck)
local budget = Sound.waitFrames(stuck)
local held = 0
for _ = 1, budget + 200 do
if not st:updateQueue() then break end
held = held + 1
end
check(stuck.playing == false, "the budget stops a source that never ends on its own")
eq(st.waitingSound, nil, "and releases the queue")
eq(held, budget - 1, "after exactly the frame budget, not forever")
local short = stub(1, 1)
local st2 = gate(short)
check(st2:updateQueue(), "the gate holds while the sfx sounds")
short.playing = false
check(st2:updateQueue() == false, "and releases the frame the sfx goes quiet")
eq(st2.waitSoundLeft, nil, "the budget is cleared with the source")
local function gate4(src)
local st4 = gate(src)
st4.game.logicSpeed = function() return 4 end
return st4
end
local natural = stub(1, 1)
local stopped = false
natural.stop = function(self) stopped = true; self.playing = false end
local steps = 0
natural.isPlaying = function(self)
return self.playing and steps < 240
end
local st3 = gate4(natural)
local held3 = 0
for _ = 1, 1000 do
if not st3:updateQueue() then break end
steps = steps + 1
held3 = held3 + 1
end
check(not stopped, "at 4X a source that plays its full length is never stopped early")
eq(held3, 240, "the gate holds every logic step of the sound's real length")
eq(st3.waitingSound, nil, "and releases when the sound goes quiet on its own")
local stuck4 = stub(1, 1)
local st4 = gate4(stuck4)
local held4 = 0
for _ = 1, budget * 4 + 200 do
if not st4:updateQueue() then break end
held4 = held4 + 1
end
check(stuck4.playing == false, "at 4X the safety stop still lands on a stuck source")
eq(held4, budget * 4 - 1, "after the same wall time, four times the logic steps")
local quiet4 = stub(1, 1)
local st5 = gate4(quiet4)
check(st5:updateQueue(), "at 4X the gate holds while the sfx sounds")
quiet4.playing = false
check(st5:updateQueue() == false, "and releases the step the sfx goes quiet")
local nanSpeed = gate(stub(1, 1))
nanSpeed.game.logicSpeed = function() return 0 / 0 end
check(nanSpeed:updateQueue(), "a NaN speed counts as 1X")
eq(nanSpeed.waitSoundLeft, budget - 1, "and decrements by a whole frame")
local Game = require("src.core.Game")
local Game2 = require("src.core.Game2")
require("src.core.FixedStep"):init(function() end)
local function gen1(speed)
return setmetatable({
speedOverride = speed,
audioAccum = 0,
stack = { top = function() return nil end },
logicSpeed = function(self)
return require("src.core.GameSpeed").clamp(self.speedOverride or 1)
end,
updateSync = function() end,
}, { __index = Game })
end
local function gen2(speed)
return setmetatable({
phase = "boot",
speedOverride = speed,
audioAccum = 0,
data = {},
stack = { top = function() return nil end },
}, { __index = Game2 })
end
Sound.setRate(1)
gen1(4):update(0)
eq(Sound.rate(), 1, "a Gen 1 frame does not pitch SFX from GAME SPEED")
gen2(4):update(0)
eq(Sound.rate(), 1, "and neither does a Gen 2 frame")
gen2(100):update(0)
eq(Sound.rate(), 1, "even at a clamped 200X logic multiplier")
Sound.setRate(4)
require("src.core.SessionLifecycle").endGameSession(nil)
eq(Sound.rate(), 1, "session teardown resets the rate for the next boot")
T.finish("fanfare_speed_bug1952")