mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
461 lines
22 KiB
Lua
461 lines
22 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local ROM_PATH = os.getenv("POKEPORT_EMERALD_ROM") or "../pokeemerald/pokeemerald.gba"
|
|
local f = io.open(ROM_PATH, "rb")
|
|
if not f then
|
|
print("emerald_contest_ui_test: skipped (no ROM at " .. ROM_PATH .. ")")
|
|
os.exit(0)
|
|
end
|
|
local data = f:read("*a")
|
|
f:close()
|
|
if data:sub(0xAD, 0xB0) ~= "BPEE" then
|
|
print("emerald_contest_ui_test: skipped (" .. ROM_PATH .. " is not Emerald)")
|
|
os.exit(0)
|
|
end
|
|
|
|
local GV = require("src.core.GameVersion")
|
|
|
|
local function stubAnims(fn)
|
|
local Anim = require("src.core.game3.battle.anim")
|
|
local launchMove = Anim.launchMove
|
|
Anim.launchMove = function(_, opts)
|
|
if opts and opts.onEnd then opts.onEnd() end
|
|
return true
|
|
end
|
|
local ok, err = pcall(fn)
|
|
Anim.launchMove = launchMove
|
|
if not ok then error(err, 0) end
|
|
end
|
|
local Versions = require("src.import.gba.versions")
|
|
local Rom = require("src.import.gba.rom")
|
|
GV.set("emerald")
|
|
Versions.select("emerald")
|
|
local sha = GV.VERSIONS.emerald.sha1
|
|
local rom = assert(Rom.open({
|
|
info = function() return { size = #data, md5 = sha } end,
|
|
read = function(_, _, off, len) return data:sub(off + 1, off + len) end,
|
|
}, GV.forSha1(sha)))
|
|
|
|
local ROOT = "data/generated/gba"
|
|
local files = {}
|
|
local cache = {
|
|
write = function(_, rel, bytes) files[rel] = bytes; return true end,
|
|
read = function(_, rel) return files[rel] end,
|
|
}
|
|
|
|
local home = os.getenv("HOME") or ""
|
|
local identity = os.getenv("POKEPORT_IDENTITY")
|
|
local IDROOT
|
|
if identity and identity ~= "" then
|
|
for _, r in ipairs({ home .. "/Library/Application Support/LOVE/" .. identity .. "/emerald/",
|
|
home .. "/.local/share/love/" .. identity .. "/emerald/" }) do
|
|
local h = io.open(r .. ROOT .. "/scripts/text.lua", "rb")
|
|
if h then h:close(); IDROOT = r; break end
|
|
end
|
|
end
|
|
|
|
local function readFile(path)
|
|
if files[path] then return files[path] end
|
|
if not IDROOT then return nil end
|
|
local h = io.open(IDROOT .. path, "rb")
|
|
if not h then return nil end
|
|
local s = h:read("*a")
|
|
h:close()
|
|
return s
|
|
end
|
|
|
|
package.loaded["src.core.game3.dataset"] = {
|
|
cache = function() return { read = function(_, rel) return readFile(rel) end, exists = function(_, rel) return readFile(rel) ~= nil end } end,
|
|
mountExtractRoots = function() end,
|
|
}
|
|
|
|
print("[test] extractors (pokeemerald/src/contest.c:1298, contest_painting.c:66)")
|
|
local GX = require("src.import.gba.rse.contest_gfx_extract")
|
|
local PX = require("src.import.gba.rse.contest_painting_extract")
|
|
local CX = require("src.import.gba.rse.contest_extract")
|
|
eq((GX.run(rom, cache, { cacheRoot = ROOT, game = "emerald" })), true, "contest stage gfx extractor ran")
|
|
eq((PX.run(rom, cache, { cacheRoot = ROOT, game = "emerald" })), true, "contest painting extractor ran")
|
|
CX.run(rom, cache, { cacheRoot = ROOT, game = "emerald" })
|
|
for _, rel in ipairs(GX.REQUIRED) do check(files[ROOT .. "/" .. rel] ~= nil, rel .. " written") end
|
|
for _, rel in ipairs(PX.REQUIRED) do check(files[ROOT .. "/" .. rel] ~= nil, rel .. " written") end
|
|
check(GX.ready(cache, ROOT) and PX.ready(cache, ROOT), "both manifests are ready")
|
|
|
|
local Vram = require("src.ui.game3.rse.contest_vram")
|
|
Vram.reset()
|
|
Vram.reader = readFile
|
|
local gm = Vram.manifest("rse/contest_gfx")
|
|
eq(#files[gm.gfx.interface.path], 0x2000, "interface tiles decompress to 8 KB (256 tiles)")
|
|
eq(#files[gm.gfx.audience.path], 0x2000, "audience tiles hold two 0x1000 frames")
|
|
eq(gm.maps.interface.entries, 32 * 64, "interface tilemap is 32x64")
|
|
eq(gm.sliderHeartY[0] .. "," .. gm.sliderHeartY[3], "36,156", "slider heart rows (contest.c:369)")
|
|
eq(gm.windows[4].left .. "," .. gm.windows[4].top .. "," .. gm.windows[4].width .. "," .. gm.windows[4].height, "1,15,17,4",
|
|
"general text window template (contest.c:896)")
|
|
eq(gm.windows[10].left, 11, "move description window starts at column 11")
|
|
eq(#gm.palettes.interface_audience, 256, "interface + audience palette is 256 colors")
|
|
eq(gm.sine[64], 256, "gSineTable[64] is 256")
|
|
eq(#gm.affine.sliderHeart[1], 3, "slider heart disappear anim has two frames + end")
|
|
local pm = Vram.manifest("rse/contest_painting")
|
|
eq(pm.frames.lobby.entries, 1024, "lobby frame tilemap RL-decompresses to 32x32")
|
|
eq(pm.frames.cool.tiles, 176, "cool frame RL-decompresses to 176 tiles")
|
|
eq(pm.pointillism.count, 3200, "3200 pointillism points")
|
|
eq(#pm.framePalette, 128, "eight frame palettes")
|
|
eq(pm.captions[4], "gContestPaintingBeauty2", "museum caption table order")
|
|
|
|
print("[test] image processing (pokeemerald/src/image_processing_effects.c)")
|
|
local Fx = require("src.ui.game3.rse.contest_image_fx")
|
|
local function canvas(fill)
|
|
local px = {}
|
|
for i = 0, 64 * 64 - 1 do px[i] = fill end
|
|
return px
|
|
end
|
|
local px = canvas(Fx.ALPHA)
|
|
px[10 * 64 + 10] = Fx.rgb2(10, 20, 30)
|
|
local ctx = Fx.context(px, { effect = Fx.EFFECT.GRAYSCALE_LIGHT, quantizeEffect = Fx.QUANTIZE.GRAYSCALE })
|
|
Fx.apply(ctx)
|
|
eq(px[10 * 64 + 10], Fx.rgb2(21, 21, 21), "grayscale light: Q8.8 weights then red + 3")
|
|
Fx.quantize(ctx)
|
|
eq(px[10 * 64 + 10], 22, "grayscale quantize maps gray 21 to index 22")
|
|
eq(px[0], 0, "transparent pixels quantize to the palette start")
|
|
eq(ctx.palette[22], Fx.rgb2(21, 21, 21), "grayscale preset palette")
|
|
|
|
px = canvas(Fx.ALPHA)
|
|
for y = 20, 22 do for x = 20, 22 do px[y * 64 + x] = Fx.rgb2(28, 28, 28) end end
|
|
ctx = Fx.context(px, { effect = Fx.EFFECT.OUTLINE_COLORED, personality = 3 })
|
|
Fx.apply(ctx)
|
|
eq(px[20 * 64 + 20], Fx.rgb2(23, 0, 0), "outline pixel takes the personality color (type 3 red, strength 0)")
|
|
eq(px[21 * 64 + 21], 0x7FFF, "interior light pixel turns white")
|
|
eq(px[0], Fx.ALPHA, "outline keeps transparency")
|
|
|
|
px = canvas(Fx.rgb2(3, 5, 7))
|
|
ctx = Fx.context(px, { effect = Fx.EFFECT.INVERT })
|
|
Fx.apply(ctx)
|
|
eq(px[100], Fx.rgb2(28, 26, 24), "invert")
|
|
px = canvas(Fx.rgb2(5, 9, 13))
|
|
ctx = Fx.context(px, { quantizeEffect = Fx.QUANTIZE.STANDARD_LIMITED_COLORS })
|
|
Fx.quantize(ctx)
|
|
eq(px[5], 1, "standard quantize: first color gets index 1")
|
|
eq(ctx.palette[1], Fx.rgb2(8, 12, 16), "standard quantize rounds channels up to multiples of 4")
|
|
eq(ctx.palette[0xDF], Fx.rgb2(15, 15, 15), "limited palette overflow gray at 0xDF")
|
|
|
|
local points = {}
|
|
local pb = files[pm.pointillism.path]
|
|
for i = 1, #pb do points[i - 1] = pb:byte(i) end
|
|
px = canvas(Fx.rgb2(16, 16, 16))
|
|
ctx = Fx.context(px, { effect = Fx.EFFECT.POINTILLISM, pointillism = points, pointillismCount = pm.pointillism.count })
|
|
Fx.apply(ctx)
|
|
local changed = 0
|
|
for i = 0, 64 * 64 - 1 do if px[i] ~= Fx.rgb2(16, 16, 16) then changed = changed + 1 end end
|
|
check(changed > 2000, "pointillism touches most of the canvas (" .. changed .. " pixels)")
|
|
eq(px[29 * 64 + 0] ~= Fx.rgb2(16, 16, 16), true, "first pointillism point lands at column 0 row 29")
|
|
|
|
print("[test] contest stage + results play the engine's events (pokeemerald/src/contest.c:1726)")
|
|
local Contest = require("src.core.game3.rse.contest")
|
|
local Util = require("src.core.game3.rse.contest_util")
|
|
local man = assert(load(files[ROOT .. "/" .. CX.SUB .. "/manifest.lua"], "@m", "t", {}))()
|
|
local moves = require("src.import.gba.contest_moves_extract").extract(rom)
|
|
local D = Contest.buildData(moves, man)
|
|
|
|
local function lcg(seed)
|
|
local Rng = require("src.core.game3.rng")
|
|
local v = seed
|
|
return function()
|
|
v = (Rng.mulU32(v, 1103515245) + 24691) % 4294967296
|
|
return math.floor(v / 65536)
|
|
end
|
|
end
|
|
|
|
local Stage = require("src.ui.game3.rse.contest")
|
|
local textTable
|
|
if IDROOT then
|
|
local chunk = loadfile(IDROOT .. ROOT .. "/scripts/text.lua")
|
|
textTable = chunk and chunk() or nil
|
|
end
|
|
Stage.ir = function(key)
|
|
if textTable and textTable[key] then return textTable[key] end
|
|
return { { t = "text", s = key }, { t = "eos" } }
|
|
end
|
|
Stage.has = function(key) return true end
|
|
|
|
local C = require("src.core.game3.constants").of("emerald")
|
|
local function mv(name) return C:require("moves", name) end
|
|
local PLAYER = {
|
|
species = C:require("species", "SPECIES_SALAMENCE"), nickname = "SALAMENCE", trainerName = "NICK",
|
|
moves = { mv("MOVE_HEADBUTT"), mv("MOVE_EMBER"), mv("MOVE_DRAGON_BREATH"), mv("MOVE_FLY") },
|
|
cool = 120, beauty = 40, cute = 10, smart = 20, tough = 80, sheen = 30, personality = 12345, otId = 1,
|
|
}
|
|
|
|
local function newContest(seed, category)
|
|
local c = Contest.new({ data = D, category = category or 0, rank = 0, rng = lcg(seed) })
|
|
c:setContestants({ gameClear = false, player = PLAYER })
|
|
c:calculateRound1Points()
|
|
return c
|
|
end
|
|
|
|
local function heartsOf(appeal)
|
|
local q = appeal / 10
|
|
local h = q >= 0 and math.floor(q) or -math.floor(-q)
|
|
if h > 16 then h = 16 elseif h < -16 then h = -16 end
|
|
return h
|
|
end
|
|
|
|
local function countHearts(screen, i)
|
|
local row = screen.c.turnOrder[i] * 5 + 2
|
|
local red, black = 0, 0
|
|
local base = ({ [0] = 0x5011, 0x6011, 0x7011 })[i] or 0x8011
|
|
for y = row, row + 1 do
|
|
for x = 22, 29 do
|
|
local e = screen.bg[0]:get(x, y)
|
|
if e == base + 1 then red = red + 1 elseif e == base + 3 then black = black + 1 end
|
|
end
|
|
end
|
|
return red, black
|
|
end
|
|
|
|
for _, case in ipairs({ { seed = 0x1234, sched = { 2, 0, 2, 3, 1 }, cat = 0 }, { seed = 0xBEEF, sched = { 1, 1, 0, 2, 3 }, cat = 0 },
|
|
{ seed = 0x77, sched = { 3, 2, 1, 0, 2 }, cat = 4 } }) do
|
|
local ref = newContest(case.seed, case.cat)
|
|
ref:init()
|
|
ref:run(case.sched)
|
|
local c = newContest(case.seed, case.cat)
|
|
local turns, heartsOk, heartsChecked = 0, true, 0
|
|
local screen
|
|
local sched = case.sched
|
|
local Anim = require("src.core.game3.battle.anim")
|
|
local launchMove, launched = Anim.launchMove, {}
|
|
Anim.launchMove = function(move, opts)
|
|
launched[#launched + 1] = { move = move, opts = opts }
|
|
if opts.onEnd then opts.onEnd() end
|
|
return true
|
|
end
|
|
screen = Stage.open({
|
|
contest = c, headless = true, moveName = function(m) return "MOVE" .. m end,
|
|
autoInput = function(s)
|
|
if s.m.tasks:isActive(s:func("taskHandleMoveSelectInput")) then
|
|
c.contest.playerMoveChoice = sched[c.contest.appealNumber + 1]
|
|
end
|
|
return { new = { a = true }, held = { a = true }, rep = {} }
|
|
end,
|
|
})
|
|
Anim.launchMove = launchMove
|
|
eq(screen.done, true, string.format("seed %04X: the stage finishes all five appeals", case.seed))
|
|
eq(c.contest.appealNumber, 5, "five appeal rounds ran through the UI")
|
|
local same = true
|
|
for i = 0, 3 do
|
|
if c.totals[i] ~= ref.totals[i] or c.standings[i] ~= ref.standings[i] or c.round2[i] ~= ref.round2[i] then same = false end
|
|
end
|
|
check(same, string.format("seed %04X: UI playback leaves the engine result unchanged (totals %d %d %d %d)", case.seed,
|
|
c.totals[0], c.totals[1], c.totals[2], c.totals[3]))
|
|
check(screen.frames > 2000 and screen.frames < 20000, "stage ran " .. screen.frames .. " frames")
|
|
check(#launched > 0, "appeal turns launch the shared move-animation VM")
|
|
local sample = launched[1]
|
|
eq(sample.opts.ctx.isContest, true, "contest animations enter jumpifcontest context")
|
|
eq(sample.opts.attackerId .. "/" .. sample.opts.targetId, "2/3", "contest move animation uses battlers 2 and 3")
|
|
eq(sample.opts.coordinateOverrides[2].x .. "/" .. sample.opts.coordinateOverrides[3].x, "112/48",
|
|
"contest animation uses opponent-right and player-right coordinates")
|
|
end
|
|
|
|
print("[test] hearts on BG0 track every contestant's appeal at each turn end (pokeemerald/src/contest.c:3737)")
|
|
do
|
|
local c = newContest(0x4242, 0)
|
|
local mismatches, checks = 0, 0
|
|
local turnEnd = Stage.turnEndStep
|
|
Stage.turnEndStep = function(self, t)
|
|
for i = 0, 3 do
|
|
local red, black = countHearts(self, i)
|
|
local h = heartsOf(self.c.status[i].appeal)
|
|
checks = checks + 1
|
|
if (h >= 0 and (red ~= h or black ~= 0)) or (h < 0 and (black ~= -h or red ~= 0)) then mismatches = mismatches + 1 end
|
|
end
|
|
return turnEnd(self, t)
|
|
end
|
|
stubAnims(function() Stage.open({ contest = c, headless = true, moveName = function(m) return "M" .. m end }) end)
|
|
Stage.turnEndStep = turnEnd
|
|
eq(checks, 80, "checked 4 contestants at 20 turn ends")
|
|
eq(mismatches, 0, "heart tiles match hearts(appeal) for every contestant")
|
|
end
|
|
|
|
local Results = require("src.ui.game3.rse.contest_results")
|
|
do
|
|
local c = newContest(0x99, 0)
|
|
stubAnims(function() Stage.open({ contest = c, headless = true, moveName = function(m) return "M" .. m end }) end)
|
|
local sess = { gameStats = {}, party = {} }
|
|
local r = Results.open({ contest = c, session = sess, headless = true, noFieldHooks = true })
|
|
eq(r.done, true, "results screen reaches the end")
|
|
eq(sess.gameStats[36], 1, "GAME_STAT_ENTERED_CONTEST incremented")
|
|
eq(sess.gameStats[37] or 0, c.standings[c.playerIndex] == 0 and 1 or 0, "GAME_STAT_WON_CONTEST follows the standings")
|
|
local rd = Util.resultsData(c)
|
|
for i = 0, 3 do
|
|
local want = rd[i].barLengthPreliminary + (rd[i].lostPoints and -rd[i].barLengthRound2 or rd[i].barLengthRound2)
|
|
eq(r.d.barLength[i], want, "result bar " .. i .. " ends at its last target (contest_util.c:1856)")
|
|
end
|
|
eq(r.d.numStandingsPrinted, 4, "four final standing numbers drawn")
|
|
local winner = Util.winnerId(c)
|
|
local L = r.bg[2]
|
|
eq(math.floor(L:get(5, winner * 3 + 5) / 4096), 9, "the winner's box row is switched to palette 9")
|
|
end
|
|
|
|
print("[test] painting build (pokeemerald/src/contest_painting.c:474)")
|
|
do
|
|
local Painting = require("src.ui.game3.rse.contest_painting")
|
|
Painting.resetCache()
|
|
local sp = C:require("species", "SPECIES_SALAMENCE")
|
|
local winner = { species = sp, personality = 7, trainerId = 1, contestCategory = 3 * 1 + 2, contestRank = 3,
|
|
monName = "SALAMENCE", trainerName = "NICK" }
|
|
local st = Painting.build({ winner = winner, saveIdx = 9, isForArtist = false })
|
|
eq(st.mon.effect, Fx.EFFECT.SHIMMER, "beauty museum painting uses the shimmer effect")
|
|
eq(st.frameKey, "beauty", "beauty museum frame")
|
|
local maxIdx = 0
|
|
for i = 0, 64 * 64 - 1 do if st.mon.indices[i] > maxIdx then maxIdx = st.mon.indices[i] end end
|
|
check(maxIdx < 0xE0, "limited quantization stays below 0xE0 (" .. maxIdx .. ")")
|
|
if IDROOT then check(maxIdx > 4, "the Salamence front pic quantizes to several colors") end
|
|
local art = Painting.build({ winner = winner, saveIdx = 9, isForArtist = true })
|
|
eq(art.map[0], 0x1015, "artist painting background tile (contest_painting.c:418)")
|
|
eq(art.caption, nil, "artist painting has no caption")
|
|
local hall = Painting.build({ winner = { species = sp, personality = 7, trainerId = 1, contestCategory = 4, contestRank = 0,
|
|
monName = "SALAMENCE", trainerName = "NICK" }, saveIdx = 0 })
|
|
eq(hall.frameKey, "lobby", "hall painting uses the lobby frame")
|
|
eq(hall.mon.effect, Fx.EFFECT.GRAYSCALE_LIGHT, "tough hall painting is light grayscale")
|
|
if IDROOT then
|
|
local spinda = C:require("species", "SPECIES_SPINDA")
|
|
local a = Painting.monPixels({ species = spinda, personality = 0x12345678, trainerId = 0x00010001 })
|
|
local b = Painting.monPixels({ species = spinda, personality = 0xFEDCBA98, trainerId = 0x00010001 })
|
|
local changed = 0
|
|
for i = 0, 64 * 64 - 1 do if a[i] ~= b[i] then changed = changed + 1 end end
|
|
check(changed > 0, "contest painting applies each Spinda personality's spot pattern (" .. changed .. " pixels)")
|
|
end
|
|
if textTable then
|
|
check(type(hall.caption) == "string" and hall.caption:find("NICK", 1, true) ~= nil, "hall caption names the trainer")
|
|
end
|
|
end
|
|
|
|
print("[test] contest specials (pokeemerald/src/contest_util.c:1958)")
|
|
do
|
|
local Pokeblock = require("src.core.game3.rse.pokeblock")
|
|
local sess = { version = "emerald", name = "NICK", gender = "male", party = {}, gameStats = {} }
|
|
local mon = { species = C:require("species", "SPECIES_SALAMENCE"), level = 50, hp = 100, maxHp = 100,
|
|
moves = { mv("MOVE_HEADBUTT"), mv("MOVE_EMBER"), mv("MOVE_DRAGON_BREATH"), mv("MOVE_FLY") },
|
|
personality = 12345, otId = 1, nickname = IDROOT and "" or "SALAMENCE" }
|
|
local cond = Pokeblock.contest(mon)
|
|
cond.cool, cond.tough, cond.beauty, cond.sheen = 200, 100, 100, 50
|
|
sess.party[1] = mon
|
|
package.loaded["src.core.game3.runtime"] = { getSession = function() return sess end }
|
|
local Contest2 = require("src.core.game3.rse.contest")
|
|
local realData = Contest2.data
|
|
Contest2.data = function() return D end
|
|
local NC = require("src.core.game3.scripting.natives_contest")
|
|
local Rse = require("src.core.game3.rse.init")
|
|
local ctx = { specialVars = {}, stringVars = { "", "", "" } }
|
|
local function set(id, v) ctx.specialVars[id] = v end
|
|
local function get(id) return ctx.specialVars[id] or 0 end
|
|
NC.partyIndex = 0
|
|
set(0x8011, 0)
|
|
set(0x8010, 0)
|
|
NC.BY_NAME.TryEnterContestMon(ctx)
|
|
eq(get(0x800D), 1, "TryEnterContestMon: ribbonless mon, Normal rank -> CAN_ENTER_CONTEST_EQUAL_RANK")
|
|
local c = Util.current()
|
|
check(c ~= nil and c.mons[3].species == mon.species, "the party mon becomes contestant 4")
|
|
check(c.mons[3].nickname ~= "", "an unnamed mon enters under its species name (contest.c:2798)")
|
|
set(0x8010, 1)
|
|
NC.BY_NAME.TryEnterContestMon(ctx)
|
|
eq(get(0x800D), 0, "Super rank without a Normal ribbon -> CANT_ENTER_CONTEST")
|
|
set(0x8010, 0)
|
|
NC.BY_NAME.TryEnterContestMon(ctx)
|
|
c = Util.current()
|
|
set(0x8006, 3)
|
|
NC.BY_NAME.GetContestMonCondition(ctx)
|
|
eq(get(0x8004), c.round1[3], "GetContestMonCondition reads round 1 points")
|
|
NC.BY_NAME.BufferContestTrainerAndMonNames(ctx)
|
|
eq(ctx.stringVars[1], "NICK", "BufferContestTrainerAndMonNames: STR_VAR_1 trainer")
|
|
eq(get(0x8004), mon.species, "BufferContestTrainerAndMonNames: VAR_0x8004 species")
|
|
c:init()
|
|
c:run({ 0, 1, 2, 3, 0 })
|
|
NC.BY_NAME.GetContestWinnerId(ctx)
|
|
eq(get(0x8005), Util.winnerId(c), "GetContestWinnerId")
|
|
NC.BY_NAME.BufferContestWinnerMonName(ctx)
|
|
eq(ctx.stringVars[1], Util.monName(c.mons[Util.winnerId(c)]), "BufferContestWinnerMonName")
|
|
NC.BY_NAME.ShouldReadyContestArtist(ctx)
|
|
eq(get(0x8004), 0, "ShouldReadyContestArtist is false below Master rank")
|
|
local _, n = NC.BY_NAME.CountPlayerMuseumPaintings(ctx)
|
|
eq(n, 0, "no museum paintings yet (winners initialized from gDefaultContestWinners)")
|
|
check(type(sess.contestWinners) == "table" and (sess.contestWinners[1].species or 0) ~= 0, "hall defaults filled on first use")
|
|
set(0x800D, 8)
|
|
NC.BY_NAME.GenerateContestRand(ctx)
|
|
check(get(0x800D) < 8, "GenerateContestRand reduces VAR_RESULT modulo itself")
|
|
NC.BY_NAME.TryContestGModeLinkup(ctx)
|
|
eq(get(0x800D), 5, "link contest linkup reports LINKUP_FAILED")
|
|
local _, won = NC.BY_NAME.HasMonWonThisContestBefore(ctx)
|
|
eq(won, 0, "HasMonWonThisContestBefore is false without a ribbon")
|
|
Contest2.data = realData
|
|
Rse.reset()
|
|
end
|
|
|
|
print("[test] paintings match pygba captures pixel for pixel (pokeemerald/src/contest_painting.c:474)")
|
|
if not IDROOT then
|
|
print(" skipped: POKEPORT_IDENTITY cache with mon pics not found")
|
|
else
|
|
local Painting = require("src.ui.game3.rse.contest_painting")
|
|
Painting.resetCache()
|
|
local CASES = {
|
|
{ name = "museum_cool", species = 397, personality = 0x12345678, trainerId = 0x00010001, category = 2, rank = 3, saveIdx = 8, artist = false, fnv = 0x183FBBA8 },
|
|
{ name = "museum_beauty", species = 329, personality = 0x0BADBEEF, trainerId = 0x00020002, category = 3, rank = 3, saveIdx = 9, artist = false, fnv = 0x7CCDE6FC },
|
|
{ name = "museum_cute", species = 315, personality = 0x00000007, trainerId = 0x00030003, category = 7, rank = 3, saveIdx = 10, artist = false, fnv = 0xDA489FEC },
|
|
{ name = "museum_smart", species = 65, personality = 0x0000FF10, trainerId = 0x00040004, category = 11, rank = 3, saveIdx = 11, artist = false, fnv = 0x39AC0EEC },
|
|
{ name = "museum_tough", species = 68, personality = 0x00ABCDEF, trainerId = 0x00050005, category = 12, rank = 3, saveIdx = 12, artist = false, fnv = 0xDA6AB98D },
|
|
{ name = "hall_cute", species = 25, personality = 0x00000042, trainerId = 0x00060006, category = 2, rank = 1, saveIdx = 0, artist = false, fnv = 0x753BA90D },
|
|
{ name = "artist_cool", species = 394, personality = 0x7777AAAA, trainerId = 0x00070007, category = 1, rank = 3, saveIdx = 8, artist = true, fnv = 0x653DAC72 },
|
|
}
|
|
local bit = require("bit")
|
|
local function render(st)
|
|
local W = 240
|
|
local out = {}
|
|
for i = 0, W * 112 * 3 - 1 do out[i] = 0 end
|
|
local gfx, pal = st.gfx, st.framePalette
|
|
local tiles = math.floor(#gfx / 32)
|
|
for ty = 0, 13 do for tx = 0, 29 do
|
|
local e = st.map[ty * 32 + tx] or 0
|
|
local tile, hf, vf, bank = e % 1024, math.floor(e / 1024) % 2 == 1, math.floor(e / 2048) % 2 == 1, math.floor(e / 4096) % 16
|
|
for py = 0, 7 do for px = 0, 7 do
|
|
local sx, sy = hf and 7 - px or px, vf and 7 - py or py
|
|
local v = 0
|
|
if tile < tiles then
|
|
local b = gfx:byte(tile * 32 + sy * 4 + math.floor(sx / 2) + 1) or 0
|
|
v = sx % 2 == 0 and b % 16 or math.floor(b / 16)
|
|
end
|
|
if v ~= 0 then
|
|
local c = pal[bank * 16 + v + 1] or 0
|
|
local o = ((ty * 8 + py) * W + tx * 8 + px) * 3
|
|
out[o], out[o + 1], out[o + 2] = c % 32, math.floor(c / 32) % 32, math.floor(c / 1024) % 32
|
|
end
|
|
end end
|
|
end end
|
|
for i = 0, 64 * 64 - 1 do
|
|
local v = st.mon.indices[i]
|
|
if v ~= 0 then
|
|
local c = st.mon.palette[v] or 0
|
|
local o = ((24 + math.floor(i / 64)) * W + 88 + i % 64) * 3
|
|
out[o], out[o + 1], out[o + 2] = c % 32, math.floor(c / 32) % 32, math.floor(c / 1024) % 32
|
|
end
|
|
end
|
|
local h = 0x811C9DC5
|
|
for i = 0, W * 112 * 3 - 1 do
|
|
h = bit.bxor(h, out[i]) % 4294967296
|
|
h = require("src.core.game3.rng").mulU32(h, 0x01000193)
|
|
end
|
|
return h
|
|
end
|
|
for _, cs in ipairs(CASES) do
|
|
local st = Painting.build({ winner = { species = cs.species, personality = cs.personality, trainerId = cs.trainerId,
|
|
contestCategory = cs.category, contestRank = cs.rank, monName = "TESTMON", trainerName = "NICK" },
|
|
saveIdx = cs.saveIdx, isForArtist = cs.artist })
|
|
eq(string.format("%08X", render(st)), string.format("%08X", cs.fnv), cs.name .. " frame + processed mon match the ROM")
|
|
end
|
|
local st = Painting.build({ winner = { species = 397, personality = 0x12345678, trainerId = 0x00010001, contestCategory = 2,
|
|
contestRank = 3, monName = "TESTMON", trainerName = "NICK" }, saveIdx = 8 })
|
|
eq(st.caption, "The marvelous, wonderful, and\nvery great TESTMON", "museum caption 3 of the cool set")
|
|
end
|
|
|
|
T.finish()
|