mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aad86fb351 |
@@ -42,8 +42,10 @@ Game Boy equivalent:
|
|||||||
rows above the player recede and rows below come toward the viewer. Only
|
rows above the player recede and rows below come toward the viewer. Only
|
||||||
things that actually *stand* on the ground draw as upright billboards,
|
things that actually *stand* on the ground draw as upright billboards,
|
||||||
unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
|
unscaled and pixel-identical to flat mode: the player, NPCs, item balls,
|
||||||
and the screen-anchored FX attached to them (heal machine glow, emote
|
and the standing FX attached to them (emote bubbles, the fishing rod,
|
||||||
bubbles, the fishing rod, the FLY bird). An earlier revision tried
|
the FLY bird). The Poké Center heal-machine overlay stays on the ground
|
||||||
|
plane with the machine tiles (it is OAM glued to a BG graphic, not a
|
||||||
|
standing sprite). An earlier revision tried
|
||||||
billboarding buildings/trees/signs too (cutting them out of the ground
|
billboarding buildings/trees/signs too (cutting them out of the ground
|
||||||
per hand-curated per-tileset tables); that chased an endless tail of
|
per hand-curated per-tileset tables); that chased an endless tail of
|
||||||
special cases, dense tree canopy, fences fused into grass, building
|
special cases, dense tree canopy, fences fused into grass, building
|
||||||
|
|||||||
@@ -230,6 +230,25 @@ function ChipAudio.ensureMusicPlaying()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Threaded playMusic returns an empty QueueableSource and only calls
|
||||||
|
-- Source:play once the first worker buffer lands (~1 frame later). Until
|
||||||
|
-- then Source:isPlaying is false -- callers that treat that as "song over"
|
||||||
|
-- (Music.oneShotPlaying / pendingRestore) must wait here instead, or a
|
||||||
|
-- playOnce jingle like Music_PkmnHealed is cut off before it starts.
|
||||||
|
local forceAwaitingFirstBuffer -- test-only override (see _simulate*)
|
||||||
|
|
||||||
|
function ChipAudio.awaitingFirstBuffer()
|
||||||
|
if forceAwaitingFirstBuffer then return true end
|
||||||
|
local m = currentMusic
|
||||||
|
if not (m and m.threaded and not m.started and not m.finished) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
-- a dead worker will never deliver the first buffer
|
||||||
|
if workerReady == false then return false end
|
||||||
|
if worker and worker.getError and worker:getError() then return false end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
function ChipAudio.stopMusic()
|
function ChipAudio.stopMusic()
|
||||||
if currentMusic and currentMusic.source then
|
if currentMusic and currentMusic.source then
|
||||||
pcall(currentMusic.source.stop, currentMusic.source)
|
pcall(currentMusic.source.stop, currentMusic.source)
|
||||||
@@ -240,6 +259,7 @@ function ChipAudio.stopMusic()
|
|||||||
end
|
end
|
||||||
pendingBuf = nil
|
pendingBuf = nil
|
||||||
currentMusic = nil
|
currentMusic = nil
|
||||||
|
forceAwaitingFirstBuffer = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- hot reload: the next play re-reads programs.bin (a mod may have swapped the
|
-- hot reload: the next play re-reads programs.bin (a mod may have swapped the
|
||||||
@@ -302,6 +322,20 @@ end
|
|||||||
-- test hooks (headless): synchronous synthesis straight through ChipSynth
|
-- test hooks (headless): synchronous synthesis straight through ChipSynth
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Force the "threaded, first buffer not yet queued" window so Music's
|
||||||
|
-- playOnce / pendingRestore race can be asserted without love.thread.
|
||||||
|
-- Returns a clear() that drops the override (call after the assertion).
|
||||||
|
function ChipAudio._simulateAwaitingFirstBufferForTest()
|
||||||
|
local m = currentMusic
|
||||||
|
if not m or not m.source then return nil end
|
||||||
|
m.threaded = true
|
||||||
|
m.started = false
|
||||||
|
m.finished = false
|
||||||
|
pcall(function() m.source.playing = false end)
|
||||||
|
forceAwaitingFirstBuffer = true
|
||||||
|
return function() forceAwaitingFirstBuffer = nil end
|
||||||
|
end
|
||||||
|
|
||||||
function ChipAudio._renderMusicForTest(data, header, seconds)
|
function ChipAudio._renderMusicForTest(data, header, seconds)
|
||||||
local engine = ChipSynth.newEngine(data, header, { allowLoops = true })
|
local engine = ChipSynth.newEngine(data, header, { allowLoops = true })
|
||||||
return ChipSynth.soundData(engine, math.floor(seconds * SAMPLE_RATE), 2)
|
return ChipSynth.soundData(engine, math.floor(seconds * SAMPLE_RATE), 2)
|
||||||
|
|||||||
+14
-1
@@ -254,6 +254,7 @@ function Music.stop()
|
|||||||
require("src.core.ChipAudio").stopMusic()
|
require("src.core.ChipAudio").stopMusic()
|
||||||
state.current, state.source, state.loopSource, state.fade = nil, nil, nil, nil
|
state.current, state.source, state.loopSource, state.fade = nil, nil, nil, nil
|
||||||
state.chip = false
|
state.chip = false
|
||||||
|
state.pendingRestore = nil
|
||||||
if previous and Runtime.wants("music.stopped") then
|
if previous and Runtime.wants("music.stopped") then
|
||||||
Runtime.emit("music.stopped", { song = previous })
|
Runtime.emit("music.stopped", { song = previous })
|
||||||
end
|
end
|
||||||
@@ -347,14 +348,24 @@ end
|
|||||||
function Music.playOnce(data, song)
|
function Music.playOnce(data, song)
|
||||||
if not songDef(data, song) then return false end
|
if not songDef(data, song) then return false end
|
||||||
Music.play(data, song, false, { reason = "once" })
|
Music.play(data, song, false, { reason = "once" })
|
||||||
|
-- play() can no-op (hook silence, failed def); only arm restore when
|
||||||
|
-- the jingle actually became current
|
||||||
|
if state.current ~= song then return false end
|
||||||
state.pendingRestore = true
|
state.pendingRestore = true
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function chipAwaitingFirstBuffer()
|
||||||
|
return state.chip
|
||||||
|
and require("src.core.ChipAudio").awaitingFirstBuffer()
|
||||||
|
end
|
||||||
|
|
||||||
-- is a playOnce jingle still sounding? (AnimateHealingMachine's
|
-- is a playOnce jingle still sounding? (AnimateHealingMachine's
|
||||||
-- .waitLoop2 holds the healing machine until MUSIC_PKMN_HEALED ends)
|
-- .waitLoop2 holds the healing machine until MUSIC_PKMN_HEALED ends)
|
||||||
function Music.oneShotPlaying()
|
function Music.oneShotPlaying()
|
||||||
if not state.pendingRestore then return false end
|
if not state.pendingRestore then return false end
|
||||||
|
-- threaded chip songs start silent for ~1 frame; that gap is not "over"
|
||||||
|
if chipAwaitingFirstBuffer() then return true end
|
||||||
local src = state.source
|
local src = state.source
|
||||||
if not src then return false end
|
if not src then return false end
|
||||||
local ok, playing = pcall(src.isPlaying, src)
|
local ok, playing = pcall(src.isPlaying, src)
|
||||||
@@ -441,8 +452,10 @@ function Music.update(data)
|
|||||||
state.source = loopSrc
|
state.source = loopSrc
|
||||||
pcall(loopSrc.play, loopSrc)
|
pcall(loopSrc.play, loopSrc)
|
||||||
end
|
end
|
||||||
|
-- do not treat "threaded source still waiting on its first buffer" as
|
||||||
|
-- ended, or playOnce jingles get restored over before they can sound
|
||||||
if state.pendingRestore and sourceStopped(state.source)
|
if state.pendingRestore and sourceStopped(state.source)
|
||||||
and not state.loopSource then
|
and not state.loopSource and not chipAwaitingFirstBuffer() then
|
||||||
Music.restoreMap(data)
|
Music.restoreMap(data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3415,8 +3415,11 @@ function OverworldState:drawWorld()
|
|||||||
love.graphics.setShader(shader)
|
love.graphics.setShader(shader)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local ox = ha.px - 64 - cam.x
|
-- TileRenderer windows with -floor(cam), so the overlay must use the
|
||||||
local oy = ha.py - 64 - cam.y
|
-- same snap or a fractional camera (odd fill/tilt view sizes) parks
|
||||||
|
-- the balls a pixel off the machine tiles
|
||||||
|
local ox = ha.px - 64 - math.floor(cam.x)
|
||||||
|
local oy = ha.py - 64 - math.floor(cam.y)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(img, self.healMachineQuads[1], ox + 44, oy + 20)
|
love.graphics.draw(img, self.healMachineQuads[1], ox + 44, oy + 20)
|
||||||
for i = 1, math.min(ha.lit, #HEAL_BALL_XY) do
|
for i = 1, math.min(ha.lit, #HEAL_BALL_XY) do
|
||||||
@@ -3632,11 +3635,13 @@ function OverworldState:drawWorld()
|
|||||||
else
|
else
|
||||||
-- === TILT PATH: ground-hugging FX stay on the projected ground, all
|
-- === TILT PATH: ground-hugging FX stay on the projected ground, all
|
||||||
-- standing things billboard upright over it in a separate pass. ======
|
-- standing things billboard upright over it in a separate pass. ======
|
||||||
-- Dust is ground-hugging smoke -> ground canvas (puts it
|
-- Dust / cut / the Poké Center heal overlay hug the BG (the heal
|
||||||
-- with the flat layer, so it projects with the ground). Flat mode
|
-- machine is a tileset graphic; its OAM balls must ride that plane or
|
||||||
-- draws it last, over the sprites, in the same canvas; here the two
|
-- they float off the machine once the ground foreshortens). Flat mode
|
||||||
|
-- draws them last, over the sprites, in the same canvas; here the two
|
||||||
-- layers are separate and composited ground-under-upright, so drawing
|
-- layers are separate and composited ground-under-upright, so drawing
|
||||||
-- it now into the still-active ground canvas is order-equivalent.
|
-- them now into the still-active ground canvas is order-equivalent.
|
||||||
|
fxHeal()
|
||||||
fxDust()
|
fxDust()
|
||||||
fxCutTree()
|
fxCutTree()
|
||||||
|
|
||||||
@@ -3692,18 +3697,11 @@ function OverworldState:drawWorld()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Screen-anchored world FX : each billboards at the
|
-- Standing world FX: each billboards at the ground foot of the
|
||||||
-- ground foot of the character it belongs to, so it stands upright and
|
-- character it belongs to, so it stays upright over the tilted ground.
|
||||||
-- scales with that character's depth.
|
|
||||||
-- heal machine -> the healed player's foot (the machine stands on
|
|
||||||
-- the ground in front of where the player was)
|
|
||||||
-- emote bubble -> the spotting NPC's foot (rides above its head)
|
-- emote bubble -> the spotting NPC's foot (rides above its head)
|
||||||
-- fly bird, rod -> the player's foot
|
-- fly bird, rod -> the player's foot
|
||||||
if self.healAnim then
|
-- (heal machine is ground-hugging -- drawn above with dust/cut)
|
||||||
local fx = self.healAnim.px - cam.x + 8
|
|
||||||
local fy = self.healAnim.py - cam.y + 16
|
|
||||||
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false, fxHeal)
|
|
||||||
end
|
|
||||||
if self.emote and self.emote.npc then
|
if self.emote and self.emote.npc then
|
||||||
local fx = self.emote.npc.px - cam.x + 8
|
local fx = self.emote.npc.px - cam.x + 8
|
||||||
local fy = self.emote.npc.py - cam.y + 16
|
local fy = self.emote.npc.py - cam.y + 16
|
||||||
|
|||||||
@@ -344,6 +344,24 @@ check(lastSource().queueable and lastSource().playing,
|
|||||||
"a chip song still plays after a file song")
|
"a chip song still plays after a file song")
|
||||||
check(not body.playing, "the outgoing file song was stopped")
|
check(not body.playing, "the outgoing file song was stopped")
|
||||||
|
|
||||||
|
-- playOnce must survive the threaded "empty QueueableSource" window:
|
||||||
|
-- Source:isPlaying is false until the first worker buffer lands, and that
|
||||||
|
-- gap must not look like the jingle already ended (Poké Center heal).
|
||||||
|
data = reset(fixtureData())
|
||||||
|
Music.playMap(data, "PALLET_TOWN", false, false)
|
||||||
|
check(Music.playOnce(data, "Music_Chip"), "playOnce starts a chip jingle")
|
||||||
|
local jingle = lastSource()
|
||||||
|
local clearAwait = ChipAudio._simulateAwaitingFirstBufferForTest()
|
||||||
|
check(clearAwait ~= nil, "test can force the awaiting-first-buffer window")
|
||||||
|
check(Music.oneShotPlaying(),
|
||||||
|
"oneShotPlaying stays true while the first buffer is still in flight")
|
||||||
|
Music.update(data)
|
||||||
|
check(jingle == lastSource() and jingle.queueable,
|
||||||
|
"pendingRestore does not swap the map theme over a pending chip jingle")
|
||||||
|
check(ChipAudio.awaitingFirstBuffer(),
|
||||||
|
"awaitingFirstBuffer reports the forced window")
|
||||||
|
clearAwait()
|
||||||
|
|
||||||
-- sfx shape dispatch
|
-- sfx shape dispatch
|
||||||
check(Sound.play(data, "Beep") == nil, "Sound.play returns nothing")
|
check(Sound.play(data, "Beep") == nil, "Sound.play returns nothing")
|
||||||
check(lastSource().file == "assets/beep.wav", "a bare string sfx is a static source")
|
check(lastSource().file == "assets/beep.wav", "a bare string sfx is a static source")
|
||||||
|
|||||||
Reference in New Issue
Block a user