mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
CLOSES #223, CLOSES #233, CLOSES #236, CLOSES #240, CLOSES #241, CLOSES #249, CLOSES #252, CLOSES #255, CLOSES #257, CLOSES #258, CLOSES #263, CLOSES #265, CLOSES #274, CLOSES #275, CLOSES #276, CLOSES #279, CLOSES #280, CLOSES #282, CLOSES #283, CLOSES #287, CLOSES #291, CLOSES #292, CLOSES #293, CLOSES #301, CLOSES #304, CLOSES #315, CLOSES #316, CLOSES #317, CLOSES #321, CLOSES #322, CLOSES #330
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
-- Driver: in SGB mode a character wears the palette of the map it stands on,
|
||||
-- not the GBC boot ROM's object palette (#301). pokered never sends OBJ_TRN
|
||||
-- (data/sgb/sgb_packets.asm), and home/fade.asm:68 FadePal4 leaves rOBP0 = $D0,
|
||||
-- so OBJ colours lift to DMG shades 0/1/3 and the zone shader owns the colour.
|
||||
-- POKEPORT_DRIVER=tests/drivers/sgb_people_palette_bug301_test.lua \
|
||||
-- POKEPORT_IDENTITY=bug301 POKEPORT_VERSION=red SHOT_DIR=/tmp/shots love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Probe = dofile("tests/drivers/shot_probe.lua")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local fails = 0
|
||||
local function check(ok, msg)
|
||||
U.log(ok and "PASS" or "FAIL", msg)
|
||||
if not ok then fails = fails + 1 end
|
||||
return ok
|
||||
end
|
||||
local function rgb(c)
|
||||
return c and ("(%d,%d,%d)"):format(c[1], c[2], c[3]) or "nil"
|
||||
end
|
||||
local function ramp(p)
|
||||
if not p then return "nil" end
|
||||
local s = {}
|
||||
for i = 1, 4 do s[i] = rgb(p[i]) end
|
||||
return table.concat(s, " ")
|
||||
end
|
||||
|
||||
-- a party + starter flag so the overworld behaves like a real save
|
||||
game.save.flags.EVENT_GOT_STARTER = true
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
if #game.save.party == 0 then
|
||||
table.insert(game.save.party, Pokemon.new(game.data, "CHARMANDER", 5))
|
||||
end
|
||||
-- Game:applyOptions re-reads save.options.colors every frame's worth of
|
||||
-- option handling, so a bare setMode() would be reverted under us.
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.colors = "gbc"
|
||||
PaletteFX.setMode("gbc")
|
||||
|
||||
-- ---- Part 1: the render decisions no screenshot can spell out -----------
|
||||
check(PaletteFX.usesSpriteObp("gbc") == false,
|
||||
"SGB bakes NO object palette (it cannot colour an OBJ apart from the BG)")
|
||||
check(PaletteFX.usesSpriteObp("ogred") == true,
|
||||
"OG RED still does (the GBC boot ROM really does hand out one OBJ palette)")
|
||||
check(PaletteFX.usesSpriteObp("og") == false
|
||||
and PaletteFX.usesSpriteObp("og_inv") == false
|
||||
and PaletteFX.usesSpriteObp("gbc_inv") == false
|
||||
and PaletteFX.usesSpriteObp("classic") == false
|
||||
and PaletteFX.usesSpriteObp("redpp") == false,
|
||||
"no other mode bakes one either")
|
||||
|
||||
local obp = PaletteFX.dmgObj()
|
||||
U.log("rOBP0 bake ramp:", ramp(obp))
|
||||
-- FadePal4's second entry, `dc 3,1,0,0` = $D0. Index 1 is never read (OBJ
|
||||
-- colour 0 is keyed to alpha); 2..4 are OBJ colours 1..3 as shades 0/1/3.
|
||||
check(obp ~= nil and obp[2][1] == 255, "OBJ colour 1 -> DMG shade 0 (255)")
|
||||
check(obp ~= nil and obp[3][1] == 170, "OBJ colour 2 -> DMG shade 1 (170)")
|
||||
check(obp ~= nil and obp[4][1] == 0, "OBJ colour 3 -> DMG shade 3 (0)")
|
||||
local grey = true
|
||||
for i = 1, 4 do
|
||||
if obp[i][1] ~= obp[i][2] or obp[i][2] ~= obp[i][3] then grey = false end
|
||||
end
|
||||
check(grey, "the bake stays in DMG GREYS, so the zone shader still owns the colour")
|
||||
|
||||
local OGOBJ = PaletteFX.ogObj()
|
||||
local clash = false
|
||||
for i = 1, 4 do
|
||||
for j = 1, 4 do
|
||||
if obp[i][1] == OGOBJ[j][1] and obp[i][2] == OGOBJ[j][2]
|
||||
and obp[i][3] == OGOBJ[j][3] and OGOBJ[j][1] ~= OGOBJ[j][2] then
|
||||
clash = true
|
||||
end
|
||||
end
|
||||
end
|
||||
check(not clash, "no boot-ROM object colour is baked into an SGB sprite")
|
||||
|
||||
-- ---- Part 2: stand next to a person, on two maps with unlike palettes ---
|
||||
local function npcNamed(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
|
||||
|
||||
-- Teleport to `stand`; if a map edit or a mod moved the NPC out of reach,
|
||||
-- take any free walkable neighbour of where it actually is and face back.
|
||||
local function standNextTo(mapId, npcName, stand)
|
||||
U.teleport(game, mapId, stand.x, stand.y, stand.facing)
|
||||
U.wait(12)
|
||||
local ow = game.overworld
|
||||
local npc = ow and npcNamed(ow, npcName)
|
||||
if not npc then
|
||||
check(false, npcName .. " object loaded on " .. mapId)
|
||||
return ow, nil
|
||||
end
|
||||
local fx, fy = ow.player:facingCell()
|
||||
if ow:npcAtCell(fx, fy) ~= npc then
|
||||
local sides = {
|
||||
{ 0, 1, "up" }, { 0, -1, "down" }, { 1, 0, "left" }, { -1, 0, "right" },
|
||||
}
|
||||
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
|
||||
U.log(("stand cell (%d,%d) is no longer beside %s -- using")
|
||||
:format(stand.x, stand.y, npcName), cx, cy, "facing", s[3])
|
||||
U.teleport(game, mapId, cx, cy, s[3])
|
||||
U.wait(12)
|
||||
ow = game.overworld
|
||||
npc = npcNamed(ow, npcName)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local ax, ay = ow.player:facingCell()
|
||||
check(npc ~= nil and ow:npcAtCell(ax, ay) == npc,
|
||||
"player is standing face to face with " .. npcName .. " on " .. mapId)
|
||||
return ow, npc
|
||||
end
|
||||
|
||||
-- Every colour a correct SGB frame may contain: the map's own four plus the
|
||||
-- letterbox black. Before the fix the boot-ROM greens were on screen and
|
||||
-- belonged to no palette the map ever asked for.
|
||||
local function paletteAudit(label, palette)
|
||||
local shot = Probe.grab()
|
||||
if not shot then
|
||||
U.log("WARN pixel probe unavailable; judge", label, "by eye only")
|
||||
return
|
||||
end
|
||||
local top, total = Probe.top(shot, 8)
|
||||
U.log(("probe[%s] %d px sampled, top colours: %s")
|
||||
:format(label, total, Probe.fmt(top)))
|
||||
local allowed = { { 0, 0, 0 } }
|
||||
for i = 1, 4 do allowed[#allowed + 1] = palette[i] end
|
||||
local strays = {}
|
||||
for _, c in ipairs(top) do
|
||||
local ok = false
|
||||
for _, a in ipairs(allowed) do
|
||||
if c[1] == a[1] and c[2] == a[2] and c[3] == a[3] then ok = true end
|
||||
end
|
||||
-- ignore the thin filtered edges: only a colour with real area on
|
||||
-- screen is evidence of a wrong palette
|
||||
if not ok and c.share > 0.002 then
|
||||
strays[#strays + 1] = ("(%d,%d,%d) %.1f%%")
|
||||
:format(c[1], c[2], c[3], c.share * 100)
|
||||
end
|
||||
end
|
||||
check(#strays == 0, label
|
||||
.. ": every colour with real area comes from the map's own palette"
|
||||
.. (#strays > 0 and (" -- strays: " .. table.concat(strays, ", ")) or ""))
|
||||
return shot
|
||||
end
|
||||
|
||||
local function countIn(shot, wanted)
|
||||
if not shot then return nil end
|
||||
local counts = Probe.count(shot, wanted)
|
||||
local parts = {}
|
||||
for name, n in pairs(counts) do parts[#parts + 1] = ("%s=%d"):format(name, n) end
|
||||
table.sort(parts)
|
||||
U.log(" ", table.concat(parts, " "))
|
||||
return counts
|
||||
end
|
||||
|
||||
-- OBJ colour 0 is transparent on the hardware and the bake keys it to alpha.
|
||||
-- A love Image exposes no pixels, so the only way to read that back is to
|
||||
-- draw it into a cleared canvas. A guard, not a gate: the extracted sheets
|
||||
-- already ship a tRNS key, so this only fires if a later change hands a
|
||||
-- pipeline or the tilt pass an unkeyed image and the sprite grows a backdrop.
|
||||
local function transparentShare(spr)
|
||||
if not (spr and spr.resolveImage and love.graphics.newCanvas) then return nil end
|
||||
local ok, share = pcall(function()
|
||||
local img = spr:resolveImage()
|
||||
local w, h = img:getWidth(), img:getHeight()
|
||||
local cv = love.graphics.newCanvas(w, h)
|
||||
love.graphics.setCanvas(cv)
|
||||
love.graphics.clear(0, 0, 0, 0)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(img, 0, 0)
|
||||
love.graphics.setCanvas()
|
||||
local id = cv:newImageData()
|
||||
local clear = 0
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
local _, _, _, a = id:getPixel(x, y)
|
||||
if a < 0.02 then clear = clear + 1 end
|
||||
end
|
||||
end
|
||||
return clear / (w * h)
|
||||
end)
|
||||
love.graphics.setCanvas()
|
||||
return ok and share or nil
|
||||
end
|
||||
|
||||
local GBC_OBJ = PaletteFX.ogObj()
|
||||
local BOOT = { bootGreen = GBC_OBJ[2], bootDarkGreen = GBC_OBJ[3] }
|
||||
|
||||
-- ---- ROUTE_1, beside a YOUNGSTER, in grass -----------------------------
|
||||
-- ../pokered/data/maps/objects/Route1.asm: ROUTE1_YOUNGSTER2 walks
|
||||
-- left/right on (15, 13), so (14, 13) facing right is beside it.
|
||||
local ROUTE = PaletteFX.pal(game.data, "ROUTE")
|
||||
U.log("ROUTE palette:", ramp(ROUTE))
|
||||
local ow = standNextTo("ROUTE_1", "ROUTE1_YOUNGSTER2",
|
||||
{ x = 14, y = 13, facing = "right" })
|
||||
U.wait(40) -- let the grass / flower tile animation cycle
|
||||
U.shot(game, DIR .. "/bug301_1_route1_sgb.png")
|
||||
local shot = paletteAudit("ROUTE_1 / SGB", ROUTE)
|
||||
local c = countIn(shot, {
|
||||
bootGreen = BOOT.bootGreen, bootDarkGreen = BOOT.bootDarkGreen,
|
||||
routeGrassGreen = ROUTE[2], routeLightBlue = ROUTE[3],
|
||||
})
|
||||
if c then
|
||||
check(c.bootGreen == 0 and c.bootDarkGreen == 0,
|
||||
"no Game Boy Color boot-ROM green anywhere on the SGB screen (#301)")
|
||||
check(c.routeGrassGreen > 0,
|
||||
"ROUTE's own grass green IS on screen (the cap and the grass share it, #150)")
|
||||
end
|
||||
|
||||
local spr = ow and ow.player and ow.player.sprite
|
||||
check(spr ~= nil and spr.image ~= nil and spr:resolveImage() ~= spr.image,
|
||||
"the player's sprite resolves to a BAKED image in SGB, not the raw sheet")
|
||||
local share = transparentShare(spr)
|
||||
if share == nil then
|
||||
U.log("WARN could not read the baked sprite back; judge transparency by eye")
|
||||
else
|
||||
U.log(("baked player sheet is %.1f%% fully transparent"):format(share * 100))
|
||||
check(share > 0.1,
|
||||
"OBJ colour 0 stays keyed to alpha, so a character has no backdrop")
|
||||
end
|
||||
|
||||
-- ---- LAVENDER_TOWN, beside a COOLTRAINER, on a PINK palette ------------
|
||||
-- ../pokered/data/maps/objects/LavenderTown.asm: LAVENDERTOWN_COOLTRAINER_M
|
||||
-- stands on (9, 10), so (9, 9) facing down is beside it. No green anywhere
|
||||
-- in this palette, so a green person here is unmistakable.
|
||||
local LAVENDER = PaletteFX.pal(game.data, "LAVENDER")
|
||||
U.log("LAVENDER palette:", ramp(LAVENDER))
|
||||
standNextTo("LAVENDER_TOWN", "LAVENDERTOWN_COOLTRAINER_M",
|
||||
{ x = 9, y = 9, facing = "down" })
|
||||
U.wait(24)
|
||||
U.shot(game, DIR .. "/bug301_2_lavender_sgb.png")
|
||||
shot = paletteAudit("LAVENDER_TOWN / SGB", LAVENDER)
|
||||
c = countIn(shot, {
|
||||
bootGreen = BOOT.bootGreen, bootDarkGreen = BOOT.bootDarkGreen,
|
||||
lavenderPink = LAVENDER[2], routeGrassGreen = ROUTE[2],
|
||||
})
|
||||
if c then
|
||||
check(c.bootGreen == 0 and c.bootDarkGreen == 0,
|
||||
"no boot-ROM green in LAVENDER TOWN either")
|
||||
check(c.lavenderPink > 0,
|
||||
"the characters here wear LAVENDER's pink -- they changed with the map")
|
||||
check(c.routeGrassGreen == 0,
|
||||
"and no ROUTE green followed them over (nothing is palette-pinned)")
|
||||
end
|
||||
|
||||
-- ---- OG RED, the mode that IS supposed to have an object palette -------
|
||||
-- A GBC boots the cartridge with one BG and one OBJ palette, so there the
|
||||
-- people really are green. Losing that green means the fix went too far.
|
||||
game.save.options.colors = "ogred"
|
||||
PaletteFX.setMode("ogred")
|
||||
standNextTo("ROUTE_1", "ROUTE1_YOUNGSTER2",
|
||||
{ x = 14, y = 13, facing = "right" })
|
||||
U.wait(30)
|
||||
U.shot(game, DIR .. "/bug301_3_route1_ogred.png")
|
||||
local ogShot = Probe.grab()
|
||||
c = countIn(ogShot, {
|
||||
bootGreen = BOOT.bootGreen, bootDarkGreen = BOOT.bootDarkGreen,
|
||||
ogBgRed = PaletteFX.GBC_BG[3], ogBgPink = PaletteFX.GBC_BG[2],
|
||||
})
|
||||
if c then
|
||||
check(c.bootGreen + c.bootDarkGreen > 0,
|
||||
"OG RED keeps its boot-ROM green characters (regression guard)")
|
||||
check(c.ogBgRed + c.ogBgPink > 0, "over OG RED's red terrain")
|
||||
end
|
||||
game.save.options.colors = "gbc"
|
||||
PaletteFX.setMode("gbc")
|
||||
U.wait(20)
|
||||
|
||||
-- ---- tilt mode, as a guard rather than a gate --------------------------
|
||||
-- Tilt colorizes each billboard on its own, outside the whole-canvas zone
|
||||
-- pass, so it is the path most likely to disagree with the flat one. Pure
|
||||
-- white and raw DMG grey belong to no SGB palette (they all open on paper,
|
||||
-- 255,239,255), so either one here means an uncolorized sheet got through.
|
||||
standNextTo("ROUTE_1", "ROUTE1_YOUNGSTER2",
|
||||
{ x = 14, y = 13, facing = "right" })
|
||||
game.save.options.tilt = 2
|
||||
require("src.render.Tilt").setLevel(2)
|
||||
U.wait(70) -- the tilt tween is presentational and runs on real time
|
||||
U.shot(game, DIR .. "/bug301_4_route1_tilt_sgb.png")
|
||||
local tiltShot = Probe.grab()
|
||||
c = countIn(tiltShot, {
|
||||
pureWhite = { 255, 255, 255 }, rawGrey170 = { 170, 170, 170 },
|
||||
bootGreen = BOOT.bootGreen, paper = ROUTE[1],
|
||||
})
|
||||
if c then
|
||||
check(c.pureWhite == 0 and c.rawGrey170 == 0,
|
||||
"tilt billboards carry no uncolorized sheet pixels")
|
||||
check(c.bootGreen == 0, "and no boot-ROM green in tilt mode either")
|
||||
end
|
||||
game.save.options.tilt = 0
|
||||
require("src.render.Tilt").setLevel(0)
|
||||
U.wait(50)
|
||||
|
||||
U.log(fails == 0 and "all #301 preconditions passed"
|
||||
or (fails .. " #301 precondition(s) FAILED -- read up"))
|
||||
|
||||
-- ---- Part 3: hand off, standing next to somebody, in the default mode ---
|
||||
standNextTo("ROUTE_1", "ROUTE1_YOUNGSTER2",
|
||||
{ x = 14, y = 13, facing = "right" })
|
||||
|
||||
U.log("You are on ROUTE 1 in SGB colour, face to face with a YOUNGSTER. Both")
|
||||
U.log("of you should take colour from the ground's own palette (Red's cap on")
|
||||
U.log("ROUTE's grass green) and change with every map you walk onto. #301")
|
||||
U.log("was everyone painted boot-ROM lime green everywhere. Shots in " .. DIR)
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user