Files
gen1recomp/tests/drivers/pet_cries_bug1687_1649_test.lua
T
bryanthaboi 1905261c5b Spider Car Unleashed
CLOSES #1483, CLOSES #1610, CLOSES #1615, CLOSES #1646, CLOSES #1649, CLOSES #1651, CLOSES #1653, CLOSES #1656, CLOSES #1683, CLOSES #1685, CLOSES #1686, CLOSES #1687, CLOSES #1688, CLOSES #1689, CLOSES #1690, CLOSES #1693, CLOSES #1694, CLOSES #1695, CLOSES #1696, CLOSES #1702, CLOSES #1704, CLOSES #1705, CLOSES #1706, CLOSES #1707, CLOSES #1708, CLOSES #1710, CLOSES #1711, CLOSES #1712, CLOSES #1713, CLOSES #1716, CLOSES #1717, CLOSES #1718, CLOSES #1719, CLOSES #1720, CLOSES #1721, CLOSES #1725, CLOSES #1732, CLOSES #1745, CLOSES #1748, CLOSES #1749, CLOSES #1751, CLOSES #1754
2026-08-24 07:52:05 -04:00

304 lines
12 KiB
Lua

-- Talks to all six pet Pokemon for you and lets each one cry: #1687
-- (S.S. Anne WIGGLYTUFF + MACHOKE) and #1649 (Vermilion PIDGEY, Vermilion
-- City MACHOP, Fan Club PIKACHU + SEEL). pokered/scripts/*.asm, e.g.
-- VermilionPidgeyHouse.asm:15 text_far then PlayCry at :19. Never add
-- POKEPORT_SPEED: only the logic clock scales, so the cry lands in the
-- wrong place relative to the typing this run exists to judge.
-- POKEPORT_DRIVER=tests/drivers/pet_cries_bug1687_1649_test.lua POKEPORT_TOUCH=0 POKEPORT_VERSION=red POKEPORT_IDENTITY=petcries SHOT_DIR=/tmp/petcries love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local TextBox = require("src.render.TextBox")
local Runtime = require("src.mods.Runtime")
local GameVersion = require("src.core.GameVersion")
local init = require("data.scripts.init")
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local yellow = GameVersion.isYellow()
local failures = 0
local function check(label, ok)
if not ok then failures = failures + 1 end
U.log(ok and "PASS" or "FAIL", label)
return ok
end
-- own tick counter: the ordering claim is "cry after the last character",
-- so the cry and the end of typing have to be stamped on the same clock
local ticks = 0
local function step() ticks = ticks + 1; coroutine.yield() end
local function waitTicks(n) for _ = 1, n do step() end end
local function tap(btn) U.tap(game, btn); ticks = ticks + 1 end
-- sound.played is the engine's own cue feed (src/core/Sound.lua played()),
-- which is the only way a script can tell "no cry" from "a cry nobody
-- could hear"; both sound identical on a quiet machine.
local cries = {}
local heard = false
if type(Runtime.events.on) == "function" then
Runtime.events:on("sound.played", function(p)
if p and p.kind == "cry" then
heard = true
cries[#cries + 1] = { name = p.name, species = p.species, tick = ticks }
end
end, 0, "driver")
end
-- pokered/data/maps/objects, all cell coordinates: VermilionPidgeyHouse
-- PIDGEY (3,5), VermilionCity MACHOP (29,9), PokemonFanClub PIKACHU (6,4)
-- and SEEL (1,4), SSAnne1FRooms WIGGLYTUFF (3,11), SSAnneB1FRooms MACHOKE
-- (11,12). The two S.S. Anne rooms maps slide their objects between
-- cabins at load (src/world/SsAnneLayout.lua), so every position below is
-- read back off the live map instead of trusted from the list.
local PETS = {
{ map = "VERMILION_PIDGEY_HOUSE", object = "VERMILIONPIDGEYHOUSE_PIDGEY",
const = "TEXT_VERMILIONPIDGEYHOUSE_PIDGEY", species = "PIDGEY",
label = "_VermilionPidgeyHousePidgeyText",
control = "VERMILIONPIDGEYHOUSE_YOUNGSTER" },
{ map = "SS_ANNE_1F_ROOMS", object = "SSANNE1FROOMS_WIGGLYTUFF",
const = "TEXT_SSANNE1FROOMS_WIGGLYTUFF", species = "WIGGLYTUFF",
label = "_SSAnne1FRoomsWigglytuffText",
control = "SSANNE1FROOMS_LITTLE_GIRL" },
{ map = "SS_ANNE_B1F_ROOMS", object = "SSANNEB1FROOMS_MACHOKE",
const = "TEXT_SSANNEB1FROOMS_MACHOKE", species = "MACHOKE",
label = "_SSAnneB1FRoomsMachokeText" },
{ map = "POKEMON_FAN_CLUB", object = "POKEMONFANCLUB_SEEL",
const = "TEXT_POKEMONFANCLUB_SEEL", species = "SEEL",
label = "_PokemonFanClubSeelText" },
-- two boxes, cry on the first (VermilionCity.asm:224, PlayCry at :228)
{ map = "VERMILION_CITY", object = "VERMILIONCITY_MACHOP",
const = "TEXT_VERMILIONCITY_MACHOP", species = "MACHOP",
label = "_VermilionCityMachopText", extraBoxes = 1 },
}
if yellow then
-- pokeyellow/data/maps/objects/PokemonFanClub.asm: the pet is a CLEFAIRY
PETS[#PETS + 1] = { map = "POKEMON_FAN_CLUB",
object = "POKEMONFANCLUB_CLEFAIRY",
const = "TEXT_POKEMONFANCLUB_CLEFAIRY", species = "CLEFAIRY",
label = "_PokemonFanClubClefairyText" }
else
PETS[#PETS + 1] = { map = "POKEMON_FAN_CLUB",
object = "POKEMONFANCLUB_PIKACHU",
const = "TEXT_POKEMONFANCLUB_PIKACHU", species = "PIKACHU",
label = "_PokemonFanClubPikachuText" }
end
-- ---------------------------------------------------------------- checks
-- Everything below produces the same silence the bug did, so it is worth
-- knowing before anyone bothers listening.
local vol = game.save and game.save.options and game.save.options.sfxVol
U.log("sfx volume is", tostring(vol), "of 7")
if not vol or vol == 0 then
check("sfx volume is above zero (a muted run sounds exactly like the bug)",
false)
end
if yellow and game.save.options.pikaVol == 0 then
U.log("pikaVol is 0: Yellow's Pikachu clips are muted independently")
end
for _, pet in ipairs(PETS) do
local tag = pet.species .. " on " .. pet.map
local script = init.talkScript(pet.map, pet.const)
if check(tag .. " has a ported talk script", type(script) == "table") then
local cryAt
for i, row in ipairs(script) do
if type(row) == "table" and row[1] == "play_cry" then cryAt = i end
end
if check(tag .. " has a play_cry row", cryAt ~= nil) then
check(tag .. " cries " .. pet.species, script[cryAt][2] == pet.species)
check(tag .. " uses the waitForButton form", script[cryAt][3] == true)
local next_ = script[cryAt + 1]
check(tag .. " arms " .. pet.label .. " on the very next row",
type(next_) == "table" and next_[1] == "show_text"
and next_[2] == pet.label)
end
end
check(tag .. " has a cry program in this cache",
game.data.audio.cries and game.data.audio.cries[pet.species] ~= nil)
check(pet.label .. " resolves to text",
type(game.data.text[pet.label]) == "string")
end
-- Sound.playCry sends PIKACHU to the PCM clips whenever the cache carries
-- them; that voiced "Pikachuuu" is Yellow's, and hearing it in Red is the
-- failure #1649 warned about up front.
if not yellow then
check("this cache has no Pikachu voice clips (Red must get the chip cry)",
game.data.audio.pikaCries == nil)
end
if failures > 0 then
U.log(failures, "checks failed above, so do not bother listening yet;")
U.log("a missing row or an unresolved label sounds exactly like the bug.")
end
-- ---------------------------------------------------------------- walking
local function findNpc(ow, name)
for _, n in ipairs(ow.npcs or {}) do
if n.def and n.def.name == name then return n end
end
return nil
end
-- {dx, dy, facing}: the offset from the pet to the cell to stand on plus
-- the direction that looks back at it, so +1 on y means stand below.
local SIDES = {
{ 0, 1, "up" }, { 0, -1, "down" }, { 1, 0, "left" }, { -1, 0, "right" },
}
local function approach(ow, npc)
for _, s in ipairs(SIDES) do
local cx, cy = npc.cellX + s[1], npc.cellY + s[2]
if ow.map:isWalkableCell(cx, cy) and not ow:npcAtCell(cx, cy) then
return cx, cy, s[3]
end
end
return nil
end
-- PIDGEY and MACHOP are WALK objects and drift off their spawn cell, so
-- pin them once the final teleport has rebuilt the npc list.
local function pin(npc)
npc.wanders = false
npc:resetToSpawn()
npc.wanders = false
end
local function standAt(mapId, objName)
local def
for _, o in ipairs((game.data.maps[mapId] or {}).objects or {}) do
if o.name == objName then def = o end
end
if not def then return nil end
-- first hop just mounts the map so the live cells can be read
U.teleport(game, mapId, def.x, math.max(0, def.y + 1), "up")
waitTicks(6)
local ow = game.overworld
local npc = ow and findNpc(ow, objName)
if not npc then return nil end
pin(npc)
local cx, cy, facing = approach(ow, npc)
if not cx then return nil end
U.teleport(game, mapId, cx, cy, facing)
waitTicks(6)
ow = game.overworld
npc = findNpc(ow, objName)
if not npc then return nil end
pin(npc)
local fx, fy = ow.player:facingCell()
if ow:npcAtCell(fx, fy) ~= npc then return nil end
return npc
end
-- Opens the box, holds still while it types and cries, then reports where
-- the cry landed against the last typed character.
local function talk(name)
local before = #cries
tap("a")
local box, armed, doneTick
for _ = 1, 300 do
local top = game.stack:top()
if getmetatable(top) == TextBox then
box = top
-- box.auto is cleared the moment the cry finishes, so snapshot it
if armed == nil then armed = top.auto or false end
if top.done and not doneTick then doneTick = ticks end
end
if doneTick and #cries > before then break end
step()
end
if not check(name .. ": pressing A opened a text box", box ~= nil) then
return nil
end
local lines = {}
for _, page in ipairs(box.pages or {}) do
for _, line in ipairs(page) do lines[#lines + 1] = line end
end
U.log("box reads:", table.concat(lines, " / "))
return box, armed, doneTick, before
end
U.log("about to walk to each pet and talk to it; turn the volume up now.")
waitTicks(180)
for _, pet in ipairs(PETS) do
local tag = pet.species .. " on " .. pet.map
U.log("walking to the", pet.species)
local npc = standAt(pet.map, pet.object)
if not check(tag .. ": standing face to face with it", npc ~= nil) then
U.log("skipping", pet.species, "-- could not reach it")
else
local box, armed, doneTick, before = talk(tag)
if box then
-- the dropped `true` shows up here: without it the box pops itself
-- when the cry ends and never blinks its arrow
check(tag .. ": the box was armed with a cry",
type(armed) == "table" and type(armed.sound) == "function")
check(tag .. ": the cry keeps the A/B wait (waitForButton)",
type(armed) == "table" and armed.wait == true)
local fired = cries[before + 1]
if check(tag .. ": a cry actually played", fired ~= nil) then
check(tag .. ": the cry was " .. pet.species,
fired.species == pet.species)
-- near miss: a cry that fires as the line starts typing
check(tag .. ": it played after the last character, not before",
doneTick ~= nil and fired.tick >= doneTick)
if pet.species == "PIKACHU" and not yellow then
check("PIKACHU used the chip cry, not Yellow's voice clip",
fired.name == "PIKACHU")
end
check(tag .. ": exactly one cry for this box",
#cries == before + 1)
end
waitTicks(90)
U.shot(game, SHOT_DIR .. "/pet_cry_" .. pet.species:lower() .. ".png")
check(tag .. ": the box is still up waiting for A",
getmetatable(game.stack:top()) == TextBox)
for _ = 1, (pet.extraBoxes or 0) + 1 do
tap("a")
waitTicks(40)
end
if pet.extraBoxes then
U.log("the MACHOP's second box is the stomping line; it has no cry")
end
waitTicks(30)
end
end
-- a neighbour that is meant to stay silent, so a run with no audio
-- device at all cannot pass itself off as a working cry
if pet.control then
local silent = standAt(pet.map, pet.control)
if silent then
local mark = #cries
talk(pet.control .. " (control)")
check(pet.control .. ": the silent neighbour played no cry",
#cries == mark)
tap("a")
waitTicks(30)
tap("a")
waitTicks(20)
end
end
end
-- ---------------------------------------------------------------- verdict
U.log("talked to all", #PETS, "pets;", #cries, "cries fired.")
if not heard then
U.log("no cry reached the mixer at all, which is the #1687 / #1649 bug")
end
U.log(failures == 0 and "all checks passed" or (failures .. " checks FAILED"))
U.log("what you should have heard, once per pet: the box types the line,")
U.log("then a short cry, and only then the down arrow starts blinking.")
U.log("a cry that starts while the line is still typing is wrong, and so")
U.log("is a box that closes itself when the cry ends instead of waiting.")
U.log("in Red the Fan Club PIKACHU is a short chip cry; the long voiced")
U.log("\"Pikachuuu\" belongs to Yellow, where the pet is a CLEFAIRY.")
U.log("re-run with POKEPORT_VERSION=blue and POKEPORT_VERSION=yellow.")
U.log("screenshots are in", SHOT_DIR)
-- parked in front of the last pet, so another A press repeats its cry
while true do
coroutine.yield()
end
end