CLOSES #1396, CLOSES #1398, CLOSES #1400, CLOSES #1401, CLOSES #1406, CLOSES #1407, CLOSES #1411, CLOSES #1413, CLOSES #1415, CLOSES #1416, CLOSES #1417, CLOSES #1419, CLOSES #1421, CLOSES #1422, CLOSES #1423, CLOSES #1424, CLOSES #1425, CLOSES #1427, CLOSES #1428, CLOSES #1429, CLOSES #1431, CLOSES #1432, CLOSES #1433, CLOSES #1435, CLOSES #1437, CLOSES #1440, CLOSES #1441, CLOSES #1442, CLOSES #1443, CLOSES #1447, CLOSES #1449, CLOSES #1456, CLOSES #1464, CLOSES #1465, CLOSES #1468, CLOSES #1469, CLOSES #1470

This commit is contained in:
bryanthaboi
2026-08-17 10:15:06 -04:00
parent 3cca70608f
commit 45519ad550
61 changed files with 2481 additions and 255 deletions
+112
View File
@@ -0,0 +1,112 @@
-- Driver: Fly / Teleport must take Pikachu with it (#1400). _LeaveMapAnim
-- drops the companion sprite before the bird flaps (home/pikachu.asm:1) and
-- EnterMapAnim only puts it back once the swoop or the spin has landed, so
-- Pikachu is invisible for the whole sequence and never stands on the landing
-- cell ahead of the player.
--
-- POKEPORT_DRIVER=tests/drivers/fly_pikachu_bug1400_test.lua \
-- POKEPORT_VERSION=yellow POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
--
-- Never add POKEPORT_SPEED; the run needs an imported Yellow cache.
return function(game)
local U = dofile("tests/drivers/util.lua")
local Pokemon = require("src.pokemon.Pokemon")
local GameVersion = require("src.core.GameVersion")
local PF = require("src.world.PikachuFollower")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
check("running as Yellow (needs POKEPORT_VERSION=yellow)",
GameVersion.isYellow())
game.save.flags = game.save.flags or {}
game.save.flags.EVENT_GOT_STARTER = true
game.save.flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
-- DisablePikachuOverworldSpriteDrawing is what keeps it in the ball
-- (pokeyellow scripts/OaksLab.asm); out of the ball is what follows (#1009)
game.save.pikachuInBall = false
game.save.party = { Pokemon.new(game.data, "PIKACHU", 20) }
game.save.onBike = false
local function drawn()
local ow = game.overworld
local npc = ow and PF.current(ow)
if not npc then return false end
for _, e in ipairs(ow.entities or {}) do
if e == npc then return true end
end
return false
end
U.teleport(game, "ROUTE_17", 4, 10, "down")
U.wait(30)
local ow = game.stack:top()
check("Pikachu is out and drawn before the flight", drawn())
U.shot(game, DIR .. "/bug1400_0_before.png")
ow:flyTo("PALLET_TOWN")
U.wait(12) -- mid in-place flap
check("hidden during the departure flap", not drawn())
U.shot(game, DIR .. "/bug1400_1_flap.png")
U.wait(60) -- the bird's path out
check("still hidden while the bird flies off", not drawn())
local guard = 0
while ow.map.id == "ROUTE_17" and guard < 900 do
guard = guard + 1
coroutine.yield()
end
guard = 0
while not ow.flyArrive and guard < 900 do
guard = guard + 1
coroutine.yield()
end
U.wait(12) -- mid swoop, the frame the old bug showed Pikachu already landed
check("hidden through the arrival swoop", not drawn())
U.shot(game, DIR .. "/bug1400_2_arrive.png")
guard = 0
while ow.flyArrive and guard < 900 do
guard = guard + 1
coroutine.yield()
end
U.wait(10)
check("back out once the bird has landed", drawn())
do
local npc = PF.current(ow)
local p = ow.player
check("and it comes back on the player's own cell",
npc and p and npc.cellX == p.cellX and npc.cellY == p.cellY)
end
U.shot(game, DIR .. "/bug1400_3_landed.png")
-- Dig / Teleport / Escape Rope take the same _LeaveMapAnim path
game.save.lastHeal = { map = "VIRIDIAN_CITY", x = 23, y = 26 }
ow:beginTeleportOut()
U.wait(20)
check("hidden during the teleport-out spin", not drawn())
U.shot(game, DIR .. "/bug1400_4_spin_out.png")
guard = 0
while (ow.teleportOut or ow.player.spinning or ow.transitioning)
and guard < 900 do
guard = guard + 1
coroutine.yield()
end
U.wait(10)
check("back out once the arrival spin has landed", drawn())
U.shot(game, DIR .. "/bug1400_5_spin_in.png")
U.log("Watch the shots in order: Pikachu stands beside the player before")
U.log("the flight, is nowhere on screen for the flap, the fade and the")
U.log("swoop, and only reappears under him once the bird sets him down.")
U.log("A Pikachu standing on the landing cell during the swoop is the bug.")
U.log("The pad is yours -- fly around and watch the departures.")
while true do coroutine.yield() end
end
+99
View File
@@ -0,0 +1,99 @@
-- #1401: $d9/$da battlergfx loaded the wrong row count
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/gold_bug1401_test.lua love .
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
-- data/moves/animations.asm:1840 Growl, :3605 Icy Wind
local CASES = {
{ move = "GROWL", cmd = "battlergfx_2row", rows = 1, head = 7, feet = 6 },
{ move = "ICY_WIND", cmd = "battlergfx_1row", rows = 2, head = 14, feet = 12 },
}
local function sheet(runner, gfx)
for _, entry in ipairs(runner.loaded or {}) do
if entry.gfx == gfx then return entry end
end
return nil
end
local function liftedStrip(runner)
local structs = runner.objects and runner.objects.structs or {}
for slot = 1, #structs do
local id = structs[slot].objectId
if type(id) == "string" and id:match("PLAYERHEAD") then return id end
if type(id) == "string" and id:match("ENEMYFEET") then return id end
end
return nil
end
return function(game)
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
game.save.party = { Mon.new(game.data, "CYNDAQUIL", 30) }
assert(world:startBattle({ wild = Mon.new(game.data, "PIDGEY", 30) }),
"startBattle failed")
local battle
for _ = 1, 600 do
local top = game.stack:top()
if top and top.battle then battle = top break end
U.wait(1)
end
assert(battle and battle.battle, "battle screen is not on the stack")
assert(battle.anims and battle.anims.scripts,
"battle_anims.lua has no scripts -- re-import Gold")
local failed = false
for _, case in ipairs(CASES) do
battle.anim = nil
if not battle:animForMove(case.move, "player") then
U.log("FAIL no animation for", case.move)
failed = true
else
local head, feet
for _ = 1, 400 do
local runner = battle.anim
if not runner then break end
head = head or sheet(runner, "BATTLE_ANIM_GFX_PLAYERHEAD")
feet = feet or sheet(runner, "BATTLE_ANIM_GFX_ENEMYFEET")
U.wait(1)
end
if not (head and feet) then
U.log("FAIL", case.move, "never loaded the battler sheets")
failed = true
else
U.log(("%-9s %-16s rows=%d head=%d feet=%d (want rows=%d head=%d feet=%d)")
:format(case.move, case.cmd, head.rows, head.tiles, feet.tiles,
case.rows, case.head, case.feet))
if head.rows ~= case.rows or head.tiles ~= case.head
or feet.tiles ~= case.feet then
U.log("FAIL", case.move, "loaded the wrong row count")
failed = true
end
end
end
end
U.log(failed and "FAIL #1401" or "PASS #1401 battlergfx rows follow the jumptable")
U.log("look: Growl lifts ONE row of the Cyndaquil's own tiles, not two")
battle.anim = nil
battle:animForMove("GROWL", "player")
for _ = 1, 400 do
if not battle.anim then break end
local strip = liftedStrip(battle.anim)
if strip then
U.log("parked on", strip)
break
end
U.wait(1)
end
while true do
coroutine.yield()
end
end
+161
View File
@@ -0,0 +1,161 @@
-- #1421: the Gold battle HUD ran ahead of its own animations.
--
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/gold_bug1421_test.lua love .
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1421 (default)
--
-- Two symptoms, one seam, and neither can be asserted -- both are "what is on
-- screen while an animation runs":
--
-- caught a wild mon that is NOT in the dex is caught with a MASTER BALL.
-- The caught mark ($5d at (1,1)) must be absent for every frame of
-- the throw and every line after it, because DrawEnemyHUDBorder --
-- the only thing that paints it -- is never called again during a
-- capture (engine/battle/trainer_huds.asm:134-151). The control
-- run right after it re-enters a battle with the same species now
-- in the dex, where the mark IS there from the first frame.
--
-- status THUNDER WAVE on the enemy. PAR may not be on the HUD until the
-- animation and the "is paralyzed!" line are done with, which is
-- where UpdateBattleHuds runs (home/battle.asm:150).
--
-- Every frame of both animations is logged as `live` (the engine's byte, a
-- whole turn ahead) against `hud` (what the HUD is allowed to print), so the
-- lag is a column of text as well as a strip of pictures. The run ends in a
-- third wild battle with THUNDER WAVE under the cursor: press A, A, A and
-- watch the tag land with its own line.
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1421"
local WILD = "PIDGEY"
local hostVisible = love.visible
love.visible = function(v) if hostVisible then pcall(hostVisible, v) end end
local function tap(game, button, frames)
game.input.pressQueue[#game.input.pressQueue + 1] = button
game.input.state[button] = true
U.wait(2)
game.input.state[button] = false
U.wait(frames or 6)
end
local function openBattle(game, opts)
assert(game.world:startBattle(opts), "startBattle failed")
for _ = 1, 900 do
local top = game.stack:top()
if top and top.battle then return top end
U.wait(1)
end
error("battle screen never came up")
end
local function toMenu(game, screen)
for _ = 1, 400 do
if screen.phase == "menu" then return true end
if screen.battle.over then return false end
tap(game, "a", 2)
end
return screen.phase == "menu"
end
local function giveMoves(game, mon, moves)
mon.moves = {}
for i, id in ipairs(moves) do
local def = assert(game.data.moves[id], id .. " is not in moves.lua")
mon.moves[i] = { id = id, pp = def.pp, maxPp = def.pp }
end
return mon
end
-- Shoot and log while the screen is busy: `probe` returns the two values the
-- run is about, and the caller gets them for every frame.
local function watch(game, screen, prefix, label, probe, limit)
local frames = 0
while frames < (limit or 240) do
if frames % 3 == 0 then
U.shot(game, ("%s-%03d.png"):format(prefix, frames))
local live, hud = probe()
U.log(("[driver] %s f%03d live=%s hud=%s")
:format(label, frames, tostring(live), tostring(hud)))
end
if not screen.anim and screen.phase ~= "resolving" then break end
frames = frames + 1
U.wait(1)
end
return frames
end
return function(game)
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
local save = game.save
save.pokedex = save.pokedex or { seen = {}, caught = {} }
save.pokedex.caught[WILD] = nil
save.inventory = { MASTER_BALL = 5, POKE_BALL = 10 }
save.boxes = nil
save.party = { giveMoves(game, Mon.new(game.data, "CYNDAQUIL", 30),
{ "THUNDER_WAVE", "TACKLE" }) }
------------------------------------------------------------------ caught
local screen = openBattle(game, { wild = Mon.new(game.data, WILD, 5) })
assert(toMenu(game, screen), "never reached the battle menu")
U.shot(game, OUT .. "/caught-00-menu.png")
U.log(("[driver] caught: dex before = %s, hud latch = %s")
:format(tostring(save.pokedex.caught[WILD]), tostring(screen.caughtMark)))
screen:useItem("MASTER_BALL")
watch(game, screen, OUT .. "/caught-01-throw", "caught",
function()
return save.pokedex.caught[WILD] and true or false, screen.caughtMark
end, 200)
-- The lines that follow the throw: "Gotcha!", the dex entry, the nickname
-- prompt. The mark may not appear on any of them either.
for i = 0, 7 do
U.shot(game, ("%s/caught-02-after-%d.png"):format(OUT, i))
if screen.phase == "ask-nickname" then tap(game, "b", 4)
else tap(game, "a", 4) end
if screen.phase == "done" or not game.stack:top() then break end
end
U.log(("[driver] caught: dex after = %s, hud latch = %s")
:format(tostring(save.pokedex.caught[WILD]), tostring(screen.caughtMark)))
for _ = 1, 300 do
if game.world and not (game.stack:top() or {}).battle then break end
tap(game, "a", 2)
end
------------------------------------------------------------------ control
-- Same species, now in the dex: the mark IS on the HUD from the first frame
-- the enemy HUD is drawn, because DrawEnemyHUDBorder runs at battle start.
save.party = { giveMoves(game, Mon.new(game.data, "CYNDAQUIL", 30),
{ "THUNDER_WAVE", "TACKLE" }) }
screen = openBattle(game, { wild = Mon.new(game.data, WILD, 5) })
assert(toMenu(game, screen), "never reached the battle menu")
U.shot(game, OUT .. "/control-00-mark.png")
U.log(("[driver] control: dex = %s, hud latch = %s")
:format(tostring(save.pokedex.caught[WILD]), tostring(screen.caughtMark)))
------------------------------------------------------------------ status
screen:submit({ kind = "move", move = "THUNDER_WAVE" })
watch(game, screen, OUT .. "/status-01-anim", "status",
function()
local enemy = screen.battle.enemy
return enemy and enemy.status, screen:hudStatus(enemy, "enemy")
end, 240)
U.shot(game, OUT .. "/status-02-line.png")
for i = 0, 4 do
tap(game, "a", 4)
U.shot(game, ("%s/status-03-after-%d.png"):format(OUT, i))
end
U.log(("[driver] status: live=%s hud=%s"):format(
tostring(screen.battle.enemy and screen.battle.enemy.status),
tostring(screen:hudStatus(screen.battle.enemy, "enemy"))))
U.log("[driver] shots in " .. OUT
.. " -- the battle is yours: FIGHT, THUNDER WAVE, watch the tag land")
end
+97
View File
@@ -0,0 +1,97 @@
-- #1425 (and #1424): the PACK's ×NN column.
--
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/gold_bug1425_test.lua love .
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1425 (default)
--
-- Nothing here can be asserted: the bug IS the column a digit lands in.
-- PlaceMenuItemQuantity's `lb bc, 1, 2` (engine/menus/menu_2.asm:24) is a
-- two-digit field with the leading digit blanked, so a ×5 and a ×50 must have
-- their ones digit in the SAME column; the port printed "×5" hard against the
-- cross. Each pocket is seeded with a one-digit and a two-digit count stacked
-- next to each other so the two rows can be read off against one another, and
-- the TM pocket is here because its rows used to print no count at all.
--
-- The run ends with the PACK still open on the TM pocket, so a human takes the
-- controls exactly where the screenshots stop.
local U = require("tests.drivers.util")
local Bag = require("src.inventory.Bag")
local PackMenu = require("src.ui.gen2.PackMenu")
-- One-digit and two-digit counts side by side in every pocket that shows one.
local SEED = {
{ "POTION", 5 },
{ "SUPER_POTION", 50 },
{ "ANTIDOTE", 1 },
{ "FULL_HEAL", 99 },
{ "POKE_BALL", 7 },
{ "GREAT_BALL", 12 },
{ "TM_DYNAMICPUNCH", 1 },
{ "TM_HEADBUTT", 3 },
{ "TM_THUNDER", 24 },
{ "HM_CUT", 1 },
{ "HM_SURF", 1 },
}
return function(game)
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1425"
local function shot(name)
U.wait(3)
U.shot(game, ("%s/%s.png"):format(out, name))
end
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
local save = game.save
save.inventory = {}
save.bagOrder = {}
for _, entry in ipairs(SEED) do
local id, count = entry[1], entry[2]
if not (game.data.items and game.data.items[id]) then
U.log("[driver] SKIP", id, "-- not in this cache")
else
save.inventory[id] = count
table.insert(Bag.order(save), id)
end
end
local pack = PackMenu.new(game, { save = save, world = game.world,
onClose = function() end })
game.stack:push(pack)
shot("00-items") -- ×5 over ×50: the ones digits line up
U.tap(game, "right")
shot("01-balls") -- ×7 over ×12
U.tap(game, "right")
shot("02-key-items") -- no counts at all here
U.tap(game, "right")
shot("03-tmhm") -- TMs carry ×NN, the two HMs carry none
-- SELECT arms the row instead of registering it (#1427): the hollow ▷ marks
-- TM_HEADBUTT while "Where should this be moved to?" holds the box.
U.tap(game, "down")
U.tap(game, "select")
shot("04-tmhm-armed")
U.tap(game, "up")
shot("05-tmhm-destination")
U.tap(game, "a")
shot("06-tmhm-moved") -- HEADBUTT is now the first TM row
U.log("[driver] bag order:", table.concat(Bag.order(save), " "))
-- Back to the ITEM pocket and into a TOSS, whose own box prints the count
-- with leading zeros (×01, not × 1).
U.tap(game, "left")
U.tap(game, "left")
U.tap(game, "left")
U.tap(game, "a")
U.tap(game, "down")
U.tap(game, "down")
U.tap(game, "a")
shot("07-toss-quantity")
U.log("[driver] shots in " .. out .. " -- the PACK is yours")
end
+174
View File
@@ -0,0 +1,174 @@
-- #1428: the Gold battle HUD never drew the party ball rows.
--
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/gold_bug1428_test.lua love .
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1428 (default)
--
-- BattleStart_TrainerHuds (engine/battle/trainer_huds.asm:1-9) runs from
-- inside BattleStartMessage, so the rows are on screen UNDER the opening line
-- and nowhere else: the player's six always, the opponent's only outside a
-- wild battle. EnemySwitch_TrainerHud (:11-15) brings the opponent's row
-- back for the "will you switch?" prompt after one of its mons drops.
--
-- The party is seeded so all four staged tiles are on screen at once
-- (StageBallTilesData, :47-99): healthy, statused, fainted, and the empty
-- slots past the party count.
--
-- trainer the intro line, both rows, then the shift prompt after the first
-- enemy mon faints -- the opponent's row again, one ball darkened
-- wild the same intro with only the player's row, per the `dec a / ret z`
--
-- Nothing here is assertable: the fix IS twelve sprites. The run ends on the
-- trainer battle's own menu with a human holding the controls.
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
local Trainers = require("src.world.gen2.Trainers")
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1428"
local hostVisible = love.visible
love.visible = function(v) if hostVisible then pcall(hostVisible, v) end end
local function tap(game, button, frames)
game.input.pressQueue[#game.input.pressQueue + 1] = button
game.input.state[button] = true
U.wait(2)
game.input.state[button] = false
U.wait(frames or 6)
end
local function openBattle(game, opts)
assert(game.world:startBattle(opts), "startBattle failed")
for _ = 1, 900 do
local top = game.stack:top()
if top and top.battle then return top end
U.wait(1)
end
error("battle screen never came up")
end
-- Four healthy, one poisoned, one fainted, and one slot left empty: every
-- tile StageBallTilesData can stage, in one row. The lead is handed a
-- damaging move outright, because slot 1 of its level-up set is LEER.
local function seedParty(game)
local party = {}
for i = 1, 5 do
party[i] = Mon.new(game.data, "CYNDAQUIL", 20 + i)
end
party[1].moves = {}
for i, id in ipairs({ "EMBER", "TACKLE" }) do
local def = assert(game.data.moves[id], id .. " is not in moves.lua")
party[1].moves[i] = { id = id, pp = def.pp, maxPp = def.pp }
end
party[4].status = "poison"
party[5].hp = 0
return party
end
local function rowState(screen)
local rows = screen.ballRows or {}
return ("player=%s enemy=%s balls=%s"):format(
tostring(rows.player), tostring(rows.enemy),
tostring(screen.hud and screen.hud:image("balls") ~= nil))
end
return function(game)
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
local save = game.save
save.inventory = {}
save.party = seedParty(game)
--------------------------------------------------------------- trainer
local entry = game.world:trainerParty(36, 1) -- BUG_CATCHER member 1
assert(entry, "no BUG_CATCHER member 1 in trainers.lua")
entry.party = Trainers.party(game.data, entry)
local screen = openBattle(game, { trainer = entry })
-- The opening line: both rows, both borders, and no names or bars yet.
-- Not before the intro slide has settled, or the shots catch the pics
-- mid-transform instead of the HUD.
U.wait(150)
for i = 0, 6 do
U.shot(game, ("%s/trainer-00-intro-%d.png"):format(OUT, i))
U.log("[driver] intro " .. i .. ": " .. rowState(screen))
U.wait(6)
end
U.log(("[driver] OT party = %d"):format(#(screen.battle.enemyParty or {})))
-- Page through the pic slide and the send-out: the rows go with the OAM the
-- send-out animation takes over.
for i = 0, 7 do
tap(game, "a", 5)
U.shot(game, ("%s/trainer-01-sendout-%d.png"):format(OUT, i))
end
for _ = 1, 400 do
if screen.phase == "menu" then break end
tap(game, "a", 2)
end
U.shot(game, OUT .. "/trainer-02-menu.png")
U.log("[driver] menu: " .. rowState(screen))
--------------------------------------------------------------- shift prompt
-- Drop the enemy's lead so HandleEnemySwitch offers the switch: its row is
-- redrawn for the prompt, with the fainted ball darkened.
if #(screen.battle.enemyParty or {}) > 1 then
for _ = 1, 400 do
if screen.phase == "ask-shift" or screen.phase == "shift-intro" then
break
end
if screen.battle.over then break end
if screen.phase == "menu" then
local enemy = screen.battle.enemy
if enemy then enemy.hp = 1 end
screen:chooseMenu("fight")
U.wait(2)
screen:chooseMove(1)
U.wait(4)
else
tap(game, "a", 6)
end
end
U.log("[driver] shift prompt: phase=" .. tostring(screen.phase))
for i = 0, 5 do
U.shot(game, ("%s/trainer-03-shift-%d.png"):format(OUT, i))
U.log("[driver] shift " .. i .. ": phase=" .. tostring(screen.phase)
.. " " .. rowState(screen))
U.wait(6)
end
-- NO: the enemy sends its next mon and the row goes with the animation.
tap(game, "down", 4)
tap(game, "a", 4)
for i = 0, 5 do
U.shot(game, ("%s/trainer-04-after-shift-%d.png"):format(OUT, i))
tap(game, "a", 5)
end
else
U.log("[driver] this trainer has one mon; no shift prompt to show")
end
--------------------------------------------------------------- wild
-- ShowPlayerMonsRemaining runs for a wild battle too; the `dec a / ret z`
-- right after it is what keeps the OT row off.
for _ = 1, 600 do
if not (game.stack:top() or {}).battle then break end
tap(game, "a", 2)
end
save.party = seedParty(game)
screen = openBattle(game, { wild = Mon.new(game.data, "PIDGEY", 5) })
U.wait(150)
for i = 0, 6 do
U.shot(game, ("%s/wild-00-intro-%d.png"):format(OUT, i))
U.log("[driver] wild " .. i .. ": " .. rowState(screen))
U.wait(6)
end
for _ = 1, 400 do
if screen.phase == "menu" then break end
tap(game, "a", 2)
end
U.shot(game, OUT .. "/wild-01-menu.png")
U.log("[driver] shots in " .. OUT .. " -- the battle is yours")
end
+142
View File
@@ -0,0 +1,142 @@
-- Driver: the shiny sparkle on a SENT OUT mon (#1431).
--
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/gold_bug1431_test.lua love .
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1431 (default)
--
-- SendOutPlayerMon calls BattleCheckPlayerShininess and replays
-- ANIM_SEND_OUT_MON with wBattleAnimParam 1 -- the script's `.Shiny` arm --
-- between the plain animation and the cry (engine/battle/core.asm:3820-3837);
-- ShowSetEnemyMonAndSendOutAnimation does the same for every enemy send-out
-- (:3364-3383). Only the wild-intro path (BattleStartMessage, :8701-8715) had
-- been ported, so a shiny lead-off, a shiny switch-in and a shiny enemy
-- replacement all came out silent.
--
-- Three send-outs to watch, none of them the wild intro: the player's lead-off,
-- the player's own switch, and the opponent's replacement after a faint.
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
local GbcPalette = require("src.render.GbcPalette")
-- constants/battle_constants.asm: the DV pair BATTLETYPE_FORCESHINY forces.
local SHINY_DVS = { attack = 14, defense = 10, speed = 10, special = 10 }
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1431"
local function tap(game, button, frames)
game.input.pressQueue[#game.input.pressQueue + 1] = button
game.input.state[button] = true
U.wait(2)
game.input.state[button] = false
U.wait(frames or 6)
end
local function openBattle(game, opts)
assert(game.world:startBattle(opts), "startBattle failed")
for _ = 1, 900 do
local top = game.stack:top()
if top and top.battle then return top end
U.wait(1)
end
error("battle screen never came up")
end
-- Stop on the frame the `.Shiny` arm is actually running: animId plus the
-- wBattleAnimParam the arm branches on (data/moves/animations.asm:414-417).
local function shinyArm(screen)
local anim = screen.anim
return anim ~= nil and anim.animId == "ANIM_SEND_OUT_MON"
and anim.param == 1
end
local function watch(game, screen, label, frames)
for _ = 1, (frames or 400) do
if shinyArm(screen) then
U.log("[driver] " .. label .. ": .Shiny arm running")
U.shot(game, ("%s/%s.png"):format(OUT, label))
return true
end
U.wait(1)
end
U.log("[driver] " .. label .. ": NO shiny arm seen")
return false
end
return function(game)
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
-- A shiny only reads as one in colour, and the sparkle rides the same anim.
GbcPalette.setMode("gbc")
local save = game.save
save.inventory = {}
local lead = Mon.new(game.data, "CYNDAQUIL", 40, { dvs = SHINY_DVS })
local bench = Mon.new(game.data, "TOTODILE", 40, { dvs = SHINY_DVS })
assert(lead.shiny and bench.shiny, "the FORCESHINY DVs did not take")
local move = assert(game.data.moves.EMBER, "no EMBER in moves.lua")
lead.moves = { { id = "EMBER", pp = move.pp, maxPp = move.pp } }
save.party = { lead, bench }
local entry = game.world:trainerParty(36, 1) -- BUG_CATCHER member 1
assert(entry, "no BUG_CATCHER member 1 in trainers.lua")
local Trainers = require("src.world.gen2.Trainers")
entry.party = Trainers.party(game.data, entry)
for _, mon in ipairs(entry.party) do mon.shiny = true end
local screen = openBattle(game, { trainer = entry })
------------------------------------------------------------ enemy lead
-- ShowSetEnemyMonAndSendOutAnimation, out of the opening sequence.
watch(game, screen, "00-enemy-sendout", 900)
----------------------------------------------------------- player lead
-- SendOutPlayerMon behind the "Go!" line: this is the one the report is
-- about, and it never sparkled before the fix.
local sawPlayer = watch(game, screen, "01-player-sendout", 900)
for _ = 1, 600 do
if screen.phase == "menu" then break end
tap(game, "a", 2)
end
---------------------------------------------------------- player switch
-- The same routine again, this time as a voluntary mid-battle switch.
local sawSwitch = false
if screen.phase == "menu" and (screen.battle.party or {})[2] then
screen:submit({ kind = "switch", index = 2 })
sawSwitch = watch(game, screen, "02-player-switch", 900)
for _ = 1, 600 do
if screen.phase == "menu" or screen.battle.over then break end
tap(game, "a", 2)
end
end
------------------------------------------------------ enemy replacement
local sawReplace = false
if #(screen.battle.enemyParty or {}) > 1 then
for _ = 1, 600 do
if screen.battle.over then break end
if screen.phase == "menu" then
local enemy = screen.battle.enemy
if enemy then enemy.hp = 1 end
screen:chooseMenu("fight")
U.wait(2)
screen:chooseMove(1)
break
end
tap(game, "a", 2)
end
sawReplace = watch(game, screen, "03-enemy-replacement", 900)
end
U.log(("[driver] player lead-off %s, player switch %s, enemy replacement %s")
:format(tostring(sawPlayer), tostring(sawSwitch), tostring(sawReplace)))
U.log("[driver] shots in " .. OUT)
U.log("[driver] every send-out of a shiny must flash, sparkle and chime")
U.log("[driver] BEFORE its cry, not only the wild mon at battle start.")
while true do
coroutine.yield()
end
end
+72
View File
@@ -0,0 +1,72 @@
-- #1441: the Magnet Train ride drew a flat white screen.
--
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/gold_bug1441_test.lua love .
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1441 (default)
--
-- Nothing here is assertable: the bug IS what the screen shows. The ride
-- bakes its background out of data.gen2Field.magnetTrain, TILESET_TRAIN_
-- STATION's sheet and the TOWN palettes, and every one of those was read
-- under its Gen 1 key, so all four came back nil and GbcPalette's fallback
-- filled the panel with DMG_SHADES[1].
--
-- The run boots into the Goldenrod station, prints whether the four tables
-- are actually there, shoots the Saffron-bound ride from JUMPTABLE_INIT
-- through to the arrival, then plays the Goldenrod-bound one back at 1x with
-- no screenshots in the way -- a human watching the window sees the whole
-- animation, which is the only place this bug ever showed.
local U = require("tests.drivers.util")
local STATION = "GOLDENROD_MAGNET_TRAIN_STATION"
return function(game)
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1441"
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
world:setMap(STATION, 11, 8, "up")
U.wait(10)
local data = game.data or {}
local field = data.gen2Field and data.gen2Field.magnetTrain
U.log("gen2Field.magnetTrain:", field and #field.bgTiles or "MISSING")
U.log("gen2Tilesets.TILESET_TRAIN_STATION:",
data.gen2Tilesets and data.gen2Tilesets.TILESET_TRAIN_STATION
and data.gen2Tilesets.TILESET_TRAIN_STATION.image or "MISSING")
U.log("gen2Palettes:", data.gen2Palettes and "ok" or "MISSING")
U.log("gen2Sprites.SPRITE_CHRIS:",
data.gen2Sprites and data.gen2Sprites.SPRITE_CHRIS and "ok" or "MISSING")
local done = false
world:magnetTrain(true, function() done = true end)
U.wait(2)
local ride = game.stack:top()
U.log("ride background:", ride and ride.background
and (ride:background() and "baked" or "nil canvas") or "no screen")
-- SFX_TRAIN_ARRIVED lands a good while in; shoot across the whole ride so
-- the departure, the three scrolling bands and the stop are all on disk.
for index = 0, 11 do
U.shot(game, ("%s/%02d-ride.png"):format(out, index))
U.wait(20)
if done then break end
end
for _ = 1, 600 do
if done then break end
U.wait(5)
end
U.log("ride finished at frame", U.frame())
U.log("shots in " .. out)
-- The return leg, played out in full with nothing else on screen: watch the
-- window, not the PNGs. The ride reads no input, so it ends on its own.
local back = false
world:magnetTrain(false, function() back = true end)
for _ = 1, 900 do
if back then break end
U.wait(1)
end
U.wait(60)
end
+57
View File
@@ -0,0 +1,57 @@
-- Eyeball driver (#1442): the radio card's tuning knob. PokegearRadio_Init
-- spawns SPRITE_ANIM_OBJ_RADIO_TUNING_KNOB on tile $08 and AnimateTuningKnob
-- writes wRadioTuningKnob into its XOFFSET, so a red needle stands in the dial
-- box at screen x = 72 + knob and steps with every UP/DOWN. Before the fix the
-- card drew the dial art and nothing in it.
--
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold \
-- POKEPORT_SHOTS=/tmp/radioknob \
-- POKEPORT_DRIVER=tests/drivers/gold_radio_knob_bug1442.lua \
-- perl -e 'alarm 300; exec @ARGV' \
-- python3 -c "import pty; pty.spawn(['love','.'])"
--
-- The run parks on the radio card with the knob on 08.5, so UP and DOWN move
-- the needle by hand.
local U = require("tests.drivers.util")
local SHOTS = os.getenv("POKEPORT_SHOTS") or "/tmp/radioknob"
return function(game)
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
-- Goldenrod, where the card is handed out, and the two engine flags the
-- START menu and the strip read: ENGINE_POKEGEAR and ENGINE_RADIO_CARD.
assert(world:setMap("GOLDENROD_CITY", 12, 20, "down"),
"setMap failed for GOLDENROD_CITY")
world:setEngineFlag(4, true) -- ENGINE_POKEGEAR
world:setEngineFlag(0, true) -- ENGINE_RADIO_CARD
U.wait(5)
game:openStartMenuItem("pokegear")
U.wait(5)
local gear = game.stack:top()
assert(gear and gear.cards, "the POKeGEAR did not open")
gear.mode = "card"
for index, card in ipairs(gear.cards) do
if card.id == "radio" then gear.cardIndex = index end
end
assert(gear:card().id == "radio", "the RADIO card is missing from the strip")
U.wait(5)
-- 04.5, the bottom of the dial: the needle sits at the left of the box.
U.shot(game, SHOTS .. "/01-radio-04.5.png")
U.log("knob", tostring(gear:currentStation().knob), "at 04.5")
U.tap(game, "up") U.wait(4)
U.tap(game, "up") U.wait(4)
U.shot(game, SHOTS .. "/02-radio-08.5.png")
U.log("knob", tostring(gear:currentStation().knob), "at 08.5")
U.log("compare 01 and 02: a red needle stands in the dial box and has",
"moved right; UP/DOWN now walk it by hand")
while true do
coroutine.yield()
end
end
@@ -0,0 +1,89 @@
-- Driver: the starter Pikachu's battle entrance (#1429).
--
-- POKEPORT_DRIVER=tests/drivers/pikachu_entrance_bug1429_test.lua \
-- POKEPORT_VERSION=yellow POKEPORT_IDENTITY=bug1429 POKEPORT_TOUCH=0 \
-- SHOT_DIR=/tmp/shots love . (never under POKEPORT_SPEED: audio-timed)
--
-- SendOutMon branches on IsThisPartyMonStarterPikachu (pokeyellow
-- engine/battle/core.asm:1798-1819): the starter never gets POOF_ANIM or
-- AnimateSendingOutMon. It walks in instead --
-- StarterPikachuBattleEntranceAnimation (engine/battle/pikachu_entrance_anim.asm)
-- paints the back pic one column at a time from hlcoord 0,5, eight columns two
-- frames apart, and only then does PlayPikachuSoundClip voice it.
--
-- The run halts in the middle of the walk-in, then again on the finished pic.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
local GameVersion = require("src.core.GameVersion")
local PF = require("src.world.PikachuFollower")
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
check("running as Yellow (needs POKEPORT_VERSION=yellow)",
GameVersion.isYellow())
game.save.player.name = "bryan"
local pika = Pokemon.new(game.data, "PIKACHU", 20)
BattleState.stampOT(game.save, pika)
game.save.party = { pika, Pokemon.new(game.data, "CHARMANDER", 20) }
check("the lead reads as the starter Pikachu",
PF.isStarterPikachu(game.save, pika))
U.teleport(game, "ROUTE_1", 5, 20, "down")
U.wait(20)
local ow = game.overworld
check("overworld is up to push the battle from", ow ~= nil)
local battle = BattleState.newWild(game, "PIDGEY", 5)
battle.onFinish = function() end
ow:pushBattle(battle)
local function tapUntil(cond, taps, gap)
for _ = 1, (taps or 90) do
if cond() then return true end
U.tap(game, "a")
for _ = 1, (gap or 4) do
if cond() then return true end
U.wait(1)
end
end
return cond()
end
-- Poll every frame: the slide is 16 frames long and the ball poof, if the
-- bug were back, would never set this slot at all.
local sliding = tapUntil(function()
return battle.picOff ~= nil and battle.picOff.playerMon ~= nil
end, 120, 2)
check("the send-out started the entrance slide, not the ball poof", sliding)
check("no grow-in is running alongside it", battle.growIn == nil)
if sliding then
U.log("slide x =", tostring(battle.picOff.playerMon.x))
check("mid-slide screenshot reached disk",
U.shot(game, DIR .. "/bug1429_pikachu_sliding.png"))
U.log("captured", DIR .. "/bug1429_pikachu_sliding.png")
for _ = 1, 40 do
if battle:picOffset("playerMon") == 0 then break end
U.wait(1)
end
check("the pic landed on its own column", battle:picOffset("playerMon") == 0)
check("landed screenshot reached disk",
U.shot(game, DIR .. "/bug1429_pikachu_landed.png"))
U.log("captured", DIR .. "/bug1429_pikachu_landed.png")
end
U.log("Pikachu should have walked in from the LEFT edge of the screen,")
U.log("column by column, with no ball, no poof and no grow-out-of-the-ball,")
U.log("and voiced its PCM clip only once it was fully drawn (#1429).")
while true do
coroutine.yield()
end
end
@@ -0,0 +1,114 @@
-- Driver: where the "Will PLAYER change POKéMON?" YES/NO box sits (#1398).
--
-- POKEPORT_DRIVER=tests/drivers/shift_prompt_box_bug1398_test.lua \
-- POKEPORT_IDENTITY=bug1398 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
-- SHOT_DIR=/tmp/shots love .
--
-- EnemySendOutFirstMon (engine/battle/core.asm:1378-1384) does NOT go through
-- InitYesNoTextBoxParameters for this one prompt: it inlines its own
-- TWO_OPTION_MENU at hlcoord 0,7, i.e. hard against the LEFT edge, while every
-- other YES/NO in the game sits at hlcoord 14,7 on the right. The port only
-- had the shared right-hand box, so the SHIFT offer came up on the wrong side.
--
-- The run stops with the prompt on screen and the controls in a human's hands.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
local Theme = require("src.ui.Theme")
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
-- roster 1 is RATTATA 11 / EKANS 11: two mons, so the KO of the first one
-- reaches EnemySendOutFirstMon with a reserve to announce.
local OPP, ROSTER = "OPP_YOUNGSTER", 1
check("trainer class " .. OPP .. " is in the data",
game.data.trainers ~= nil and game.data.trainers[OPP] ~= nil)
check("Theme carries the left-edge box",
Theme.trainerSwitchBox ~= nil and Theme.trainerSwitchBox.tx == 0
and Theme.trainerSwitchBox.ty == 7)
-- SHIFT is what makes the prompt appear at all (the BIT_BATTLE_SHIFT test at
-- core.asm:1375-1377 skips it under SET).
game.save.options = game.save.options or {}
game.save.options.battleStyle = "shift"
game.save.player.name = "bryan"
game.save.party = {
Pokemon.new(game.data, "MEWTWO", 70),
Pokemon.new(game.data, "CHARIZARD", 50),
}
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
U.wait(20)
local ow = game.overworld
check("overworld is up to push the battle from", ow ~= nil)
local ok, battle = pcall(BattleState.newTrainer, game, OPP, ROSTER)
check("trainer battle constructed", ok and battle ~= nil)
if not ok then
U.log("could not start", OPP, "->", tostring(battle))
while true do coroutine.yield() end
end
check("the foe has a reserve to send out", #battle.enemyParty >= 2)
battle.onFinish = function() end
ow:pushBattle(battle)
local function tapUntil(cond, taps, gap)
for _ = 1, (taps or 60) do
if cond() then return true end
U.tap(game, "a")
for _ = 1, (gap or 6) do
if cond() then return true end
U.wait(1)
end
end
return cond()
end
if not check("reached the FIGHT/PKMN/ITEM/RUN menu",
tapUntil(function() return battle.phase == "menu" end, 200)) then
U.log("phase is", tostring(battle.phase))
end
-- FIGHT, first move: L70 MEWTWO one-shots the L11 lead.
U.tap(game, "a")
U.wait(10)
U.tap(game, "a")
U.wait(10)
-- Stop the moment the YES/NO goes up over the still-visible prompt page.
local function choiceBox()
local top = game.stack:top()
return (top and top.tx and top.labels and top.labels[1] == "YES") and top
or nil
end
local reached = tapUntil(function() return choiceBox() ~= nil end, 200, 4)
if not check("the SHIFT prompt opened its YES/NO box", reached) then
U.log("phase", tostring(battle.phase),
"enemy hp", tostring(battle.enemy and battle.enemy.mon.hp),
"top", tostring(game.stack:top()))
end
local box = choiceBox()
if box then
check("the box is at hlcoord 0,7 (core.asm:1378-1384)",
box.tx == 0 and box.ty == 7)
U.log("box tx=" .. tostring(box.tx) .. " ty=" .. tostring(box.ty))
check("prompt screenshot reached disk",
U.shot(game, DIR .. "/bug1398_shift_prompt.png"))
U.log("captured", DIR .. "/bug1398_shift_prompt.png")
end
U.log("The foe's lead is down and the SHIFT offer is on screen.")
U.log("YES/NO must sit against the LEFT edge, over the text box's left half,")
U.log("not on the right where every other YES/NO in the game lives (#1398).")
while true do
coroutine.yield()
end
end
+73
View File
@@ -0,0 +1,73 @@
-- Driver: nobody is caught mid-stride while a text box is up (#1435).
-- UpdatePlayerSprite and UpdateNPCSprite both jump to their standing frame
-- while BIT_FONT_LOADED is set (engine/overworld/movement.asm:57, :139), so a
-- box that opens on a walk frame freezes a standing sprite, never a leg out.
--
-- POKEPORT_DRIVER=tests/drivers/talk_pose_bug1435_test.lua \
-- POKEPORT_IDENTITY=bug1435 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
-- SHOT_DIR=/tmp/shots love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local TextBox = require("src.render.TextBox")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
-- Mom stands at (5,4) in Red's house (pokered
-- data/maps/objects/RedsHouse1F.asm); park the player beside her and push
-- into her so the walk-in-place cycle is running when the box opens.
U.teleport(game, "REDS_HOUSE_1F", 6, 4, "down")
U.wait(20)
local ow = game.overworld
local p = ow.player
local walking = false
for _ = 1, 90 do
table.insert(game.input.pressQueue, "left")
game.input.state.left = true
coroutine.yield()
if p:walkPhase() == 1 then walking = true break end
end
game.input.state.left = false
check("bumping into MOM shows the walk frame", walking)
U.shot(game, DIR .. "/bug1435_1_midstride.png")
U.tap(game, "a")
U.wait(4)
check("talking to MOM opened a box over the overworld",
game.stack:top() ~= ow)
check("the walk clock is still mid-cycle underneath",
(p.bumpFrames or 0) > 0 or p.moving)
check("but the player is drawn standing", p:walkPhase() == 0)
U.shot(game, DIR .. "/bug1435_2_talking.png")
-- The same gate for an NPC caught mid-step: Pallet Town's wanderers walk
-- on their own, so wait for one and open a box while it is between cells.
U.teleport(game, "PALLET_TOWN", 8, 8, "down")
U.wait(30)
ow = game.overworld
local mover
for _ = 1, 900 do
for _, npc in ipairs(ow.npcs) do
if npc.moving and npc:walkPhase() == 1 then mover = npc break end
end
if mover then break end
coroutine.yield()
end
if check("caught a Pallet Town NPC mid-step", mover ~= nil) then
game.stack:push(TextBox.new(game, "TESTING THE POSE."))
U.wait(4)
check("the NPC is still mid-step underneath", mover.moving)
check("but it is drawn standing too", mover:walkPhase() == 0)
U.shot(game, DIR .. "/bug1435_3_npc.png")
end
U.log("Look at bug1435_2_talking.png and bug1435_3_npc.png: with the box")
U.log("up, both sprites stand square on their feet. A leg out, or the")
U.log("split-stride frame held for the whole conversation, is the bug.")
while true do coroutine.yield() end
end
+176
View File
@@ -0,0 +1,176 @@
-- Manual check for #1453: the SGB zone scissors must stay contiguous in
-- framebuffer pixels on a fractional-DPI surface (Android), where LOVE 11
-- truncates the scissor to whole units before scaling it. Zones are pokered
-- data/sgb/sgb_packets.asm BlkPacket_Titlescreen (rows 0-7 / 8-9 / 10-17).
-- POKEPORT_DRIVER=tests/drivers/title_seam_bug1453_test.lua POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
-- PROBE_DPI picks the density to emulate (default 2.625, the reporter's).
-- Do not set POKEPORT_SPEED: fast-forward desynchronizes the title music.
return function(game)
local U = dofile("tests/drivers/util.lua")
local PaletteFX = require("src.render.PaletteFX")
local Renderer = require("src.render.Renderer")
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local DPI = tonumber(os.getenv("PROBE_DPI") or "") or 2.625
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
-- past the copyright splash / attract movie (engine/movie/splash.asm)
local title
for _ = 1, 120 do
local top = game.stack:top()
if top and top.screenId == "TitleState" and top.sgbPalettes then
title = top
break
end
U.tap(game, "start")
U.wait(9)
end
check("the title screen is on top", title ~= nil)
if not (title and title.sgbPalettes) then
U.log("No title state to look at; nothing below can run.")
while true do coroutine.yield() end
end
local opts = game.save.options
local mode = opts and opts.colors or PaletteFX.mode
if PaletteFX.mode ~= "gbc" then
U.log("COLORS is", tostring(mode) .. "; switching the view to SGB for the check")
PaletteFX.setMode("gbc")
U.wait(20)
end
local zones = PaletteFX.ensureZones(title:sgbPalettes(game))
check("the title builds three SGB zones", zones ~= nil and #zones == 3)
-- A canvas carries its own dpiscale, so the emulated Android surface goes
-- through the same LOVE scissor path the phone does, on this desktop.
local probe = love.graphics.newCanvas(math.floor(1920 / DPI),
math.floor(1080 / DPI),
{ dpiscale = DPI })
local PW, PH = probe:getPixelWidth(), probe:getPixelHeight()
local Sp = math.max(1, math.floor(math.min(PW / 160, PH / 144)))
local Ux, Uy = Sp / DPI, Sp / DPI
local uox = math.floor((PW - 160 * Sp) / 2) / DPI
local uoy = math.floor((PH - 144 * Sp) / 2) / DPI
local uvpw, uvph = 160 * Ux, 144 * Uy
U.log(("emulating %dx%d px at dpi %.4f, fit scale %d"):format(PW, PH, DPI, Sp))
-- what the pre-fix Renderer handed LOVE: fractional units with a half
-- framebuffer pixel of bias, which LOVE 11 truncates away
local function scissorOld(x, y, w, h)
local x2, y2 = math.min(x + w, uox + uvpw), math.min(y + h, uoy + uvph)
x, y = math.max(x, uox), math.max(y, uoy)
if x2 <= x or y2 <= y then return false end
local px1, py1 = math.floor(x * DPI), math.floor(y * DPI)
local px2, py2 = math.ceil(x2 * DPI), math.ceil(y2 * DPI)
love.graphics.setScissor((px1 + 0.5) / DPI, (py1 + 0.5) / DPI,
(px2 - px1 + 0.5) / DPI, (py2 - py1 + 0.5) / DPI)
return true
end
local function renderProbe(old)
love.graphics.setCanvas(probe)
love.graphics.clear(0, 0, 0, 1)
love.graphics.setColor(1, 1, 1, 1)
if old then
local shader = PaletteFX.shader()
love.graphics.setShader(shader)
for _, z in ipairs(zones) do
PaletteFX.sendColors(shader, z.colors)
if scissorOld(uox + z.x * Ux, uoy + z.y * Uy, z.w * Ux, z.h * Uy) then
love.graphics.draw(Renderer.canvas, uox, uoy, 0, Ux, Uy)
end
end
love.graphics.setScissor()
love.graphics.setShader()
else
Renderer:blitCanvas(Renderer.canvas, Ux, Uy, zones, Ux, Uy,
uox, uoy, uox, uoy, uvpw, uvph, DPI, DPI)
end
love.graphics.setCanvas()
return probe:newImageData()
end
local top = math.floor(uoy * DPI)
local bottom = math.floor((uoy + uvph) * DPI)
local col = math.floor(uox * DPI) + 2
local function blackRows(data)
local rows = {}
for py = top, math.min(bottom - 1, data:getHeight() - 1) do
local r, g, b = data:getPixel(col, py)
if r < 0.02 and g < 0.02 and b < 0.02 then rows[#rows + 1] = py end
end
return rows
end
local dataNew = renderProbe(false)
local dataOld = renderProbe(true)
local rowsNew, rowsOld = blackRows(dataNew), blackRows(dataOld)
U.log(("picture rows %d-%d, sampling the background column at x=%d")
:format(top, bottom - 1, col))
U.log("pre-fix math left", #rowsOld, "black rows:",
#rowsOld > 0 and table.concat(rowsOld, ",") or "none")
check("no letterbox row shows through the zone boundaries", #rowsNew == 0)
check("the emulated surface reproduces the seam without the fix", #rowsOld > 0)
local ZOOM, CW, CH = 4, 220, 28
local cy = math.max(top, math.floor((uoy + 64 * Uy) * DPI) - math.floor(CH / 2))
local cx = math.floor(uox * DPI)
local function zoomOf(data)
local out = love.image.newImageData(CW * ZOOM, CH * ZOOM)
out:mapPixel(function(x, y)
return data:getPixel(cx + math.floor(x / ZOOM), cy + math.floor(y / ZOOM))
end)
return out
end
local function writePng(imageData, path)
local dir = path:match("^(.*)[/\\][^/\\]+$")
if dir and dir ~= "" then os.execute('mkdir -p "' .. dir .. '" 2>/dev/null') end
local f = io.open(path, "wb")
if not f then return false end
f:write(imageData:encode("png"):getString())
f:close()
return true
end
local zoomOld, zoomNew = zoomOf(dataOld), zoomOf(dataNew)
check("wrote " .. SHOT_DIR .. "/bug1453_before.png",
writePng(zoomOld, SHOT_DIR .. "/bug1453_before.png"))
check("wrote " .. SHOT_DIR .. "/bug1453_after.png",
writePng(zoomNew, SHOT_DIR .. "/bug1453_after.png"))
local imgOld = love.graphics.newImage(zoomOld)
local imgNew = love.graphics.newImage(zoomNew)
local baseDraw = love.draw
love.draw = function()
baseDraw()
local w, h = love.graphics.getDimensions()
local s = math.min((w - 24) / imgOld:getWidth(),
(h * 0.5 - 48) / (imgOld:getHeight() * 2))
local bw = imgOld:getWidth() * s + 16
local bh = imgOld:getHeight() * 2 * s + 56
local bx, by = (w - bw) / 2, h - bh - 8
love.graphics.setColor(0, 0, 0, 0.75)
love.graphics.rectangle("fill", bx, by, bw, bh)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print("BEFORE (dpi " .. DPI .. ")", bx + 8, by + 4)
love.graphics.draw(imgOld, bx + 8, by + 20, 0, s, s)
love.graphics.print("AFTER", bx + 8, by + 24 + imgOld:getHeight() * s)
love.graphics.draw(imgNew, bx + 8, by + 40 + imgOld:getHeight() * s, 0, s, s)
end
PaletteFX.setMode(mode)
U.wait(10)
U.shot(game, SHOT_DIR .. "/bug1453_title.png")
U.log("captured", SHOT_DIR .. "/bug1453_title.png")
U.log("The title is live and the pad is yours; the strip along the bottom is")
U.log("the row-64 zone boundary of an emulated " .. DPI .. "x Android surface,")
U.log("magnified " .. ZOOM .. "x: BEFORE carries the black hairline across the")
U.log("picture, AFTER is unbroken off-white. PROBE_DPI=2.75 tries another one.")
while true do
coroutine.yield()
end
end
@@ -0,0 +1,72 @@
-- Driver: the TOWN MAP player marker wears the OBJ palette (#1437).
-- LoadPlayerSpriteGraphics hands the town map the same walking sheet the
-- overworld draws (engine/items/town_map.asm:342), so the marker has to go
-- through the OBJ ramp and the shade-0 keying every other sprite gets --
-- green on Red, pink on Blue, never a raw white block on the BG ramp.
--
-- POKEPORT_DRIVER=tests/drivers/town_map_marker_bug1437_test.lua \
-- POKEPORT_IDENTITY=bug1437 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
-- SHOT_DIR=/tmp/shots love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local Screens = require("src.ui.Screens")
local PaletteFX = require("src.render.PaletteFX")
local SpriteRenderer = require("src.render.SpriteRenderer")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local function check(label, ok)
U.log(ok and "PASS" or "FAIL", label)
return ok
end
game.save.player.name = "bryan"
game.save.visited = {
PALLET_TOWN = true, VIRIDIAN_CITY = true, PEWTER_CITY = true,
CERULEAN_CITY = true, CELADON_CITY = true,
}
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
U.wait(10)
local function openMap(mode, tag)
game.save.options = game.save.options or {}
game.save.options.colors = mode
PaletteFX.setMode(mode)
U.wait(4)
Screens.push(game, "TownMap")
U.wait(20)
local top = game.stack:top()
check(tag .. ": the TOWN MAP is up", top and top.playerQuad ~= nil)
-- the marker must be a baked OBJ image, not a fresh newImage of the sheet
local sprites = game.data.sprites or {}
local red = sprites.SPRITE_RED
local colors, group
if PaletteFX.usesSpriteObp() then
colors, group = PaletteFX.ogObj()
else
colors, group = PaletteFX.dmgObj()
end
local want = red and SpriteRenderer.obpImage(red.image, colors, group)
check(tag .. ": the marker is the OBP-baked sheet",
top and want ~= nil and top.playerSheet == want)
-- hold the shot on a frame the blink is showing the marker
for _ = 1, 40 do
if top.blink < 16 then break end
coroutine.yield()
end
U.shot(game, DIR .. "/bug1437_" .. tag .. ".png")
U.tap(game, "b")
U.wait(10)
end
openMap("ogred", "ogred")
openMap("gbc", "gbc")
openMap("og", "og")
U.log("Open the shots: in OG RED the marker over PALLET TOWN is the green")
U.log("boot-ROM trainer, in the other modes it is coloured by the map zone")
U.log("like any overworld sprite. A white box, or a red/black marker on")
U.log("the BG ramp, is the bug. The pad is yours -- the TOWN MAP is one")
U.log("B away and the ITEM bag reopens it.")
while true do coroutine.yield() end
end
@@ -47,12 +47,32 @@ do
T.eq(feet.tile, (0x80 - 6 * 2) - 49, "2ROW base")
end
-- engine/battle_anims/anim_commands.asm:317
do
local runner = AnimRunner.new({})
runner:start(nil)
AnimRunner.COMMANDS.battlergfx_1row(runner)
local head = findLoaded(runner, "BATTLE_ANIM_GFX_PLAYERHEAD")
local feet = findLoaded(runner, "BATTLE_ANIM_GFX_ENEMYFEET")
T.eq(head and head.battler, "enemy", "the script command routes the same way")
T.eq(head.rows, 2, "$da's macro says 1row, its jumptable slot says _2Row")
T.eq(head.tiles, 14, "two enemy rows")
T.eq(head.tile, (0x80 - 6 * 2 - 7 * 2) - 49, "at the _2Row base")
T.eq(feet.tiles, 12, "two player rows")
T.eq(feet.tile, (0x80 - 6 * 2) - 49, "at the _2Row base")
end
do
local runner = AnimRunner.new({})
runner:start(nil)
AnimRunner.COMMANDS.battlergfx_2row(runner)
local head = findLoaded(runner, "BATTLE_ANIM_GFX_PLAYERHEAD")
local feet = findLoaded(runner, "BATTLE_ANIM_GFX_ENEMYFEET")
T.eq(head.rows, 1, "$d9's macro says 2row, its jumptable slot says _1Row")
T.eq(head.tiles, 7, "one enemy row")
T.eq(head.tile, (0x80 - 6 - 7) - 49, "at the _1Row base")
T.eq(feet.tiles, 6, "one player row")
T.eq(feet.tile, (0x80 - 6) - 49, "at the _1Row base")
end
T.finish("gen2 battler gfx row attribution bug 1231")
+2 -2
View File
@@ -151,8 +151,8 @@ do
local fresh = { mods = {} }
SaveData.setModEnabled(fresh, "b", true, "gold")
eq(fresh.modsByVersion.gold.b, nil,
"no shared flag reads as enabled, so agreeing with it stores nothing")
eq(fresh.modsByVersion.gold.b, true,
"with no shared flag the answer is stored, not dropped against an assumed default")
end
do
+2 -2
View File
@@ -357,8 +357,8 @@ local options = OptionsMenu.new(optionsGame, {
options = Save.defaultOptions(),
})
-- The cart's seven rows, then the port's: CONTROLS, audio, speed, display,
-- video mode, the mobile-gated touch three (buildRows) and CANCEL.
check("twenty-one rows", #OptionsMenu.ROWS, 21)
-- video mode, the mobile-gated touch three (buildRows), MAX FPS and CANCEL.
check("twenty-two rows", #OptionsMenu.ROWS, 22)
check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame")
check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls")
check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol")