From 5714555847d7220f30e937ba57d54d4a37c8c0c4 Mon Sep 17 00:00:00 2001 From: 1jamie Date: Thu, 20 Aug 2026 20:12:41 -0500 Subject: [PATCH] feat: enhance Goldenrod Game Corner graphics extraction Added functions to write raw graphics assets for the Slot Machine and Card Flip games, ensuring that the necessary files are generated when the corresponding symbols are present in the ROM. Updated the manifest to include required symbols for these assets, preventing fallback to labelled cells when graphics are missing. --- src/import/RomExtractorGen2.lua | 58 +++++++++----- src/import/RomImporter.lua | 6 ++ src/ui/gen2/CardFlip.lua | 12 ++- src/ui/gen2/SlotMachine.lua | 136 +++++++++++++++++--------------- tools/make_gold_manifest.py | 8 ++ 5 files changed, 133 insertions(+), 87 deletions(-) diff --git a/src/import/RomExtractorGen2.lua b/src/import/RomExtractorGen2.lua index d3736b8e..a044833c 100644 --- a/src/import/RomExtractorGen2.lua +++ b/src/import/RomExtractorGen2.lua @@ -5258,9 +5258,26 @@ function RomExtractorGen2:extractMenuGfx() end -- Goldenrod Game Corner: Slot Machine graphics assets + local CacheFs = require("src.import.CacheFs") + local function packBytes(bytes) + local chars = {} + for i = 1, #bytes do chars[i] = string.char(bytes[i]) end + return table.concat(chars) + end + local function writeRaw(relative, bytes) + local ok, writeError = CacheFs.write( + "assets/generated/" .. relative, packBytes(bytes)) + if not ok then + error("could not write " .. relative .. ": " .. tostring(writeError)) + end + end + + local slots = nil if self.symbols["Slots1LZ"] then local raw1 = self:decompressLz3Symbol("Slots1LZ") self:write2bpp(raw1, 16, #raw1 / 4, "slots/gold_slots_1.png") + slots = slots or {} + slots.sheet1 = "assets/generated/slots/gold_slots_1.png" end if self.symbols["Slots2LZ"] then local raw2 = self:decompressLz3Symbol("Slots2LZ") @@ -5269,6 +5286,8 @@ function RomExtractorGen2:extractMenuGfx() raw2[i] = bit.band(bit.bnot(raw2[i]), 0xFF) end self:write2bpp(raw2, 16, #raw2 / 4, "slots/gold_slots_2.png") + slots = slots or {} + slots.sheet2 = "assets/generated/slots/gold_slots_2.png" end if self.symbols["Slots3LZ"] then local raw3 = self:decompressLz3Symbol("Slots3LZ") @@ -5283,55 +5302,58 @@ function RomExtractorGen2:extractMenuGfx() -- Y=192: Chansey 5 (Egg Drop pose, 24x32) -- Y=224: Egg (8x16 at X=0) self:write2bpp(raw3, 24, #raw3 / 6, "slots/gold_slots_actors.png", true) + slots = slots or {} + slots.sheet3 = "assets/generated/slots/gold_slots_3.png" end if self.symbols["SlotsTilemap"] then local symbol = self:symbol("SlotsTilemap") local tm = self.rom:bytes(symbol.bank, symbol.address, 20 * 12) - self:save(tm, "slots/gold_slots.tilemap") + writeRaw("slots/gold_slots.tilemap", tm) + slots = slots or {} + slots.tilemap = "assets/generated/slots/gold_slots.tilemap" end + if slots then out.slots = slots end -- Goldenrod Game Corner: Card Flip graphics assets + local cardFlip = nil if self.symbols["CardFlipLZ01"] then local raw1 = self:decompressLz3Symbol("CardFlipLZ01") self:write2bpp(raw1, 128, #raw1 / 32, "card_flip/card_flip_1.png") + cardFlip = cardFlip or {} + cardFlip.sheet1 = "assets/generated/card_flip/card_flip_1.png" end if self.symbols["CardFlipLZ02"] then local raw2 = self:decompressLz3Symbol("CardFlipLZ02") self:write2bpp(raw2, 24, #raw2 / 6, "card_flip/card_flip_2.png") + cardFlip = cardFlip or {} + cardFlip.sheet2 = "assets/generated/card_flip/card_flip_2.png" end if self.symbols["CardFlipLZ03"] then local raw3 = self:decompressLz3Symbol("CardFlipLZ03") self:write2bpp(raw3, 8, #raw3 / 2, "card_flip/card_flip_3.png") + cardFlip = cardFlip or {} + cardFlip.sheet3 = "assets/generated/card_flip/card_flip_3.png" end if self.symbols["CardFlipOnButtonGFX"] then local symbol = self:symbol("CardFlipOnButtonGFX") self:write2bpp(self.rom:bytes(symbol.bank, symbol.address, 16), 8, 8, "card_flip/on.png") + cardFlip = cardFlip or {} + cardFlip.on = "assets/generated/card_flip/on.png" end if self.symbols["CardFlipOffButtonGFX"] then local symbol = self:symbol("CardFlipOffButtonGFX") self:write2bpp(self.rom:bytes(symbol.bank, symbol.address, 16), 8, 8, "card_flip/off.png") + cardFlip = cardFlip or {} + cardFlip.off = "assets/generated/card_flip/off.png" end if self.symbols["CardFlipTilemap"] then local symbol = self:symbol("CardFlipTilemap") local tm = self.rom:bytes(symbol.bank, symbol.address, 11 * 12) - self:save(tm, "card_flip/card_flip.tilemap") + writeRaw("card_flip/card_flip.tilemap", tm) + cardFlip = cardFlip or {} + cardFlip.tilemap = "assets/generated/card_flip/card_flip.tilemap" end - - out.slots = { - sheet1 = "assets/generated/slots/gold_slots_1.png", - sheet2 = "assets/generated/slots/gold_slots_2.png", - sheet3 = "assets/generated/slots/gold_slots_3.png", - tilemap = "assets/generated/slots/gold_slots.tilemap", - } - - out.cardFlip = { - sheet1 = "assets/generated/card_flip/card_flip_1.png", - sheet2 = "assets/generated/card_flip/card_flip_2.png", - sheet3 = "assets/generated/card_flip/card_flip_3.png", - on = "assets/generated/card_flip/on.png", - off = "assets/generated/card_flip/off.png", - tilemap = "assets/generated/card_flip/card_flip.tilemap", - } + if cardFlip then out.cardFlip = cardFlip end self:write("menu_gfx", out) self:tick("Menu graphics", 1, 1) diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 3b2c2ac2..5b7fe222 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -140,6 +140,12 @@ local VERSION_REQUIRED_FILES_OVERRIDE = { -- before the four ball tiles were extracted (#1502). "assets/generated/battle/hud/balls.png", "assets/generated/audio/programs.bin", + -- Goldenrod Game Corner reel + board art (#1581). menu_gfx.lua used to + -- advertise these paths even when Slots*LZ / CardFlip* were absent from + -- the manifest, so a cache that never wrote the PNGs still looked + -- complete and SlotMachine crashed on its labelled-cell fallback. + "assets/generated/slots/gold_slots_1.png", + "assets/generated/card_flip/card_flip_1.png", }, } -- Same Gen 2 extract, so a Silver cache is complete when the same files exist. diff --git a/src/ui/gen2/CardFlip.lua b/src/ui/gen2/CardFlip.lua index a1b596ce..b9db6352 100644 --- a/src/ui/gen2/CardFlip.lua +++ b/src/ui/gen2/CardFlip.lua @@ -49,9 +49,9 @@ -- Textbox at (0,12) with an 18x4 interior -- -- The cart's own art (gfx/card_flip/card_flip_1..3.2bpp.lz and --- gfx/card_flip/card_flip.tilemap) is NOT in the cache: no `cardFlip` entry is --- written into menu_gfx.lua yet, so the board draws as labelled cells until one --- appears. +-- gfx/card_flip/card_flip.tilemap) is extracted into assets/generated/card_flip/ +-- when the Gold/Silver manifest carries CardFlip*. Until those files exist, +-- the board draws as labelled cells. local Chrome = require("src.ui.gen2.Chrome") local CoinCase = require("src.core.gen2.CoinCase") @@ -616,10 +616,8 @@ local TILEMAP = nil local function getCardFlipTilemap() if TILEMAP == nil then local path = "assets/generated/card_flip/card_flip.tilemap" - local f = io.open(path, "rb") - if f then - local data = f:read("*a") - f:close() + local data = love and love.filesystem and love.filesystem.read(path) + if data and #data > 0 then TILEMAP = {} for i = 1, #data do TILEMAP[i] = string.byte(data, i) diff --git a/src/ui/gen2/SlotMachine.lua b/src/ui/gen2/SlotMachine.lua index 83bd38fb..a85006dd 100644 --- a/src/ui/gen2/SlotMachine.lua +++ b/src/ui/gen2/SlotMachine.lua @@ -39,11 +39,9 @@ -- tiles at (2,13),(3,13),(2,14),(3,14) and the ▼ at -- (18,17) -- --- The cart's own reel art (gfx/slots/slots_1..3.2bpp.lz plus --- gfx/slots/slots.tilemap) is NOT in the cache: src/import/RomExtractorGen2.lua --- writes no `slots` entry into menu_gfx.lua yet. SlotMachine:sheet() reads one --- the moment it appears and falls back to labelled cells until then, the same --- way src/ui/gen2/PackGfx.lua degrades. +-- Reel art (gfx/slots/slots_1..3.2bpp.lz + slots.tilemap) is extracted into +-- assets/generated/slots/ when the Gold/Silver manifest carries Slots*LZ. +-- Until those files exist, drawReels falls back to labelled cells. local Chrome = require("src.ui.gen2.Chrome") local CoinCase = require("src.core.gen2.CoinCase") @@ -1160,10 +1158,8 @@ local TILEMAP = nil local function getTilemap() if TILEMAP == nil then local path = "assets/generated/slots/gold_slots.tilemap" - local f = io.open(path, "rb") - if f then - local data = f:read("*a") - f:close() + local data = love and love.filesystem and love.filesystem.read(path) + if data and #data > 0 then TILEMAP = {} for i = 1, #data do TILEMAP[i] = string.byte(data, i) @@ -1175,6 +1171,14 @@ local function getTilemap() return TILEMAP or nil end +-- Placeholder 2x2 symbol cell used when reel sheets are not in the cache yet. +local function cell(tx, ty, label) + local G = love.graphics + G.setColor(0, 0, 0, 1) + G.rectangle("line", tx * 8, ty * 8, 16, 16) + Chrome.print(label, tx, ty + 1) +end + function SlotMachine:sheets() if self.sheet1 == nil then self.sheet1 = TileSheet.new({ path = "assets/generated/slots/gold_slots_1.png", wide = 2, firstTile = 0 }) @@ -1253,36 +1257,40 @@ function SlotMachine:drawReels() -- Draw 4 consecutive 2x2 symbols from bottom to top, exactly matching SlotMachine.window for row = 0, 3 do local sym = strip[a + row + 1] - local py = 64 - (row * 16) + dy - local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0] - s2.palette = pal - - -- 2x2 tiles in 2-wide sheet: - -- sym + 0 = top-left (col 0, row 0) - -- sym + 1 = top-right (col 1, row 0) - -- sym + 2 = bottom-left (col 0, row 1) - -- sym + 3 = bottom-right (col 1, row 1) - local t0 = s2:quad(sym + 0) - local t1 = s2:quad(sym + 1) - local t2 = s2:quad(sym + 2) - local t3 = s2:quad(sym + 3) - local img = s2:image() - - if img and t0 and t1 and t2 and t3 then - local function drawSym() - G.draw(img, t0, rx, py) - G.draw(img, t1, rx + 8, py) - G.draw(img, t2, rx, py + 8) - G.draw(img, t3, rx + 8, py + 8) - end - if GbcPalette.available() then - GbcPalette.with(pal, drawSym) - else - drawSym() - end + if type(sym) ~= "number" then + cell(REEL_X[i], REEL_ROW[row + 1], "?") else - -- Fallback label - cell(REEL_X[i], REEL_ROW[row + 1], SlotMachine.LABELS[sym] or "?") + local py = 64 - (row * 16) + dy + local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0] + s2.palette = pal + + -- 2x2 tiles in 2-wide sheet: + -- sym + 0 = top-left (col 0, row 0) + -- sym + 1 = top-right (col 1, row 0) + -- sym + 2 = bottom-left (col 0, row 1) + -- sym + 3 = bottom-right (col 1, row 1) + local t0 = s2:quad(sym + 0) + local t1 = s2:quad(sym + 1) + local t2 = s2:quad(sym + 2) + local t3 = s2:quad(sym + 3) + local img = s2:image() + + if img and t0 and t1 and t2 and t3 then + local function drawSym() + G.draw(img, t0, rx, py) + G.draw(img, t1, rx + 8, py) + G.draw(img, t2, rx, py + 8) + G.draw(img, t3, rx + 8, py + 8) + end + if GbcPalette.available() then + GbcPalette.with(pal, drawSym) + else + drawSym() + end + else + -- Fallback label + cell(REEL_X[i], REEL_ROW[row + 1], SlotMachine.LABELS[sym] or "?") + end end end end @@ -1439,31 +1447,35 @@ function SlotMachine:drawMessage() and self.phase == "payoutText" then local _, s2 = self:sheets() local sym = self.matched - local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0] - s2.palette = pal - local t0 = s2:quad(sym + 0) - local t1 = s2:quad(sym + 1) - local t2 = s2:quad(sym + 2) - local t3 = s2:quad(sym + 3) - local img = s2:image() - local G = love.graphics - local px, py = PAYOUT_SYMBOL_X * 8, PAYOUT_SYMBOL_Y * 8 - if img and t0 and t1 and t2 and t3 then - local function drawWin() - G.setColor(1, 1, 1, 1) - G.draw(img, t0, px, py) - G.draw(img, t1, px + 8, py) - G.draw(img, t2, px, py + 8) - G.draw(img, t3, px + 8, py + 8) - end - G.setColor(1, 1, 1, 1) - if GbcPalette.available() then - GbcPalette.with(pal, drawWin) - else - drawWin() - end + if type(sym) ~= "number" then + cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, "?") else - cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, SlotMachine.LABELS[self.matched] or "?") + local pal = GBC_PALS.obj[math.floor(sym / 4)] or GBC_PALS.obj[0] + s2.palette = pal + local t0 = s2:quad(sym + 0) + local t1 = s2:quad(sym + 1) + local t2 = s2:quad(sym + 2) + local t3 = s2:quad(sym + 3) + local img = s2:image() + local G = love.graphics + local px, py = PAYOUT_SYMBOL_X * 8, PAYOUT_SYMBOL_Y * 8 + if img and t0 and t1 and t2 and t3 then + local function drawWin() + G.setColor(1, 1, 1, 1) + G.draw(img, t0, px, py) + G.draw(img, t1, px + 8, py) + G.draw(img, t2, px, py + 8) + G.draw(img, t3, px + 8, py + 8) + end + G.setColor(1, 1, 1, 1) + if GbcPalette.available() then + GbcPalette.with(pal, drawWin) + else + drawWin() + end + else + cell(PAYOUT_SYMBOL_X, PAYOUT_SYMBOL_Y, SlotMachine.LABELS[sym] or "?") + end end end end diff --git a/tools/make_gold_manifest.py b/tools/make_gold_manifest.py index 0176f072..02828172 100644 --- a/tools/make_gold_manifest.py +++ b/tools/make_gold_manifest.py @@ -785,6 +785,14 @@ REQUIRED_SYMBOLS = { "KabutoPuzzleLZ", "OmanytePuzzleLZ", "AerodactylPuzzleLZ", "HoOhPuzzleLZ", "UnownPuzzleStartCancelLZ", "UnownPuzzleCursorGFX", "PuzzlePieceBorderData.TileBordersGFX", + # Goldenrod Game Corner (engine/games/slot_machine.asm + card_flip.asm). + # Slots1LZ/2LZ/3LZ are the reel + actor sheets; SlotsTilemap is the 20x12 + # BG map. CardFlipLZ01..03 + On/Off button tiles and CardFlipTilemap are + # the odds-board art. Without these in the manifest the extractor skips + # the files and SlotMachine/CardFlip fall back to labelled cells. + "Slots1LZ", "Slots2LZ", "Slots3LZ", "SlotsTilemap", + "CardFlipLZ01", "CardFlipLZ02", "CardFlipLZ03", + "CardFlipOnButtonGFX", "CardFlipOffButtonGFX", "CardFlipTilemap", # Emote bubbles (data/sprites/emotes.asm): showemote's ! over a trainer # who just spotted the player, and the other faces scripts use. "ShockEmote", "QuestionEmote", "HappyEmote", "SadEmote",