mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
1905261c5b
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
304 lines
12 KiB
Lua
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
|