mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
282 lines
11 KiB
Lua
282 lines
11 KiB
Lua
-- Driver: eyeball the party screen's colors (#274, absorbing #272). pokered
|
|
-- SetPal_PartyMenu (engine/gfx/palettes.asm:90) sends a four-palette block
|
|
-- packet: MEWMON over the icon column, GREENBAR elsewhere, and one block per
|
|
-- HP-bar row from GetHealthBarColor (palettes.asm:293-325).
|
|
-- POKEPORT_DRIVER=tests/drivers/party_hp_palette_bug274_test.lua \
|
|
-- POKEPORT_IDENTITY=bug274 SHOT_DIR=/tmp/shots love .
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
local PaletteFX = require("src.render.PaletteFX")
|
|
local PartyMenu = require("src.ui.PartyMenu")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local Screens = require("src.ui.Screens")
|
|
local Strings = require("src.core.Strings")
|
|
|
|
local function check(label, ok)
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
|
|
-- Set the SAVED option, not just the live mode: Game:applyOptions re-reads
|
|
-- save.options.colors, so a bare setMode gets reverted.
|
|
game.save.options = game.save.options or {}
|
|
game.save.options.colors = "redpp"
|
|
PaletteFX.setMode("redpp")
|
|
check("COLORS is ADVANCED (the mode #274 was reported in)",
|
|
PaletteFX.mode == "redpp" and PaletteFX.modeLabel() == "ADVANCED")
|
|
|
|
-- A 48/48 bar is one pixel per HP, so GetHealthBarColor's 27 / 10 pixel
|
|
-- thresholds land on plain numbers.
|
|
local ROWS = {
|
|
{ species = "BULBASAUR", hp = 48, pal = "GREENBAR", fill = true },
|
|
{ species = "PIDGEY", hp = 26, pal = "YELLOWBAR", fill = true },
|
|
{ species = "RATTATA", hp = 9, pal = "REDBAR", fill = true },
|
|
{ species = "CATERPIE", hp = 0, pal = "REDBAR", fill = false },
|
|
}
|
|
|
|
-- ---- preconditions the eye cannot check --------------------------------
|
|
-- A missing palette name, an absent shader or a stale zone rect all read as
|
|
-- "the colors look wrong", exactly like the bug did.
|
|
local function pal(name) return PaletteFX.pal(game.data, name) end
|
|
local function rgb(c)
|
|
return c and ("{" .. c[1] .. "," .. c[2] .. "," .. c[3] .. "}") or "nil"
|
|
end
|
|
|
|
local mew = pal("MEWMON")
|
|
local green = pal("GREENBAR")
|
|
check("MEWMON and the three bar palettes all resolve",
|
|
mew ~= nil and green ~= nil and pal("YELLOWBAR") ~= nil
|
|
and pal("REDBAR") ~= nil)
|
|
-- the exact purple the reporter's screenshot was full of
|
|
check("MEWMON color 2 is the reported purple {115,33,165}",
|
|
mew ~= nil and mew[3][1] == 115 and mew[3][2] == 33 and mew[3][3] == 165)
|
|
U.log(" MEWMON :", rgb(mew and mew[1]), rgb(mew and mew[2]),
|
|
rgb(mew and mew[3]), rgb(mew and mew[4]))
|
|
for _, n in ipairs({ "GREENBAR", "YELLOWBAR", "REDBAR" }) do
|
|
local c = pal(n)
|
|
U.log((" %-9s fill color 2: %s"):format(n, rgb(c and c[3])))
|
|
end
|
|
-- the base zone swapped from MEWMON to GREENBAR, which stays invisible only
|
|
-- because the two agree on paper and ink: the nicknames, level line, HP
|
|
-- numbers, box border and cursor are all color 3 on color 0
|
|
check("MEWMON and GREENBAR share color 0 and color 3 (text/box unchanged)",
|
|
mew ~= nil and green ~= nil
|
|
and mew[1][1] == green[1][1] and mew[1][2] == green[1][2]
|
|
and mew[1][3] == green[1][3]
|
|
and mew[4][1] == green[4][1] and mew[4][2] == green[4][2]
|
|
and mew[4][3] == green[4][3])
|
|
-- with no shade-remap shader nothing colorizes the canvas and the pixel
|
|
-- counts below are meaningless
|
|
check("the SGB shade-remap shader compiled", PaletteFX.shader() ~= nil)
|
|
|
|
for _, row in ipairs(ROWS) do
|
|
check(row.species .. " is a known species",
|
|
game.data.pokemon[row.species] ~= nil)
|
|
check(("%d/48 HP is a %s bar"):format(row.hp, row.pal),
|
|
PaletteFX.barPalName(row.hp, 48) == row.pal)
|
|
end
|
|
|
|
-- ---- park the player, then open the party list yourself -----------------
|
|
-- pokered data/maps/objects/PalletTown.asm: (10, 12) carries no warp_event,
|
|
-- bg_event or object_event. Any open cell would do; the fallback below
|
|
-- finds one if a map edit ever closes this one.
|
|
local party = {}
|
|
for i, row in ipairs(ROWS) do
|
|
local mon = Pokemon.new(game.data, row.species, 20)
|
|
mon.stats.hp = 48
|
|
mon.hp = row.hp
|
|
party[i] = mon
|
|
end
|
|
game.save.party = party
|
|
game.save.player.name = game.save.player.name or "RED"
|
|
|
|
U.teleport(game, "PALLET_TOWN", 10, 12, "down")
|
|
U.wait(10)
|
|
local ow = game.overworld
|
|
if ow and ow.map and not ow.map:isWalkableCell(10, 12) then
|
|
local fx, fy
|
|
for y = 1, 16 do
|
|
for x = 1, 18 do
|
|
if not fx and ow.map:isWalkableCell(x, y) and not ow:npcAtCell(x, y) then
|
|
fx, fy = x, y
|
|
end
|
|
end
|
|
end
|
|
if fx then
|
|
U.log("(10, 12) is blocked now; standing on", fx, fy)
|
|
U.teleport(game, "PALLET_TOWN", fx, fy, "down")
|
|
U.wait(10)
|
|
end
|
|
end
|
|
|
|
-- START -> POKéMON -> A, walked rather than pushed, so a broken start menu
|
|
-- shows up here instead of masquerading as a palette bug
|
|
U.tap(game, "start")
|
|
U.wait(12)
|
|
local menu = game.stack:top()
|
|
local target = 1
|
|
if menu and menu.items then
|
|
local want = Strings("POKéMON")
|
|
for i, it in ipairs(menu.items) do
|
|
if it.label == want then target = i break end
|
|
end
|
|
check("START menu lists POKéMON", menu.items[target] ~= nil
|
|
and menu.items[target].label == want)
|
|
for _ = 2, target do
|
|
U.tap(game, "down")
|
|
U.wait(4)
|
|
end
|
|
else
|
|
check("START opened the start menu", false)
|
|
end
|
|
U.tap(game, "a")
|
|
U.wait(16)
|
|
|
|
local pm = game.stack:top()
|
|
if getmetatable(pm) ~= PartyMenu then
|
|
U.log("start-menu walk did not land on the party list; pushing it directly")
|
|
Screens.push(game, "PartyMenu", {})
|
|
U.wait(12)
|
|
pm = game.stack:top()
|
|
end
|
|
check("the party list is open", getmetatable(pm) == PartyMenu)
|
|
|
|
-- ---- the block packet this screen asks for ------------------------------
|
|
local zones = getmetatable(pm) == PartyMenu and pm:sgbPalettes(game) or nil
|
|
check("sgbPalettes returns base + icon column + one block per bar row",
|
|
type(zones) == "table" and #zones == 2 + #ROWS)
|
|
zones = zones or {}
|
|
check("the base zone is GREENBAR over the whole screen",
|
|
zones[1] ~= nil and zones[1].colors == green and zones[1].x == 0
|
|
and zones[1].y == 0 and zones[1].w == 160 and zones[1].h == 144)
|
|
check("the icon column is a MEWMON block at tiles 1-2, rows 0-11",
|
|
zones[2] ~= nil and zones[2].colors == mew and zones[2].x == 8
|
|
and zones[2].y == 0 and zones[2].w == 16 and zones[2].h == 96)
|
|
for i, row in ipairs(ROWS) do
|
|
local z = zones[2 + i]
|
|
check(("row %d (%d/48 HP) carries the %s block palette")
|
|
:format(i, row.hp, row.pal),
|
|
z ~= nil and z.colors == pal(row.pal))
|
|
check(("row %d's block covers the bar's cap + six fill tiles"):format(i),
|
|
z ~= nil and z.x == 48 and z.w == 56 and z.y == (i * 2 - 1) * 8
|
|
and z.h == 8)
|
|
end
|
|
|
|
-- ---- what the screen actually holds -------------------------------------
|
|
-- Render the live menu into a clean 160x144 canvas and run the same zone
|
|
-- pass Renderer:endFrame does (shade-remap shader, sendColors per zone,
|
|
-- scissor to its rect, redraw), at scale 1 so a pixel is a pixel.
|
|
local function colorized()
|
|
local raw = love.graphics.newCanvas(160, 144)
|
|
love.graphics.setCanvas(raw)
|
|
love.graphics.clear(1, 1, 1, 1)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
pm:draw()
|
|
love.graphics.setCanvas()
|
|
|
|
local out = love.graphics.newCanvas(160, 144)
|
|
local shader = PaletteFX.shader()
|
|
love.graphics.setCanvas(out)
|
|
love.graphics.clear(0, 0, 0, 1)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
if shader then love.graphics.setShader(shader) end
|
|
for _, z in ipairs(pm:sgbPalettes(game) or {}) do
|
|
if shader and z.colors then PaletteFX.sendColors(shader, z.colors) end
|
|
love.graphics.setScissor(z.x, z.y, z.w, z.h)
|
|
love.graphics.draw(raw, 0, 0)
|
|
end
|
|
love.graphics.setScissor()
|
|
love.graphics.setShader()
|
|
love.graphics.setCanvas()
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
return out:newImageData()
|
|
end
|
|
|
|
-- count pixels in a rect that match a palette color (the zone pass writes
|
|
-- palette colors exactly, so the tolerance only absorbs 8-bit rounding)
|
|
local function countColor(id, c, x0, y0, x1, y1)
|
|
if not c then return -1 end
|
|
local want = { c[1] / 255, c[2] / 255, c[3] / 255 }
|
|
local n = 0
|
|
for y = y0, y1 do
|
|
for x = x0, x1 do
|
|
local r, g, b = id:getPixel(x, y)
|
|
if math.abs(r - want[1]) < 0.02 and math.abs(g - want[2]) < 0.02
|
|
and math.abs(b - want[3]) < 0.02 then
|
|
n = n + 1
|
|
end
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
if getmetatable(pm) == PartyMenu and love.graphics.newCanvas
|
|
and PaletteFX.shader() then
|
|
local id = colorized()
|
|
|
|
-- (a) the icons: OBP0 "3100" means an object never displays shade 2, so
|
|
-- MEWMON's third color must appear nowhere on the screen at all
|
|
local purple = countColor(id, mew and mew[3], 0, 0, 159, 143)
|
|
U.log(" MEWMON color-2 (purple) pixels on the whole screen:", purple)
|
|
check("no purple anywhere -- the icons never show shade 2", purple == 0)
|
|
-- and the icons are there at all: MEWMON color 1 is the warm orange they
|
|
-- read as after the OBP bake
|
|
local flesh = countColor(id, mew and mew[2], 8, 0, 23, 95)
|
|
U.log(" MEWMON color-1 (orange) pixels in the icon column:", flesh)
|
|
check("the icon column is drawn (not blank)", flesh > 0)
|
|
|
|
-- (b) the bars: each row's fill must be ITS OWN bar color and none of the
|
|
-- other two. Fill tiles run x 56..103 (drawHPBar at tile 5: "HP" 40,
|
|
-- ":[" 48, six fill tiles 56..103, cap 104).
|
|
for i, row in ipairs(ROWS) do
|
|
local y0 = (i * 2 - 1) * 8
|
|
local own = pal(row.pal)
|
|
-- the three bar palettes share color 0/1/3; only color 2, the fill
|
|
-- shade, tells them apart
|
|
local mine = countColor(id, own and own[3], 56, y0, 103, y0 + 7)
|
|
U.log((" row %d (%d/48 HP) %s fill pixels: %d")
|
|
:format(i, row.hp, row.pal, mine))
|
|
if row.fill then
|
|
check(("row %d's bar is filled with %s"):format(i, row.pal), mine > 0)
|
|
end
|
|
for _, other in ipairs({ "GREENBAR", "YELLOWBAR", "REDBAR" }) do
|
|
if other ~= row.pal then
|
|
local c = pal(other)
|
|
local n = countColor(id, c and c[3], 56, y0, 103, y0 + 7)
|
|
check(("row %d's bar carries no %s"):format(i, other), n == 0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ---- screenshots ---------------------------------------------------------
|
|
U.wait(4)
|
|
if U.shot(game, DIR .. "/bug274_party_advanced.png") then
|
|
U.log("captured", DIR .. "/bug274_party_advanced.png")
|
|
end
|
|
-- the same screen in SGB: the block packet changed for every mode, not just
|
|
-- ADVANCED
|
|
game.save.options.colors = "gbc"
|
|
PaletteFX.setMode("gbc")
|
|
U.wait(10)
|
|
if U.shot(game, DIR .. "/bug274_party_sgb.png") then
|
|
U.log("captured", DIR .. "/bug274_party_sgb.png")
|
|
end
|
|
game.save.options.colors = "redpp"
|
|
PaletteFX.setMode("redpp")
|
|
U.wait(10)
|
|
|
|
-- ---- hand off -----------------------------------------------------------
|
|
U.log("The party list is open in COLORS = ADVANCED: BULBASAUR 48/48, PIDGEY")
|
|
U.log("26/48, RATTATA 9/48, CATERPIE 0/48. Three different bar colors run")
|
|
U.log("down the screen (green, yellow, red) and the icons carry no purple.")
|
|
U.log("#274 was every bar solid black at full health and purple-blotched icons.")
|
|
U.log("OPTION -> COLORS cycles modes; OG and CLASSIC are mono by design.")
|
|
U.log("Captures: bug274_party_advanced.png and bug274_party_sgb.png in " .. DIR)
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|