Files
gen1recomp/tests/gen2_screen_layout_test.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

221 lines
9.0 KiB
Lua

-- Gen 2 screen geometry: the blit scale a widescreen screen paints its
-- 160x144 panel at, and the naming keyboard's cursor bracket.
--
-- Both are pure layout, so they are asserted as coordinates rather than as
-- pixels. tests/drivers/gold_menu_shots.lua is what shows the result.
package.path = "./?.lua;" .. package.path
love = love or {}
love.graphics = love.graphics or {
getColor = function() return 1, 1, 1, 1 end,
setColor = function() end,
rectangle = function() end,
print = function() end,
printf = function() end,
draw = function() end,
newQuad = function() return {} end,
newImage = function() return nil end,
getShader = function() return nil end,
setShader = function() end,
newShader = function() error("no shaders in this harness") end,
getDimensions = function() return 160, 144 end,
push = function() end, pop = function() end,
translate = function() end, scale = function() end,
circle = function() end, clear = function() end,
}
love.math = love.math or { random = function(a, b) return b and a or 0.5 end }
love.filesystem = love.filesystem or {
load = function() return nil end,
getInfo = function() return nil end,
read = function() return nil end,
}
require("src.core.Logger").warn = function() end
local Chrome = require("src.ui.gen2.Chrome")
local NamingScreen = require("src.ui.gen2.NamingScreen")
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
local checks, failures = 0, 0
local function check(label, got, want)
checks = checks + 1
if got ~= want then
failures = failures + 1
print(("FAIL %s: got %s want %s"):format(label, tostring(got),
tostring(want)))
end
end
-- ------------------------------------------------------- panel blit scale
--
-- A GB pixel has to cover a whole number of window pixels: a fractional blit
-- gives one row of an 8x8 tile two device pixels and its neighbour one, which
-- is what tore box borders and ate rows out of glyphs.
check("1024x768 fits five whole pixels", Chrome.fitScale(1024, 768), 5)
check("exactly 160x144 is 1:1", Chrome.fitScale(160, 144), 1)
check("640x576 is a clean four", Chrome.fitScale(640, 576), 4)
check("a window smaller than the screen still blits at 1",
Chrome.fitScale(100, 90), 1)
check("height constrains a wide window", Chrome.fitScale(1920, 480), 3)
check("width constrains a tall window", Chrome.fitScale(480, 1920), 3)
local ox, oy = Chrome.fitOrigin(1024, 768)
check("panel centred horizontally", ox, math.floor((1024 - 160 * 5) / 2))
check("panel centred vertically", oy, math.floor((768 - 144 * 5) / 2))
check("origin is whole pixels", ox, math.floor(ox))
-- The scale and the origin have to agree, or a nested TextBox drawn by Game2
-- would land on a different grid than the panel under it.
for _, size in ipairs({ { 1024, 768 }, { 1280, 720 }, { 800, 600 },
{ 1366, 768 } }) do
local s = Chrome.fitScale(size[1], size[2])
local x, y = Chrome.fitOrigin(size[1], size[2], s)
check(("%dx%d panel fits inside the window"):format(size[1], size[2]),
(x >= 0 and y >= 0 and 160 * s + 2 * x <= size[1] + 1
and 144 * s + 2 * y <= size[2] + 1), true)
end
-- ------------------------------------------------------- naming cursor
--
-- OAMData_TextEntryCursor is a box stamped AROUND the character cell out of
-- one corner tile; .LetterEntries steps its XOFFSET by $10 a column from an
-- XCOORD of 24 (OAM), i.e. screen tile 2 for column 0. The old cursor sat a
-- tile to the left, between two letters.
local naming = NamingScreen.new({}, { type = "player" })
local KEYBOARD_TOP = 8
for col = 0, 8 do
naming.col, naming.row = col, 0
local tx, ty, wide = naming:cursorTile()
check(("column %d brackets its own letter tile"):format(col), tx, 2 + col * 2)
check(("column %d stays on the top keyboard row"):format(col), ty,
KEYBOARD_TOP)
check(("column %d bracket is one tile wide"):format(col), wide, 1)
end
for row = 0, 3 do
naming.col, naming.row = 4, row
local _, ty = naming:cursorTile()
check(("row %d steps two tiles"):format(row), ty, KEYBOARD_TOP + row * 2)
end
-- The bottom row's three fat targets: .CaseDelEnd adds $00 / $30 / $60, so the
-- bracket lands on tiles 2 / 8 / 14 and is five tiles wide.
local BOTTOM_TX = { [0] = 2, [3] = 8, [6] = 14 }
for col, want in pairs(BOTTOM_TX) do
naming.row = naming:bottomRow()
naming.col = col
local tx, ty, wide = naming:cursorTile()
check(("bottom target at column %d"):format(col), tx, want)
check(("bottom target row"):format(col), ty, KEYBOARD_TOP + 4 * 2)
check(("bottom bracket is five tiles"):format(col), wide, 5)
end
-- Every letter row's bracket has to line up with the tile the letter is
-- printed on (drawPanel prints at 2 + col * 2, keyboardTop + row * 2).
naming.col, naming.row = 3, 2
local tx, ty = naming:cursorTile()
check("bracket x matches the printed letter", tx, 2 + 3 * 2)
check("bracket y matches the printed letter", ty, KEYBOARD_TOP + 2 * 2)
-- A box screen shifts the whole keyboard up two rows and gains a sixth row;
-- the bracket follows keyboardTop rather than a baked constant.
local box = NamingScreen.new({}, { type = "box" })
box.col, box.row = 0, 0
local btx, bty = box:cursorTile()
check("box keyboard bracket x", btx, 2)
check("box keyboard bracket y", bty, 6)
box.row = box:bottomRow()
local _, bby, bbw = box:cursorTile()
check("box bottom row", bby, 6 + 5 * 2)
check("box bottom bracket is five tiles", bbw, 5)
-- ------------------------------------------------------- OPTION rows
--
-- StringOptions is "TEXT SPEED<LF> :<LF>...", so on the cart the value
-- sits on its OWN row under the label with the colon at column 10 and the
-- value at 11 (Options_TextSpeed prints at hlcoord 11, 3). Pinned because it
-- reads like a layout bug and is not one.
local printed, cursorAt = {}, nil
local realPrint, realCursor = Chrome.print, Chrome.cursor
local realClear, realBox = Chrome.clear, Chrome.textbox
Chrome.print = function(text, tx, ty)
printed[#printed + 1] = { text = text, x = tx, y = ty }
return 0
end
Chrome.cursor = function(tx, ty) cursorAt = { x = tx, y = ty } end
Chrome.clear = function() end
Chrome.textbox = function() end
local options = OptionsMenu.new({}, { options = {} })
options:drawPanel()
Chrome.print, Chrome.cursor = realPrint, realCursor
Chrome.clear, Chrome.textbox = realClear, realBox
local function findPrint(text)
for _, row in ipairs(printed) do
if row.text == text then return row end
end
end
local label = findPrint("TEXT SPEED")
check("TEXT SPEED sits at the PlaceString origin x", label and label.x, 2)
check("TEXT SPEED sits at the PlaceString origin y", label and label.y, 2)
local colon
for _, row in ipairs(printed) do
if row.text == ":" and row.y == 3 then colon = row break end
end
check("its colon is on the next row at column 10", colon and colon.x, 10)
local value
for _, row in ipairs(printed) do
if row.y == 3 and row.x == 11 then value = row break end
end
check("the value prints at hlcoord 11, 3", value ~= nil, true)
check("and it is the TEXT SPEED setting",
value and (value.text == "FAST" or value.text == "MID "
or value.text == "SLOW"), true)
check("the cursor is the column-1 arrow on the label row",
cursorAt and cursorAt.x, 1)
check("the cursor starts on the first row", cursorAt and cursorAt.y, 2)
check("FRAME still prints its literal TYPE", findPrint(":TYPE") ~= nil, true)
-- ------------------------------------------------------- one blit scale
--
-- Every Gold screen paints its 160x144 panel through the same helper. A
-- screen that re-derives the scale gets math.min(winW / 160, winH / 144),
-- which is fractional on almost every window, and then its own tile rows tear
-- while the screen beside it is clean. Source-level because a draw needs a
-- canvas and there is no canvas in this harness.
local listing = io.popen and io.popen("ls src/ui/gen2/*.lua 2>/dev/null")
local screens = {}
if listing then
for path in listing:lines() do screens[#screens + 1] = path end
listing:close()
end
check("the gen2 screen list was readable", #screens > 0, true)
for _, path in ipairs(screens) do
local f = io.open(path, "r")
local src = f and f:read("*a") or ""
if f then f:close() end
check(path .. " does not re-derive the blit scale",
src:find("math%.min%(win[WH] */ *[%dSCREN_WH]+", 1, false) == nil, true)
if src:find("function [%w_]+:drawWidescreen") then
check(path .. " blits through Chrome.fitScale",
src:find("fitScale", 1, true) ~= nil, true)
end
-- An opaque screen is one nothing under it is drawn for: its cart routine
-- ran ClearBGPalettes / ClearTilemap, so the map is gone and the screen owns
-- the window edge to edge. It therefore has to declare its own surround, or
-- Game2:drawScene resolves the widescreen layer to something else (or to
-- nothing) and the letterbox shows whatever was behind it.
if src:find("isOpaque = true", 1, true) then
check(path .. " is opaque, so it declares drawsWidescreen",
src:find("function [%w_]+:drawsWidescreen") ~= nil, true)
end
end
print(("gen2 screen layout: %d checks, %d failures"):format(checks, failures))
if failures > 0 then
error(("%d assertion(s) failed"):format(failures), 0)
end