skin studio updates, save sync CLOSES #1533

This commit is contained in:
bryanthaboi
2026-08-19 05:57:44 -04:00
parent fd73ab2a11
commit 93374fbbbb
54 changed files with 9762 additions and 271 deletions
+128
View File
@@ -0,0 +1,128 @@
return function(game)
local U = dofile("tests/drivers/util.lua")
local RomImporter = require("src.import.RomImporter")
local dir = os.getenv("SHOT_DIR") or "/tmp/syncmodal"
os.execute('mkdir -p "' .. dir .. '" 2>/dev/null')
love.window.setMode(1024, 768, { resizable = true, highdpi = true })
U.wait(2)
local eng = {
phase = "idle", status = "Ready", conflicts = {}, state = { enabled = true },
isLinked = false, isBusy = false,
linked = function(self) return self.isLinked end,
busy = function(self) return self.isBusy end,
createAccount = function(self)
self.isLinked = true
self.codes = { code1 = "1234-5678", code2 = "8765-4321" }
self.status = "Sync account created"
return true
end,
linkDevice = function(self) self.isLinked = true return true end,
syncNow = function(self) self.status = "Checking for changes..." return true end,
unlink = function(self) self.isLinked, self.codes = false, nil return true end,
shareMods = function(self) self.shareCode = "K7QW3M" return true end,
fetchShare = function(self) return true end,
applyModPlan = function(self) self.modPlan = nil return true end,
resolveConflict = function(self) self.conflicts = {} self.phase = "idle" return true end,
}
local imp = RomImporter.new(function() end, { launcher = true })
imp._sync = eng
imp._syncTransportOk = true
local pending = nil
love.draw = function()
imp:draw()
if pending then
local path = pending
pending = nil
love.graphics.captureScreenshot(function(imagedata)
local f = io.open(path, "wb")
if f then f:write(imagedata:encode("png"):getString()) f:close() end
end)
end
end
local function shot(name)
pending = dir .. "/" .. name
for _ = 1, 90 do
if not pending then break end
imp:update(1 / 60)
coroutine.yield()
end
U.wait(3)
local f = io.open(dir .. "/" .. name, "rb")
U.log(f and "shot" or "FAIL shot", name)
if f then f:close() end
end
imp:_openSync()
U.wait(3)
U.log("modal view:", imp._syncModal.view, "linked:", tostring(eng:linked()))
shot("sync_new.png")
imp:_syncView("link")
imp:_syncFocusField("code1")
imp:textinput("1234-5678")
imp:_syncFocusField("code2")
imp:textinput("8765ab4321")
U.wait(2)
U.log("codes typed:", imp._syncModal.code1, imp._syncModal.code2)
shot("sync_link.png")
imp:_syncView("home")
imp:_syncCreate()
U.wait(2)
U.log("codes shown:", eng.codes.code1, eng.codes.code2)
shot("sync_codes.png")
eng.isBusy = true
eng.status = "Uploading saves..."
U.wait(2)
shot("sync_busy.png")
eng.isBusy = false
eng.status = "Ready"
imp:_syncView("mods")
imp:_syncShareMods()
eng.modPlan = {
indexes = { "https://example.invalid/index.json" },
toInstall = { { id = "jp_green" }, { id = "randomizer" } },
toEnable = { { id = "jp_green", version = "red" } },
missing = { { id = "gone" } },
}
U.wait(2)
U.log("share code:", tostring(eng.shareCode))
shot("sync_mods.png")
eng.phase = "conflict"
eng.status = "These saves were played at the same time."
eng.conflicts = { {
key = "red/abcd1234", version = "red", overlap = true,
localMeta = { savedAt = os.time(), sessionStart = os.time() - 3600,
summary = { name = "ASH", badges = 3, timeText = "5:42", dexCount = 40 } },
remoteMeta = { savedAt = os.time() - 600, sessionStart = os.time() - 4200,
summary = { name = "ASH", badges = 4, timeText = "6:10", dexCount = 44 } },
} }
U.wait(2)
shot("sync_conflict.png")
love.window.setMode(520, 760, { resizable = true, highdpi = true })
U.wait(3)
shot("sync_conflict_narrow.png")
eng.phase = "idle"
eng.conflicts = {}
imp:_syncView("home")
U.wait(2)
shot("sync_home_narrow.png")
imp:_closeSync()
U.wait(2)
U.log("closed:", tostring(imp._syncModal == nil))
shot("sync_closed.png")
U.log("done")
love.event.quit()
while true do coroutine.yield() end
end
+115
View File
@@ -0,0 +1,115 @@
return function(game)
local U = dofile("tests/drivers/util.lua")
local RomImporter = require("src.import.RomImporter")
local TouchSkin = require("src.core.TouchSkin")
local Fetch = require("src.net.Fetch")
local dir = os.getenv("SHOT_DIR") or "/tmp/skinurl"
os.execute('mkdir -p "' .. dir .. '" 2>/dev/null')
love.window.setMode(1024, 768, { resizable = true, highdpi = true })
U.wait(2)
local imp = RomImporter.new(function() end, {
launcher = true,
onOpenSkinStudio = function() end,
})
local pending = nil
love.draw = function()
imp:draw()
if pending then
local path = pending
pending = nil
love.graphics.captureScreenshot(function(imagedata)
local f = io.open(path, "wb")
if f then f:write(imagedata:encode("png"):getString()) f:close() end
end)
end
end
local function shot(name)
pending = dir .. "/" .. name
for _ = 1, 90 do
if not pending then break end
imp:update(1 / 60)
coroutine.yield()
end
U.wait(3)
local f = io.open(dir .. "/" .. name, "rb")
U.log(f and "shot" or "FAIL shot", name)
if f then f:close() end
end
imp:_switchTab("skins")
U.wait(3)
U.log("name from url:", RomImporter.skinUrlName(
"https://example.com/pads/Neon.deltaskin"))
U.log("name from cfg:", RomImporter.skinUrlName(
"https://example.com/overlay.cfg"))
imp.skinUrl = "https://example.com/pads/neon.deltaskin"
imp._skinUrlFocus = true
U.wait(2)
shot("skins_url_typed.png")
local realDownload, realPoll, realRelease =
Fetch.download, Fetch.poll, Fetch.release
local state = { status = "pending", progress = 0.4 }
Fetch.download = function(url, dest)
U.log("download:", url, "->", dest)
return 1
end
Fetch.poll = function() return state end
Fetch.release = function() end
imp._skinUrlFocus = false
imp:_addSkinFromUrl()
U.log("in flight:", tostring(imp._skinFetch ~= nil))
U.wait(2)
shot("skins_url_downloading.png")
state = { status = "error", err = "could not resolve host" }
imp:_pumpSkinFetch()
U.log("failure notice:", imp._skinNotice.text)
U.wait(2)
shot("skins_url_failed.png")
Fetch.download, Fetch.poll, Fetch.release = realDownload, realPoll, realRelease
local staged = TouchSkin.export(
assert(TouchSkin.load("assets/skins/gb_anim", "gb_anim")),
"skins/url_probe.zip")
local raw = love.filesystem.read("skins/url_probe.zip")
love.filesystem.remove("skins/url_probe.zip")
U.log("staged:", tostring(staged), "bytes:", raw and #raw or 0)
imp:_installSkinData("downloaded_pad.zip", raw)
U.log("install notice:", imp._skinNotice.text)
U.wait(2)
shot("skins_url_installed.png")
local skins = imp:_ensureSkins(true)
for _, e in ipairs(skins) do
U.log((" %s format=%s buttons=%d"):format(e.id, tostring(e.format),
e.controls))
end
imp._skinActions = { id = skins[1] and skins[1].id }
U.wait(2)
shot("skins_actions_sheet.png")
for _, kind in ipairs({ "native", "retroarch", "delta" }) do
local path = imp:_exportSkin(skins[1].id, kind)
U.log("export " .. kind .. ":", tostring(path))
end
imp._skinActions = nil
U.wait(2)
shot("skins_exported.png")
love.window.setMode(520, 820, { resizable = true, highdpi = true })
U.wait(3)
shot("skins_url_narrow.png")
U.log("done")
love.event.quit()
while true do coroutine.yield() end
end
+227
View File
@@ -0,0 +1,227 @@
return function(game)
local U = dofile("tests/drivers/util.lua")
local TouchControls = require("src.core.TouchControls")
local TouchSkin = require("src.core.TouchSkin")
local Playfield = require("src.render.Playfield")
local dir = os.getenv("SHOT_DIR") or "/tmp/skin-cutout"
local gen2 = game.overworld == nil
local failures, checks = 0, 0
local SKIN = [[
return {
name = "containment_probe",
pages = {
{
name = "probe",
fullScreen = true,
viewport = { x = 0.25, y = 0.1, w = 0.5, h = 0.6 },
controls = { { bind = "nul", x = 0.5, y = 0.92, w = 0.04, h = 0.04 } },
},
},
}
]]
love.filesystem.createDirectory("skins/containment_probe")
assert(love.filesystem.write("skins/containment_probe/skin.lua", SKIN))
love.window.setMode(1280, 720, { resizable = true, highdpi = true })
love.graphics.setBackgroundColor(0, 0, 0, 1)
U.wait(2)
local options = gen2 and game.options or game.save.options
options.touchControls = { enabled = true, skin = "containment_probe" }
options.tilt = 0
options.zoom = 0
options.pipelines = {}
options.videoMode = "windowed"
options.faithfulRes = 0
game:applyOptions()
love.window.setMode(1280, 720, { resizable = true, highdpi = true })
U.wait(4)
U.log("gen:", gen2 and 2 or 1, "skin:", tostring(TouchControls.skinId),
"err:", tostring(TouchControls.skinError))
U.log("drawable:", TouchSkin.drawable(), "hasViewport:", TouchSkin.hasViewport())
local function cutoutPx()
local pw, ph = love.graphics.getPixelDimensions()
local x, y, w, h = Playfield.cutout(pw, ph)
return x, y, w, h, pw, ph
end
local cx, cy, cw, ch, pw, ph = cutoutPx()
if not cx then
U.log("FAIL no cutout is active; nothing to prove")
love.event.quit()
while true do coroutine.yield() end
end
U.log(("cutout px: %d,%d %dx%d in %dx%d"):format(cx, cy, cw, ch, pw, ph))
local INSET = 4
local function scan(label, data)
local w, h = data:getWidth(), data:getHeight()
local sx, sy = w / pw, h / ph
local x1, y1 = math.floor(cx * sx) - INSET, math.floor(cy * sy) - INSET
local x2 = math.ceil((cx + cw) * sx) + INSET
local y2 = math.ceil((cy + ch) * sy) + INSET
local bad, firstX, firstY, worst = 0, nil, nil, 0
local inked = 0
local step = math.max(2, math.floor(math.min(w, h) / 360))
for y = 0, h - 1, step do
for x = 0, w - 1, step do
local r, g, b = data:getPixel(x, y)
local lit = math.max(r, g, b)
local outside = x < x1 or x >= x2 or y < y1 or y >= y2
if outside then
if lit > 0.02 then
bad = bad + 1
if not firstX then firstX, firstY = x, y end
if lit > worst then worst = lit end
end
elseif lit > 0.02 then
inked = inked + 1
end
end
end
checks = checks + 1
if bad > 0 then
failures = failures + 1
U.log(("FAIL %s: %d lit samples outside the cutout (first %d,%d, max %.2f)")
:format(label, bad, firstX, firstY, worst))
elseif inked == 0 then
failures = failures + 1
U.log("FAIL " .. label .. ": nothing drew inside the cutout either")
else
U.log(("ok %s: contained (%d lit samples inside)"):format(label, inked))
end
end
local pending = nil
local function probe(label)
U.wait(2)
pending = label
love.graphics.captureScreenshot(function(data)
scan(pending, data)
pending = nil
end)
for _ = 1, 180 do
if not pending then break end
coroutine.yield()
end
if pending then
failures = failures + 1
U.log("FAIL " .. tostring(pending) .. ": screenshot never arrived")
pending = nil
end
if os.getenv("SHOT_PNG") == "1" then
U.shot(game, ("%s/%s.png"):format(dir, label:gsub("[^%w]+", "_")))
end
end
if gen2 then
for i = 1, 2 do
probe("gold_boot_" .. i)
U.wait(60)
end
for _ = 1, 240 do
if game.world and game.world.map then break end
game.input.pressQueue[#game.input.pressQueue + 1] = "start"
U.wait(4)
end
if game.world and game.world.map then
probe("gold_overworld")
local Zoom = require("src.render.Zoom")
Zoom.allowSurvey = true
for _, off in ipairs({ -2, -1, 1, 2 }) do
Zoom.offset = off
probe("gold_zoom_" .. (off < 0 and "out" or "in") .. math.abs(off))
end
Zoom.offset = 0
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 12)
end
tap("start", 24)
probe("gold_start_menu")
tap("b", 12)
probe("gold_after_menu")
else
U.log("FAIL gold world never booted")
failures = failures + 1
end
else
local Pokemon = require("src.pokemon.Pokemon")
game.save.party = { Pokemon.new(game.data, "CHARIZARD", 50) }
game.save.player.name = "bryan"
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
U.wait(12)
probe("red_overworld")
local Zoom = require("src.render.Zoom")
local Renderer = require("src.render.Renderer")
Zoom.allowSurvey = true
local lo, hi = Zoom.offsetRange(Renderer:fitScale())
for off = lo, hi do
Zoom.offset = off
probe("red_zoom_" .. Zoom.offsetLabel(off))
end
Zoom.offset = 0
U.tap(game, "start")
U.wait(20)
probe("red_start_menu")
local function stress(label, mutate)
local state = game.stack:top()
local original = state.draw
state.draw = function(...)
original(...)
mutate()
end
probe(label)
state.draw = original
end
stress("red_screen_veil", function()
Renderer.screenVeil = { 1, 0.85 }
end)
stress("red_battle_wipe", function()
Renderer.battleWipe = { style = "spiralin", prog = 0.45 }
end)
stress("red_letterbox_paper", function()
Renderer.extendedWorldBand = true
end)
stress("red_ui_anchor", function()
Renderer.uiCentered = false
Renderer:setUIAnchor(0, 96, 160, 48, "bottom")
end)
U.tap(game, "b")
U.wait(10)
game.save.options.uiLayout = "dynamic"
game:applyOptions()
U.tap(game, "start")
U.wait(20)
probe("red_dynamic_start_menu")
U.tap(game, "b")
U.wait(10)
game.save.options.uiLayout = "centered"
game:applyOptions()
local Tilt = require("src.render.Tilt")
game.save.options.tilt = 1
Tilt.applyOptions(game.save.options)
U.wait(30)
probe("red_tilt")
game.save.options.tilt = 0
Tilt.applyOptions(game.save.options)
U.wait(20)
end
U.log(("done: %d/%d frames contained, %d failures")
:format(checks - failures, checks, failures))
love.event.quit()
while true do coroutine.yield() end
end