Files
gen1recomp/tests/gen2_crystal_mobile_gfx_test.lua
T
bryanthaboi ca4d3d283c Add Pokemon Crystal as a sixth supported version
Crystal boots from a user-supplied ROM, imports a full cache and is playable:
copyright, the Crystal intro movie, the animated title, gender select, Oak,
and out into Johto. 122 of the cart's 169 script specials are implemented.

Import and data
- tools/make_crystal_manifest.py derives the manifest by importing
  make_gold_manifest as a library, with three additive keyword seams. Gold and
  Silver still regenerate byte-identical, which is the standing requirement for
  touching that generator.
- crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta:
  Crystal renames the credits mons, splits the trainer card, Pokegear and
  pack-pal blocks by gender, and replaces the intro and title outright.
- Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the
  badge block sits one higher) and unownCharmap (the main charmap parser stops
  at the first newcharmap so the two cannot contaminate each other).

Extractor
- RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK
  0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole,
  seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim
  steps with per-row degrade, and the column-major trainer card portraits.
- New: animated front sprites (frames, bitmasks, play and idle scripts), the
  Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal
  intro and title via src/import/CrystalMovie.lua.

Engine
- GameVersion gains engine(id) and fixes(id). Gold and Silver keep their
  original bugs where the bug is not hardware dependent; Crystal gets the fixes
  Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the
  Reflect and Light Screen defence overflow.
- Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins
  of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower
  including the wInBattleTowerBattle badge-boost guard.
- Kris and the gender flag, animated fronts in battle and the summary screen,
  and mon caught data.

Verification
- Every extracted asset is pixel-compared against pret's own source PNGs.
- Gold caches are byte-identical before and after, file for file.
- New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
2026-08-23 12:10:28 -04:00

464 lines
19 KiB
Lua

-- Crystal's Kris battle/Hall-of-Fame art and the Mobile System GB sheets.
-- luajit tests/gen2_crystal_mobile_gfx_test.lua
--
-- Two imports that had no consumer at the time they landed, so nothing else
-- would notice them rotting: Kris's backpic and KRIS trainer-class pic (wave A
-- shipped the female protagonist but battles and the Hall of Fame still drew
-- Chris), and the whole gfx/mobile + gfx/mystery_gift set, imported ahead of
-- the phase that uses it so Crystal never has to re-import for it.
--
-- What is held down: the stage exists and run() calls it, it is gated on the
-- crystal edition so Gold and Silver emit nothing, every label it reads is in
-- the generated Crystal manifest at the address pokecrystal.sym gives, and the
-- blobs it wrote are ../pokecrystal/gfx/mobile byte for byte.
--
-- ROM-free. The decomp half SKIPs with no ../pokecrystal beside the repo, the
-- cache half with no Crystal cache.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 crystal mobile gfx")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
local function readFile(path)
local f = io.open(path, "rb")
if not f then return nil end
local body = f:read("*a")
f:close()
return body
end
-- ------------------------------------------------------- the stage itself
local extractorSource = assert(readFile("src/import/RomExtractorGen2.lua"))
local RomExtractorGen2 = require("src.import.RomExtractorGen2")
check(type(RomExtractorGen2.extractMobileGfx) == "function",
"RomExtractorGen2:extractMobileGfx exists")
check(extractorSource:find("results.mobileGfx = self:extractMobileGfx()",
1, true) ~= nil, "and RomExtractorGen2:run calls it")
check(extractorSource:find(
'if self.edition ~= "crystal" then return nil end', 1, true) ~= nil,
"gated on the edition, so Gold and Silver write no mobile art")
-- The two suites that pin the progress bar read this same literal; a new
-- stage must not move it (tests/gen2_diploma_test.lua:45).
check(extractorSource:find("local STAGE_COUNT = 27", 1, true) ~= nil,
"and it did not disturb STAGE_COUNT")
-- ------------------------------------------------------------ the symbols
-- ../pokecrystal-symbols/pokecrystal.sym, international v1.0. Every one of
-- these resolves in that build: the Japanese-only part of Mobile System GB is
-- the text and the menu entries that reach it, not the art.
local SYMBOLS = {
AsciiFontGFX = { 0x5c, 0x5db1 },
PichuAnimatedMobileGFX = { 0x5c, 0x4d16 },
ElectroBallMobileGFX = { 0x5c, 0x55a4 },
PichuBorderMobileGFX = { 0x5c, 0x5848 },
Stadium2N64GFX = { 0x5c, 0x6f1f },
Stadium2N64Tilemap = { 0x5c, 0x73af },
Stadium2N64Attrmap = { 0x5c, 0x7517 },
PasswordTopTilemap = { 0x5c, 0x6491 },
PasswordBottomTilemap = { 0x5c, 0x651d },
PasswordShiftTilemap = { 0x5c, 0x65f9 },
ChooseMobileCenterTilemap = { 0x5c, 0x6685 },
MobilePasswordAttrmap = { 0x5c, 0x67ed },
ChooseMobileCenterAttrmap = { 0x5c, 0x6955 },
MobilePasswordPalettes = { 0x5c, 0x5d71 },
MobileCardGFX = { 0x5e, 0x59ef },
ChrisSilhouetteGFX = { 0x5e, 0x5bef },
KrisSilhouetteGFX = { 0x5e, 0x5e1f },
MobileCard2GFX = { 0x5e, 0x604f },
CardLargeSpriteAndFolderGFX = { 0x5e, 0x61bf },
CardSpriteGFX = { 0x5e, 0x664f },
DialpadTilemap = { 0x5e, 0x6cd5 },
DialpadAttrmap = { 0x5e, 0x6e3d },
DialpadGFX = { 0x5e, 0x6fa5 },
DialpadCursorGFX = { 0x5e, 0x7465 },
MobileCardListGFX = { 0x5e, 0x74b9 },
HaveWantGFX = { 0x5f, 0x4083 },
MobileSelectGFX = { 0x5f, 0x4983 },
HaveWantMap = { 0x5f, 0x4b83 },
PokemonNewsGFX = { 0x5f, 0x66fe },
PokemonNewsTileAttrmap = { 0x5f, 0x6b8e },
PokemonNewsPalettes = { 0x5f, 0x6ff6 },
["MobileSystemSplashScreen_InitGFX.Tiles"] = { 0x5b, 0x4173 },
["MobileSystemSplashScreen_InitGFX.Tilemap"] = { 0x5b, 0x4633 },
["MobileSystemSplashScreen_InitGFX.Attrmap"] = { 0x5b, 0x479b },
MobileSplashScreenPalettes = { 0x5b, 0x4903 },
MobileAdapterCheckGFX = { 0x5b, 0x4ca3 },
MobileTradeSpritesGFX = { 0x42, 0x4d27 },
MobileTradeGFX = { 0x42, 0x4da7 },
MobileTradeTilemapLZ = { 0x42, 0x4fe7 },
MobileTradeAttrmapLZ = { 0x42, 0x50a7 },
MobileCable1GFX = { 0x42, 0x51c7 },
MobileCable2GFX = { 0x42, 0x52c7 },
UnusedMobilePulsePalettes = { 0x42, 0x50f7 },
MobileTradeBGPalettes = { 0x42, 0x5107 },
MobileTradeOB1Palettes = { 0x42, 0x5147 },
MobileTradeOB2Palettes = { 0x42, 0x5187 },
MobileAdapterPalettes = { 0x42, 0x53c7 },
MobileTradeLightsGFX = { 0x40, 0x72a2 },
MobileTradeLightsPalettes = { 0x40, 0x72e2 },
PichuBorderMobileOBPalettes = { 0x45, 0x730e },
PichuBorderMobileBGPalettes = { 0x45, 0x734e },
PichuBorderMobileTilemapAttrmap = { 0x45, 0x7356 },
MobileDialingGFX = { 0x45, 0x601a },
MobileUpArrowGFX = { 0x12, 0x48c3 },
MobileDownArrowGFX = { 0x12, 0x48cb },
EZChatCursorGFX = { 0x22, 0x540b },
MobileDialingFrameGFX = { 0x41, 0x6514 },
SelectStartGFX = { 0x47, 0x567e },
MobileMenuGFX = { 0x12, 0x5c0c },
MobilePhoneTilesGFX = { 0x3e, 0x5234 },
MysteryGiftGFX = { 0x41, 0x5258 },
CardTradeGFX = { 0x41, 0x5930 },
CardTradeSpriteGFX = { 0x41, 0x5d30 },
}
-- Kris's own two pics. KrisBackpic needs no delta row: embedded_symbols in
-- make_gold_manifest.py adds every unqualified *Backpic label on sight.
local KRIS_SYMBOLS = {
KrisBackpic = { 0x22, 0x4ed6 },
KrisPic = { 0x22, 0x4bb9 },
ChrisPic = { 0x22, 0x48a9 },
}
do
local deltas = assert(readFile("tools/crystal_symbol_deltas.py"))
for label in pairs(SYMBOLS) do
check(deltas:find('"' .. label .. '"', 1, true) ~= nil,
label .. " is in crystal_symbol_deltas.MOBILE_SYMBOLS")
end
end
local Json = require("src.link.Json")
local function manifestSymbols(path)
local body = readFile(path)
if not body then return nil end
return (Json.decode(body) or {}).symbols or {}
end
do
local symbols = manifestSymbols("tools/rom_manifest_crystal.json")
if not symbols then
check(true, "no tools/rom_manifest_crystal.json (SKIP)")
else
local wrong = {}
for label, at in pairs(SYMBOLS) do
local got = symbols[label]
if not got or got[1] ~= at[1] or got[2] ~= at[2] then
wrong[#wrong + 1] = label
end
end
check(#wrong == 0, #wrong == 0
and "the Crystal manifest carries all 63 mobile symbols where "
.. "pokecrystal.sym puts them"
or ("mobile symbols missing or misplaced: " .. table.concat(wrong, ", ")))
for label, at in pairs(KRIS_SYMBOLS) do
local got = symbols[label]
if not got then
check(false, "the Crystal manifest carries " .. label)
else
eq(got[1], at[1], label .. " is in the bank pokecrystal.sym says")
eq(got[2], at[2], label .. " is at the address it says")
end
end
end
end
do
-- Gold has no mobile banks; a delta that leaked into the shared set would
-- break the Gold import rather than show up here as a missing sheet.
local symbols = manifestSymbols("tools/rom_manifest_gold.json")
if not symbols then
check(true, "no tools/rom_manifest_gold.json (SKIP)")
else
local leaked = {}
for label in pairs(SYMBOLS) do
if symbols[label] then leaked[#leaked + 1] = label end
end
check(#leaked == 0, #leaked == 0
and "and the Gold manifest carries none of them"
or ("mobile symbols leaked into Gold: " .. table.concat(leaked, ", ")))
check(symbols.KrisBackpic == nil, "nor Gold's non-existent KrisBackpic")
end
end
-- --------------------------------------------------------------- the cache
local cache = os.getenv("CRYSTAL_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal"
end
local function loadCache(rel)
local chunk = loadfile(cache .. "/data/generated/" .. rel .. ".lua")
if not chunk then return nil end
local ok, value = pcall(chunk)
return ok and value or nil
end
-- PNG IHDR: width and height are big-endian at bytes 17 and 21.
local function pngSize(relative)
local png = readFile(cache .. "/" .. relative)
if not png then return nil end
local function be32(s, i)
local a, b, c, d = s:byte(i, i + 3)
return ((a * 256 + b) * 256 + c) * 256 + d
end
return be32(png, 17), be32(png, 21)
end
-- ../pokecrystal/gfx/player/kris_back.png is 48x48 and kris.png 56x56, the
-- same six and seven tiles square Chris uses.
local BACK_PIC_PX = 48
local TRAINER_PIC_PX = 56
-- Kris's art and the mobile sheets ride one import, so mobile_gfx.lua doubles
-- as the marker for both: a cache built before this unit has neither, and the
-- whole cache half stands down rather than failing on an old sandbox.
local mobile = loadCache("mobile_gfx")
local menuGfx = mobile and loadCache("menu_gfx") or nil
if not menuGfx then
check(true, "no Crystal cache with mobile_gfx.lua at " .. cache
.. " : re-import for gfx/mobile (SKIP)")
else
local hud = menuGfx.battleHud or {}
eq(hud.playerBackFemale, "assets/generated/battle/player_back_female.png",
"menu_gfx points battles and the Hall of Fame at Kris's backpic")
local w, h = pngSize("assets/generated/battle/player_back_female.png")
eq(w, BACK_PIC_PX, "GetKrisBackpic's pic is 48px wide")
eq(h, BACK_PIC_PX, "and 48 tall, not the 7*7 tiles of VRAM it asks for")
check(hud.playerBack ~= hud.playerBackFemale,
"and it is not the same file Chris gets")
local pics = hud.trainerPics or {}
eq(pics.KRIS, "assets/generated/battle/trainers/kris.png",
"HOF_LoadTrainerFrontpic's KRIS class has a pic of its own")
eq(pics.CHRIS, "assets/generated/battle/trainers/chris.png",
"as does CHRIS, which TrainerPicPointers has no row for")
for _, key in ipairs({ "kris", "chris" }) do
local pw, ph = pngSize("assets/generated/battle/trainers/" .. key .. ".png")
eq(pw, TRAINER_PIC_PX, key .. ".png is 56px wide")
eq(ph, TRAINER_PIC_PX, "and 56 tall, like every other trainer pic")
end
end
local oakSpeech = mobile and loadCache("oak_speech") or nil
if not oakSpeech then
check(true, "no oak_speech.lua beside a mobile_gfx.lua cache (SKIP)")
else
eq(oakSpeech.playerPicFemale, "assets/generated/intro/kris.png",
"and DrawIntroPlayerPic's female arm is named in oak_speech.lua")
end
-- Sheets whose tile count fills the pret PNG's grid exactly once padded, so
-- the cache PNG must come out at the source PNG's own size. The three left
-- out (mobile_splash, electro_ball, phone_tiles) are built with
-- --remove-duplicates / --remove-whitespace, so the ROM holds fewer tiles than
-- the PNG does and only the tilemap puts them back (../pokecrystal/Makefile:
-- 343,344,348).
local SHEETS = {
{ "asciiFont", "mobile/ascii_font.png", 110, 16, "gfx/mobile/ascii_font.png" },
{ "card", "mobile/card.png", 32, 16, "gfx/mobile/card.png" },
{ "card2", "mobile/card_2.png", 23, 12, "gfx/mobile/card_2.png" },
{ "cardLargeSprite", "mobile/card_large_sprite.png", 8, 4,
"gfx/mobile/card_large_sprite.png" },
{ "cardFolder", "mobile/card_folder.png", 65, 6,
"gfx/mobile/card_folder.png" },
{ "cardList", "mobile/card_list.png", 24, 12, "gfx/mobile/card_list.png" },
{ "cardSprite", "mobile/card_sprite.png", 4, 2, "gfx/mobile/card_sprite.png" },
{ "chrisSilhouette", "mobile/chris_silhouette.png", 35, 5,
"gfx/mobile/chris_silhouette.png" },
{ "krisSilhouette", "mobile/kris_silhouette.png", 35, 5,
"gfx/mobile/kris_silhouette.png" },
{ "dialing", "mobile/dialing.png", 20, 2, "gfx/mobile/dialing.png" },
{ "dialingFrame", "mobile/dialing_frame.png", 8, 2,
"gfx/mobile/dialing_frame.png" },
{ "dialpad", "mobile/dialpad.png", 76, 16, "gfx/mobile/dialpad.png" },
{ "dialpadCursor", "mobile/dialpad_cursor.png", 5, 2,
"gfx/mobile/dialpad_cursor.png" },
{ "ezChatCursor", "mobile/ez_chat_cursor.png", 2, 1,
"gfx/mobile/ez_chat_cursor.png" },
{ "haveWant", "mobile/havewant.png", 144, 16, "gfx/mobile/havewant.png" },
{ "select", "mobile/select.png", 32, 4, "gfx/mobile/select.png" },
{ "selectStart", "mobile/select_start.png", 6, 3,
"gfx/mobile/select_start.png" },
{ "cable1", "mobile/mobile_cable_1.png", 16, 4,
"gfx/mobile/mobile_cable_1.png" },
{ "cable2", "mobile/mobile_cable_2.png", 16, 4,
"gfx/mobile/mobile_cable_2.png" },
{ "menu", "mobile/mobile_menu.png", 13, 13, "gfx/mobile/mobile_menu.png" },
{ "adapterCheck", "mobile/mobile_splash_check.png", 48, 16,
"gfx/mobile/mobile_splash_check.png" },
{ "tradeLights", "mobile/mobile_trade_lights.png", 4, 2,
"gfx/mobile/mobile_trade_lights.png" },
{ "pichuBorder", "mobile/pichu_border.png", 24, 4,
"gfx/mobile/pichu_border.png" },
{ "pokemonNews", "mobile/pokemon_news.png", 72, 8,
"gfx/mobile/pokemon_news.png" },
{ "stadium2N64", "mobile/stadium2_n64.png", 73, 11,
"gfx/mobile/stadium2_n64.png" },
{ "pichuAnimated", "mobile/pichu_animated.png", 193, 16,
"gfx/mobile/pichu_animated.png" },
{ "trade", "mobile/mobile_trade.png", 128, 8, "gfx/mobile/mobile_trade.png" },
{ "tradeSprites", "mobile/mobile_trade_sprites.png", 16, 4,
"gfx/mobile/mobile_trade_sprites.png" },
{ "upArrow", "mobile/up_arrow.png", 1, 1, "gfx/mobile/up_arrow.png" },
{ "downArrow", "mobile/down_arrow.png", 1, 1, "gfx/mobile/down_arrow.png" },
{ "mysteryGift", "mobile/mystery_gift/mystery_gift.png", 67, 16,
"gfx/mystery_gift/mystery_gift.png" },
{ "cardTrade", "mobile/mystery_gift/card_trade.png", 64, 16,
"gfx/mystery_gift/card_trade.png" },
{ "cardTradeSprite", "mobile/mystery_gift/card_sprite.png", 8, 4,
"gfx/mystery_gift/card_sprite.png" },
}
-- The three the dedupe passes shrink: only the declared tile count is checked.
local DEDUPED = {
{ "splash", "mobile/mobile_splash.png", 76, 14 },
{ "electroBall", "mobile/electro_ball.png", 83, 16 },
{ "phoneTiles", "mobile/phone_tiles.png", 17, 2 },
}
-- key, cache file, byte length, ../pokecrystal source it must equal.
local MAPS = {
{ "passwordTop", "password_top", 140, "gfx/mobile/password_top.tilemap" },
{ "passwordBottom", "password_bottom", 220,
"gfx/mobile/password_bottom.tilemap" },
{ "passwordShift", "password_shift", 140,
"gfx/mobile/password_shift.tilemap" },
{ "passwordAttrmap", "password_attrmap", 360,
"gfx/mobile/password.attrmap" },
{ "centerTilemap", "mobile_center_tilemap", 360,
"gfx/mobile/mobile_center.tilemap" },
{ "centerAttrmap", "mobile_center_attrmap", 360,
"gfx/mobile/mobile_center.attrmap" },
{ "stadium2N64Tilemap", "stadium2_n64_tilemap", 360,
"gfx/mobile/stadium2_n64.tilemap" },
{ "stadium2N64Attrmap", "stadium2_n64_attrmap", 360,
"gfx/mobile/stadium2_n64.attrmap" },
{ "dialpadTilemap", "dialpad_tilemap", 360, "gfx/mobile/dialpad.tilemap" },
{ "dialpadAttrmap", "dialpad_attrmap", 360, "gfx/mobile/dialpad.attrmap" },
{ "haveWantMap", "havewant_map", 1136, "gfx/mobile/havewant_map.bin" },
{ "newsAttrmap", "pokemon_news_attrmap", 1128,
"gfx/mobile/pokemon_news.bin" },
{ "splashTilemap", "mobile_splash_tilemap", 360,
"gfx/mobile/mobile_splash.tilemap" },
{ "splashAttrmap", "mobile_splash_attrmap", 360,
"gfx/mobile/mobile_splash.attrmap" },
{ "pichuBorderTilemap", "pichu_border_tilemap", 384,
"gfx/mobile/pichu_border.tilemap" },
{ "pichuBorderAttrmap", "pichu_border_attrmap", 384,
"gfx/mobile/pichu_border.attrmap" },
{ "tradeTilemap", "mobile_trade_tilemap", 1024,
"gfx/mobile/mobile_trade.tilemap" },
{ "tradeAttrmap", "mobile_trade_attrmap", 1024,
"gfx/mobile/mobile_trade.attrmap" },
}
local PALETTE_COLORS = {
splash = 32, password = 32, pokemonNews = 32, pichuBorderOB = 32,
pichuBorderBG = 4, tradeBG = 32, tradeOB1 = 32, tradeOB2 = 32,
tradeLights = 16, adapters = 8, unusedPulses = 8,
}
local pokecrystal = "../pokecrystal"
local havePret = readFile(pokecrystal .. "/gfx/mobile/ascii_font.2bpp") ~= nil
if not mobile then
check(true, "no mobile_gfx.lua in the Crystal cache (SKIP)")
else
eq(mobile.generation, 2, "mobile_gfx.lua is a Gen 2 table")
local sheets = mobile.sheets or {}
local maps = mobile.maps or {}
local palettes = mobile.palettes or {}
local function checkSheet(row, pretPng)
local key, rel, tiles, wide = row[1], row[2], row[3], row[4]
local entry = sheets[key]
if type(entry) ~= "table" then
check(false, "mobile_gfx carries the " .. key .. " sheet")
return
end
eq(entry.tiles, tiles, key .. " is the tile count the ROM block holds")
eq(entry.tilesWide, wide, "and is written " .. wide .. " tiles across")
eq(entry.path, "assets/generated/" .. rel, "at the path it advertises")
local w, h = pngSize("assets/generated/" .. rel)
eq(w, wide * 8, key .. ".png is " .. (wide * 8) .. "px wide")
eq(h, math.ceil(tiles / wide) * 8, "and tall enough for every tile")
if pretPng and havePret then
local png = readFile(pokecrystal .. "/" .. pretPng)
if png then
local function be32(s, i)
local a, b, c, d = s:byte(i, i + 3)
return ((a * 256 + b) * 256 + c) * 256 + d
end
eq(w, be32(png, 17), "which is " .. pretPng .. "'s own width")
eq(h, be32(png, 21), "and its own height")
end
end
end
for _, row in ipairs(SHEETS) do checkSheet(row, row[5]) end
for _, row in ipairs(DEDUPED) do checkSheet(row, nil) end
for _, row in ipairs(MAPS) do
local key, file, bytes, source = row[1], row[2], row[3], row[4]
local entry = maps[key]
local rel = "assets/generated/mobile/" .. file .. ".bin"
if type(entry) ~= "table" then
check(false, "mobile_gfx carries the " .. key .. " map")
else
eq(entry.bytes, bytes, key .. " is " .. bytes .. " bytes")
eq(entry.path, rel, "at the path it advertises")
local blob = readFile(cache .. "/" .. rel)
if not blob then
check(false, rel .. " was written")
else
eq(#blob, bytes, "and the file on disk is that long")
local want = havePret and readFile(pokecrystal .. "/" .. source) or nil
if not want then
check(true, "no ../pokecrystal: " .. source .. " not diffed (SKIP)")
else
check(blob == want:sub(1, bytes),
"it is ../pokecrystal/" .. source .. " byte for byte")
end
end
end
end
for key, colors in pairs(PALETTE_COLORS) do
local pal = palettes[key]
if type(pal) ~= "table" then
check(false, "mobile_gfx carries the " .. key .. " palette block")
else
eq(#pal, colors, key .. " is " .. colors .. " RGB words")
eq(#(pal[1] or {}), 3, "each one decoded to an r,g,b triple")
end
end
end
-- A Gold cache must have gone nowhere near any of this.
local goldCache = os.getenv("GOLD_CACHE")
if not goldCache then
local home = os.getenv("HOME") or ""
goldCache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
if not loadfile(goldCache .. "/data/generated/menu_gfx.lua") then
check(true, "no Gold cache at " .. goldCache .. " (SKIP)")
else
check(loadfile(goldCache .. "/data/generated/mobile_gfx.lua") == nil,
"a Gold cache has no mobile_gfx.lua")
local chunk = loadfile(goldCache .. "/data/generated/menu_gfx.lua")
local ok, gold = pcall(chunk)
local hud = (ok and gold or {}).battleHud or {}
check(hud.playerBackFemale == nil, "and no Kris backpic")
check((hud.trainerPics or {}).KRIS == nil, "and no KRIS trainer pic")
end
S.finish()