mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
326 lines
14 KiB
Lua
326 lines
14 KiB
Lua
-- RetroArch-format touch skins (#1386): .cfg parsing, hitboxes, the press /
|
|
-- hotkey path through TouchControls, and the screen viewport the renderer
|
|
-- fits the Game Boy picture into.
|
|
-- luajit tests/engine/touch_skin_test.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local TouchSkin = require("src.core.TouchSkin")
|
|
local TouchControls = require("src.core.TouchControls")
|
|
local Input = require("src.core.Input")
|
|
|
|
local CFG = [[
|
|
overlays = 2
|
|
|
|
overlay0_name = "shell"
|
|
overlay0_overlay = img/back.png
|
|
overlay0_full_screen = true
|
|
overlay0_normalized = true
|
|
overlay0_alpha_mod = 0.001
|
|
overlay0_range_mod = 1.5
|
|
overlay0_viewport = "0.0,0.0,1.0,0.5"
|
|
overlay0_viewport_fill = true
|
|
|
|
overlay0_descs = 7
|
|
overlay0_desc0 = "a,0.80,0.75,radial,0.10,0.05"
|
|
overlay0_desc0_overlay = img/a.png
|
|
overlay0_desc1 = "left|down,0.20,0.90,rect,0.05,0.05"
|
|
overlay0_desc2 = "nul,0.20,0.75,rect,0.15,0.10"
|
|
overlay0_desc2_overlay = img/dpad.png
|
|
overlay0_desc3 = "overlay_next,0.95,0.55,radial,0.03,0.02"
|
|
overlay0_desc3_next_target = "second"
|
|
overlay0_desc4 = "hold_fast_forward,0.05,0.55,radial,0.03,0.02"
|
|
overlay0_desc5 = "reset,0.50,0.98,rect,0.04,0.02"
|
|
overlay0_desc6 = "key:f5,0.10,0.98,rect,0.04,0.02"
|
|
|
|
overlay1_name = "second"
|
|
overlay1_full_screen = true
|
|
overlay1_normalized = true
|
|
overlay1_descs = 1
|
|
overlay1_desc0 = "start,0.50,0.50,rect,0.10,0.10"
|
|
]]
|
|
|
|
local skin = assert(TouchSkin.parse(CFG))
|
|
eq(#skin.pages, 2, "two overlay pages parsed")
|
|
|
|
local page = skin.pages[1]
|
|
eq(page.name, "shell", "page name")
|
|
eq(page.imagePath, "img/back.png", "page background path")
|
|
check(page.fullScreen, "full_screen parsed")
|
|
eq(page.alphaMod, 0.001, "overlay alpha_mod parsed")
|
|
eq(#page.controls, 7, "seven descs parsed")
|
|
|
|
check(page.viewport ~= nil, "viewport parsed")
|
|
eq(page.viewport.h, 0.5, "viewport height")
|
|
check(page.viewportFill, "viewport_fill parsed")
|
|
|
|
local a, diag, decor, nextBtn, ff, reset, keyBtn = unpack(page.controls)
|
|
eq(a.buttons[1], "a", "desc0 binds GB a")
|
|
eq(a.shape, "radial", "desc0 is radial")
|
|
eq(a.alphaMod, 0.001, "desc inherits the overlay alpha_mod")
|
|
eq(a.rangeMod, 1.5, "desc inherits the overlay range_mod")
|
|
eq(table.concat(diag.buttons, "+"), "left+down", "pipe-separated binds are one control")
|
|
check(decor.decorative, "nul desc is decoration only")
|
|
eq(decor.imagePath, "img/dpad.png", "decoration still carries art")
|
|
eq(nextBtn.hotkeys[1], "overlay_next", "overlay_next mapped")
|
|
eq(nextBtn.nextTarget, "second", "next_target parsed")
|
|
eq(ff.hotkeys[1], "fast_forward_hold", "hold_fast_forward mapped")
|
|
eq(reset.hotkeys[1], "soft_reset", "reset mapped")
|
|
eq(keyBtn.keys[1], "f5", "key: binds a keyboard key")
|
|
|
|
-- geometry: x/y are the centre, range_x/range_y are half extents, both
|
|
-- normalized to the window when full_screen is set
|
|
local W, H = 400, 800
|
|
local cx, cy, halfW, halfH = TouchSkin.controlGeometry(page, a, W, H)
|
|
eq(cx, 320, "control centre x")
|
|
eq(cy, 600, "control centre y")
|
|
eq(halfW, 40, "control half width")
|
|
eq(halfH, 40, "control half height")
|
|
|
|
-- range_mod 1.5 widens the hitbox but not the art
|
|
check(TouchSkin.hits(page, a, W, H, 320, 600), "centre hits")
|
|
check(TouchSkin.hits(page, a, W, H, 320 + 55, 600), "inside range_mod hits")
|
|
check(not TouchSkin.hits(page, a, W, H, 320 + 65, 600), "past range_mod misses")
|
|
-- radial, so the corner of the bounding box is outside
|
|
check(not TouchSkin.hits(page, a, W, H, 320 + 55, 600 + 55), "radial corner misses")
|
|
-- rect hitbox does take its corner
|
|
check(TouchSkin.hits(page, diag, W, H, 80 + 19, 720 + 19), "rect corner hits")
|
|
|
|
-- ---------------------------------------------------------------- runtime
|
|
|
|
TouchControls:init()
|
|
TouchControls.active = true
|
|
TouchControls.enabled = true
|
|
TouchSkin.setOverlayLive(true)
|
|
TouchSkin.setActive(skin)
|
|
Input:init()
|
|
|
|
local ww, wh = love.graphics.getDimensions()
|
|
local function at(nx, ny) return nx * ww, ny * wh end
|
|
|
|
check(TouchSkin.hasViewport(), "active skin reports a viewport")
|
|
|
|
-- the renderer fits the Game Boy picture into this rect instead of the window
|
|
local sx, sy, sw, sh, fill = TouchSkin.viewport(W, H)
|
|
eq(sx, 0, "viewport x") eq(sy, 0, "viewport y")
|
|
eq(sw, 400, "viewport w") eq(sh, 400, "viewport h")
|
|
check(fill, "viewport fill flag returned")
|
|
eq(TouchSkin.viewport(W, H), 0, "viewport is window-relative, not page-relative")
|
|
|
|
check(TouchControls:touchpressed("f1", at(0.80, 0.75)), "press on A is captured")
|
|
check(Input:isDown("a"), "skin A presses GB a")
|
|
TouchControls:touchreleased("f1", at(0.80, 0.75))
|
|
check(not Input:isDown("a"), "lifting releases GB a")
|
|
|
|
-- one finger inside a combined desc holds both directions
|
|
TouchControls:touchpressed("f2", at(0.20, 0.90))
|
|
check(Input:isDown("left") and Input:isDown("down"), "combined desc holds both buttons")
|
|
-- sliding onto the decoration drops them without capturing anything new
|
|
TouchControls:touchmoved("f2", at(0.20, 0.75))
|
|
check(not Input:isDown("left") and not Input:isDown("down"),
|
|
"sliding off a control releases it")
|
|
TouchControls:touchreleased("f2", at(0.20, 0.75))
|
|
|
|
check(TouchControls:touchpressed("f3", at(0.20, 0.75)) == nil,
|
|
"a press on decoration alone is not captured")
|
|
|
|
-- hotkeys reach the host once per press edge and once per release
|
|
local fired = {}
|
|
TouchControls:setHotkeyHandler(function(action, pressed)
|
|
fired[#fired + 1] = action .. ":" .. tostring(pressed)
|
|
end)
|
|
TouchControls:touchpressed("f4", at(0.05, 0.55))
|
|
eq(fired[1], "fast_forward_hold:true", "held hotkey fires on press")
|
|
TouchControls:touchreleased("f4", at(0.05, 0.55))
|
|
eq(fired[2], "fast_forward_hold:false", "held hotkey fires on release")
|
|
|
|
-- a stranded finger unwinds through reset, not just through touchreleased
|
|
fired = {}
|
|
TouchControls:touchpressed("f5", at(0.05, 0.55))
|
|
TouchControls:touchpressed("f6", at(0.80, 0.75))
|
|
check(Input:isDown("a"), "second finger holds A")
|
|
TouchControls:reset()
|
|
eq(fired[2], "fast_forward_hold:false", "reset releases held hotkeys")
|
|
check(not Input:isDown("a"), "reset releases held buttons")
|
|
|
|
-- overlay_next swaps the page, and the new page's controls are what hit
|
|
eq(TouchSkin.page().name, "shell", "starts on page 1")
|
|
TouchControls:touchpressed("f7", at(0.95, 0.55))
|
|
eq(TouchSkin.page().name, "second", "overlay_next honours next_target")
|
|
TouchControls:touchreleased("f7", at(0.95, 0.55))
|
|
TouchControls:touchpressed("f8", at(0.50, 0.50))
|
|
check(Input:isDown("start"), "page 2 control presses GB start")
|
|
TouchControls:touchreleased("f8", at(0.50, 0.50))
|
|
TouchSkin.setPage("shell")
|
|
|
|
-- ------------------------------------------------------------ persistence
|
|
|
|
local cfg = TouchControls.normalizeConfig({ enabled = true, skin = "gb_anim" })
|
|
eq(cfg.skin, "gb_anim", "normalizeConfig keeps the skin id")
|
|
eq(TouchControls.normalizeConfig({ skin = "" }).skin, nil, "empty skin id drops")
|
|
eq(TouchControls.normalizeConfig({ skin = 7 }).skin, nil, "non-string skin id drops")
|
|
|
|
TouchControls.skinId = "gb_anim"
|
|
eq(TouchControls:config().skin, "gb_anim", "config() round-trips the skin id")
|
|
|
|
-- ----------------------------------------------- screen-hole detection
|
|
|
|
-- Border art usually ships with a transparent screen hole and no viewport
|
|
-- key. Detection is verified against real art by
|
|
-- tests/drivers/tv_skin_shot.lua; here it only has to fail safely.
|
|
eq(TouchSkin.detectViewport("assets/skins/tv_crt", nil), nil,
|
|
"no bezel path detects nothing")
|
|
eq(TouchSkin.detectViewport("assets/skins/tv_crt", ""), nil,
|
|
"empty bezel path detects nothing")
|
|
eq(TouchSkin.detectViewport("assets/skins/tv_crt", "img/does_not_exist.png"), nil,
|
|
"a missing bezel detects nothing")
|
|
|
|
-- --------------------------------------------------------- bundled skin
|
|
|
|
local bundled = TouchSkin.load("assets/skins/gb_anim", "gb_anim")
|
|
check(bundled ~= nil, "bundled gb_anim skin loads")
|
|
if bundled then
|
|
eq(#bundled.pages, 2, "gb_anim has a DMG and a Color page")
|
|
eq(bundled.pages[1].name, "GameBoy", "gb_anim page 1")
|
|
check(bundled.pages[1].viewport ~= nil, "gb_anim declares a screen viewport")
|
|
eq(bundled.pages[1].imagePath, "img/gb_back.png", "gb_anim bezel art")
|
|
local named = {}
|
|
for _, ctl in ipairs(bundled.pages[1].controls) do
|
|
for _, btn in ipairs(ctl.buttons) do named[btn] = true end
|
|
end
|
|
for _, btn in ipairs({ "a", "b", "start", "select", "up", "down", "left", "right" }) do
|
|
check(named[btn], "gb_anim binds GB " .. btn)
|
|
end
|
|
end
|
|
|
|
local tv = TouchSkin.load("assets/skins/tv_crt", "tv_crt")
|
|
check(tv ~= nil, "bundled tv_crt desktop bezel loads")
|
|
if tv then
|
|
eq(#tv.pages, 1, "tv_crt is a single page")
|
|
eq(#tv.pages[1].controls, 0, "tv_crt binds nothing: it is a frame")
|
|
check(tv.pages[1].viewport ~= nil, "tv_crt names the tube as its viewport")
|
|
eq(tv.pages[1].imagePath, "img/tv-integer.png", "tv_crt bezel art")
|
|
TouchSkin.setActive(tv)
|
|
TouchSkin.setOverlayLive(false)
|
|
check(TouchSkin.decorativeOnly(), "tv_crt counts as decoration")
|
|
check(TouchSkin.drawable(), "so it draws on a desktop window")
|
|
end
|
|
|
|
-- ------------------------------------------- decorative desktop bezels
|
|
|
|
-- A skin whose page binds nothing is a frame, not a pad: it draws where the
|
|
-- touch overlay does not (desktop), and a gamepad must not hide it.
|
|
local BEZEL = [[
|
|
overlays = 1
|
|
overlay0_name = "tv"
|
|
overlay0_overlay = img/tv.png
|
|
overlay0_full_screen = true
|
|
overlay0_normalized = true
|
|
overlay0_descs = 1
|
|
overlay0_desc0 = "nul,0.5,0.5,rect,0.5,0.5"
|
|
overlay0_viewport = "0.2,0.1,0.6,0.8"
|
|
]]
|
|
local bezel = assert(TouchSkin.parse(BEZEL))
|
|
TouchSkin.setActive(bezel)
|
|
|
|
TouchSkin.setOverlayLive(false)
|
|
check(TouchSkin.decorativeOnly(), "a descs-only-nul skin is decoration")
|
|
check(TouchSkin.drawable(), "a decorative skin draws with the overlay off (desktop)")
|
|
check(TouchSkin.hasViewport(), "and its viewport still places the picture")
|
|
TouchControls.active = false
|
|
TouchControls.enabled = true
|
|
TouchControls.controllerHidden = true
|
|
check(TouchControls:visible(), "a gamepad does not hide a decorative bezel")
|
|
|
|
-- a skin that binds buttons keeps following the mobile / POKEPORT_TOUCH gate
|
|
TouchSkin.setActive(skin)
|
|
TouchSkin.setOverlayLive(false)
|
|
check(not TouchSkin.decorativeOnly(), "a skin with binds is not decoration")
|
|
check(not TouchSkin.drawable(), "and it does not draw where the overlay is off")
|
|
check(not TouchSkin.hasViewport(), "so it cannot shrink the picture either")
|
|
check(not TouchControls:visible(), "nor draw over a desktop window")
|
|
TouchSkin.setOverlayLive(true)
|
|
check(TouchSkin.drawable(), "with the overlay live it draws again")
|
|
TouchControls.active = true
|
|
TouchControls.controllerHidden = false
|
|
|
|
-- ------------------------------------------------------- native format
|
|
|
|
local native = TouchSkin.serialize(skin)
|
|
check(native:find("^return {"), "serialize emits a Lua data chunk")
|
|
local back, nerr = TouchSkin.parseNative(native)
|
|
check(back ~= nil, "native round-trip parses: " .. tostring(nerr))
|
|
if back then
|
|
eq(#back.pages, #skin.pages, "round-trip keeps the page count")
|
|
local bp, sp = back.pages[1], skin.pages[1]
|
|
eq(#bp.controls, #sp.controls, "round-trip keeps the control count")
|
|
eq(bp.controls[1].spec, sp.controls[1].spec, "round-trip keeps binds")
|
|
eq(bp.controls[1].rangeX, sp.controls[1].rangeX, "round-trip keeps half extents")
|
|
eq(bp.controls[1].shape, sp.controls[1].shape, "round-trip keeps the hitbox shape")
|
|
eq(bp.viewport.h, sp.viewport.h, "round-trip keeps the viewport")
|
|
check(bp.viewportFill, "round-trip keeps viewport_fill")
|
|
eq(bp.alphaMod, sp.alphaMod, "round-trip keeps alpha_mod")
|
|
eq(bp.controls[3].decorative, true, "round-trip keeps decoration")
|
|
eq(bp.controls[4].nextTarget, "second", "round-trip keeps next_target")
|
|
end
|
|
|
|
-- the native format carries the separate pressed image a .cfg cannot
|
|
local twoState = assert(TouchSkin.parseNative([[
|
|
return { pages = { { name = "p", controls = {
|
|
{ bind = "a", x = 0.5, y = 0.5, w = 0.2, h = 0.2, shape = "radial",
|
|
image = "up.png", imagePressed = "down.png" },
|
|
} } } }
|
|
]]))
|
|
eq(twoState.pages[1].controls[1].imagePath, "up.png", "native idle image")
|
|
eq(twoState.pages[1].controls[1].pressedImagePath, "down.png", "native pressed image")
|
|
eq(twoState.pages[1].controls[1].rangeX, 0.1, "native w is a full width, not a range")
|
|
|
|
check(TouchSkin.parseNative("return 5") == nil, "a non-table skin.lua is rejected")
|
|
check(TouchSkin.parseNative("return { pages = {} }") == nil, "a pageless skin.lua is rejected")
|
|
check(TouchSkin.parseNative("this is not lua") == nil, "a broken skin.lua is rejected")
|
|
-- skins are third-party data: the chunk must not see the host globals
|
|
check(TouchSkin.parseNative("return { pages = { { controls = {} } }, hit = love ~= nil }")
|
|
~= nil, "skin.lua loads in an empty environment")
|
|
|
|
-- --------------------------------------------------------------- export
|
|
|
|
local SkinZip = require("src.core.SkinZip")
|
|
local blob = SkinZip.encode({
|
|
{ name = "skin.lua", data = "return {}\n" },
|
|
{ name = "img/a.png", data = "\137PNG\r\n\26\n binary \0 bytes" },
|
|
})
|
|
eq(blob:sub(1, 4), "PK\3\4", "zip starts with a local file header")
|
|
check(blob:find("PK\5\6", 1, true) ~= nil, "zip ends with a central directory record")
|
|
check(blob:find("img/a.png", 1, true) ~= nil, "zip carries the entry name")
|
|
check(blob:find("binary", 1, true) ~= nil, "stored entries keep their bytes verbatim")
|
|
|
|
local bundledSkin = TouchSkin.load("assets/skins/gb_anim", "gb_anim")
|
|
if bundledSkin then
|
|
eq(#TouchSkin.assetPaths(bundledSkin), 17, "gb_anim names 17 distinct images")
|
|
local tmp = os.tmpname() .. ".zip"
|
|
local path, missing = TouchSkin.export(bundledSkin, tmp)
|
|
eq(path, tmp, "export writes to the requested path")
|
|
eq(#missing, 0, "every gb_anim image was found")
|
|
local f = io.open(tmp, "rb")
|
|
check(f ~= nil, "exported zip reached disk")
|
|
if f then
|
|
local bytes = f:read("*a")
|
|
f:close()
|
|
eq(bytes:sub(1, 4), "PK\3\4", "exported file is a zip")
|
|
check(bytes:find("skin.lua", 1, true) ~= nil, "export includes the native skin.lua")
|
|
check(bytes:find("img/gb_back.png", 1, true) ~= nil, "export includes the bezel")
|
|
check(bytes:find("overlay.cfg", 1, true) ~= nil,
|
|
"a .cfg-sourced skin also exports its original config")
|
|
os.remove(tmp)
|
|
end
|
|
end
|
|
|
|
TouchSkin.setActive(nil)
|
|
TouchControls:setHotkeyHandler(nil)
|
|
|
|
T.finish("touch_skin")
|