From 60df2fbb657d8d412345aa13b4ddb5aa6ec5374a Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Wed, 9 Sep 2026 06:05:00 -0400 Subject: [PATCH] CLOSES #2230, CLOSES #2232 --- src/ui/gen2/BattleState.lua | 26 ++-- src/ui/gen2/Chrome.lua | 6 + src/ui/gen2/MagnetTrainRide.lua | 8 +- tests/drivers/crystal_day_wheel_bug2232.lua | 83 ++++++++++++ .../drivers/crystal_skin_newgame_bug2230.lua | 121 ++++++++++++++++++ 5 files changed, 223 insertions(+), 21 deletions(-) create mode 100644 tests/drivers/crystal_day_wheel_bug2232.lua create mode 100644 tests/drivers/crystal_skin_newgame_bug2230.lua diff --git a/src/ui/gen2/BattleState.lua b/src/ui/gen2/BattleState.lua index 9171c059..a6e36b65 100644 --- a/src/ui/gen2/BattleState.lua +++ b/src/ui/gen2/BattleState.lua @@ -1086,23 +1086,19 @@ function BattleState:drawPic(mon, back) local bandY = (back and BattleState.PLAYER_PIC_TILE_Y or BattleState.ENEMY_PIC_TILE_Y) * 8 + lifted[1] * 8 local bandH = lifted[2] * 8 - local psx, psy, psw, psh - if G.getScissor then psx, psy, psw, psh = G.getScissor() end - if self.liftedPass then - G.setScissor(0, bandY, 160, bandH) + local function band(y, h) + G.push("all") + Chrome.clipTo(0, y, 160, h) paint() - else - if bandY > 0 then - G.setScissor(0, 0, 160, bandY) - paint() - end - local below = 144 - bandY - bandH - if below > 0 then - G.setScissor(0, bandY + bandH, 160, below) - paint() - end + G.pop() + end + if self.liftedPass then + band(bandY, bandH) + else + if bandY > 0 then band(0, bandY) end + local below = 144 - bandY - bandH + if below > 0 then band(bandY + bandH, below) end end - if psx then G.setScissor(psx, psy, psw, psh) else G.setScissor() end end -- MonsterSpriteGFX (gfx/sprites.asm:82): the facing-DOWN 16x16 frame for the diff --git a/src/ui/gen2/Chrome.lua b/src/ui/gen2/Chrome.lua index 94af5f44..3124950d 100644 --- a/src/ui/gen2/Chrome.lua +++ b/src/ui/gen2/Chrome.lua @@ -118,6 +118,12 @@ end -- pokegold engine/battle/core.asm:8646, engine/events/halloffame.asm:270 local function clipTo(x, y, w, h) local G = love.graphics + if G.transformPoint then + local x1, y1 = G.transformPoint(x, y) + local x2, y2 = G.transformPoint(x + w, y + h) + x, y = math.floor(math.min(x1, x2)), math.floor(math.min(y1, y2)) + w, h = math.ceil(math.abs(x2 - x1)), math.ceil(math.abs(y2 - y1)) + end if G.intersectScissor then G.intersectScissor(x, y, w, h) else G.setScissor(x, y, w, h) end end diff --git a/src/ui/gen2/MagnetTrainRide.lua b/src/ui/gen2/MagnetTrainRide.lua index 52d3d5e1..dd1bbc25 100644 --- a/src/ui/gen2/MagnetTrainRide.lua +++ b/src/ui/gen2/MagnetTrainRide.lua @@ -427,11 +427,7 @@ function MagnetTrainRide:drawPanel() end function MagnetTrainRide:draw() - local G = love.graphics - G.push("all") - G.setScissor(0, 0, SCREEN_W, SCREEN_H) - self:drawPanel() - G.pop() + Chrome.withClip(function() self:drawPanel() end) end -- MagnetTrain_LoadGFX_PlayMusic opens on ClearBGPalettes / ClearSprites @@ -449,7 +445,7 @@ function MagnetTrainRide:drawWidescreen(winW, winH) local scale = Chrome.fitScale(winW, winH) local ox, oy = Chrome.fitOrigin(winW, winH, scale) G.push("all") - G.setScissor(ox, oy, SCREEN_W * scale, SCREEN_H * scale) + Chrome.clipTo(ox, oy, SCREEN_W * scale, SCREEN_H * scale) G.translate(ox, oy) G.scale(scale, scale) self:drawPanel() diff --git a/tests/drivers/crystal_day_wheel_bug2232.lua b/tests/drivers/crystal_day_wheel_bug2232.lua new file mode 100644 index 00000000..b143e493 --- /dev/null +++ b/tests/drivers/crystal_day_wheel_bug2232.lua @@ -0,0 +1,83 @@ +-- Mom's SetDayOfWeek wheel drawn over the living room (#2232). +-- ../pokecrystal/engine/rtc/timeset.asm:385 +-- ../pokecrystal/maps/PlayersHouse1F.asm:49 +-- +-- POKEPORT_VERSION=crystal POKEPORT_IDENTITY= POKEPORT_TOUCH=0 \ +-- POKEPORT_SHOT_DIR= POKEPORT_DRIVER=tests/drivers/crystal_day_wheel_bug2232.lua love . +local U = require("tests.drivers.util") +local Chrome = require("src.ui.gen2.Chrome") + +local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots" + +return function(game) + local fails = 0 + local function say(line) print("[2232] " .. line) end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + end + + local function tap(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 4) + end + + local function dayWheel() + local top = game.stack and game.stack:top() + if top and top.mode == "day" and top.pickerBox then return top end + return nil + end + + U.wait(45) + local world = game.world + if not (world and world.map) then + say("FAIL the crystal world did not boot") + return + end + + -- ../pokecrystal/maps/PlayersHouse1F.asm:395 + world:setMap("PLAYERS_HOUSE_1F", 7, 3, "down") + U.wait(20) + for _ = 1, 3 do tap("down", 8) end + U.wait(40) + ok(world.map and world.map.id == "PLAYERS_HOUSE_1F", "in the living room") + + local wheel + for _ = 1, 300 do + wheel = dayWheel() + if wheel then break end + tap("a", 4) + end + ok(wheel ~= nil, "Mom's scene reached the day-of-week wheel") + if not wheel then return end + + local seen + local drawPanel = wheel.drawPanel + wheel.drawPanel = function(self) + seen = { love.graphics.getScissor() } + return drawPanel(self) + end + U.wait(3) + wheel.drawPanel = drawPanel + local w, h = love.graphics.getDimensions() + local scale = Chrome.fitScale(w, h) + local ox, oy = Chrome.fitOrigin(w, h, scale) + ok(seen ~= nil, "the wheel drew its panel") + if seen then + ok(seen[1] == ox and seen[2] == oy + and seen[3] == 160 * scale and seen[4] == 144 * scale, + ("the wheel clips to the panel %d,%d %dx%d, got %s,%s %sx%s"):format( + ox, oy, 160 * scale, 144 * scale, + tostring(seen[1]), tostring(seen[2]), tostring(seen[3]), tostring(seen[4]))) + end + U.shot(game, SHOT_DIR .. "/2232_day_wheel.png") + + say(fails == 0 and "all claims passed" or (fails .. " claims failed")) + say("the SUNDAY picker at the top right and \"What day is it?\" should both") + say("be on screen over the living room. a bare room with Mom next to you") + say("and nothing to answer is the bug. up/down changes the day, A confirms.") + while true do U.wait(60) end +end diff --git a/tests/drivers/crystal_skin_newgame_bug2230.lua b/tests/drivers/crystal_skin_newgame_bug2230.lua new file mode 100644 index 00000000..5b689618 --- /dev/null +++ b/tests/drivers/crystal_skin_newgame_bug2230.lua @@ -0,0 +1,121 @@ +-- NEW GAME with a touch skin active, from the intro menu to the bedroom (#2230). +-- ../pokecrystal/engine/menus/init_gender.asm:23 +-- ../pokecrystal/engine/menus/intro_menu.asm:80 +-- +-- POKEPORT_VERSION=crystal POKEPORT_IDENTITY= POKEPORT_TOUCH=1 \ +-- POKEPORT_BOOT_CINEMA=1 POKEPORT_SKIN= POKEPORT_SHOT_DIR= \ +-- POKEPORT_DRIVER=tests/drivers/crystal_skin_newgame_bug2230.lua love . +local U = require("tests.drivers.util") +local TouchSkin = require("src.core.TouchSkin") +local TouchControls = require("src.core.TouchControls") +local Playfield = require("src.render.Playfield") +local MainMenu = require("src.ui.gen2.MainMenu") +local GenderSelect = require("src.ui.gen2.GenderSelect") +local InitClock = require("src.ui.gen2.InitClock") +local OakSpeech = require("src.ui.gen2.OakSpeech") +local NamePick = require("src.ui.gen2.NamePick") + +local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots" + +return function(game) + local fails = 0 + local function say(line) print("[2230] " .. line) end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + end + local function top() return game.stack:top() end + local function isA(class) return top() ~= nil and getmetatable(top()) == class end + + local function tap(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 4) + end + + local function waitFor(label, predicate, frames, spam) + for _ = 1, frames or 900 do + if predicate() then return true end + if spam then tap(spam, 2) else U.wait(1) end + end + ok(false, "stalled waiting for " .. label .. " (top is " .. tostring(top()) .. ")") + U.shot(game, SHOT_DIR .. "/2230_stall.png") + return false + end + + -- Counts the non-white, non-black pixels inside the skin's screen cutout: + -- a GB page that actually painted there has plenty, a page clipped away + -- leaves only the letterbox fill. + local function paintedInCutout(path) + local f = io.open(path, "rb") + if not f then return nil end + local bytes = f:read("*a") + f:close() + local image = love.image.newImageData(love.filesystem.newFileData(bytes, "shot.png")) + local w, h = love.graphics.getDimensions() + local cx, cy, cw, ch = Playfield.cutout(w, h) + if not cx then cx, cy, cw, ch = 0, 0, w, h end + local sx = image:getWidth() / w + local painted = 0 + for y = cy, cy + ch - 1, 4 do + for x = cx, cx + cw - 1, 4 do + local r, g, b = image:getPixel(math.floor(x * sx), math.floor(y * sx)) + local lit = r > 0.05 or g > 0.05 or b > 0.05 + local white = r > 0.95 and g > 0.95 and b > 0.95 + if lit and not white then painted = painted + 1 end + end + end + return painted + end + + local skins = TouchSkin.list() + local want = os.getenv("POKEPORT_SKIN") + if not want or want == "" then want = skins[1] and skins[1].id end + ok(want ~= nil, "a skin is installed to select") + if not want then return end + local controls = game.touchControls or TouchControls + local skin, err = controls:selectSkin(want) + ok(skin ~= nil, "selected skin " .. tostring(want) .. " " .. tostring(err or "")) + ok(TouchSkin.drawable(), "the skin is live, so Playfield cuts the screen out") + + if not waitFor("the intro menu", function() return isA(MainMenu) end, 2400, "start") then return end + local menu = top() + for i, item in ipairs(menu.list.items) do + if item.value == "new" then menu.list.index = i end + end + tap("a") + + if not waitFor("the gender menu", + function() return isA(GenderSelect) and top():menuOpen() end, 900) then return end + U.wait(5) + local genderShot = SHOT_DIR .. "/2230_01_gender_in_cutout.png" + U.shot(game, genderShot) + local painted = paintedInCutout(genderShot) + ok(painted and painted > 200, + ("the gender page painted inside the cutout (%s coloured samples)"):format(tostring(painted))) + tap("a") + + if not waitFor("the clock", function() return isA(InitClock) end, 900) then return end + U.shot(game, SHOT_DIR .. "/2230_02_clock.png") + if not waitFor("Oak", function() return isA(OakSpeech) end, 1200, "a") then return end + U.wait(30) + U.shot(game, SHOT_DIR .. "/2230_03_oak.png") + if not waitFor("the name picker", function() return isA(NamePick) end, 1500, "a") then return end + U.wait(60) + top().cursor = 2 + tap("a") + if not waitFor("the bedroom", function() + return game.phase == "play" and game.world and game.world.map ~= nil + end, 2400, "a") then return end + ok(game.world.map.id == "PLAYERS_HOUSE_2F", "new game landed in the bedroom under the skin") + U.wait(30) + U.shot(game, SHOT_DIR .. "/2230_04_bedroom.png") + + say(fails == 0 and "all claims passed" or (fails .. " claims failed")) + say("2230_01_gender_in_cutout.png should show the boy/girl prompt inside the") + say("skin's screen window. an empty white window there, or a lua error after") + say("the name picker, is the bug.") + love.event.quit(fails == 0 and 0 or 1) +end