mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-03 08:07:07 -04:00
@@ -34,25 +34,22 @@ end
|
||||
|
||||
M.VIRIDIAN_CITY = {
|
||||
talk = {
|
||||
-- ViridianCityGambler1Text (scripts/ViridianCity.asm): normally
|
||||
-- comments that the gym is "always closed"; once the 7th badge is
|
||||
-- earned (badges == ~EARTHBADGE) but Giovanni hasn't been beaten
|
||||
-- yet, he instead says the gym leader returned.
|
||||
-- pokered/scripts/ViridianCity.asm:150
|
||||
-- pokeyellow/scripts/ViridianCity_2.asm:10
|
||||
TEXT_VIRIDIANCITY_GAMBLER1 = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
local sevenBadges = game.save.inventory and
|
||||
game.save.inventory.BOULDERBADGE and game.save.inventory.CASCADEBADGE
|
||||
and game.save.inventory.THUNDERBADGE and game.save.inventory.RAINBOWBADGE
|
||||
and game.save.inventory.SOULBADGE and game.save.inventory.MARSHBADGE
|
||||
and game.save.inventory.VOLCANOBADGE
|
||||
-- (pokered checks EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI; the port's flag for
|
||||
-- that win is EVENT_BEAT_GIOVANNI, set by victories.lua OPP_GIOVANNI#3)
|
||||
if sevenBadges and not (game.save.flags and game.save.flags.EVENT_BEAT_GIOVANNI) then
|
||||
push(game, t._ViridianCityGambler1GymLeaderReturnedText
|
||||
or "VIRIDIAN GYM's\nLEADER returned!", done)
|
||||
and game.save.inventory.VOLCANOBADGE and not game.save.inventory.EARTHBADGE
|
||||
local flags = game.save.flags or {}
|
||||
local beatGiovanni = flags.EVENT_BEAT_GIOVANNI
|
||||
or flags.EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI
|
||||
if sevenBadges or beatGiovanni then
|
||||
push(game, t._ViridianCityGambler1GymLeaderReturnedText, done)
|
||||
else
|
||||
push(game, t._ViridianCityGambler1GymAlwaysClosedText
|
||||
or "This POKéMON GYM\nis always closed.\nI wonder who the\nLEADER is?", done)
|
||||
push(game, t._ViridianCityGambler1GymAlwaysClosedText, done)
|
||||
end
|
||||
end,
|
||||
|
||||
|
||||
@@ -1297,6 +1297,8 @@ local function disobedient(M)
|
||||
local species = tonumber(user.species)
|
||||
local ob = 0
|
||||
if not ((species == 151 or species == 410) and mon.fatefulEncounter == false) then
|
||||
-- pokeemerald/src/battle_util.c:3924
|
||||
if BattleProfile.isRse(st) and Kinds.has(st, "frontier") then return nil end
|
||||
if not Engine.isTradedMon(st, mon) or Engine.hasBadge(st, 8) then return nil end
|
||||
ob = 10
|
||||
if Engine.hasBadge(st, 2) then ob = 30 end
|
||||
|
||||
@@ -402,6 +402,10 @@ local function finish(result)
|
||||
require("src.core.game3.battle.link_guard").disarm()
|
||||
Battle._active = false
|
||||
Battle._phase = nil
|
||||
Ui.clearCaughtDexScene()
|
||||
Battle._catchDexReturn = nil
|
||||
if Battle._rseDex then require("src.ui.game3.rse.pokedex").reset() end
|
||||
Battle._rseDex = nil
|
||||
Battle._residualEvents = nil
|
||||
Battle._residualIndex = 1
|
||||
Battle._residualStepState = nil
|
||||
@@ -3551,12 +3555,15 @@ local function finish_catch_flow(catchRes, ename, nicknamed)
|
||||
return
|
||||
end
|
||||
end
|
||||
Ui.clearCaughtDexScene()
|
||||
Battle._actions = {}
|
||||
Battle._pendingEnd = "catch"
|
||||
Battle._phase = "ending"
|
||||
end
|
||||
|
||||
local function start_post_catch_flow(catchRes)
|
||||
Ui.clearCaughtDexScene()
|
||||
Battle._catchDexReturn = nil
|
||||
-- pokefirered/data/battle_scripts_2.s:99 BattleScript_OldMan_Pokedude_CaughtMessage
|
||||
if Battle._headless or (Battle._st and (Battle._st.oldManTutorial or Battle._st.pokedude
|
||||
or Wally.active(Battle._st))) then
|
||||
@@ -3580,14 +3587,17 @@ local function start_post_catch_flow(catchRes)
|
||||
or Pokemon.name(sp)
|
||||
local gender = (mon and (mon.gender or (mon.isFemale and 1))) or (enemy and enemy.gender) or 0
|
||||
local personality = (mon and mon.personality) or 0
|
||||
local caughtState = Battle._st
|
||||
|
||||
local function prompt_nickname()
|
||||
Battle._phase = "catch_nickname_prompt"
|
||||
-- pokefirered/src/battle_message.c:477
|
||||
Ui.askYesNo(BattleText.get("STRINGID_GIVENICKNAMECAPTURED", { opponentMon1 = ename }), function(yes)
|
||||
if not Battle._active or Battle._st ~= caughtState or Battle._phase ~= "catch_nickname_prompt" then return end
|
||||
if yes then
|
||||
local okN, Naming = pcall(require, "src.ui.game3.naming")
|
||||
if okN and Naming and Naming.open then
|
||||
Ui.clearCaughtDexScene()
|
||||
Battle._phase = "catch_naming"
|
||||
Naming.open({
|
||||
template = "CAUGHT_MON",
|
||||
@@ -3603,6 +3613,7 @@ local function start_post_catch_flow(catchRes)
|
||||
-- was 141px wide and spilled over the frame's right edge.
|
||||
title = Naming.monTitle(Pokemon.name(sp)),
|
||||
onDone = function(nick)
|
||||
if not Battle._active or Battle._st ~= caughtState or Battle._phase ~= "catch_naming" then return end
|
||||
if nick and nick ~= "" and nick ~= ename then
|
||||
if mon then mon.nickname = nick end
|
||||
end
|
||||
@@ -3622,12 +3633,20 @@ local function start_post_catch_flow(catchRes)
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
Battle._phase = "pokedex_reg"
|
||||
Battle._rseDex = true
|
||||
-- pokeemerald/src/battle_script_commands.c:10115
|
||||
local dexMon = (enemy and enemy.mon) or mon
|
||||
require("src.ui.game3.rse.pokedex").showCaughtMon(sp, {
|
||||
session = Runtime and Runtime.getSession and Runtime.getSession(),
|
||||
personality = personality,
|
||||
onDone = function()
|
||||
personality = dexMon and dexMon.personality or personality,
|
||||
otId = dexMon and dexMon.otId,
|
||||
otSecretId = dexMon and dexMon.otSecretId,
|
||||
shiny = Pokemon.isShiny(dexMon),
|
||||
onDone = function(caught)
|
||||
if not Battle._active or Battle._st ~= caughtState or Battle._phase ~= "pokedex_reg" then return end
|
||||
Battle._rseDex = nil
|
||||
prompt_nickname()
|
||||
Ui.beginCaughtDexScene(caught)
|
||||
Battle._phase = "catch_dex_return"
|
||||
Battle._catchDexReturn = prompt_nickname
|
||||
end,
|
||||
})
|
||||
return
|
||||
@@ -3641,7 +3660,19 @@ local function start_post_catch_flow(catchRes)
|
||||
Pokedex.showRegistration(sp, {
|
||||
session = session,
|
||||
onDone = function()
|
||||
prompt_nickname()
|
||||
if not Battle._active or Battle._st ~= caughtState or Battle._phase ~= "pokedex_reg" then return end
|
||||
-- pokefirered/src/battle_script_commands.c:9699
|
||||
local target = (enemy and enemy.mon) or mon
|
||||
local pid = target and target.personality or personality
|
||||
local shiny = Pokemon.isShiny(target)
|
||||
local pic = Pokemon.frontPic(Pokemon.picSpecies(sp, pid), nil, shiny, pid)
|
||||
Ui.beginCaughtDexScene({
|
||||
family = "frlg", personality = pid,
|
||||
otId = target and target.otId, otSecretId = target and target.otSecretId,
|
||||
shiny = shiny, sprite = { img = pic.image, x = 120, y = 64 },
|
||||
})
|
||||
Battle._phase = "catch_dex_return"
|
||||
Battle._catchDexReturn = prompt_nickname
|
||||
end,
|
||||
})
|
||||
return
|
||||
@@ -4030,6 +4061,15 @@ update_body = function(dt, game)
|
||||
return
|
||||
end
|
||||
|
||||
if Battle._phase == "catch_dex_return" then
|
||||
if Ui.updateCaughtDexScene() then
|
||||
local cb = Battle._catchDexReturn
|
||||
Battle._catchDexReturn = nil
|
||||
if cb then cb() end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if Battle._phase == "catch_naming" then
|
||||
return
|
||||
end
|
||||
@@ -4196,6 +4236,10 @@ end
|
||||
|
||||
-- pokefirered/src/main.c:480
|
||||
function Battle.reset()
|
||||
Ui.clearCaughtDexScene()
|
||||
Battle._catchDexReturn = nil
|
||||
if Battle._rseDex then require("src.ui.game3.rse.pokedex").reset() end
|
||||
Battle._rseDex = nil
|
||||
stop_low_hp_song()
|
||||
require("src.core.game3.battle.link_guard").disarm()
|
||||
Battle._active = false
|
||||
|
||||
@@ -204,6 +204,7 @@ end
|
||||
|
||||
function Ui.reset(opts)
|
||||
opts = opts or {}
|
||||
Ui._caughtDexScene = nil
|
||||
Ui._timed = nil
|
||||
Ui._linger = false
|
||||
Ui._queue = {}
|
||||
@@ -2551,11 +2552,55 @@ local function draw_party_bars(stage)
|
||||
end
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_script_commands.c:10131
|
||||
function Ui.beginCaughtDexScene(caught)
|
||||
local Pal = require("src.core.game3.pal_fade")
|
||||
caught.pal = Pal.new()
|
||||
-- pokefirered/src/battle_script_commands.c:9709
|
||||
caught.pal:beginFade(caught.family == "frlg" and 0x1FFFF or Pal.BG, 0, 16, 0, Pal.BLACK)
|
||||
Ui._caughtDexScene = caught
|
||||
end
|
||||
|
||||
-- pokeemerald/src/pokedex.c:4079
|
||||
function Ui.updateCaughtDexScene()
|
||||
local c = Ui._caughtDexScene
|
||||
if not c then return true end
|
||||
local spr = c.sprite
|
||||
local centerY = c.family == "frlg" and 64 or 80
|
||||
if spr.x < 120 then spr.x = math.min(120, spr.x + 2) end
|
||||
if spr.x > 120 then spr.x = math.max(120, spr.x - 2) end
|
||||
if spr.y < centerY then spr.y = math.min(centerY, spr.y + 1) end
|
||||
if spr.y > centerY then spr.y = math.max(centerY, spr.y - 1) end
|
||||
c.pal:updateFade()
|
||||
return not c.pal:fadeActive()
|
||||
end
|
||||
|
||||
function Ui.clearCaughtDexScene()
|
||||
Ui._caughtDexScene = nil
|
||||
end
|
||||
|
||||
function Ui.draw(w, h)
|
||||
if not (love and love.graphics) then return end
|
||||
w = w or Display.W
|
||||
h = h or Display.H
|
||||
|
||||
local caught = Ui._caughtDexScene
|
||||
if caught then
|
||||
-- pokeemerald/src/battle_script_commands.c:10133
|
||||
local Fx = require("src.core.game3.gba_fx")
|
||||
Fx.draw(function()
|
||||
BattleChrome.drawPostDexBg(BattleBg.sheetKey())
|
||||
BattleChrome.drawPanel("none")
|
||||
end, caught.pal:fx(0))
|
||||
local spr = caught.sprite
|
||||
Fx.draw(function()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(spr.img, spr.x + (spr.x2 or 0), spr.y + (spr.y2 or 0), 0, 1, spr.scaleY or 1, 32, 32)
|
||||
end, caught.pal:fx(16))
|
||||
if Choice and Choice.active and Choice.draw then Choice.draw() end
|
||||
return
|
||||
end
|
||||
|
||||
local st = Ui._st
|
||||
local stage = Anim.stage and Anim.stage()
|
||||
|
||||
|
||||
@@ -183,6 +183,8 @@ local function objectEventCollision(game, x, y, dir, beh)
|
||||
local P = player()
|
||||
if Collision.ledgeLanding(game, P.cellX, P.cellY, dir) then return C.LEDGE_JUMP end
|
||||
if not Collision.inBounds(x, y) then return C.NONE end
|
||||
-- pokeemerald/src/event_object_movement.c:4663
|
||||
if Collision.isWarpDoor(beh) then return C.IMPASSABLE end
|
||||
local ok, why = Collision.canEnter(game, x, y, {
|
||||
fromX = P.cellX, fromY = P.cellY, dir = dir, surfing = P.surfing, elevation = P.currentElevation,
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ function Arena.init(sess)
|
||||
if bit.band(tonumber(f.winStreakActiveFlags) or 0, streakFlag(sess)) == 0 then
|
||||
Util.set1(f.arenaWinStreaks, lvl(sess), 0)
|
||||
end
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF, x = sess.x, y = sess.y }
|
||||
sess.frontierOpponentA = 0
|
||||
end
|
||||
|
||||
@@ -74,10 +74,11 @@ end
|
||||
-- pokeemerald/src/battle_arena.c:731
|
||||
function Arena.save(ctx, sess)
|
||||
local f = Util.frontier(sess)
|
||||
f.challengeStatus = Rse.specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.saveChallenge(sess, "VAR_TEMP_CHALLENGE_STATUS", function()
|
||||
f.challengeStatus = Rse.specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_arena.c:739
|
||||
|
||||
@@ -148,7 +148,7 @@ function Dome.init(sess)
|
||||
if bit.band(tonumber(f.winStreakActiveFlags) or 0, streakFlag(mode, lvl)) == 0 then
|
||||
Util.set2(f.domeWinStreaks, mode, lvl, 0)
|
||||
end
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF, x = sess.x, y = sess.y }
|
||||
sess.frontierOpponentA = 0
|
||||
end
|
||||
|
||||
@@ -639,10 +639,11 @@ end
|
||||
-- pokeemerald/src/battle_dome.c:3020
|
||||
function Dome.save(ctx, sess)
|
||||
local f = Util.frontier(sess)
|
||||
f.challengeStatus = Rse.specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.saveChallenge(sess, "VAR_TEMP_CHALLENGE_STATUS", function()
|
||||
f.challengeStatus = Rse.specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_dome.c:5415
|
||||
|
||||
@@ -109,7 +109,7 @@ function Factory.init(sess)
|
||||
local r = rentals(sess)
|
||||
for i = 1, Factory.RENTAL_COUNT do r[i].monId = Factory.NO_MON end
|
||||
sess.frontierTempParty = { Factory.NO_MON, Factory.NO_MON, Factory.NO_MON }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF, x = sess.x, y = sess.y }
|
||||
sess.frontierOpponentA = 0
|
||||
end
|
||||
|
||||
@@ -155,10 +155,11 @@ end
|
||||
-- pokeemerald/src/battle_factory.c:269
|
||||
function Factory.save(ctx, sess)
|
||||
local f = Util.frontier(sess)
|
||||
f.challengeStatus = specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.saveChallenge(sess, "VAR_TEMP_CHALLENGE_STATUS", function()
|
||||
f.challengeStatus = specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_factory.c:298
|
||||
|
||||
@@ -48,7 +48,7 @@ function Palace.init(sess)
|
||||
if bit.band(tonumber(f.winStreakActiveFlags) or 0, streakFlag(mode, lvl)) == 0 then
|
||||
Util.set2(f.palaceWinStreaks, mode, lvl, 0)
|
||||
end
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF, x = sess.x, y = sess.y }
|
||||
sess.frontierOpponentA = 0
|
||||
end
|
||||
|
||||
@@ -131,10 +131,11 @@ end
|
||||
-- pokeemerald/src/battle_palace.c:182
|
||||
function Palace.save(ctx, sess)
|
||||
local f = Util.frontier(sess)
|
||||
f.challengeStatus = Rse.specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.saveChallenge(sess, "VAR_TEMP_CHALLENGE_STATUS", function()
|
||||
f.challengeStatus = Rse.specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_palace.c:190
|
||||
|
||||
@@ -207,7 +207,7 @@ function Pike.save(ctx, sess)
|
||||
f.challengeStatus = specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_CHALLENGE_STATUS", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.persist()
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_pike.c:727
|
||||
|
||||
@@ -27,7 +27,7 @@ function Tents.init(sess)
|
||||
f.challengeStatus = 0
|
||||
f.curChallengeBattleNum = 0
|
||||
f.challengePaused = 0
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF, x = sess.x, y = sess.y }
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_tent.c:118
|
||||
@@ -43,10 +43,11 @@ end
|
||||
-- pokeemerald/src/battle_tent.c:140
|
||||
function Tents.save(ctx, sess)
|
||||
local f = Util.frontier(sess)
|
||||
f.challengeStatus = specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_0", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.saveChallenge(sess, "VAR_TEMP_0", function()
|
||||
f.challengeStatus = specialVar(ctx, Util.VAR_0x8005)
|
||||
Rse.setVar("VAR_TEMP_0", 0, sess)
|
||||
f.challengePaused = 1
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_tent.c:148
|
||||
|
||||
@@ -58,7 +58,7 @@ function Tower.init(sess)
|
||||
if bit.band(tonumber(f.winStreakActiveFlags) or 0, streakFlag(mode, lvl)) == 0 then
|
||||
Util.set2(f.towerWinStreaks, mode, lvl, 0)
|
||||
end
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF }
|
||||
sess.dynamicWarp = { map = sess.map, warpId = 0xFF, x = sess.x, y = sess.y }
|
||||
sess.frontierOpponentA = 0
|
||||
end
|
||||
|
||||
@@ -262,16 +262,17 @@ end
|
||||
-- pokeemerald/src/battle_tower.c:2194
|
||||
function Tower.save(ctx, sess)
|
||||
local f = Util.frontier(sess)
|
||||
local mode, lvl = modeLvl(sess)
|
||||
local challengeNum = math.floor(Util.get2(f.towerWinStreaks, mode, lvl) / D.STAGES_PER_CHALLENGE)
|
||||
local status = specialVar(ctx, Util.VAR_0x8005)
|
||||
if status == 0 and (challengeNum > 1 or (tonumber(f.curChallengeBattleNum) or 0) ~= 0) then
|
||||
Tower.saveBattleTowerRecord(sess)
|
||||
end
|
||||
f.challengeStatus = status
|
||||
Rse.setVar("VAR_TEMP_0", 0, sess)
|
||||
f.challengePaused = 1
|
||||
return Util.saveGameFrontier(sess)
|
||||
return Util.saveChallenge(sess, "VAR_TEMP_0", function()
|
||||
local mode, lvl = modeLvl(sess)
|
||||
local challengeNum = math.floor(Util.get2(f.towerWinStreaks, mode, lvl) / D.STAGES_PER_CHALLENGE)
|
||||
local status = specialVar(ctx, Util.VAR_0x8005)
|
||||
if status == 0 and (challengeNum > 1 or (tonumber(f.curChallengeBattleNum) or 0) ~= 0) then
|
||||
Tower.saveBattleTowerRecord(sess)
|
||||
end
|
||||
f.challengeStatus = status
|
||||
Rse.setVar("VAR_TEMP_0", 0, sess)
|
||||
f.challengePaused = 1
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_tower.c:1936
|
||||
|
||||
@@ -200,7 +200,7 @@ function Util.newGame(sess)
|
||||
Util.clearRankingHallRecords(sess)
|
||||
end
|
||||
|
||||
Util.SAVE_FIELDS = { "frontier", "hallRecords1P", "hallRecords2P" }
|
||||
Util.SAVE_FIELDS = { "frontier", "hallRecords1P", "hallRecords2P", "savedPlayerParty" }
|
||||
|
||||
local okS, SaveSections = pcall(require, "src.core.game3.save_sections")
|
||||
if okS and SaveSections then
|
||||
@@ -786,14 +786,70 @@ end
|
||||
|
||||
-- pokeemerald/src/frontier_util.c:2421
|
||||
function Util.saveGameFrontier(sess)
|
||||
local warp = sess.dynamicWarp
|
||||
if type(warp) ~= "table" or type(warp.map) ~= "string" or not tonumber(warp.x) or not tonumber(warp.y) then
|
||||
return false, "The saved Frontier entrance is incomplete. The challenge was not saved."
|
||||
end
|
||||
if type(sess.savedPlayerParty) ~= "table" then
|
||||
return false, "The original party is unavailable. The challenge was not saved."
|
||||
end
|
||||
local party = {}
|
||||
for i, mon in ipairs(sess.party or {}) do party[i] = deepCopy(mon) end
|
||||
story().loadPlayerParty(sess)
|
||||
sess.continueGameWarp = deepCopy(sess.dynamicWarp)
|
||||
sess.specialSaveWarpFlags = bit.bor(tonumber(sess.specialSaveWarpFlags) or 0, 1)
|
||||
local f = Util.frontier(sess)
|
||||
local savedGame = f.savedGame
|
||||
f.savedGame = 1
|
||||
local ok = Util.persist()
|
||||
local ok, written = pcall(Util.persist)
|
||||
sess.specialSaveWarpFlags = bit.band(tonumber(sess.specialSaveWarpFlags) or 0, bit.bnot(1))
|
||||
sess.party = party
|
||||
return ok
|
||||
if not ok or written ~= true then
|
||||
f.savedGame = savedGame
|
||||
return false, "The challenge could not be saved."
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Util.saveChallenge(sess, tempName, prepare)
|
||||
local f = Util.frontier(sess)
|
||||
local before = deepCopy(f)
|
||||
local id = assert(Rse.varId(tempName, sess))
|
||||
local store = Rse.store()
|
||||
local vars = store and store.vars
|
||||
local temp = vars and vars[id]
|
||||
prepare()
|
||||
local ok, err = Util.saveGameFrontier(sess)
|
||||
if not ok then
|
||||
for k in pairs(f) do f[k] = nil end
|
||||
for k, v in pairs(before) do f[k] = v end
|
||||
if vars then vars[id] = temp end
|
||||
end
|
||||
return ok, err
|
||||
end
|
||||
|
||||
function Util.saveFromNative(ctx, adapters, sess, save)
|
||||
local ok, err = save(ctx, sess)
|
||||
if ok then return false end
|
||||
local done = false
|
||||
if adapters and adapters.closeMessage then adapters.closeMessage() end
|
||||
ctx.messageOpen = true
|
||||
ctx.mode, ctx.status = "native", "waiting"
|
||||
ctx.nativePoll = function()
|
||||
if not done then return false end
|
||||
if adapters and adapters.closeMessage then adapters.closeMessage() end
|
||||
ctx.messageOpen = false
|
||||
ctx.pc, ctx.stack = nil, {}
|
||||
return true
|
||||
end
|
||||
if adapters and adapters.openMessageAsync then
|
||||
adapters.openMessageAsync(err, function() done = true end)
|
||||
else
|
||||
if adapters and adapters.openMessage then adapters.openMessage(err) end
|
||||
if adapters and adapters.waitButton then adapters.waitButton(function() done = true end)
|
||||
else done = true end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_tower.c:1956
|
||||
|
||||
@@ -16,7 +16,7 @@ function NativesArena.call(ctx, adapters)
|
||||
if id == F.INIT then A.init(s)
|
||||
elseif id == F.GET_DATA then A.getData(ctx, s)
|
||||
elseif id == F.SET_DATA then A.setData(ctx, s)
|
||||
elseif id == F.SAVE then A.save(ctx, s)
|
||||
elseif id == F.SAVE then return Util.saveFromNative(ctx, adapters, s, A.save)
|
||||
elseif id == F.SET_PRIZE then A.setPrize(s)
|
||||
elseif id == F.GIVE_PRIZE then A.givePrize(ctx, adapters, s)
|
||||
elseif id == F.GET_TRAINER_NAME then A.bufferOpponentName(ctx, adapters, s)
|
||||
|
||||
@@ -19,7 +19,9 @@ function NativesDome.call(ctx, adapters)
|
||||
local id = Rse.specialVar(ctx, Util.VAR_0x8004)
|
||||
if not s then return false end
|
||||
local F = Dome().FUNC
|
||||
if id == F.SHOW_OPPONENT_INFO then
|
||||
if id == F.SAVE then
|
||||
return Util.saveFromNative(ctx, adapters, s, Dome().save)
|
||||
elseif id == F.SHOW_OPPONENT_INFO then
|
||||
-- pokeemerald/src/battle_dome.c:3043
|
||||
local D = Dome()
|
||||
local tid = D.tournamentId(s, D.playerOpponentTrainerId(s))
|
||||
|
||||
@@ -11,7 +11,7 @@ local FUNCS = {}
|
||||
FUNCS[FN.INIT] = function(_, _, s) Factory.init(s) end
|
||||
FUNCS[FN.GET_DATA] = function(ctx, _, s) Factory.getData(ctx, s) end
|
||||
FUNCS[FN.SET_DATA] = function(ctx, _, s) Factory.setData(ctx, s) end
|
||||
FUNCS[FN.SAVE] = function(ctx, _, s) Factory.save(ctx, s) end
|
||||
FUNCS[FN.SAVE] = function(ctx, adapters, s) return Util.saveFromNative(ctx, adapters, s, Factory.save) end
|
||||
FUNCS[FN.NULL] = function() end
|
||||
FUNCS[FN.NULL2] = function() end
|
||||
FUNCS[FN.SELECT_RENT_MONS] = function(ctx, adapters, s) return Factory.selectScreen(ctx, adapters, s) end
|
||||
|
||||
@@ -20,7 +20,7 @@ function NativesPalace.call(ctx, adapters)
|
||||
elseif id == F.SET_OPPONENT then P.setOpponent(s)
|
||||
elseif id == F.GET_OPPONENT_INTRO then P.opponentIntro(ctx, adapters, s)
|
||||
elseif id == F.INCREMENT_STREAK then P.incrementStreak(s)
|
||||
elseif id == F.SAVE then P.save(ctx, s)
|
||||
elseif id == F.SAVE then return Util.saveFromNative(ctx, adapters, s, P.save)
|
||||
elseif id == F.SET_PRIZE then P.setPrize(s)
|
||||
elseif id == F.GIVE_PRIZE then P.givePrize(ctx, adapters, s)
|
||||
else
|
||||
|
||||
@@ -30,14 +30,14 @@ local function build()
|
||||
VERDANTURF[V.SET_OPPONENT_GFX] = function(_, _, s) Tents().setVerdanturfGfx(s) end
|
||||
-- pokeemerald/src/battle_tent.c:134
|
||||
VERDANTURF[V.GET_OPPONENT_INTRO] = function(ctx, adapters, s) Tents().opponentIntro(ctx, adapters, s, F.PALACE) end
|
||||
VERDANTURF[V.SAVE] = function(ctx, _, s) Tents().save(ctx, s) end
|
||||
VERDANTURF[V.SAVE] = function(ctx, adapters, s) return Util.saveFromNative(ctx, adapters, s, Tents().save) end
|
||||
VERDANTURF[V.SET_RANDOM_PRIZE] = function(_, _, s) Tents().setRandomPrize(s, "verdanturf") end
|
||||
VERDANTURF[V.GIVE_PRIZE] = function(ctx, adapters, s) Tents().givePrize(ctx, adapters, s, "verdanturf") end
|
||||
-- pokeemerald/src/battle_tent.c:172
|
||||
FALLARBOR[Fa.INIT] = function(_, _, s) Tents().init(s) end
|
||||
FALLARBOR[Fa.GET_PRIZE] = function(ctx, _, s) Tents().getPrize(ctx, s, "fallarbor") end
|
||||
FALLARBOR[Fa.SET_PRIZE] = function(ctx, _, s) Tents().setPrize(ctx, s, "fallarbor") end
|
||||
FALLARBOR[Fa.SAVE] = function(ctx, _, s) Tents().save(ctx, s) end
|
||||
FALLARBOR[Fa.SAVE] = function(ctx, adapters, s) return Util.saveFromNative(ctx, adapters, s, Tents().save) end
|
||||
FALLARBOR[Fa.SET_RANDOM_PRIZE] = function(_, _, s) Tents().setRandomPrize(s, "fallarbor") end
|
||||
FALLARBOR[Fa.GIVE_PRIZE] = function(ctx, adapters, s) Tents().givePrize(ctx, adapters, s, "fallarbor") end
|
||||
-- pokeemerald/src/battle_tent.c:203
|
||||
@@ -46,7 +46,7 @@ local function build()
|
||||
SLATEPORT[S.INIT] = function(_, _, s) Tents().init(s) end
|
||||
SLATEPORT[S.GET_PRIZE] = function(ctx, _, s) Tents().getPrize(ctx, s, "slateport") end
|
||||
SLATEPORT[S.SET_PRIZE] = function(ctx, _, s) Tents().setPrize(ctx, s, "slateport") end
|
||||
SLATEPORT[S.SAVE] = function(ctx, _, s) Tents().save(ctx, s) end
|
||||
SLATEPORT[S.SAVE] = function(ctx, adapters, s) return Util.saveFromNative(ctx, adapters, s, Tents().save) end
|
||||
SLATEPORT[S.SET_RANDOM_PRIZE] = function(_, _, s) Tents().setRandomPrize(s, "slateport") end
|
||||
SLATEPORT[S.GIVE_PRIZE] = function(ctx, adapters, s) Tents().givePrize(ctx, adapters, s, "slateport") end
|
||||
-- pokeemerald/src/battle_tent.c:272
|
||||
|
||||
@@ -25,7 +25,7 @@ local function build()
|
||||
-- pokeemerald/src/battle_tower.c:2769
|
||||
FUNCS[F.GIVE_RIBBONS] = function(ctx, _, s) Tower().giveRibbons(ctx, s) end
|
||||
-- pokeemerald/src/battle_tower.c:2194
|
||||
FUNCS[F.SAVE] = function(ctx, _, s) Tower().save(ctx, s) end
|
||||
FUNCS[F.SAVE] = function(ctx, adapters, s) return Util.saveFromNative(ctx, adapters, s, Tower().save) end
|
||||
-- pokeemerald/src/battle_tower.c:1936
|
||||
FUNCS[F.GET_OPPONENT_INTRO] = function(ctx, adapters, s) Tower().opponentIntro(ctx, adapters, s) end
|
||||
-- pokeemerald/src/battle_tower.c:2209
|
||||
@@ -57,8 +57,7 @@ function NativesTowerRse.call(ctx, adapters)
|
||||
Rse.missing("tower", "CallBattleTowerFunc " .. tostring(id), adapters and adapters.log)
|
||||
return false
|
||||
end
|
||||
fn(ctx, adapters, s)
|
||||
return false
|
||||
return fn(ctx, adapters, s) == true
|
||||
end
|
||||
|
||||
NativesTowerRse.FUNCS = FUNCS
|
||||
|
||||
@@ -699,6 +699,11 @@ do
|
||||
frlg[#frlg + 1] = string.format("%s%s_%d_female.png", hoenn, side, stars)
|
||||
end
|
||||
end
|
||||
-- pokefirered/src/battle_script_commands.c:9693
|
||||
local terrainKeys = require("src.import.gba.battle_chrome_extract").TERRAIN_KEYS
|
||||
for id = 0, 19 do
|
||||
frlg[#frlg + 1] = "data/generated/gba/pokemon/battle/terrain_" .. terrainKeys[id] .. "_post_dex.rgba"
|
||||
end
|
||||
end
|
||||
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.leafgreen = {}
|
||||
for i, path in ipairs(CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.firered) do
|
||||
@@ -760,6 +765,16 @@ function CacheContract.planFilesFor(version)
|
||||
end
|
||||
for _, path in ipairs(CacheContract.PLAN_CORE_FILES) do add(path) end
|
||||
for _, path in ipairs(Plans.required(Plans.of(version), CachePaths.CACHE_ROOT)) do add(path) end
|
||||
if version == "emerald" then
|
||||
local cfg = require("src.import.gba.games.emerald").BATTLE_UI
|
||||
local keys = require("src.import.gba.battle_chrome_extract").TERRAIN_KEYS
|
||||
-- pokeemerald/src/battle_script_commands.c:10133
|
||||
local function postDex(key)
|
||||
add(CachePaths.CACHE_ROOT .. "/pokemon/battle/terrain_" .. key .. "_post_dex.rgba")
|
||||
end
|
||||
for id = 0, cfg.terrain_count - 1 do postDex(keys[id]) end
|
||||
for _, scene in ipairs(cfg.scenes) do postDex(scene.key) end
|
||||
end
|
||||
composed[version] = files
|
||||
return files
|
||||
end
|
||||
|
||||
@@ -543,6 +543,10 @@ end
|
||||
local function maxWorkers(taskCount)
|
||||
local override = tonumber(os.getenv("POKEPORT_EXTRACT_WORKERS") or "")
|
||||
if override and override >= 1 then return math.min(taskCount, math.floor(override)) end
|
||||
if os.getenv("HANDHELD") == "1" or os.getenv("POKEPORT_HANDHELD") == "1"
|
||||
or os.getenv("PORTMASTER") == "1" then
|
||||
return math.min(taskCount, 1)
|
||||
end
|
||||
local osName = love.system and love.system.getOS and love.system.getOS() or ""
|
||||
if osName == "iOS" or osName == "Android" or osName == "NX" then
|
||||
return math.min(taskCount, 2)
|
||||
|
||||
@@ -471,13 +471,23 @@ local function bake_terrain(get, cache, root, key, tr)
|
||||
local tGfx = Lz77.decompress(get, tr.tiles)
|
||||
local tPal = Lz77.decompress(get, tr.pal)
|
||||
local tMap = Lz77.decompress(get, tr.tilemap)
|
||||
if byte_len(tMap) < 4096 then
|
||||
error("battle_chrome_extract: post-dex terrain map missing right screen for " .. key)
|
||||
end
|
||||
local rgba, trW, trH = bake_tilemap_rgba(tGfx, tPal, tMap, 32, 32, { transparent0 = false, bgPalBase = 2 })
|
||||
cache:write(root .. "/terrain_" .. key .. ".rgba", rgba)
|
||||
local bgRgba, enemyRgba, playerRgba = split_terrain_layers(rgba, tMap)
|
||||
cache:write(root .. "/terrain_bg_" .. key .. ".rgba", bgRgba)
|
||||
cache:write(root .. "/terrain_enemy_" .. key .. ".rgba", enemyRgba)
|
||||
cache:write(root .. "/terrain_player_" .. key .. ".rgba", playerRgba)
|
||||
return { w = trW, h = trH }
|
||||
-- pokeemerald/src/battle_script_commands.c:10133
|
||||
-- pokefirered/src/battle_script_commands.c:9693
|
||||
local postMap, bytes = {}, bytes_to_array(tMap)
|
||||
for i = 1, 2048 do postMap[i] = bytes[2048 + i] end
|
||||
local postRgba = bake_tilemap_rgba(tGfx, tPal, postMap, 32, 32, { transparent0 = false, bgPalBase = 2 })
|
||||
local postDexFile = "terrain_" .. key .. "_post_dex.rgba"
|
||||
cache:write(root .. "/" .. postDexFile, postRgba)
|
||||
return { w = trW, h = trH, postDexFile = postDexFile }
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_bg.c:859
|
||||
@@ -522,7 +532,7 @@ function BattleChromeExtract.runRse(rom, cache, opts, cfg)
|
||||
for _, key in ipairs(terrainOrder) do
|
||||
local m = terrainMeta[key]
|
||||
terrainLines[#terrainLines + 1] = string.format(
|
||||
' %s = { file = "terrain_%s.rgba", w = %d, h = %d },', key, key, m.w, m.h)
|
||||
' %s = { file = "terrain_%s.rgba", postDexFile = %q, w = %d, h = %d },', key, key, m.postDexFile, m.w, m.h)
|
||||
end
|
||||
for _, sc in ipairs(cfg.scenes or {}) do sceneKeys[#sceneKeys + 1] = string.format("%q", sc.key) end
|
||||
local envKeys = {}
|
||||
@@ -632,19 +642,7 @@ function BattleChromeExtract.run(rom, cache, opts)
|
||||
for _, t in ipairs(terrains) do
|
||||
local tr = t.cfg
|
||||
if tr then
|
||||
local tGfx = Lz77.decompress(get, tr.tiles)
|
||||
local tPal = Lz77.decompress(get, tr.pal)
|
||||
local tMap = Lz77.decompress(get, tr.tilemap)
|
||||
local rgba, trW, trH = bake_tilemap_rgba(tGfx, tPal, tMap, 32, 32, {
|
||||
transparent0 = false,
|
||||
bgPalBase = 2,
|
||||
})
|
||||
cache:write(root .. "/terrain_" .. t.key .. ".rgba", rgba)
|
||||
local bgRgba, enemyRgba, playerRgba = split_terrain_layers(rgba, tMap)
|
||||
cache:write(root .. "/terrain_bg_" .. t.key .. ".rgba", bgRgba)
|
||||
cache:write(root .. "/terrain_enemy_" .. t.key .. ".rgba", enemyRgba)
|
||||
cache:write(root .. "/terrain_player_" .. t.key .. ".rgba", playerRgba)
|
||||
terrainMeta[t.key] = { w = trW, h = trH }
|
||||
terrainMeta[t.key] = bake_terrain(get, cache, root, t.key, tr)
|
||||
terrainOrder[#terrainOrder + 1] = t.key
|
||||
end
|
||||
end
|
||||
@@ -654,7 +652,7 @@ function BattleChromeExtract.run(rom, cache, opts)
|
||||
for _, key in ipairs(terrainOrder) do
|
||||
local m = terrainMeta[key]
|
||||
terrainLines[#terrainLines + 1] = string.format(
|
||||
' %s = { file = "terrain_%s.rgba", w = %d, h = %d },', key, key, m.w, m.h)
|
||||
' %s = { file = "terrain_%s.rgba", postDexFile = %q, w = %d, h = %d },', key, key, m.postDexFile, m.w, m.h)
|
||||
end
|
||||
|
||||
-- Party summary bar (128×8); balls use elements tiles 66..69.
|
||||
|
||||
@@ -3,7 +3,7 @@ local VersionsRse = require("src.import.gba.versions_rse")
|
||||
local V = VersionsRse.new("emerald")
|
||||
local sym, count = V.sym, V.count
|
||||
|
||||
V.CACHE_VERSION = 13
|
||||
V.CACHE_VERSION = 14
|
||||
V.NATIVE_VERSION = 2
|
||||
V.OW_VERSION = 1
|
||||
V.ANIM_VERSION = 1
|
||||
|
||||
@@ -43,7 +43,7 @@ Versions.ROM_SIZE = 16777216
|
||||
-- v115: LeafGreen profiles, edition-specific title assets and Deoxys stats.
|
||||
-- v121: chrome/fonts/japanese_{normal,small}_* and japanese_widths.lua, the
|
||||
-- cart's Japanese fonts, for text a Japanese translation mod prints.
|
||||
Versions.CACHE_VERSION = 127
|
||||
Versions.CACHE_VERSION = 128
|
||||
Versions.NATIVE_VERSION = 6
|
||||
Versions.OW_VERSION = 3
|
||||
Versions.ANIM_VERSION = 1
|
||||
|
||||
+20
-8
@@ -174,11 +174,10 @@ function TextBox.new(game, text, onDone, opts)
|
||||
local marks
|
||||
text, marks = stripPauses(text)
|
||||
local save = game and game.save
|
||||
local gen1 = doubleLine
|
||||
and not (save and (save.generation == 2 or save.generation == 3
|
||||
self.fixedGen1Rows = not (save and (save.generation == 2 or save.generation == 3
|
||||
or save.version == "gold"))
|
||||
and require("src.core.GameVersion").generation() == 1
|
||||
self.pages = TextBox.paginate(text, self.maxCols, gen1)
|
||||
self.pages = TextBox.paginate(text, self.maxCols, doubleLine and self.fixedGen1Rows)
|
||||
-- opts.pauseSounds[i] is the sfx the i-th marker fires once its wait is
|
||||
-- over (text_asm SFX_SWAP, engine/pokemon/learn_move.asm:210-213)
|
||||
self.pauseSounds = opts and opts.pauseSounds
|
||||
@@ -190,6 +189,7 @@ function TextBox.new(game, text, onDone, opts)
|
||||
self.lineIndex = 1
|
||||
self.charIndex = 0
|
||||
self.shown = {} -- visible lines (max 2), each a list of glyph codes
|
||||
self.shownSource = {}
|
||||
self.waiting = false
|
||||
self.contAdvance = false
|
||||
self.done = false
|
||||
@@ -204,9 +204,12 @@ function TextBox.new(game, text, onDone, opts)
|
||||
-- twice, then the next line is written at TEXTBOX_INNERY + 2, i.e. the
|
||||
-- bottom row. Taking the first two would walk the text backwards the
|
||||
-- instant the prompt appears.
|
||||
for index = math.max(1, #page - 1), #page do
|
||||
self.shown[#self.shown + 1] = Font.encode(page[index])
|
||||
for index = 1, #page do
|
||||
self.lineIndex = index
|
||||
self:beginLine()
|
||||
self.shown[#self.shown] = self.codes
|
||||
end
|
||||
self.scrollPx = nil
|
||||
self.lineIndex = #page
|
||||
self.codes = self.shown[#self.shown] or {}
|
||||
self.charIndex = #self.codes
|
||||
@@ -381,19 +384,27 @@ end
|
||||
function TextBox:beginLine()
|
||||
self.charIndex = 0
|
||||
self.codes = Font.encode(self:currentLine())
|
||||
local conts = self.pages.contBefore and self.pages.contBefore[self.pageIndex]
|
||||
-- pokered/home/text.asm:262
|
||||
if self.fixedGen1Rows and conts and conts[self.lineIndex] and #self.shown == 1 then
|
||||
table.insert(self.shown, {})
|
||||
table.insert(self.shownSource, false)
|
||||
end
|
||||
if #self.shown >= 2 then
|
||||
table.remove(self.shown, 1)
|
||||
table.remove(self.shownSource, 1)
|
||||
self.scrollPx = 8 -- pixel scroll-up (ScrollTextUpOneLine)
|
||||
end
|
||||
table.insert(self.shown, {})
|
||||
table.insert(self.shownSource, self.lineIndex)
|
||||
end
|
||||
|
||||
function TextBox:visibleText()
|
||||
local page = self.pages[self.pageIndex]
|
||||
if not page then return nil end
|
||||
local out, count = {}, #(self.shown or {})
|
||||
for i = math.max(1, self.lineIndex - count + 1), self.lineIndex do
|
||||
if page[i] ~= nil then out[#out + 1] = page[i] end
|
||||
local out = {}
|
||||
for _, source in ipairs(self.shownSource) do
|
||||
out[#out + 1] = source and page[source] or ""
|
||||
end
|
||||
return #out > 0 and out or nil
|
||||
end
|
||||
@@ -636,6 +647,7 @@ function step(self, dt)
|
||||
self.holdFrames = Timing.TEXT_SCROLL_PAIR
|
||||
else
|
||||
self.shown = {}
|
||||
self.shownSource = {}
|
||||
self.pageIndex = self.pageIndex + 1
|
||||
self.lineIndex = 1
|
||||
self:beginLine()
|
||||
|
||||
@@ -130,6 +130,7 @@ function BattleChrome.terrain(key)
|
||||
bgImage = rgba_to_image(read_bytes(root .. "/terrain_bg_" .. key .. ".rgba"), 256, 160),
|
||||
enemyPlat = rgba_to_image(read_bytes(root .. "/terrain_enemy_" .. key .. ".rgba"), 256, 160),
|
||||
playerPlat = rgba_to_image(read_bytes(root .. "/terrain_player_" .. key .. ".rgba"), 256, 160),
|
||||
postDexImage = info.postDexFile and rgba_to_image(read_bytes(root .. "/" .. info.postDexFile), 256, 256) or nil,
|
||||
w = w,
|
||||
h = h,
|
||||
}
|
||||
@@ -137,6 +138,24 @@ function BattleChrome.terrain(key)
|
||||
return entry
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_script_commands.c:10131
|
||||
-- pokefirered/src/battle_script_commands.c:9691
|
||||
function BattleChrome.drawPostDexBg(key)
|
||||
local entry = BattleChrome.terrain(key)
|
||||
if not (entry and entry.postDexImage) then
|
||||
error("battle_chrome: missing ROM post-dex background for " .. tostring(key))
|
||||
end
|
||||
local qKey = "post_dex_" .. tostring(key)
|
||||
local q = BattleChrome._quads[qKey]
|
||||
if not q then
|
||||
q = love.graphics.newQuad(0, 0, 240, 112, 256, 256)
|
||||
BattleChrome._quads[qKey] = q
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(entry.postDexImage, q, 0, 0)
|
||||
return true
|
||||
end
|
||||
|
||||
local TERRAIN_MT = {
|
||||
__index = function(_, key) return BattleChrome.terrain(key) end,
|
||||
}
|
||||
|
||||
@@ -2004,9 +2004,15 @@ function tasks.caughtExit(s)
|
||||
if not s.pal:fadeActive() then
|
||||
local c = s.caught
|
||||
local Stack = require("src.ui.game3.stack")
|
||||
-- pokeemerald/src/pokedex.c:4069
|
||||
local species = Pokedex.speciesOf(c.dexNum)
|
||||
local pic = pokemon().frontPic(pokemon().picSpecies(species, c.personality or 0), nil, c.shiny,
|
||||
c.personality or 0)
|
||||
c.mon.img = assert(pic and pic.image, "caught mon palette missing from the cache")
|
||||
Pokedex.Host._s = nil
|
||||
Stack.pop(Pokedex.ID)
|
||||
if c.onDone then c.onDone() end
|
||||
if c.onDone then c.onDone({ sprite = c.mon, species = species, personality = c.personality,
|
||||
otId = c.otId, otSecretId = c.otSecretId, shiny = c.shiny }) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2384,7 +2390,8 @@ function Pokedex.showCaughtMon(species, opts)
|
||||
opts = opts or {}
|
||||
local s = newView(opts)
|
||||
local nat = pokemon().national(species) or species
|
||||
s.caught = { dexNum = nat, personality = opts.personality, onDone = opts.onDone }
|
||||
s.caught = { dexNum = nat, personality = opts.personality, otId = opts.otId, otSecretId = opts.otSecretId,
|
||||
shiny = opts.shiny, onDone = opts.onDone }
|
||||
s.fn = "caught"
|
||||
s.state = 0
|
||||
return push(s)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local F = require("tests.drivers.em_fa_util")
|
||||
local S = F.new("em_bike_doors_2637", os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_bike_doors_2637")
|
||||
|
||||
return function(game)
|
||||
if not F.boot(game, S) then return S.finish() end
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Bike = require("src.core.game3.bike")
|
||||
local Warp = require("src.core.game3.warp")
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local session = Runtime.getSession()
|
||||
if not S.check(session.version == "emerald", "driver runs Emerald") then return S.finish() end
|
||||
local function place(kind, x, y, facing)
|
||||
if Player.biking then Bike.rse(session).getOnOff(kind, session) end
|
||||
S.check(F.goTo(game, "EM_MAUVILLE_CITY", x, y, facing), "Mauville approach loaded")
|
||||
local item = F.give(kind == "mach" and "ITEM_MACH_BIKE" or "ITEM_ACRO_BIKE")
|
||||
F.useRegistered(game, item)
|
||||
S.check(Player.biking and Player.bikeType == kind, kind .. " mounted through registered item")
|
||||
end
|
||||
local function rideUntilLastFrame(dir, tx, ty)
|
||||
local ready = false
|
||||
F.holdKeys(game, { dir }, 180, function()
|
||||
ready = Player.moving and Player.targetX == tx and Player.targetY == ty
|
||||
and Player.progress >= Player.stepFrames - 1
|
||||
return ready
|
||||
end)
|
||||
return S.check(ready, "approach last frame " .. tx .. "," .. ty)
|
||||
end
|
||||
local function enterAndExit(kind, x, tag)
|
||||
local door = Collision.isDoorWarp(game, x, 5)
|
||||
if not S.check(door ~= nil, tag .. " has real animated door warp") then return false end
|
||||
F.holdKeys(game, { "up" }, 90, function() return F.map() ~= "EM_MAUVILLE_CITY" end)
|
||||
F.settle(game, 500)
|
||||
if not S.check(F.map() == door.destMap, tag .. " actual door enters destination") then return false end
|
||||
S.check(not Player.biking, tag .. " indoors dismounts " .. kind)
|
||||
S.check(S.still(game, tag .. "_inside.png"), tag .. " stable indoor screenshot")
|
||||
local def = game.data.maps[F.map()]
|
||||
local exit
|
||||
for _, w in ipairs(def.warps or {}) do
|
||||
if w.destMap == "EM_MAUVILLE_CITY" then exit = w break end
|
||||
end
|
||||
if not S.check(exit ~= nil, tag .. " has outdoor exit mat") then return false end
|
||||
F.holdKeys(game, { "down" }, 140, function() return F.map() == "EM_MAUVILLE_CITY" end)
|
||||
F.settle(game, 500)
|
||||
S.check(F.map() == "EM_MAUVILLE_CITY", tag .. " actual exit returns outdoors")
|
||||
return true
|
||||
end
|
||||
for _, kind in ipairs({ "mach", "acro" }) do
|
||||
for _, x in ipairs({ 22, 35 }) do
|
||||
local tag = kind .. "_" .. (x == 22 and "center" or "shop")
|
||||
place(kind, x - 1, 6, "right")
|
||||
if rideUntilLastFrame("right", x, 6) then
|
||||
F.holdKeys(game, { "up" }, 1)
|
||||
S.check(Player.cellX == x and Player.cellY == 6 and not Player.moving and not Warp.isBusy(),
|
||||
tag .. " changed heading blocks unclaimed door step")
|
||||
S.note(tag .. " facing=" .. Player.facing .. " running=" .. Bike.rse(session).state.running)
|
||||
U.wait(20)
|
||||
S.check(S.still(game, tag .. "_approach.png"), tag .. " stable approach screenshot")
|
||||
enterAndExit(kind, x, tag)
|
||||
end
|
||||
place(kind, x, 6, "up")
|
||||
enterAndExit(kind, x, tag .. "_straight")
|
||||
end
|
||||
end
|
||||
place("mach", 22, 8, "up")
|
||||
if rideUntilLastFrame("up", 22, 6) then
|
||||
U.wait(50)
|
||||
S.check(Player.cellY == 6 and not Player.moving and F.map() == "EM_MAUVILLE_CITY" and not Warp.isBusy(),
|
||||
"Mach coast stops south of Center without opening door")
|
||||
S.check(S.still(game, "mach_coast_stopped.png"), "Mach coast stable screenshot")
|
||||
enterAndExit("mach", 22, "mach_coast")
|
||||
end
|
||||
S.finish()
|
||||
end
|
||||
@@ -0,0 +1,174 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local F = require("tests.drivers.em_fa_util")
|
||||
local S = F.new("em_catch_prompt_2637", os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_catch_prompt_2637")
|
||||
|
||||
return function(game)
|
||||
if not F.boot(game, S) then return S.finish() end
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
local Bridge = require("src.core.game3.battle_bridge")
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local BagMenu = require("src.ui.game3.bag_menu")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
local Naming = require("src.ui.game3.naming")
|
||||
local RseDex = require("src.ui.game3.rse.pokedex")
|
||||
local Dex = require("src.core.game3.dex")
|
||||
local Storage = require("src.core.game3.storage")
|
||||
local session = Runtime.getSession()
|
||||
local version = os.getenv("POKEPORT_TEST_VERSION") or "emerald"
|
||||
if not S.check(session.version == version, "capture driver version " .. version) then return S.finish() end
|
||||
local C = require("src.core.game3.constants").of(version)
|
||||
local function species(name) return C:require("species", "SPECIES_" .. name) end
|
||||
local starter = species(version == "emerald" and "TREECKO" or "CHARMANDER")
|
||||
local target = species(version == "emerald" and "WURMPLE" or "MAGIKARP")
|
||||
local pcTarget = species(version == "emerald" and "POOCHYENA" or "PIDGEY")
|
||||
local master = C:require("items", "ITEM_MASTER_BALL")
|
||||
session.party, session.dex = {}, Dex.new()
|
||||
Party.giveMon(session, starter, 12)
|
||||
if version == "emerald" then
|
||||
F.setVar("VAR_LITTLEROOT_INTRO_STATE", 7)
|
||||
F.setVar("VAR_LITTLEROOT_TOWN_STATE", 4)
|
||||
F.setVar("VAR_ROUTE101_STATE", 3)
|
||||
F.setFlag("FLAG_RESCUED_BIRCH", true)
|
||||
end
|
||||
S.check(F.goTo(game, version == "emerald" and "EM_ROUTE101" or "FR_ROUTE1", 10, 10, "down"),
|
||||
"capture approach map loaded")
|
||||
|
||||
local function throw()
|
||||
Bag.add(session.bag, master, 1)
|
||||
U.tap(game, "right")
|
||||
U.wait(12)
|
||||
U.tap(game, "a")
|
||||
U.wait(60)
|
||||
for _ = 1, 6 do
|
||||
if BagMenu.currentPocket() == "POKE_BALLS" then break end
|
||||
U.tap(game, "right")
|
||||
U.wait(32)
|
||||
end
|
||||
if not S.check(BagMenu.currentPocket() == "POKE_BALLS", "capture bag reached Poke Balls") then return false end
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
return true
|
||||
end
|
||||
local function capture(sp, first, yes, pc, tag)
|
||||
local partyBefore = #session.party
|
||||
if not S.check(Bridge.startWild(Runtime._mod, game, { species = sp, level = 3 }, { fade = false }) == true,
|
||||
tag .. " wild encounter started") then return false end
|
||||
local thrown, dexSeen, promptSeen, typed, pcMessageSeen = false, false, false, false, false
|
||||
local name, snapshot, image
|
||||
for frame = 1, 12000 do
|
||||
if not Battle.isActive() then break end
|
||||
local phase = Battle._phase
|
||||
if phase == "command" and Ui._mode == "menu" and not thrown then
|
||||
thrown = true
|
||||
if not throw() then return false end
|
||||
elseif phase == "pokedex_reg" then
|
||||
dexSeen = true
|
||||
local ds = RseDex.active()
|
||||
if version == "emerald" and ds and ds.fn == "caughtInput" then
|
||||
snapshot, image = ds.caught.mon, ds.caught.mon.img
|
||||
S.check(S.still(game, tag .. "_dex.png"), tag .. " stable caught dex screenshot")
|
||||
U.tap(game, "a")
|
||||
elseif version ~= "emerald" and frame % 18 == 0 then
|
||||
U.tap(game, "a")
|
||||
else U.wait(1) end
|
||||
elseif phase == "catch_dex_return" then
|
||||
U.wait(1)
|
||||
elseif phase == "catch_nickname_prompt" and Choice.isOpen() then
|
||||
if not promptSeen then
|
||||
promptSeen = true
|
||||
local c = Ui._caughtDexScene
|
||||
if first then
|
||||
if version == "emerald" then
|
||||
S.check(c and c.sprite == snapshot, tag .. " prompt retains same dex sprite")
|
||||
end
|
||||
local cy = version == "emerald" and 80 or 64
|
||||
S.check(c and c.sprite.x == 120 and c.sprite.y == cy, tag .. " prompt source center120," .. cy)
|
||||
local battleMon = Battle._st.enemy.mon
|
||||
local expected = Pokemon.frontPic(Pokemon.picSpecies(sp, battleMon.personality), nil,
|
||||
Pokemon.isShiny(battleMon), battleMon.personality)
|
||||
S.check(c and c.sprite.img == expected.image, tag .. " prompt uses restored caught palette")
|
||||
else
|
||||
S.check(c == nil, tag .. " existing capture branch presentation preserved")
|
||||
end
|
||||
S.check(S.still(game, tag .. "_question.png"), tag .. " stable nickname question screenshot")
|
||||
U.tap(game, yes and "a" or "b")
|
||||
U.wait(24)
|
||||
else U.wait(1) end
|
||||
elseif Naming.isOpen() then
|
||||
local ns = Naming._state
|
||||
if ns and ns.pcPages then
|
||||
if Message.isTyping() then U.wait(1) else
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
end
|
||||
elseif not typed then
|
||||
typed = true
|
||||
U.wait(24)
|
||||
for _ = 1, 3 do U.tap(game, "a"); U.wait(12) end
|
||||
name = Naming._state and Naming._state.name
|
||||
S.check(name == "AAA", tag .. " three input taps type exactly AAA")
|
||||
S.check(Ui._caughtDexScene == nil, tag .. " fullscreen naming releases caught scene")
|
||||
S.check(S.still(game, tag .. "_naming.png"), tag .. " stable keyboard screenshot")
|
||||
U.tap(game, "start")
|
||||
U.wait(20)
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
else U.wait(1) end
|
||||
elseif phase == "catch_pc_msg" and not pcMessageSeen then
|
||||
pcMessageSeen = true
|
||||
S.check(Ui._caughtDexScene ~= nil == first, tag .. " PC No preserves first-capture scene")
|
||||
if first then
|
||||
local c = Ui._caughtDexScene
|
||||
S.check(c and c.sprite.x == 120 and c.sprite.y == (version == "emerald" and 80 or 64),
|
||||
tag .. " PC transfer retains source center")
|
||||
end
|
||||
for _ = 1, 180 do if not Message.isTyping() then break end; U.wait(1) end
|
||||
S.check(S.still(game, tag .. "_pc_message.png"), tag .. " stable PC transfer screenshot")
|
||||
elseif Message.isOpen() and not Message.isTyping() and frame % 18 == 0 then
|
||||
U.tap(game, "a")
|
||||
else U.wait(1) end
|
||||
end
|
||||
S.check(not Battle.isActive(), tag .. " capture completed")
|
||||
S.check(dexSeen == first, tag .. " dex registration branch correct")
|
||||
S.check(promptSeen, tag .. " nickname question reached")
|
||||
S.check(typed == yes, tag .. " naming Yes No route correct")
|
||||
S.check(pcMessageSeen == (pc and not yes), tag .. " PC transfer message route correct")
|
||||
S.check(Ui._caughtDexScene == nil and not Naming.isOpen(), tag .. " capture presentation cleared")
|
||||
local mon
|
||||
if pc then
|
||||
local storage = Storage.ensure(session)
|
||||
for _, box in ipairs(storage.boxes or {}) do
|
||||
for _, candidate in pairs(box.mons or {}) do
|
||||
if tonumber(candidate.species or candidate.speciesId) == sp and (not yes or candidate.nickname == name) then
|
||||
mon = candidate
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
mon = session.party[partyBefore + 1]
|
||||
end
|
||||
S.check(mon and tonumber(mon.species or mon.speciesId) == sp, tag .. " correct mon reached " .. (pc and "PC" or "party"))
|
||||
if yes then S.check(mon and mon.nickname == "AAA", tag .. " destination retains AAA nickname") end
|
||||
U.wait(40)
|
||||
return not Battle.isActive()
|
||||
end
|
||||
if not capture(target, true, true, false, version .. "_new_party_yes") then return S.finish() end
|
||||
if not capture(target, false, true, false, version .. "_repeat_party_yes") then return S.finish() end
|
||||
session.dex = Dex.new()
|
||||
if not capture(target, true, false, false, version .. "_new_party_no") then return S.finish() end
|
||||
if not capture(target, false, false, false, version .. "_repeat_party_no") then return S.finish() end
|
||||
while #session.party < 6 do Party.giveMon(session, starter, 12) end
|
||||
if not capture(pcTarget, true, true, true, version .. "_new_pc_yes") then return S.finish() end
|
||||
if not capture(pcTarget, false, true, true, version .. "_repeat_pc_yes") then return S.finish() end
|
||||
session.dex = Dex.new()
|
||||
if not capture(pcTarget, true, false, true, version .. "_new_pc_no") then return S.finish() end
|
||||
capture(pcTarget, false, false, true, version .. "_repeat_pc_no")
|
||||
S.finish()
|
||||
end
|
||||
@@ -0,0 +1,385 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local S = require("tests.drivers.em_story_util")
|
||||
|
||||
return function(game)
|
||||
local phase = os.getenv("EM_TENT_2638_PHASE") or "rest_setup"
|
||||
local route, step = phase:match("^(%a+)_(%a+)$")
|
||||
local d = S.new("em_tent_reload_2638_" .. phase, "/tmp/em_tent_reload_2638/" .. phase)
|
||||
local check = d.check
|
||||
local deadline = love.timer.getTime() + 25
|
||||
if not check((route == "rest" or route == "direct") and (step == "setup" or step == "resume"), "valid driver phase") then
|
||||
return d.finish()
|
||||
end
|
||||
local identity = os.getenv("POKEPORT_IDENTITY") or ""
|
||||
if not check(identity ~= "" and identity ~= "pokemon-love2d", "dedicated identity supplied") then return d.finish() end
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Schema = require("src.core.game3.save_schema_firered")
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local D = require("src.core.game3.rse.frontier.trainers")
|
||||
local Util = require("src.core.game3.rse.frontier.util")
|
||||
local Select = require("src.ui.game3.rse.factory_select")
|
||||
local SaveMenu = require("src.ui.game3.save_menu")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local LOBBY = "EM_SLATEPORT_CITY_BATTLE_TENT_LOBBY"
|
||||
local CORRIDOR = "EM_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR"
|
||||
local witnessFile = "2638_" .. route .. "_expected.lua"
|
||||
local saveFile = SaveData.saveFilename("emerald")
|
||||
local function readSave()
|
||||
local raw = love.filesystem.read(saveFile)
|
||||
return type(raw) == "string" and SaveData.decode(raw) or nil
|
||||
end
|
||||
local function copy(v)
|
||||
if type(v) ~= "table" then return v end
|
||||
local out = {}
|
||||
for k, x in pairs(v) do out[k] = copy(x) end
|
||||
return out
|
||||
end
|
||||
local function traits(party)
|
||||
local out = {}
|
||||
local keys = { "species", "speciesId", "speciesNumbering", "personality", "otId", "otSecretId", "otName", "ot",
|
||||
"nickname", "ivs", "evs", "moves", "pp", "maxPp", "ppBonusesPacked", "nature", "heldItem", "abilityNum",
|
||||
"exp", "level", "friendship", "pokeball", "metLocation", "markings" }
|
||||
for i, mon in ipairs(party or {}) do
|
||||
out[i] = {}
|
||||
for _, key in ipairs(keys) do out[i][key] = copy(mon[key]) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
local function fingerprint(party)
|
||||
local bytes = SaveData.encode(traits(party))
|
||||
return love.data.encode("string", "hex", love.data.hash("sha256", bytes))
|
||||
end
|
||||
local function sameParty(got, expected, label)
|
||||
local a, b = fingerprint(got), fingerprint(expected)
|
||||
return check(a == b and #(got or {}) == #(expected or {}), label .. " slots=" .. #(got or {}) .. " sha256=" .. a .. " expected=" .. b)
|
||||
end
|
||||
local function waitFor(pred, limit)
|
||||
for _ = 1, limit or 900 do
|
||||
if pred() then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
return pred()
|
||||
end
|
||||
local capture = d.shot
|
||||
function d.shot(game, file)
|
||||
local Fade = require("src.ui.game3.fade")
|
||||
local Transition = require("src.core.game3.battle_transition")
|
||||
local function visible()
|
||||
return game.phase == "field" and not Fade.isActive() and (Fade.t or 0) <= 0
|
||||
and not Transition.isActive()
|
||||
end
|
||||
if not check(waitFor(visible, 900), "capture ready " .. file) then
|
||||
d.note("capture phase=" .. tostring(game.phase) .. " fadeActive=" .. tostring(Fade.isActive())
|
||||
.. " fade=" .. tostring(Fade.t) .. " transition=" .. tostring(Transition.isActive()))
|
||||
return false
|
||||
end
|
||||
U.wait(2)
|
||||
if not check(waitFor(visible, 900), "capture remains visible " .. file) then return false end
|
||||
return check(capture(game, file), "capture written " .. file)
|
||||
end
|
||||
if not check(waitFor(function() return game.phase == "boot" and game.boot ~= nil end), "boot reached") then return d.finish() end
|
||||
local witness
|
||||
if step == "setup" then
|
||||
local session = Schema.newGame({ version = "emerald", name = "RELOAD", rngSeed = 0x2638 })
|
||||
session.map, session.x, session.y, session.facing = LOBBY, 6, 6, "up"
|
||||
require("src.core.game3.options").bind(session, game.options)
|
||||
game:adoptSave(session, true)
|
||||
game.sessionStartedAt = os.time()
|
||||
game:_enterField(session, "continue")
|
||||
U.wait(30)
|
||||
S.settle(game)
|
||||
session = Runtime.getSession()
|
||||
if not check(session and session.version == "emerald", "Emerald setup session") then return d.finish() end
|
||||
session.party = {}
|
||||
local names = { "SPECIES_SWAMPERT", "SPECIES_BLAZIKEN", "SPECIES_SCEPTILE", "SPECIES_METAGROSS", "SPECIES_SALAMENCE", "SPECIES_ZIGZAGOON" }
|
||||
for i, name in ipairs(names) do
|
||||
local m = D.createMon(S.species(name), 35 + i, 10 + i, 200000 + i * 37, session.trainerId,
|
||||
{ otName = session.name, moves = {} })
|
||||
D.setMoves(m, { S.move("MOVE_TACKLE"), S.move("MOVE_PROTECT") })
|
||||
D.setEvs(m, { i * 7, i * 9, i * 11, i * 13, i * 15, i * 17 })
|
||||
m.nickname, m.otSecretId, m.heldItem = "KEEP" .. i, session.secretId, S.item("ITEM_ORAN_BERRY")
|
||||
m.ivs.hp, m.pp[1], m.friendship, m.markings = i + 4, i + 2, 100 + i, i
|
||||
require("src.core.game3.save_mon").normalize(m)
|
||||
Schema.ensureMonBall(m)
|
||||
session.party[i] = m
|
||||
end
|
||||
witness = { original = copy(session.party), route = route, seed = tonumber(os.getenv("EM_TENT_2638_SEED")) or 185 }
|
||||
require("src.core.game3.encounters").onStep = function() return nil end
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local Tents = require("src.core.game3.rse.frontier.tents")
|
||||
local generateRentals = Tents.generateRentalMons
|
||||
Tents.generateRentalMons = function(sess)
|
||||
Rng.SeedRng(witness.seed)
|
||||
d.note("real rental generator seed=" .. witness.seed)
|
||||
return generateRentals(sess)
|
||||
end
|
||||
Rng.SeedRng(witness.seed)
|
||||
local ok, err = pcall(require("src.core.game3.map").load, nil, game, LOBBY, { x = 6, y = 6, facing = "up" })
|
||||
if not check(ok, "Slateport lobby loads " .. tostring(err or "")) then return d.finish() end
|
||||
U.wait(30)
|
||||
S.settle(game)
|
||||
d.shot(game, "01_original_party_lobby")
|
||||
sameParty(Runtime.getSession().party, witness.original, "2638 original hidden-stat fingerprint")
|
||||
else
|
||||
local raw = love.filesystem.read(witnessFile)
|
||||
witness = type(raw) == "string" and SaveData.decode(raw) or nil
|
||||
if not check(witness and witness.route == route and witness.original and witness.rentals, "previous process supplied exact witness") then
|
||||
return d.finish()
|
||||
end
|
||||
local disk = readSave()
|
||||
if not check(disk and disk.frontier and disk.frontier.curChallengeBattleNum == 1, "fresh process reads one-battle challenge save") then
|
||||
return d.finish()
|
||||
end
|
||||
sameParty(disk.savedPlayerParty, witness.original, "2638 persisted original hidden-stat fingerprint")
|
||||
if route == "rest" then
|
||||
sameParty(disk.party, witness.original, "2638 REST disk original fingerprint")
|
||||
check(disk.frontier.challengePaused == 1, "REST challenge paused on disk")
|
||||
local w = disk.continueGameWarp or {}
|
||||
check(w.map == LOBBY and w.x == witness.lobby.x and w.y == witness.lobby.y,
|
||||
"REST disk has exact captured lobby coordinates " .. tostring(w.x) .. "," .. tostring(w.y))
|
||||
check((tonumber(disk.specialSaveWarpFlags) or 0) % 2 == 1, "REST disk continue flag set")
|
||||
else
|
||||
sameParty(disk.party, witness.rentals, "2638 direct disk rental hidden-stat fingerprint")
|
||||
check(disk.map == CORRIDOR, "direct save retains actual corridor location")
|
||||
end
|
||||
game:_handleBootAction({ action = "continue" })
|
||||
if not check(waitFor(function() return game.phase == "field" and Runtime.getSession() ~= nil end, 1800),
|
||||
"fresh process CONTINUE reaches live field") then return d.finish() end
|
||||
if route == "rest" then
|
||||
check(S.mapNow() == LOBBY, "REST CONTINUE starts in original lobby")
|
||||
d.shot(game, "04_resume_lobby")
|
||||
sameParty(Runtime.getSession().party, witness.original, "2638 REST continued original fingerprint")
|
||||
else
|
||||
check(S.mapNow() == CORRIDOR, "direct CONTINUE starts in actual corridor")
|
||||
d.shot(game, "04_direct_continue_corridor")
|
||||
end
|
||||
end
|
||||
local session = Runtime.getSession()
|
||||
local f = Util.frontier(session)
|
||||
local battles, wins, selected = 0, 0, false
|
||||
local stopped, savedRest, captured, resumeRentals = false, false, false, false
|
||||
local aborted, lastProgress, lastStage = false, U.frame(), nil
|
||||
local function timeout(label)
|
||||
if aborted then return end
|
||||
aborted = true
|
||||
check(false, label .. " at " .. S.vmWhere())
|
||||
end
|
||||
local function driveSelect()
|
||||
local st = Select._st
|
||||
local function ready(phase)
|
||||
return st.phase == phase and (not st.picAnim or st.picAnim.finished)
|
||||
end
|
||||
local function rentalWait(pred, label, limit)
|
||||
if waitFor(pred, limit or 600) then return true end
|
||||
d.note("rental UI phase=" .. tostring(st.phase) .. " cursor=" .. tostring(st.cursor)
|
||||
.. " selected=" .. tostring(st.selectingState) .. " menu=" .. tostring(st.menuCursor)
|
||||
.. " animation=" .. tostring(st.picAnim and st.picAnim.step))
|
||||
timeout(label)
|
||||
return false
|
||||
end
|
||||
if not rentalWait(function() return ready("choose") end, "rental UI did not reach actionable selection") then return end
|
||||
check(true, "rental UI reaches selection")
|
||||
d.shot(game, "02_rental_selection")
|
||||
local rank = {}
|
||||
local Moves = require("src.core.game3.battle.moves")
|
||||
local Types = require("src.core.game3.battle.types")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
for i, row in ipairs(st.mons) do
|
||||
local m = row.monData
|
||||
local types, best, unsafe, names = Pokemon.types(m.species), 0, false, {}
|
||||
for _, id in ipairs(m.moves or {}) do
|
||||
local move = Moves.get(id)
|
||||
names[#names + 1] = tostring(id) .. "/" .. tostring(Pokemon.romMoveName(id))
|
||||
if id == 120 or id == 153 then unsafe = true end
|
||||
local stab = (move.type == types[1] or move.type == types[2]) and 1.5 or 1
|
||||
local stat = Types.isPhysical(move.type) and m.attack or m.spAtk
|
||||
best = math.max(best, (move.power or 0) * (stat or 0) * stab / 100)
|
||||
end
|
||||
local score = unsafe and -1 or best + ((m.maxHp or 0) + (m.defense or 0) + (m.spDef or 0) + (m.speed or 0)) / 5
|
||||
rank[i] = { i = i, score = score }
|
||||
d.note(string.format("rental %d %s species=%s hp=%s atk=%s spa=%s spe=%s score=%.2f moves=%s", i,
|
||||
tostring(m.name), tostring(m.species), tostring(m.maxHp), tostring(m.attack), tostring(m.spAtk), tostring(m.speed),
|
||||
score, table.concat(names, ",")))
|
||||
end
|
||||
table.sort(rank, function(a, b) if a.score == b.score then return a.i < b.i end return a.score > b.score end)
|
||||
for _, candidate in ipairs(rank) do
|
||||
if st.selectingState > 3 then break end
|
||||
if not rentalWait(function() return ready("choose") end, "rental UI did not finish closing previous picture") then return end
|
||||
for _ = 1, 6 do if st.cursor == candidate.i then break end U.tap(game, "right") U.wait(3) end
|
||||
if not rentalWait(function() return st.cursor == candidate.i end, "rental UI cursor did not reach candidate", 30) then return end
|
||||
U.tap(game, "a")
|
||||
if not rentalWait(function() return ready("menu") end, "rental UI did not open candidate menu") then return end
|
||||
U.tap(game, "down") U.wait(3)
|
||||
if not rentalWait(function() return st.menuCursor == 1 end, "rental UI did not highlight RENT", 30) then return end
|
||||
d.note("RENT candidate=" .. candidate.i .. " species=" .. tostring(st.mons[candidate.i].monData.species))
|
||||
U.tap(game, "a")
|
||||
if not rentalWait(function() return ready("choose") or ready("yesno") or st.phase == "invalid" end,
|
||||
"rental UI did not complete RENT selection") then return end
|
||||
if st.phase == "invalid" then
|
||||
U.tap(game, "a")
|
||||
if not rentalWait(function() return ready("choose") end, "rental UI did not dismiss invalid species") then return end
|
||||
end
|
||||
end
|
||||
if not rentalWait(function() return ready("yesno") end, "rental UI did not confirm three rentals") then return end
|
||||
check(st.selectingState == 4, "three rentals chosen with RENT")
|
||||
U.tap(game, "a")
|
||||
selected = rentalWait(function() return not Select.isOpen() end, "rental UI did not accept chosen party")
|
||||
check(selected, "rental UI confirms chosen party")
|
||||
end
|
||||
local function readyChoice()
|
||||
if not Choice.isOpen() then return false end
|
||||
for _, row in ipairs(Choice.options or {}) do
|
||||
local text = type(row) == "table" and (row.text or row.label or row[1]) or row
|
||||
if tostring(text):upper():find("REST", 1, true) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function watch()
|
||||
session = Runtime.getSession() or session
|
||||
f = Util.frontier(session)
|
||||
local stage = table.concat({ tostring(S.mapNow()), battles, wins, tostring(selected), tostring(captured), tostring(savedRest), tostring(resumeRentals) }, ":")
|
||||
if stage ~= lastStage then lastStage, lastProgress = stage, U.frame() end
|
||||
if love.timer.getTime() >= deadline then timeout("driver exceeded 25-second budget")
|
||||
elseif U.frame() - lastProgress > 12000 then timeout("driver made no challenge progress for 12000 frames") end
|
||||
if aborted then return end
|
||||
if step == "setup" and wins == 1 and f.curChallengeBattleNum == 1 and readyChoice() and not captured then
|
||||
captured = true
|
||||
witness.rentals, witness.rentalMons = copy(session.party), copy(f.rentalMons)
|
||||
witness.lobby = copy(session.dynamicWarp)
|
||||
check(love.filesystem.write(witnessFile, SaveData.encode(witness)), "write exact restart witness in dedicated identity")
|
||||
sameParty(session.savedPlayerParty, witness.original, "2638 before-pause original backup fingerprint")
|
||||
sameParty(session.party, witness.rentals, "2638 before-pause rental fingerprint")
|
||||
d.shot(game, "03_before_pause")
|
||||
if route == "direct" then
|
||||
check(game:saveGame() == true, "actual Game3 direct save succeeds during rentals")
|
||||
local disk = readSave()
|
||||
sameParty(disk and disk.savedPlayerParty, witness.original, "2638 direct save original backup fingerprint")
|
||||
sameParty(disk and disk.party, witness.rentals, "2638 direct save rental fingerprint")
|
||||
stopped = true
|
||||
end
|
||||
end
|
||||
if step == "setup" and route == "rest" and captured and not savedRest then
|
||||
local disk = readSave()
|
||||
savedRest = disk and disk.frontier and disk.frontier.challengePaused == 1 and disk.frontier.challengeStatus == 2
|
||||
and disk.frontier.curChallengeBattleNum == 1 and (tonumber(disk.specialSaveWarpFlags) or 0) % 2 == 1
|
||||
if savedRest then
|
||||
sameParty(disk.party, witness.original, "2638 REST written original fingerprint")
|
||||
sameParty(disk.savedPlayerParty, witness.original, "2638 REST written backup fingerprint")
|
||||
end
|
||||
end
|
||||
if step == "resume" and route == "rest" and not resumeRentals and S.mapNow() == CORRIDOR and readyChoice() then
|
||||
resumeRentals = true
|
||||
sameParty(session.party, witness.rentals, "2638 resumed same-rental hidden-stat fingerprint")
|
||||
sameParty(session.savedPlayerParty, witness.original, "2638 resumed original-backup hidden-stat fingerprint")
|
||||
check(f.curChallengeBattleNum == 1, "REST resume keeps completed battle count")
|
||||
end
|
||||
end
|
||||
local function choice()
|
||||
if stopped then return "b" end
|
||||
if readyChoice() then return step == "setup" and route == "rest" and "REST" or "GO ON" end
|
||||
if Choice.kind == "multi" then
|
||||
for _, row in ipairs(Choice.options or {}) do
|
||||
local text = type(row) == "table" and (row.text or row.label or row[1]) or row
|
||||
local label = tostring(text):upper()
|
||||
if label == "CHALLENGE" or label == "ENTER" then
|
||||
d.note("select attendant entry " .. label)
|
||||
return label
|
||||
end
|
||||
end
|
||||
end
|
||||
local page = tostring(Message.currentPage and Message.currentPage() or ""):upper()
|
||||
if page:find("SWAP", 1, true) or page:find("RECORD", 1, true) then return "no" end
|
||||
return "yes"
|
||||
end
|
||||
if step == "setup" then
|
||||
local attendant = S.objectByScript("SlateportCity_BattleTentLobby_EventScript_Attendant")
|
||||
if not check(attendant ~= nil, "real Slateport attendant found") then return d.finish() end
|
||||
S.talkTo(game, attendant)
|
||||
end
|
||||
local done = S.settle(game, {
|
||||
limit = 120000,
|
||||
until_ = function()
|
||||
if aborted then return true end
|
||||
if step == "setup" then
|
||||
return stopped or (savedRest and game.phase == "boot")
|
||||
or (battles > 0 and wins == 0 and S.mapNow() == LOBBY and not S.busy())
|
||||
end
|
||||
return battles >= 1 and S.mapNow() == LOBBY and not S.busy() and tonumber(f.challengeStatus) == 0
|
||||
end,
|
||||
choice = choice,
|
||||
watch = watch,
|
||||
onIdleUi = function()
|
||||
if aborted then return true end
|
||||
if Select.isOpen() then driveSelect() return true end
|
||||
if SaveMenu.isOpen() then U.tap(game, "a") U.wait(8) return true end
|
||||
return false
|
||||
end,
|
||||
onBattleFrame = function(st, phase)
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
if phase == "command" and Ui._mode == "moves" then
|
||||
local who = Ui.activeBattler and Ui.activeBattler() or 0
|
||||
local p, e = st.battlers and st.battlers[who], st.battlers and st.battlers[1]
|
||||
local tag = tostring(st.turn) .. ":" .. tostring(p and p.mon and p.mon.species)
|
||||
if tag ~= d.lastBattleTrace then
|
||||
d.lastBattleTrace = tag
|
||||
d.note("battle turn=" .. tostring(st.turn) .. " player=" .. tostring(p and p.mon and p.mon.species)
|
||||
.. " hp=" .. tostring(p and p.mon and p.mon.hp) .. " foe=" .. tostring(e and e.mon and e.mon.species)
|
||||
.. " foeHp=" .. tostring(e and e.mon and e.mon.hp))
|
||||
end
|
||||
end
|
||||
if love.timer.getTime() >= deadline then
|
||||
timeout("driver exceeded 25-second budget during battle")
|
||||
d.finish()
|
||||
end
|
||||
end,
|
||||
onBattleStart = function(st)
|
||||
battles = battles + 1
|
||||
check(st.kinds and st.kinds.frontier and #session.party == 3, "actual rental Frontier battle " .. battles)
|
||||
sameParty(session.savedPlayerParty, witness.original, "2638 battle original-backup fingerprint")
|
||||
S.pendingBattleShot = step == "setup" and "02_first_rental_battle" or "05_rental_battle_after_continue"
|
||||
end,
|
||||
onBattleEnd = function(result)
|
||||
if result == "win" then wins = wins + 1 end
|
||||
check(result == "win", "actual rental battle won " .. battles .. " result=" .. tostring(result))
|
||||
if result ~= "win" then
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
for i, text in ipairs(Ui.log and Ui.log() or {}) do
|
||||
if type(text) == "table" then
|
||||
local ok, plain = pcall(require("src.core.game3.scripting.text_ir").toAscii, text, {})
|
||||
text = ok and plain or tostring(text)
|
||||
end
|
||||
d.note("battle failure log " .. i .. " " .. tostring(text):gsub("\n", " "))
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
check(done and not aborted, "driver reaches requested phase endpoint")
|
||||
if aborted then return d.finish() end
|
||||
if step == "setup" then
|
||||
check(selected and wins == 1 and captured, "one real rental win precedes save/restart")
|
||||
if route == "rest" then check(savedRest and game.phase == "boot", "real REST script saves and resets to boot") end
|
||||
else
|
||||
session = Runtime.getSession()
|
||||
check(battles == 2 and wins == 2, "two remaining real rental battles completed after restart")
|
||||
if route == "rest" then check(resumeRentals, "real corridor ResumeChallenge rebuilt saved rentals") end
|
||||
check(S.mapNow() == LOBBY and tonumber(Util.frontier(session).challengeStatus) == 0, "challenge completed through real lobby script")
|
||||
sameParty(session.party, witness.original, "2638 final exact original-party hidden-stat fingerprint")
|
||||
local PartyMenu = require("src.ui.game3.party_menu")
|
||||
if not check(waitFor(function()
|
||||
local Fade = require("src.ui.game3.fade")
|
||||
return not Fade.isActive() and (Fade.t or 0) <= 0
|
||||
end, 900), "final lobby fade fully clears before party menu") then return d.finish() end
|
||||
PartyMenu.show(session.party, session.move_overlay, { session = session })
|
||||
if not check(waitFor(function()
|
||||
local top = require("src.ui.game3.stack").top()
|
||||
return PartyMenu.isOpen() and top and top.id == "party" and PartyMenu.mode == "list"
|
||||
and #PartyMenu._party == 6
|
||||
end, 300), "final six-slot party menu is ready") then return d.finish() end
|
||||
d.shot(game, "06_final_original_party")
|
||||
PartyMenu.close()
|
||||
check(game:saveGame() == true, "final original party saves through Game3")
|
||||
sameParty(readSave().party, witness.original, "2638 final disk original-party fingerprint")
|
||||
end
|
||||
return d.finish()
|
||||
end
|
||||
@@ -134,8 +134,7 @@ return function(game)
|
||||
and inVoiceover == (sh.voiceover == true) then
|
||||
sh.held = sh.held + 1
|
||||
if sh.held >= 6 then
|
||||
sh.taken = true
|
||||
U.shot(game, DIR .. "/" .. sh.file)
|
||||
sh.taken = U.still(game, DIR .. "/" .. sh.file)
|
||||
print("shot " .. sh.file)
|
||||
end
|
||||
else
|
||||
@@ -162,10 +161,13 @@ return function(game)
|
||||
if dim < lastDim and dim > 0 and not (Message.isOpen() and Message.frameKind() == "voiceover") then
|
||||
closedEarly = true
|
||||
end
|
||||
if not undimShot and Message.isHeld and Message.isHeld() and dim > 0 and dim < 0.5 then
|
||||
undimShot = true
|
||||
U.shot(game, DIR .. "/2310_09_oak_voiceover_undim.png")
|
||||
if not undimShot and Message.isHeld and Message.isHeld()
|
||||
and Message.isOpen() and Message.frameKind() == "voiceover"
|
||||
and dim < lastDim and dim > 0 and dim < 0.5 then
|
||||
undimShot = U.still(game, DIR .. "/2310_09_oak_voiceover_undim.png")
|
||||
print("shot 2310_09_oak_voiceover_undim.png")
|
||||
print("undim capture dim=" .. tostring(dim) .. " held=" .. tostring(Message.isHeld())
|
||||
.. " frame=" .. tostring(Message.frameKind()) .. " text=" .. tostring(page_text()))
|
||||
end
|
||||
lastDim = dim
|
||||
end
|
||||
@@ -217,8 +219,8 @@ return function(game)
|
||||
end
|
||||
result(oak_fx().phase == "text" and oak_fx().y == 6, "msg 1 prints once the dim reaches 6")
|
||||
U.wait(4)
|
||||
U.shot(game, DIR .. "/2310_03_oak_party_menu.png")
|
||||
result(true, "shot 2310_03_oak_party_menu.png (party-menu advice)")
|
||||
local partyShot = U.still(game, DIR .. "/2310_03_oak_party_menu.png")
|
||||
result(partyShot, "shot 2310_03_oak_party_menu.png (party-menu advice)")
|
||||
-- pokefirered/src/party_menu.c:1970
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 120 do
|
||||
@@ -228,8 +230,8 @@ return function(game)
|
||||
result(PartyMenu._oakPage == 2 and oak_fx().slot == 0 and oak_fx().y == 6,
|
||||
"the first slot lightens before msg 2")
|
||||
U.wait(4)
|
||||
U.shot(game, DIR .. "/2310_08_oak_party_menu_slot_lit.png")
|
||||
result(true, "shot 2310_08_oak_party_menu_slot_lit.png (first slot lit, msg 2)")
|
||||
local slotShot = U.still(game, DIR .. "/2310_08_oak_party_menu_slot_lit.png")
|
||||
result(slotShot, "shot 2310_08_oak_party_menu_slot_lit.png (first slot lit, msg 2)")
|
||||
for _ = 1, 40 do
|
||||
if not (PartyMenu.isOpen() and PartyMenu.mode == "oak") then break end
|
||||
U.tap(game, "a")
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
return function(game)
|
||||
U.wait(10)
|
||||
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
|
||||
local Fixture = require("tests.engine.save_editor_custom_maps")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local version = os.getenv("POKEPORT_VERSION") or GameVersion.get()
|
||||
local generation = GameVersion.generation(version)
|
||||
local Browser, Gen, Kit = require("MapBrowser"), require("Gen"), require("Kit")
|
||||
local State, Theme = require("State"), require("Theme")
|
||||
local opts, restoreNativeRoot = {}, nil
|
||||
local root = "save-editor-custom-map-fixture"
|
||||
assert(love.filesystem.createDirectory(root))
|
||||
if generation < 3 then
|
||||
opts.atlas = root .. "/atlas.png"
|
||||
local pixels = love.image.newImageData(8, 8)
|
||||
for y = 0, 7 do
|
||||
for x = 0, 7 do
|
||||
pixels:setPixel(x, y, x < 4 and 0.1 or 0.8, y < 4 and 0.9 or 0.2, 0.65, 1)
|
||||
end
|
||||
end
|
||||
assert(love.filesystem.write(opts.atlas, pixels:encode("png"):getString()))
|
||||
pixels:release()
|
||||
else
|
||||
local Dataset = require("src.core.game3.dataset")
|
||||
local maps = Dataset.buildMaps()
|
||||
Dataset.attachMidLayouts(maps)
|
||||
local source = maps[generation == 3 and GameVersion.layout(version) == "rse" and "EM_LITTLEROOT_TOWN" or "FR_PALLET_TOWN"]
|
||||
if not (source and source.midLayout) then
|
||||
for _, def in pairs(maps) do if def.midLayout then source = def; break end end
|
||||
end
|
||||
assert(source and source.midLayout, "no imported native map for custom-map visual probe")
|
||||
opts.pair = source.pair or source.midLayout.pair
|
||||
opts.mid = assert(source.midLayout:midAt(2, 2), "source metatile missing")
|
||||
local Native = require("src.core.game3.tileset_native")
|
||||
if not Native._cache then Native.install(Dataset.cache()) end
|
||||
assert(Native.get(opts.pair), "native source tileset did not load")
|
||||
end
|
||||
local data, loader, ids = Fixture.load(version, opts)
|
||||
if generation == 3 then
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
restoreNativeRoot = Extract.NATIVE_ROOT
|
||||
Extract.NATIVE_ROOT = root .. "/native"
|
||||
require("src.core.game3.dataset").invalidateManifestCache()
|
||||
assert(love.filesystem.createDirectory(Extract.NATIVE_ROOT .. "/layouts"))
|
||||
local cells = {}; for i = 1, 24 do cells[i] = { mid = ids.mid, coll = 0, elev = 0 } end
|
||||
assert(love.filesystem.write(Extract.NATIVE_ROOT .. "/layouts/" .. ids.lazy .. ".mid",
|
||||
require("src.import.gba.native_pack").encodeMidLayout({ width = 6, height = 4, cells = cells })))
|
||||
end
|
||||
local S = State.new()
|
||||
S.data, S.version, S.tab = data, version, "map"
|
||||
S.save = { generation = generation, version = version, player = { map = ids.new, x = 1, y = 1 } }
|
||||
if generation == 3 then S.save.engine = "game3"; S.save.map = ids.new; S.save.x = 1; S.save.y = 1 end
|
||||
local originalDraw, originalUpdate = game.draw, game.update
|
||||
game.update = function() end
|
||||
game.draw = function()
|
||||
require("src.render.GameViewport").reset()
|
||||
love.graphics.origin()
|
||||
love.graphics.setScissor()
|
||||
love.graphics.setShader()
|
||||
love.graphics.clear(0.035, 0.045, 0.075, 1)
|
||||
local w, h = love.graphics.getDimensions()
|
||||
Kit.layout(w, h)
|
||||
Kit.beginFrame(-1, -1, false, 0)
|
||||
Kit.text("small", "Installed mod map preview · " .. version, 20, 12, Theme.PAL.heading)
|
||||
Browser.draw(S, Kit, 20, 45, w - 40, h - 65)
|
||||
Kit.endFrame()
|
||||
end
|
||||
local errors, captures = {}, 0
|
||||
local function check(ok, message)
|
||||
if not ok then errors[#errors + 1] = message; print("FAIL " .. message)
|
||||
else print("PASS " .. message) end
|
||||
end
|
||||
local function select(id, label)
|
||||
Browser.select(S, id)
|
||||
S.mapSection, S.mapAutoFit = "view", true
|
||||
local dir = assert(os.getenv("POKEPORT_SHOT_DIR"), "POKEPORT_SHOT_DIR is required")
|
||||
assert(U.still(game, dir .. "/" .. version .. "-custom-map-" .. label .. ".png"))
|
||||
captures = captures + 1
|
||||
check(S._mapViewRect and S._mapViewRect.w > 0, label .. " actual editor viewport is visible")
|
||||
end
|
||||
local function terrainPixels(id)
|
||||
if not Browser.preview then return nil end
|
||||
S.mapId = id
|
||||
local ok, map, reason = Browser.preview(S)
|
||||
if not (ok and map.renderer and not reason) then return nil end
|
||||
local canvas = love.graphics.newCanvas(96, 64, { msaa = 0, dpiscale = 1 })
|
||||
love.graphics.push("all")
|
||||
love.graphics.setCanvas(canvas)
|
||||
love.graphics.origin()
|
||||
love.graphics.setScissor()
|
||||
love.graphics.setShader()
|
||||
love.graphics.clear(0, 0, 0, 0)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
map.renderer:draw(0, 0, 96, 64)
|
||||
love.graphics.setCanvas()
|
||||
love.graphics.pop()
|
||||
local pixels = canvas:newImageData()
|
||||
canvas:release()
|
||||
local r, g, b, a = pixels:getPixel(1, 1)
|
||||
if generation < 3 then
|
||||
check(math.abs(r - 0.1) < 0.02 and math.abs(g - 0.9) < 0.02 and math.abs(b - 0.65) < 0.02 and a > 0.98,
|
||||
id .. " actual custom atlas pixels reach terrain renderer")
|
||||
else
|
||||
local opaque = 0
|
||||
for y = 0, 15 do for x = 0, 15 do
|
||||
local _, _, _, alpha = pixels:getPixel(x, y)
|
||||
if alpha > 0.9 then opaque = opaque + 1 end
|
||||
end end
|
||||
check(opaque > 200, id .. " imported native metatile pixels render after mod load")
|
||||
end
|
||||
pixels:release()
|
||||
return map
|
||||
end
|
||||
select(ids.base, "patched")
|
||||
local expectedWidth = generation == 3 and 6 or 4
|
||||
check(Gen.maps(data)[ids.base].width == expectedWidth, "existing map uses installed mod dimensions")
|
||||
if generation == 3 then
|
||||
check(Gen.maps(data)[ids.base].midLayout:midAt(0, 0) == ids.mid, "existing map uses installed mod layout")
|
||||
end
|
||||
check(terrainPixels(ids.base) ~= nil, "patched existing map has artwork")
|
||||
select(ids.new, "new")
|
||||
check(terrainPixels(ids.new) ~= nil, "new registered map has artwork")
|
||||
if generation == 3 then
|
||||
select(ids.lazy, "native-file")
|
||||
check(data.maps[ids.lazy].midLayout ~= nil, "new mod map lazily loads its native file")
|
||||
check(terrainPixels(ids.lazy) ~= nil, "lazy native map has artwork")
|
||||
end
|
||||
select(ids.missing, "missing-art")
|
||||
check(S.mapPreviewReason and S.mapPreviewReason:find("unavailable", 1, true), "missing map artwork has a visible explanation")
|
||||
if Browser.preview then
|
||||
local ok, grid = Browser.preview(S)
|
||||
check(ok and grid:inBounds(1, 1), "missing art preserves coordinate placement bounds")
|
||||
local rect = S._mapViewRect
|
||||
local x = rect.x + (16 * 1.5 - S.mapCamX) * S.mapZoom
|
||||
local y = rect.y + (16 * 1.5 - S.mapCamY) * S.mapZoom
|
||||
local w, h = love.graphics.getDimensions()
|
||||
Kit.beginFrame(x, y, true, 0)
|
||||
Browser.draw(S, Kit, 20, 45, w - 40, h - 65)
|
||||
Kit.endFrame()
|
||||
check(S.mapClickCell and S.mapClickCell.cx == 1 and S.mapClickCell.cy == 1, "actual missing-art grid still selects a spawn cell")
|
||||
end
|
||||
game.draw, game.update = originalDraw, originalUpdate
|
||||
if restoreNativeRoot then
|
||||
require("src.import.gba.extract_island1").NATIVE_ROOT = restoreNativeRoot
|
||||
require("src.core.game3.dataset").invalidateManifestCache()
|
||||
end
|
||||
print(("custom maps %s: loaded=%d captures=%d failures=%d"):format(version, #loader:status().loaded, captures, #errors))
|
||||
love.event.quit(#errors == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
-- pokered/scripts/SilphCo10F.asm:72
|
||||
-- pokered/text/SilphCo10F.asm:1
|
||||
-- pokeyellow/text/SilphCo10F.asm:1
|
||||
return function(game)
|
||||
local U = require("tests.drivers.util")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Font = require("src.render.Font")
|
||||
local Version = require("src.core.GameVersion")
|
||||
local Timing = require("src.core.Timing")
|
||||
local dir = assert(os.getenv("POKEPORT_SHOT_DIR"), "POKEPORT_SHOT_DIR is required")
|
||||
local edition = Version.get()
|
||||
local fails = 0
|
||||
|
||||
local function check(ok, label)
|
||||
U.log((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then fails = fails + 1 end
|
||||
return ok
|
||||
end
|
||||
local function same(a, b)
|
||||
if #a ~= #b then return false end
|
||||
for i, value in ipairs(a) do if value ~= b[i] then return false end end
|
||||
return true
|
||||
end
|
||||
local function rows(box, expected, label)
|
||||
local good = #box.shown == #expected and same(box:visibleText() or {}, expected)
|
||||
for i, text in ipairs(expected) do
|
||||
good = good and same(box.shown[i] or {}, Font.encode(text))
|
||||
end
|
||||
return check(good, label)
|
||||
end
|
||||
local function prompt(box)
|
||||
for _ = 1, 400 do
|
||||
if game.stack:top() ~= box then return false end
|
||||
if box.waiting or box.done then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function capture(box, file)
|
||||
for _ = 1, 60 do
|
||||
if box.blink % 60 < 20 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
check(U.still(game, dir .. "/" .. file), "screenshot " .. file)
|
||||
end
|
||||
local function close(box)
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 120 do
|
||||
if game.stack:top() == game.overworld and not game.overworld.runner:isRunning() then
|
||||
return check(true, "NPC callback returns to overworld")
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
return check(false, "NPC callback returns to overworld")
|
||||
end
|
||||
local function talk()
|
||||
U.teleport(game, "SILPH_CO_10F", 8, 15, "right")
|
||||
local ow = game.overworld
|
||||
local worker
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def.name == "SILPHCO10F_SILPH_WORKER_F" then worker = npc end
|
||||
end
|
||||
if not check(worker ~= nil, "real Silph 10F worker is loaded") then return nil end
|
||||
-- pokered/data/maps/objects/SilphCo10F.asm:25
|
||||
worker.wanders = false
|
||||
worker:resetToSpawn()
|
||||
worker.cellX, worker.cellY = 9, 15
|
||||
worker.px, worker.py = 9 * 16, 15 * 16
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 120 do
|
||||
local box = game.stack:top()
|
||||
if getmetatable(box) == TextBox then return box end
|
||||
U.wait(1)
|
||||
end
|
||||
check(false, "NPC A dispatch opens textbox")
|
||||
end
|
||||
local function cachedRows(key, marker)
|
||||
local raw = game.data.text[key]
|
||||
if not check(type(raw) == "string", "cache contains " .. key) then return nil end
|
||||
local text = TextBox.strip(TextBox.substitute(game, raw))
|
||||
local first, actual, last = text:match("^(.-)([\n\v])(.-)$")
|
||||
if not check(first and last and actual == marker and not last:find("[\n\v\f]"),
|
||||
edition .. " cached control for " .. key) then return nil end
|
||||
return { first, last }
|
||||
end
|
||||
|
||||
if not check(edition == "red" or edition == "blue" or edition == "yellow",
|
||||
"supported Gen 1 edition") then love.event.quit(1) return end
|
||||
game.save.flags.EVENT_BEAT_SILPH_CO_10F_TRAINER_0 = true
|
||||
game.save.flags.EVENT_BEAT_SILPH_CO_10F_TRAINER_1 = true
|
||||
game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI = nil
|
||||
local scared = cachedRows("_SilphCo10FSilphWorkerFImScaredText", edition == "yellow" and "\n" or "\v")
|
||||
local quiet = cachedRows("_SilphCo10FSilphWorkerFQuietAboutMyCryingText", "\n")
|
||||
if not scared or not quiet then love.event.quit(1) return end
|
||||
local box = talk()
|
||||
if not box or not check(prompt(box), "scared text reaches prompt") then
|
||||
love.event.quit(1) return
|
||||
end
|
||||
if edition == "yellow" then
|
||||
local good = check(box.done and not box.waiting, "Yellow ordinary line needs no cont press")
|
||||
good = rows(box, scared, "Yellow scared text keeps both rows") and good
|
||||
if good then capture(box, "2634_yellow_scared_two_rows.png") end
|
||||
else
|
||||
local good = check(box.waiting and box.contAdvance, "Red Blue scared text waits on cont")
|
||||
good = rows(box, { scared[1] }, "Red Blue initial scared text upper row") and good
|
||||
if good then capture(box, "2634_scared_cont_arrow.png") end
|
||||
U.wait(Timing.TEXT_PRE_ADVANCE)
|
||||
U.tap(game, "a")
|
||||
check(prompt(box) and box.done and not box.waiting, "one cont press completes scared text")
|
||||
local expected = { "", scared[2] }
|
||||
if rows(box, expected, "Red Blue scared text blank upper bottom sentence") then
|
||||
capture(box, "2634_scared_blank_upper_bottom_sentence.png")
|
||||
end
|
||||
local instant = TextBox.new(game, game.data.text._SilphCo10FSilphWorkerFImScaredText,
|
||||
nil, { instant = true })
|
||||
rows(instant, expected, "instant scared text matches physical rows")
|
||||
end
|
||||
if not close(box) then love.event.quit(1) return end
|
||||
game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI = true
|
||||
box = talk()
|
||||
if not box or not check(prompt(box), "quiet text reaches prompt") then
|
||||
love.event.quit(1) return
|
||||
end
|
||||
local good = check(box.done and not box.waiting, "quiet text needs no cont press")
|
||||
good = rows(box, quiet, "post Giovanni quiet text keeps both rows") and good
|
||||
if good then capture(box, "2634_post_giovanni_quiet_two_rows.png") end
|
||||
close(box)
|
||||
U.log(fails == 0 and "PASS silph10f_cont_bug2634 " .. edition or "FAIL silph10f_cont_bug2634 " .. edition)
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
-- pokered/scripts/ViridianCity.asm:150
|
||||
-- pokeyellow/scripts/ViridianCity_2.asm:10
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local version = GameVersion.get()
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local failed = false
|
||||
local badges = {
|
||||
"BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE",
|
||||
"SOULBADGE", "MARSHBADGE", "VOLCANOBADGE", "EARTHBADGE",
|
||||
}
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", "2632_" .. label)
|
||||
if not ok then failed = true end
|
||||
return ok
|
||||
end
|
||||
local function topBox()
|
||||
local state = game.stack:top()
|
||||
return getmetatable(state) == TextBox and state or nil
|
||||
end
|
||||
local function flatten(pages)
|
||||
local result = {}
|
||||
for _, page in ipairs(pages) do result[#result + 1] = table.concat(page, "\n") end
|
||||
return table.concat(result, "\f")
|
||||
end
|
||||
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.textSpeed = 1
|
||||
game.save.flags = game.save.flags or {}
|
||||
game.save.flags.EVENT_GOT_POKEDEX = true
|
||||
game.save.flags.EVENT_OAK_GOT_PARCEL = true
|
||||
game.save.flags.EVENT_VIRIDIAN_GYM_OPEN = true
|
||||
game.save.inventory = game.save.inventory or {}
|
||||
|
||||
local function scene(label, count, flagName, textKey)
|
||||
for i, badge in ipairs(badges) do game.save.inventory[badge] = i <= count or nil end
|
||||
game.save.flags.EVENT_BEAT_GIOVANNI = nil
|
||||
game.save.flags.EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI = nil
|
||||
if flagName then game.save.flags[flagName] = true end
|
||||
U.teleport(game, "VIRIDIAN_CITY", 30, 9, "up")
|
||||
local ow = game.overworld
|
||||
if not check(label .. "_map", ow and ow.map.id == "VIRIDIAN_CITY") then return end
|
||||
local gambler
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def and npc.def.text == "TEXT_VIRIDIANCITY_GAMBLER1" then
|
||||
gambler = npc
|
||||
break
|
||||
end
|
||||
end
|
||||
if not check(label .. "_actual_npc", gambler ~= nil) then return end
|
||||
gambler.wanders = false
|
||||
gambler.moving = false
|
||||
gambler.cellX, gambler.cellY = gambler.def.x, gambler.def.y
|
||||
gambler.px, gambler.py = gambler.cellX * 16, gambler.cellY * 16
|
||||
local p = ow.player
|
||||
p.cellX, p.cellY = gambler.cellX, gambler.cellY + 1
|
||||
p.px, p.py = p.cellX * 16, p.cellY * 16
|
||||
p.facing = "up"
|
||||
U.wait(2)
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 60 do
|
||||
if topBox() then break end
|
||||
U.wait(1)
|
||||
end
|
||||
local box = topBox()
|
||||
if not check(label .. "_normal_talk", box ~= nil and gambler.frozen) then return end
|
||||
local source = game.data.text[textKey]
|
||||
if not check(label .. "_rom_text", type(source) == "string") then return end
|
||||
local expected = TextBox.paginate(TextBox.substitute(game, source), box.maxCols)
|
||||
check(label .. "_dialogue", flatten(box.pages) == flatten(expected))
|
||||
U.log("2632_" .. label .. "_pages", flatten(box.pages))
|
||||
for _ = 1, 600 do
|
||||
if box.done or box.waiting then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if check(label .. "_completed_page", box.done or box.waiting) then
|
||||
check(label .. "_screenshot", U.still(game,
|
||||
DIR .. "/2632_" .. version .. "_" .. label .. ".png"))
|
||||
end
|
||||
for _ = 1, 120 do
|
||||
if game.stack:top() == ow then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
check(label .. "_callback_unfreezes", game.stack:top() == ow and not gambler.frozen)
|
||||
end
|
||||
|
||||
scene("closed_before_seven", 6, nil, "_ViridianCityGambler1GymAlwaysClosedText")
|
||||
scene("returned_exact_seven", 7, nil, "_ViridianCityGambler1GymLeaderReturnedText")
|
||||
scene("returned_after_earth", 8, "EVENT_BEAT_GIOVANNI",
|
||||
"_ViridianCityGambler1GymLeaderReturnedText")
|
||||
scene("returned_canonical_win", 8, "EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI",
|
||||
"_ViridianCityGambler1GymLeaderReturnedText")
|
||||
scene("closed_eight_without_win", 8, nil, "_ViridianCityGambler1GymAlwaysClosedText")
|
||||
check("all_scenes", not failed)
|
||||
love.event.quit(failed and 1 or 0)
|
||||
end
|
||||
@@ -0,0 +1,324 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
local bit = require("bit")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
require("src.core.game3.profile").reset()
|
||||
GameVersion.set("emerald")
|
||||
local Util = require("src.core.game3.rse.frontier.util")
|
||||
local Story = require("src.core.game3.scripting.natives_frontier_story")
|
||||
local Sections = require("src.core.game3.save_sections")
|
||||
local function copy(v)
|
||||
if type(v) ~= "table" then return v end
|
||||
local out = {}
|
||||
for k, x in pairs(v) do out[k] = copy(x) end
|
||||
return out
|
||||
end
|
||||
local oldPersist = Util.persist
|
||||
local function protectedSave(sess, persist)
|
||||
Util.persist = persist
|
||||
local ok, result = pcall(Util.saveGameFrontier, sess)
|
||||
Util.persist = oldPersist
|
||||
return ok, result
|
||||
end
|
||||
local function state()
|
||||
return { version = "emerald", name = "RELOAD", trainerId = 12345, secretId = 4321,
|
||||
map = "EM_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR", x = 3, y = 5, party = {}, flags = {}, vars = {},
|
||||
dynamicWarp = { map = "EM_SLATEPORT_CITY_BATTLE_TENT_LOBBY", x = 7, y = 8, warpId = 255 },
|
||||
continueGameWarp = { map = "EM_ROUTE110", x = 10, y = 11 }, specialSaveWarpFlags = 0xA1 }
|
||||
end
|
||||
local original = { { personality = 123, nickname = "ORIGINAL", ivs = { hp = 17 }, evs = { atk = 23 } } }
|
||||
local rentals = { { personality = 987, nickname = "RENTAL", ivs = { hp = 3 }, evs = { atk = 252 } } }
|
||||
local s = state()
|
||||
s.party = copy(original)
|
||||
Story.savePlayerParty(s)
|
||||
s.party = copy(rentals)
|
||||
local exported = {}
|
||||
Sections.fields(Util.SAVE_FIELDS).export(s, exported)
|
||||
local decoded = assert(SaveData.decode(SaveData.encode(exported)))
|
||||
local restored = {}
|
||||
Sections.fields(Util.SAVE_FIELDS).restore(decoded, restored)
|
||||
T.same(restored.savedPlayerParty, original, "registered section preserves complete original backup through SaveData serializer")
|
||||
for _, fail in ipairs({ false, "throw" }) do
|
||||
s = state()
|
||||
s.party, s.savedPlayerParty = copy(rentals), copy(original)
|
||||
local ok, result = protectedSave(s, function()
|
||||
T.same(s.party, original, "persistence sees original party")
|
||||
T.same(s.continueGameWarp, s.dynamicWarp, "persistence sees captured lobby destination")
|
||||
T.eq(s.specialSaveWarpFlags, 0xA1, "persistence sets only continue bit")
|
||||
if fail == "throw" then error("2638 injected write failure") end
|
||||
return false
|
||||
end)
|
||||
T.check(not ok or result == false, "failed persistence cannot report success")
|
||||
T.same(s.party, rentals, "failed persistence restores live rentals")
|
||||
T.same(s.savedPlayerParty, original, "failed persistence preserves original backup")
|
||||
T.eq(s.specialSaveWarpFlags, 0xA0, "failed persistence clears continue bit and preserves other bits")
|
||||
T.same(s.continueGameWarp, s.dynamicWarp, "failed persistence leaves inactive source destination bytes")
|
||||
end
|
||||
for _, missing in ipairs({ "dynamicWarp", "x", "savedPlayerParty" }) do
|
||||
s = state()
|
||||
s.party, s.savedPlayerParty = copy(rentals), copy(original)
|
||||
if missing == "x" then s.dynamicWarp.x = nil else s[missing] = nil end
|
||||
local before, calls = copy(s), 0
|
||||
local ok, result = protectedSave(s, function() calls = calls + 1 return true end)
|
||||
T.check(ok and result == false and calls == 0, "incomplete " .. missing .. " is rejected before writing")
|
||||
T.same(s, before, "incomplete " .. missing .. " leaves concrete session state intact")
|
||||
end
|
||||
local cache = require("src.core.game3.dataset").cache()
|
||||
local hasCache = type(cache:read("data/generated/gba/rse/frontier/manifest.lua")) == "string"
|
||||
if hasCache then
|
||||
require("src.import.gba.versions").select("emerald")
|
||||
local C = require("src.core.game3.constants").of("emerald")
|
||||
require("src.core.game3.pokemon").install(nil)
|
||||
local D = Util.D
|
||||
local Rse = require("src.core.game3.rse.init")
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Schema = require("src.core.game3.save_schema_firered")
|
||||
local Tents = require("src.core.game3.rse.frontier.tents")
|
||||
local Factory = require("src.core.game3.rse.frontier.factory")
|
||||
local ctx = { specialVars = { [0x8005] = 2 } }
|
||||
function ctx:getVar(id) return self.specialVars[id] or 0 end
|
||||
function ctx:setVar(id, value) self.specialVars[id] = value end
|
||||
local oldGet = Runtime.getSession
|
||||
Runtime.getSession = function() return s end
|
||||
original = {}
|
||||
local names = { "SPECIES_SWAMPERT", "SPECIES_BLAZIKEN", "SPECIES_SCEPTILE", "SPECIES_METAGROSS", "SPECIES_SALAMENCE", "SPECIES_ZIGZAGOON" }
|
||||
for i, name in ipairs(names) do
|
||||
local m = D.createMon(C:require("species", name), 35 + i, 10 + i, 200000 + i * 37, 12345,
|
||||
{ otName = "RELOAD", moves = {} })
|
||||
D.setMoves(m, { C:require("moves", "MOVE_TACKLE"), C:require("moves", "MOVE_PROTECT") })
|
||||
D.setEvs(m, { i * 7, i * 9, i * 11, i * 13, i * 15, i * 17 })
|
||||
m.nickname, m.otSecretId, m.heldItem = "KEEP" .. i, 4321, C:require("items", "ITEM_ORAN_BERRY")
|
||||
m.ivs.hp, m.pp[1], m.friendship, m.markings = i + 4, i + 2, 100 + i, i
|
||||
require("src.core.game3.save_mon").normalize(m)
|
||||
Schema.ensureMonBall(m)
|
||||
original[i] = m
|
||||
end
|
||||
love = love or require("tests.love_stub")
|
||||
local oldFs, files, writeFault = SaveData.portableFs, {}, nil
|
||||
SaveData.portableFs = function()
|
||||
return {
|
||||
getInfo = function(n) return files[n] and { type = "file" } or nil end,
|
||||
read = function(n) return files[n] end,
|
||||
write = function(n, body)
|
||||
if writeFault and n == SaveData.saveFilename("emerald") .. ".tmp" then
|
||||
if writeFault == "throw" then error("2638 injected filesystem failure") end
|
||||
return false, "2638 injected filesystem failure"
|
||||
end
|
||||
files[n] = body
|
||||
return true
|
||||
end,
|
||||
remove = function(n) files[n] = nil return true end,
|
||||
createDirectory = function() return true end,
|
||||
}
|
||||
end
|
||||
local function roundtrip(sess)
|
||||
assert(SaveData.save(Schema.toSaveTable(sess)))
|
||||
return assert(SaveData.load("emerald"))
|
||||
end
|
||||
local function fresh()
|
||||
s = state()
|
||||
s.bag = require("src.core.game3.bag").new()
|
||||
s.party = copy(original)
|
||||
s.dex = { [1] = { seen = true, owned = true } }
|
||||
s.storage = require("src.core.game3.storage").new()
|
||||
Util.newGame(s)
|
||||
require("src.core.game3.scripting.space").store = { flags = s.flags, vars = s.vars }
|
||||
Rse.setVar("VAR_FRONTIER_FACILITY", D.FACILITY.FACTORY, s)
|
||||
Rse.setVar("VAR_FRONTIER_BATTLE_MODE", D.MODE.SINGLES, s)
|
||||
return s
|
||||
end
|
||||
fresh()
|
||||
s.specialSaveWarpFlags = 0xA0
|
||||
local ordinary = Schema.fromSaveTable(roundtrip(s))
|
||||
T.same(ordinary.party, original, "ordinary Emerald save preserves all original mon values")
|
||||
T.eq(ordinary.map, s.map, "ordinary save stays at current location")
|
||||
T.eq(ordinary.savedPlayerParty, nil, "ordinary and legacy saves do not invent a backup")
|
||||
s.map, s.x, s.y = s.dynamicWarp.map, 7, 8
|
||||
Tents.init(s)
|
||||
Story.savePlayerParty(s)
|
||||
s.frontier.lvlMode = D.LVL.TENT
|
||||
require("src.core.game3.rng").SeedRng(0x2638)
|
||||
Tents.generateRentalMons(s)
|
||||
Tents.generateOpponentMons(s)
|
||||
ctx.specialVars[0x8005] = 0
|
||||
Factory.setPlayerAndOpponentParties(ctx, s)
|
||||
rentals = copy(s.party)
|
||||
for _, m in ipairs(rentals) do
|
||||
require("src.core.game3.save_mon").normalize(m)
|
||||
Schema.ensureMonBall(m)
|
||||
end
|
||||
s.party = copy(rentals)
|
||||
s.map, s.x, s.y = "EM_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR", 3, 5
|
||||
s.frontier.curChallengeBattleNum = 1
|
||||
local dex, storage = copy(s.dex), copy(s.storage)
|
||||
local direct = Schema.fromSaveTable(roundtrip(s))
|
||||
T.same(direct.party, rentals, "direct engine save retains active rentals")
|
||||
T.same(direct.savedPlayerParty, original, "direct engine save retains six full original identities and traits")
|
||||
Story.loadPlayerParty(direct)
|
||||
T.same(direct.party, original, "direct continue completion restores exact original six mons in order")
|
||||
T.same(direct.dex, dex, "rental reload leaves dex unchanged")
|
||||
T.same(direct.storage, storage, "rental reload leaves storage unchanged")
|
||||
if direct.savedPlayerParty then direct.savedPlayerParty[1].ivs.hp = 99 end
|
||||
T.eq(direct.party[1].ivs.hp, original[1].ivs.hp, "restored party and backup are not aliases")
|
||||
local disk
|
||||
Util.persist = function() disk = roundtrip(s) return true end
|
||||
ctx.specialVars[0x8005] = Util.CHALLENGE_STATUS.PAUSED
|
||||
T.check(Tents.save(ctx, s), "real Tent REST save succeeds")
|
||||
Util.persist = oldPersist
|
||||
T.same(disk.party, original, "REST disk party contains originals")
|
||||
T.same(disk.continueGameWarp, s.dynamicWarp, "REST disk continue destination is captured lobby")
|
||||
T.eq(bit.band(disk.specialSaveWarpFlags, 1), 1, "REST disk has continue bit")
|
||||
T.same(disk.frontier.rentalMons, s.frontier.rentalMons, "REST keeps saved rental data")
|
||||
T.eq(disk.frontier.curChallengeBattleNum, 1, "REST keeps challenge battle counter")
|
||||
T.same(s.party, rentals, "successful REST restores live rental party")
|
||||
T.eq(s.specialSaveWarpFlags, 0xA0, "successful REST clears live bit only")
|
||||
s = Schema.fromSaveTable(disk)
|
||||
T.eq(s.map, "EM_SLATEPORT_CITY_BATTLE_TENT_LOBBY", "REST continue resumes lobby")
|
||||
T.eq(s.x, 7, "REST continue keeps lobby x")
|
||||
T.eq(s.y, 8, "REST continue keeps lobby y")
|
||||
Story.savePlayerParty(s)
|
||||
ctx.specialVars[0x8005] = 0
|
||||
Factory.setPlayerAndOpponentParties(ctx, s)
|
||||
for _, m in ipairs(s.party) do
|
||||
require("src.core.game3.save_mon").normalize(m)
|
||||
Schema.ensureMonBall(m)
|
||||
end
|
||||
T.same(s.party, rentals, "real corridor resume rebuilds the same rentals")
|
||||
Story.loadPlayerParty(s)
|
||||
T.same(s.party, original, "REST resume completion restores exact original six mons")
|
||||
for i, name in ipairs({ "tents", "factory", "tower", "palace", "arena", "dome" }) do
|
||||
fresh()
|
||||
s.map, s.x, s.y = "EM_SLATEPORT_CITY_BATTLE_TENT_LOBBY", i + 1, i + 5
|
||||
require("src.core.game3.rse.frontier." .. name).init(s)
|
||||
local expected = { map = s.map, x = s.x, y = s.y, warpId = 255 }
|
||||
T.same(s.dynamicWarp, expected, name .. " initializer captures exact entry coordinates")
|
||||
s.map, s.x, s.y = "EM_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR", 1, 2
|
||||
s.savedPlayerParty = copy(original)
|
||||
local ok, result = protectedSave(s, function() disk = roundtrip(s) return true end)
|
||||
T.check(ok and result, name .. " shared save succeeds")
|
||||
T.same(disk.continueGameWarp, expected, name .. " save uses initial location rather than current room")
|
||||
T.eq(s.specialSaveWarpFlags, 0xA0, name .. " save preserves other warp bits")
|
||||
end
|
||||
fresh()
|
||||
s.party, s.savedPlayerParty = copy(rentals), copy(original)
|
||||
local staleWarp, currentWarp = copy(s.dynamicWarp), copy(s.continueGameWarp)
|
||||
local common, calls = Util.saveGameFrontier, 0
|
||||
Util.saveGameFrontier = function() error("Pike must save in place") end
|
||||
Util.persist = function() calls = calls + 1 disk = roundtrip(s) return true end
|
||||
ctx.specialVars[0x8005] = Util.CHALLENGE_STATUS.PAUSED
|
||||
local ok, result = pcall(require("src.core.game3.rse.frontier.pike").save, ctx, s)
|
||||
Util.persist, Util.saveGameFrontier = oldPersist, common
|
||||
T.check(ok and result and calls == 1, "Pike invokes in-place persistence only")
|
||||
if calls == 1 then
|
||||
T.same(disk.party, rentals, "Pike writes active challenge party without original swap")
|
||||
T.same(disk.continueGameWarp, currentWarp, "Pike does not manufacture continue destination")
|
||||
T.same(disk.dynamicWarp, staleWarp, "Pike ignores stale unrelated dynamic warp")
|
||||
T.eq(disk.map, s.map, "Pike saves current map")
|
||||
T.eq(disk.x, s.x, "Pike saves current x")
|
||||
T.eq(disk.y, s.y, "Pike saves current y")
|
||||
T.eq(disk.frontier.challengePaused, 1, "Pike writes paused challenge state")
|
||||
T.eq(disk.specialSaveWarpFlags, 0xA1, "Pike preserves preexisting warp flags")
|
||||
end
|
||||
local Vm = require("src.core.game3.scripting.vm")
|
||||
local Adapters = require("src.core.game3.scripting.adapters")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local Game3 = require("src.core.Game3")
|
||||
local oldGame, oldMod = Runtime._game, Runtime._mod
|
||||
Natives.bind("emerald")
|
||||
local nativeSaves = {
|
||||
{ "CallSlateportTentFunction", Tents.SLATEPORT.SAVE, "VAR_TEMP_0" },
|
||||
{ "CallFallarborTentFunction", Tents.FALLARBOR.SAVE, "VAR_TEMP_0" },
|
||||
{ "CallVerdanturfTentFunction", Tents.VERDANTURF.SAVE, "VAR_TEMP_0" },
|
||||
{ "CallBattleFactoryFunction", Factory.FUNC.SAVE, "VAR_TEMP_CHALLENGE_STATUS" },
|
||||
{ "CallBattleTowerFunc", require("src.core.game3.rse.frontier.tower").FUNC.SAVE, "VAR_TEMP_0" },
|
||||
{ "CallBattleArenaFunction", require("src.core.game3.rse.frontier.arena").FUNC.SAVE, "VAR_TEMP_CHALLENGE_STATUS" },
|
||||
{ "CallBattlePalaceFunction", require("src.core.game3.rse.frontier.palace").FUNC.SAVE, "VAR_TEMP_CHALLENGE_STATUS" },
|
||||
{ "CallBattleDomeFunction", require("src.core.game3.rse.frontier.dome").FUNC.SAVE, "VAR_TEMP_CHALLENGE_STATUS" },
|
||||
}
|
||||
for _, native in ipairs(nativeSaves) do
|
||||
for _, failure in ipairs({ "false", "throw", "missingWarp", "missingParty" }) do
|
||||
fresh()
|
||||
s.savedPlayerParty, s.party = copy(original), copy(rentals)
|
||||
s.specialSaveWarpFlags = 0xA0
|
||||
s.frontier.challengeStatus, s.frontier.challengePaused = 1, 0
|
||||
local tempId = assert(Rse.varId(native[3], s))
|
||||
Rse.setVar(tempId, 73, s)
|
||||
roundtrip(s)
|
||||
local diskBefore = files[SaveData.saveFilename("emerald")]
|
||||
if failure == "missingWarp" then s.dynamicWarp.x = nil
|
||||
elseif failure == "missingParty" then s.savedPlayerParty = nil
|
||||
else writeFault = failure end
|
||||
local frontierBefore, partyBefore, backupBefore = copy(s.frontier), copy(s.party), copy(s.savedPlayerParty)
|
||||
Runtime._game, Runtime._mod = { session = s, phase = "field", saveGame = Game3.saveGame }, nil
|
||||
local closed, finishMessage, sounds, unfreezes, logs = 0, nil, 0, 0, {}
|
||||
local adapters = Adapters.stub({})
|
||||
adapters.listActiveLocalIds = function() return { 1 } end
|
||||
adapters.snapshotLocal = function() return { frozen = false } end
|
||||
adapters.freezeLocal = function() Field.locked = true end
|
||||
adapters.unfreezeLocal = function() unfreezes = unfreezes + 1 end
|
||||
adapters.log = function(msg) logs[#logs + 1] = msg end
|
||||
adapters.playSe = function() sounds = sounds + 1 end
|
||||
adapters.closeMessage = function() closed = closed + 1 end
|
||||
adapters.openMessageAsync = function(text, done)
|
||||
T.check(type(text) == "string" and text:find("saved", 1, true) ~= nil, native[1] .. " presents concrete save error")
|
||||
finishMessage = done
|
||||
end
|
||||
local vm = Vm.new({ version = "emerald", store = Rse.store(), adapters = adapters, scripts = { save = {
|
||||
{ op = "lockall" },
|
||||
{ op = "setvar", 0x8004, native[2] },
|
||||
{ op = "setvar", 0x8005, 2 },
|
||||
{ op = "special", id = C:require("specials", native[1]) },
|
||||
{ op = "playse", C:require("songs", "SE_SAVE") },
|
||||
{ op = "setvar", 0x8004, Util.FUNC.SOFT_RESET },
|
||||
{ op = "special", id = C:require("specials", "CallFrontierUtilFunc") },
|
||||
{ op = "end" },
|
||||
} } })
|
||||
T.check(vm:start("save"), native[1] .. " real VM starts")
|
||||
T.check(vm:isRunning() and finishMessage ~= nil, native[1] .. " failure awaits message dismissal")
|
||||
T.eq(sounds, 0, native[1] .. " failure skips save-success sound")
|
||||
T.eq(Runtime._game.softResetRequested, nil, native[1] .. " failure skips frontier reset")
|
||||
T.same(s.frontier, frontierBefore, native[1] .. " failure rolls back all challenge save fields")
|
||||
T.eq(Rse.var(tempId, s), 73, native[1] .. " failure restores original temporary var")
|
||||
T.same(s.party, partyBefore, native[1] .. " failure restores current live rentals")
|
||||
T.same(s.savedPlayerParty, backupBefore, native[1] .. " failure preserves concrete original backup")
|
||||
T.eq(files[SaveData.saveFilename("emerald")], diskBefore, native[1] .. " failure leaves last successful save intact")
|
||||
if finishMessage then finishMessage() end
|
||||
vm:tick()
|
||||
T.check(not vm:isRunning() and vm.ctx.status == "shutdown", native[1] .. " failure ends through normal VM halt")
|
||||
T.check(not Field.locked and unfreezes == 1 and vm.ctx.lockKind == nil, native[1] .. " failure unfreezes objects and unlocks field")
|
||||
T.check(closed >= 2 and vm.ctx.messageOpen == false, native[1] .. " failure closes error message")
|
||||
T.eq(sounds, 0, native[1] .. " dismissed error still skips save-success sound")
|
||||
T.eq(Runtime._game.softResetRequested, nil, native[1] .. " dismissed error still skips reset")
|
||||
T.eq(#logs, 0, native[1] .. " known SAVE failure never reports an unported native")
|
||||
writeFault = nil
|
||||
end
|
||||
fresh()
|
||||
s.savedPlayerParty, s.party = copy(original), copy(rentals)
|
||||
s.specialSaveWarpFlags = 0xA0
|
||||
Runtime._game, Runtime._mod = { session = s, phase = "field", saveGame = Game3.saveGame }, nil
|
||||
local sounds = 0
|
||||
local adapters = Adapters.stub({})
|
||||
adapters.playSe = function() sounds = sounds + 1 end
|
||||
local vm = Vm.new({ version = "emerald", store = Rse.store(), adapters = adapters, scripts = { save = {
|
||||
{ op = "setvar", 0x8004, native[2] },
|
||||
{ op = "setvar", 0x8005, 2 },
|
||||
{ op = "special", id = C:require("specials", native[1]) },
|
||||
{ op = "playse", C:require("songs", "SE_SAVE") },
|
||||
{ op = "setvar", 0x8004, Util.FUNC.SOFT_RESET },
|
||||
{ op = "special", id = C:require("specials", "CallFrontierUtilFunc") },
|
||||
{ op = "end" },
|
||||
} } })
|
||||
T.check(vm:start("save") and not vm:isRunning(), native[1] .. " successful save runs synchronous script tail")
|
||||
T.eq(sounds, 1, native[1] .. " success reaches sentinel save sound")
|
||||
T.eq(Runtime._game.softResetRequested, true, native[1] .. " success reaches real reset special")
|
||||
T.same(s.party, rentals, native[1] .. " success keeps live rentals")
|
||||
T.same(SaveData.load("emerald").party, original, native[1] .. " success writes exact original party")
|
||||
end
|
||||
Runtime._game, Runtime._mod = oldGame, oldMod
|
||||
Runtime.getSession, SaveData.portableFs = oldGet, oldFs
|
||||
else
|
||||
print("SKIP ROM assertions: Emerald cache unavailable; section serialization and failure cleanup still exercised")
|
||||
end
|
||||
T.finish("emerald_tent_save_reload_2638_test")
|
||||
@@ -20,12 +20,12 @@ eq(CacheContract.markerFor("emerald", SHA), "rom-cache-v3-emerald:" .. SHA, "eme
|
||||
check(CacheContract.markerMatches("emerald", "rom-cache-v3-emerald:" .. SHA), "emerald marker matches")
|
||||
check(not CacheContract.markerMatches("emerald", "rom-cache-v17-firered:" .. SHA), "a FireRed marker does not")
|
||||
|
||||
eq(#CacheContract.requiredFiles("firered"), 433, "FireRed required list size")
|
||||
eq(digest(CacheContract.requiredFiles("firered")), 3052316112, "FireRed required list")
|
||||
eq(#CacheContract.requiredFiles("leafgreen"), 434, "LeafGreen required list size")
|
||||
eq(digest(CacheContract.requiredFiles("leafgreen")), 3097867067, "LeafGreen required list")
|
||||
eq(digest(CacheContract.requiredFiles("firered", true)), 2123642333, "FireRed semantic list")
|
||||
eq(digest(CacheContract.requiredFiles("leafgreen", true)), 1337846595, "LeafGreen semantic list")
|
||||
eq(#CacheContract.requiredFiles("firered"), 453, "FireRed required list size")
|
||||
eq(digest(CacheContract.requiredFiles("firered")), 1498110830, "FireRed required list")
|
||||
eq(#CacheContract.requiredFiles("leafgreen"), 454, "LeafGreen required list size")
|
||||
eq(digest(CacheContract.requiredFiles("leafgreen")), 3355015054, "LeafGreen required list")
|
||||
eq(digest(CacheContract.requiredFiles("firered", true)), 455568495, "FireRed semantic list")
|
||||
eq(digest(CacheContract.requiredFiles("leafgreen", true)), 4104686896, "LeafGreen semantic list")
|
||||
|
||||
local em, isOverride = CacheContract.requiredFilesFor("emerald")
|
||||
check(isOverride == true, "emerald is a composed list, not the gen 1 list")
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
require("src.core.GameVersion").set("emerald")
|
||||
local session = { version = "emerald", map = "EM_TEST", party = {}, flags = {}, vars = {} }
|
||||
package.loaded["src.core.game3.runtime"] = {
|
||||
getSession = function() return session end, uiBusy = function() return false end,
|
||||
}
|
||||
package.loaded["src.core.game3.audio"] = setmetatable({}, { __index = function() return function() end end })
|
||||
package.loaded["src.core.game3.step_events"] = { onStepTaken = function() end }
|
||||
package.loaded["src.core.game3.encounters"] = { onStep = function() end }
|
||||
local entered, exit
|
||||
package.loaded["src.core.game3.warp"] = {
|
||||
isBusy = function() return entered ~= nil or exit ~= nil end,
|
||||
startDoorEntrance = function(_, _, dest) entered = dest end,
|
||||
startDoorExit = function(_, _, dest) exit = dest end,
|
||||
}
|
||||
package.loaded["src.core.game3.doors"] = {
|
||||
getDoorEntryAt = function(_, x, y) if x == 3 and y == 2 then return {} end end,
|
||||
}
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Bike = require("src.core.game3.bike")
|
||||
local MB = require("src.core.game3.mb")
|
||||
local Layout = require("src.core.game3.layout_native")
|
||||
local cells = {}
|
||||
for i = 1, 49 do cells[i] = { mid = 0, coll = 0, elev = 3 } end
|
||||
-- pokeemerald/src/event_object_movement.c:4663
|
||||
cells[2 * 7 + 3 + 1] = { mid = 1, coll = 7, elev = 0 }
|
||||
local map = { kind = "town", pair = "test", midLayout = Layout.fromDecoded({ width = 7, height = 7, cells = cells }),
|
||||
warps = { { x = 3, y = 2, destMap = "EM_CENTER", destX = 3, destY = 7 } } }
|
||||
local game = { data = { maps = { EM_TEST = map, EM_CENTER = { kind = "indoor" } } } }
|
||||
require("src.core.game3.scripting.interaction_scripts").behaviors.test = { [0] = MB.require("NORMAL"), [1] = MB.require("WARP_DOOR") }
|
||||
Field._game, Field._session, Field.locked = game, session, false
|
||||
local function input(dir, b)
|
||||
return { isDown = function(_, k) return k == dir or (k == "b" and b == true) end,
|
||||
wasPressed = function() return false end }
|
||||
end
|
||||
local function reset(kind, x, y, facing)
|
||||
entered, exit = nil, nil
|
||||
Collision.bindMap(game, "EM_TEST", map)
|
||||
Player.reset(x, y, facing)
|
||||
Player.biking, Player.bikeType = false, nil
|
||||
Bike.rse(session).getOnOff(kind, session)
|
||||
end
|
||||
local function step(dir)
|
||||
Player.update(game, input(dir))
|
||||
while Player.moving do Player.tick(game) end
|
||||
end
|
||||
T.check(Bike.rse({ version = "firered" }) == nil, "FRLG bicycle controller preserved")
|
||||
for _, kind in ipairs({ "mach", "acro" }) do
|
||||
reset(kind, 2, 3, "right")
|
||||
step("right")
|
||||
T.eq(Player.cellX, 3, kind .. " moving approach reached south of door")
|
||||
Player.update(game, input("up"))
|
||||
T.check(not Player.moving, kind .. " changed heading cannot enter unclaimed solid door")
|
||||
T.eq(Player.cellY, 3, kind .. " stays south of door")
|
||||
T.eq(entered, nil, kind .. " changed heading does not bypass facing gate")
|
||||
while Player.action do Player.tick(game) end
|
||||
Player.update(game, input("up"))
|
||||
T.eq(entered, "EM_CENTER", kind .. " next north input dispatches actual Player door trigger")
|
||||
reset(kind, 3, 3, "up")
|
||||
Player.update(game, input("up"))
|
||||
T.eq(entered, "EM_CENTER", kind .. " straight approach dispatches door")
|
||||
reset(kind, 2, 2, "right")
|
||||
Player.update(game, input("right"))
|
||||
T.check(not Player.moving, kind .. " side approach cannot occupy animated door")
|
||||
end
|
||||
reset("acro", 2, 3, "right")
|
||||
step("right")
|
||||
Player.update(game, input("up", true))
|
||||
T.check(not Player.moving and Player.cellY == 3, "Acro wheelie transition cannot bypass animated door collision")
|
||||
reset("acro", 3, 3, "up")
|
||||
Player.update(game, input("up", true))
|
||||
T.eq(entered, "EM_CENTER", "Acro B-held straight approach preserves door trigger")
|
||||
reset("mach", 3, 5, "up")
|
||||
step("up")
|
||||
step("up")
|
||||
Player.update(game, input(nil))
|
||||
T.check(not Player.moving, "Mach coast cannot occupy solid animated door")
|
||||
T.eq(entered, nil, "Mach release does not open door without held input")
|
||||
while Player.action do Player.tick(game) end
|
||||
Player.update(game, input("up"))
|
||||
T.eq(entered, "EM_CENTER", "Mach held north enters after coast collision")
|
||||
local exitCells = {}
|
||||
for i = 1, 49 do exitCells[i] = { mid = 0, coll = 0, elev = 3 } end
|
||||
exitCells[2 * 7 + 3 + 1] = { mid = 2, coll = 0, elev = 3 }
|
||||
require("src.core.game3.scripting.interaction_scripts").behaviors.test[2] = MB.require("SOUTH_ARROW_WARP")
|
||||
local exitMap = { kind = "indoor", pair = "test",
|
||||
midLayout = Layout.fromDecoded({ width = 7, height = 7, cells = exitCells }),
|
||||
warps = { { x = 3, y = 2, destMap = "EM_TEST", destX = 3, destY = 2 } } }
|
||||
game.data.maps.EM_CENTER = exitMap
|
||||
for _, kind in ipairs({ "mach", "acro" }) do
|
||||
reset(kind, 3, 2, "down")
|
||||
Collision.bindMap(game, "EM_CENTER", exitMap)
|
||||
Player.update(game, input("down"))
|
||||
T.eq(exit, "EM_TEST", kind .. " exit mat dispatch preserved")
|
||||
end
|
||||
T.finish("game3_em_bike_doors_2637")
|
||||
@@ -0,0 +1,217 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
_G.love = require("tests.love_stub")
|
||||
local T = require("tests.harness")
|
||||
local Battle = require("src.core.game3.battle.init")
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
local Dex = require("src.ui.game3.rse.pokedex")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local Text = require("src.core.game3.battle.battle_text")
|
||||
local Pal = require("src.core.game3.pal_fade")
|
||||
local Chrome = require("src.ui.game3.battle_chrome")
|
||||
local Bg = require("src.core.game3.battle.bg")
|
||||
Text.get = function(_, args) return args and args.opponentMon1 or "caught" end
|
||||
Pokemon.name = function() return "WURMPLE" end
|
||||
Pokemon.speciesFromNational = function() return 290 end
|
||||
Pokemon.picSpecies = function(sp) return sp end
|
||||
local shinyImage, normalImage = {}, {}
|
||||
Pokemon.frontPic = function(_, _, shiny) return { image = shiny and shinyImage or normalImage } end
|
||||
local dexOpts, answer, namingOpts, gave
|
||||
Dex.showCaughtMon = function(_, opts) dexOpts = opts end
|
||||
Ui.askYesNo = function(_, cb) answer = cb end
|
||||
package.loaded["src.ui.game3.naming"] = {
|
||||
open = function(opts) namingOpts = opts end,
|
||||
isOpen = function() return namingOpts ~= nil end,
|
||||
monTitle = function(name) return name end,
|
||||
}
|
||||
package.loaded["src.core.game3.battle.catching"] = {
|
||||
givePending = function(_, res) gave = res.mon; res.pending = nil end,
|
||||
}
|
||||
local Storage = require("src.core.game3.storage")
|
||||
Storage.pcTransferMessage = function() return "transfer" end
|
||||
local function begin(version, first, location)
|
||||
dexOpts, answer, namingOpts, gave = nil, nil, nil, nil
|
||||
Ui.reset({ headless = false })
|
||||
local mon = { species = 290, name = "WURMPLE", personality = 0x10001, otId = 123, otSecretId = 0 }
|
||||
local enemyMon = { species = 290, name = "WURMPLE", personality = 0x10001, otId = 0, otSecretId = 0 }
|
||||
local session = { version = version, party = {}, flags = {}, vars = {} }
|
||||
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
|
||||
Battle._active, Battle._headless = true, false
|
||||
Battle._st = { session = session, enemy = { mon = enemyMon } }
|
||||
local res = { mon = mon, firstTimeCaught = first, location = location or "party", pending = location == "pc" }
|
||||
Battle.startPostCatchFlow(res)
|
||||
return mon, res
|
||||
end
|
||||
local function leaveDex()
|
||||
local sprite = { img = normalImage, x = 92, y = 80, x2 = 0, y2 = 0 }
|
||||
local s = { pal = Pal.new(), caught = { dexNum = 265, personality = dexOpts.personality,
|
||||
otId = dexOpts.otId, otSecretId = dexOpts.otSecretId, shiny = dexOpts.shiny,
|
||||
mon = sprite, onDone = dexOpts.onDone } }
|
||||
Dex.Host._s = s
|
||||
Dex.tasks.caughtExit(s)
|
||||
T.eq(Dex.Host._s, nil, "dex fullscreen state released")
|
||||
T.check(Ui._caughtDexScene and Ui._caughtDexScene.sprite == sprite, "same caught sprite retained across callback")
|
||||
T.eq(sprite.img, shinyImage, "caught exit reloads shiny sprite palette from mon identity")
|
||||
T.eq(Battle._phase, "catch_dex_return", "background return fade owns phase before question")
|
||||
T.eq(answer, nil, "nickname input waits for BG return fade")
|
||||
T.eq(Ui._caughtDexScene and Ui._caughtDexScene.pal.fade.mask, 0xFFFF, "Emerald fades BG palettes only")
|
||||
T.eq(Ui._caughtDexScene and Ui._caughtDexScene.pal.slots[16].y, 0, "Emerald retained OBJ palette remains visible")
|
||||
Battle._headless = true
|
||||
for _ = 1, 60 do Battle.update(1 / 60, {}) end
|
||||
Battle._headless = false
|
||||
T.eq(Battle._phase, "catch_nickname_prompt", "battle dispatch reaches nickname question after fade")
|
||||
T.eq(sprite.x, 120, "retained sprite reaches Emerald x120")
|
||||
T.eq(sprite.y, 80, "retained sprite reaches Emerald y80")
|
||||
T.eq(Ui._caughtDexScene and Ui._caughtDexScene.personality, 0x10001, "handoff preserves personality")
|
||||
T.eq(Ui._caughtDexScene and Ui._caughtDexScene.otId, 0, "dex retains target OT instead of newly assigned caught OT")
|
||||
return sprite
|
||||
end
|
||||
local mon = begin("emerald", true)
|
||||
local sprite = leaveDex()
|
||||
local drewBg, drewPanel, draws, normalBattle = 0, 0, {}, 0
|
||||
Chrome.drawPostDexBg = function() drewBg = drewBg + 1 end
|
||||
Chrome.drawPanel = function(mode) if mode == "none" then drewPanel = drewPanel + 1 end end
|
||||
Bg.sheetKey = function() return "grass" end
|
||||
Bg.draw = function() normalBattle = normalBattle + 1 end
|
||||
love.graphics.draw = function(img, x, y) draws[#draws + 1] = { img, x, y } end
|
||||
Ui.draw()
|
||||
T.eq(drewBg, 1, "question draws ROM post-dex BG3 screen")
|
||||
T.eq(drewPanel, 1, "question draws battle textbox")
|
||||
T.eq(normalBattle, 0, "question does not draw normal battlefield")
|
||||
T.eq(draws[1] and draws[1][1], shinyImage, "question draws retained mon rather than captured battler")
|
||||
T.eq(draws[1] and draws[1][2], 120, "question composite uses Emerald center")
|
||||
answer(true)
|
||||
T.eq(Ui._caughtDexScene, nil, "Yes releases retained presentation before fullscreen naming")
|
||||
T.eq(namingOpts.species, 290, "Yes names caught species")
|
||||
T.eq(namingOpts.personality, 0x10001, "Yes passes caught personality")
|
||||
namingOpts.onDone("BUG")
|
||||
T.eq(mon.nickname, "BUG", "naming completion writes caught mon nickname")
|
||||
T.eq(Battle._phase, "ending", "party nickname completion ends capture")
|
||||
begin("emerald", true)
|
||||
leaveDex()
|
||||
answer(false)
|
||||
T.eq(Ui._caughtDexScene, nil, "No clears retained presentation")
|
||||
T.eq(Battle._phase, "ending", "No completes party capture")
|
||||
local pcMon = begin("emerald", true, "pc")
|
||||
leaveDex()
|
||||
answer(true)
|
||||
T.eq(namingOpts.sentToPc, true, "PC capture preserves naming destination")
|
||||
namingOpts.onDone("PCBUG")
|
||||
T.eq(gave, pcMon, "PC completion gives the same caught mon")
|
||||
T.eq(pcMon.nickname, "PCBUG", "PC destination retains nickname")
|
||||
local noPcMon = begin("emerald", true, "pc")
|
||||
leaveDex()
|
||||
answer(false)
|
||||
T.eq(gave, noPcMon, "PC No gives the same caught mon")
|
||||
T.eq(Battle._phase, "catch_pc_msg", "PC No preserves transfer message phase")
|
||||
local canceledMon = begin("emerald", true)
|
||||
leaveDex()
|
||||
answer(true)
|
||||
local staleName = namingOpts.onDone
|
||||
Battle.reset()
|
||||
staleName("STALE")
|
||||
T.eq(canceledMon.nickname, nil, "reset ignores stale naming completion")
|
||||
begin("emerald", true)
|
||||
local stale = dexOpts.onDone
|
||||
Battle.reset()
|
||||
stale({ sprite = sprite })
|
||||
T.eq(Ui._caughtDexScene, nil, "reset rejects a stale dex callback")
|
||||
T.eq(Battle._rseDex, nil, "reset clears dex ownership")
|
||||
begin("emerald", true)
|
||||
leaveDex()
|
||||
Battle._st = nil
|
||||
Battle.abort("run")
|
||||
T.eq(Ui._caughtDexScene, nil, "abort clears retained presentation")
|
||||
answer(true)
|
||||
T.eq(namingOpts, nil, "aborted question cannot open naming")
|
||||
begin("emerald", false)
|
||||
T.eq(dexOpts, nil, "repeat capture skips dex")
|
||||
T.eq(Ui._caughtDexScene, nil, "repeat capture keeps ordinary battle scene")
|
||||
T.eq(Battle._phase, "catch_nickname_prompt", "repeat capture directly asks nickname")
|
||||
answer(false)
|
||||
for _, version in ipairs({ "emerald", "firered", "leafgreen" }) do
|
||||
local registration
|
||||
package.loaded["src.ui.game3.pokedex"] = { showRegistration = function(_, opts) registration = opts end }
|
||||
for _, first in ipairs({ true, false }) do
|
||||
for _, yes in ipairs({ true, false }) do
|
||||
for _, location in ipairs({ "party", "pc" }) do
|
||||
registration = nil
|
||||
local label = version .. " " .. tostring(first) .. " " .. tostring(yes) .. " " .. location
|
||||
local caught = begin(version, first, location)
|
||||
if first then
|
||||
if version == "emerald" then leaveDex() else
|
||||
T.check(registration ~= nil, label .. " registers new capture")
|
||||
registration.onDone()
|
||||
local c = Ui._caughtDexScene
|
||||
T.check(c and c.sprite.img == shinyImage, label .. " creates target shiny front sprite")
|
||||
T.eq(c and c.otId, 0, label .. " keeps enemy OT")
|
||||
T.eq(c and c.personality, 0x10001, label .. " keeps enemy personality")
|
||||
T.eq(c and c.pal.fade.mask, 0x1FFFF, label .. " BG and OBJ0 fade mask")
|
||||
if c then c.pal:updateFade() end
|
||||
T.eq(c and c.pal.slots[16].y, 16, label .. " caught OBJ0 fades from black")
|
||||
T.eq(answer, nil, label .. " input waits for return fade")
|
||||
Battle._headless = true
|
||||
for _ = 1, 60 do Battle.update(1 / 60, {}) end
|
||||
Battle._headless = false
|
||||
end
|
||||
local c = Ui._caughtDexScene
|
||||
T.eq(c and c.sprite.x, 120, label .. " caught center X")
|
||||
T.eq(c and c.sprite.y, version == "emerald" and 80 or 64, label .. " family center Y")
|
||||
drewBg, normalBattle, draws = 0, 0, {}
|
||||
Ui.draw()
|
||||
T.eq(drewBg, 1, label .. " draws ROM second screen")
|
||||
T.eq(normalBattle, 0, label .. " excludes ordinary battlefield")
|
||||
T.eq(draws[1] and draws[1][1], shinyImage, label .. " draws caught sprite")
|
||||
else
|
||||
T.eq(registration, nil, label .. " repeat skips FRLG registration")
|
||||
T.eq(dexOpts, nil, label .. " repeat skips RSE registration")
|
||||
T.eq(Ui._caughtDexScene, nil, label .. " repeat retains ordinary scene")
|
||||
end
|
||||
T.eq(Battle._phase, "catch_nickname_prompt", label .. " reaches question")
|
||||
answer(yes)
|
||||
if yes then
|
||||
T.eq(Ui._caughtDexScene, nil, label .. " naming resets sprites")
|
||||
T.eq(namingOpts.sentToPc, location == "pc", label .. " naming destination")
|
||||
namingOpts.onDone("NAMED")
|
||||
T.eq(caught.nickname, "NAMED", label .. " stores nickname")
|
||||
T.eq(Ui._caughtDexScene, nil, label .. " naming return does not resurrect scene")
|
||||
elseif location == "pc" then
|
||||
T.eq(Ui._caughtDexScene ~= nil, first, label .. " PC No keeps first-capture scene")
|
||||
T.eq(Battle._phase, "catch_pc_msg", label .. " PC message phase")
|
||||
if first then
|
||||
drewBg, normalBattle, draws = 0, 0, {}
|
||||
Ui.draw()
|
||||
T.eq(drewBg, 1, label .. " PC message draws right screen")
|
||||
T.eq(normalBattle, 0, label .. " PC message excludes ordinary battlefield")
|
||||
T.eq(draws[1] and draws[1][3], version == "emerald" and 80 or 64, label .. " PC message center Y")
|
||||
end
|
||||
else T.eq(Ui._caughtDexScene, nil, label .. " party terminal clears scene") end
|
||||
if location == "pc" then T.eq(gave, caught, label .. " gives same pending mon") end
|
||||
local staleAnswer = answer
|
||||
Battle.reset()
|
||||
T.eq(Ui._caughtDexScene, nil, label .. " reset clears scene")
|
||||
namingOpts = nil
|
||||
staleAnswer(true)
|
||||
T.eq(namingOpts, nil, label .. " stale question cannot reopen naming")
|
||||
end
|
||||
end
|
||||
end
|
||||
begin(version, true)
|
||||
local staleRegistration = version == "emerald" and dexOpts.onDone or registration.onDone
|
||||
Battle.reset()
|
||||
staleRegistration({ sprite = sprite })
|
||||
T.eq(Ui._caughtDexScene, nil, version .. " stale registration cannot restore scene")
|
||||
begin(version, true)
|
||||
if version == "emerald" then leaveDex() else
|
||||
registration.onDone()
|
||||
Battle._headless = true
|
||||
for _ = 1, 60 do Battle.update(1 / 60, {}) end
|
||||
Battle._headless = false
|
||||
end
|
||||
local abortedAnswer = answer
|
||||
Battle._st = nil
|
||||
Battle.abort("run")
|
||||
T.eq(Ui._caughtDexScene, nil, version .. " abort clears scene")
|
||||
abortedAnswer(true)
|
||||
T.eq(namingOpts, nil, version .. " aborted question stays closed")
|
||||
end
|
||||
T.finish("game3_em_catch_prompt_2637")
|
||||
@@ -0,0 +1,65 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set("emerald")
|
||||
require("src.import.gba.versions").select("emerald")
|
||||
require("tests.game3_cache").mountOrSkip("game3_frontier_obedience_2638", "pokemon/battle_moves.lua")
|
||||
require("tests.fixture_data.game3_items").install()
|
||||
|
||||
local Battle = require("src.core.game3.battle.init")
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
local Adapter = require("src.core.game3.battle.adapter")
|
||||
local BattleProfile = require("src.core.game3.battle.profile")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local T = require("tests.harness")
|
||||
|
||||
local function mon(species, otId, fateful)
|
||||
return { species = species, level = 30, hp = 200, maxHp = 200,
|
||||
attack = 80, defense = 80, spAtk = 80, spDef = 80, speed = 80, ability = 0,
|
||||
nickname = "RENTAL", otId = otId, otName = otId == 1 and "PLAYER" or "RENTAL",
|
||||
fatefulEncounter = fateful, moves = { 33, 0, 0, 0 }, pp = { 35, 0, 0, 0 } }
|
||||
end
|
||||
|
||||
local function run(o)
|
||||
GameVersion.set(o.version or "emerald")
|
||||
local user, foe = mon(o.species or 1, o.own and 1 or 2, o.fateful), mon(1, 3)
|
||||
local session = { version = o.version or "emerald", name = "PLAYER", trainerId = 1, party = { user } }
|
||||
Battle.start({ headless = false, autoFight = false, session = session, playerParty = session.party,
|
||||
foe = { species = 1, level = 30, mon = foe }, wild = o.wild == true, frontier = o.frontier,
|
||||
rng = function(lo, hi) if lo == 1 and hi == 100 then return 1 end return hi end })
|
||||
local st = assert(Battle.getState())
|
||||
st.badges, st.playerTrainerId, st.playerOtName = {}, 1, "PLAYER"
|
||||
st.link = o.link == true
|
||||
if o.badge then
|
||||
st.badges = nil
|
||||
Space.store = Flags.newStore()
|
||||
Flags.setFlag(Space.store, nil, BattleProfile.of(st).badgeFlags[o.badge], true)
|
||||
T.check(Engine.hasBadge(st, o.badge), o.label .. " canonical Emerald badge flag is read")
|
||||
end
|
||||
T.eq(st.kinds.frontier == true, o.frontier == true, o.label .. " Battle.start propagates Frontier kind")
|
||||
T.check(o.own or Engine.isTradedMon(st, st.player.mon), o.label .. " foreign rental OT is preserved")
|
||||
local hp, pp = st.enemy.mon.hp, st.player.mon.pp[1]
|
||||
local out = {}
|
||||
Engine.resolveMove(st.player, st.enemy, 33, 1, Adapter.new(st), st, out)
|
||||
T.eq(out._anim.cancelled ~= true, o.obey, o.label .. " real move caller follows obedience gate")
|
||||
T.eq(st.enemy.mon.hp < hp, o.obey, o.label .. " selected move damages foe only when obedient")
|
||||
T.eq(st.player.mon.pp[1] < pp, o.obey, o.label .. " selected move consumes PP only when obedient")
|
||||
Battle.reset()
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_util.c:3909
|
||||
run({ frontier = true, obey = true, label = "Emerald Frontier rental" })
|
||||
run({ obey = false, label = "Emerald ordinary traded trainer" })
|
||||
run({ wild = true, obey = false, label = "Emerald ordinary traded wild" })
|
||||
run({ frontier = true, species = 151, fateful = false, own = true, obey = false, label = "Illegal Mew Frontier" })
|
||||
run({ frontier = true, species = 410, fateful = false, own = true, obey = false, label = "Illegal Deoxys Frontier" })
|
||||
run({ frontier = true, species = 151, fateful = true, obey = true, label = "Legal Mew Frontier" })
|
||||
run({ frontier = true, species = 410, fateful = true, obey = true, label = "Legal Deoxys Frontier" })
|
||||
run({ version = "firered", frontier = true, obey = false, label = "FireRed Frontier flag control" })
|
||||
run({ version = "leafgreen", frontier = true, obey = false, label = "LeafGreen Frontier flag control" })
|
||||
run({ own = true, obey = true, label = "Emerald own OT" })
|
||||
run({ link = true, obey = true, label = "Emerald link" })
|
||||
run({ badge = 2, obey = true, label = "Emerald badge two level 30" })
|
||||
run({ badge = 8, obey = true, label = "Emerald badge eight" })
|
||||
T.finish("game3_frontier_obedience_2638")
|
||||
@@ -0,0 +1,94 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
local Extractor = require("src.import.RomExtractorGen3")
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local plans = {
|
||||
frlg = require("src.import.gba.plans.frlg"),
|
||||
rse = require("src.import.gba.plans.rse"),
|
||||
}
|
||||
local getenv = os.getenv
|
||||
local flags = { "HANDHELD", "POKEPORT_HANDHELD", "PORTMASTER" }
|
||||
local cases = {
|
||||
{ "Linux desktop", "Linux", 8, {}, 6 },
|
||||
{ "Linux four cores", "Linux", 4, {}, 3 },
|
||||
{ "single core", "Linux", 1, {}, 1 },
|
||||
{ "two cores", "Linux", 2, {}, 1 },
|
||||
{ "unknown OS", "", 8, {}, 6 },
|
||||
{ "Android", "Android", 8, {}, 2 },
|
||||
{ "iOS", "iOS", 8, {}, 2 },
|
||||
{ "NX", "NX", 8, {}, 2 },
|
||||
{ "disabled flags", "Linux", 8, { HANDHELD = "0", POKEPORT_HANDHELD = "0", PORTMASTER = "0" }, 6 },
|
||||
{ "override two", "Linux", 8, { HANDHELD = "1", POKEPORT_EXTRACT_WORKERS = "2" }, 2 },
|
||||
{ "override one", "Linux", 8, { PORTMASTER = "1", POKEPORT_EXTRACT_WORKERS = "1" }, 1 },
|
||||
{ "override floor", "Linux", 8, { POKEPORT_HANDHELD = "1", POKEPORT_EXTRACT_WORKERS = "2.9" }, 2 },
|
||||
{ "override cap", "Linux", 8, { PORTMASTER = "1", POKEPORT_EXTRACT_WORKERS = "100" }, 100 },
|
||||
{ "invalid override", "Linux", 8, { PORTMASTER = "1", POKEPORT_EXTRACT_WORKERS = "bad" }, 1 },
|
||||
{ "zero override", "Linux", 8, { PORTMASTER = "1", POKEPORT_EXTRACT_WORKERS = "0" }, 1 },
|
||||
}
|
||||
for _, flag in ipairs(flags) do
|
||||
for _, platform in ipairs({ "Linux", "OS X" }) do
|
||||
cases[#cases + 1] = { platform .. " " .. flag, platform, 8, { [flag] = "1" }, 1 }
|
||||
end
|
||||
end
|
||||
|
||||
local function run(plan, case, failure)
|
||||
local live, maximum, starts, completions, pending, writes = 0, 0, {}, {}, {}, {}
|
||||
local env = case[4]
|
||||
os.getenv = function(key)
|
||||
if key == "POKEPORT_NO_THREAD" or key == "POKEPORT_EXTRACT_WORKERS" then return env[key] end
|
||||
for _, flag in ipairs(flags) do if key == flag then return env[key] end end
|
||||
return getenv(key)
|
||||
end
|
||||
CacheFs.exists = function() return false end
|
||||
CacheFs.write = function(path, bytes) writes[path] = bytes; return true end
|
||||
_G.love = {
|
||||
system = { getOS = function() return case[2] end, getProcessorCount = function() return case[3] end },
|
||||
timer = { getTime = function() return 10 end, sleep = function() end },
|
||||
thread = {
|
||||
getChannel = function()
|
||||
return { pop = function()
|
||||
local task = table.remove(pending, 1)
|
||||
if not task then return nil end
|
||||
live = live - 1
|
||||
completions[task] = (completions[task] or 0) + 1
|
||||
return { type = "done", task = task, ok = task ~= failure, error = task == failure and "fixture failure" or nil }
|
||||
end }
|
||||
end,
|
||||
newThread = function()
|
||||
return {
|
||||
start = function(_, task, _, rom, sha1)
|
||||
T.eq(rom, "ROM fixture", "scheduler transfers unchanged ROM")
|
||||
T.eq(sha1, "fixture sha1", "scheduler transfers SHA")
|
||||
live = live + 1
|
||||
maximum = math.max(maximum, live)
|
||||
starts[task] = (starts[task] or 0) + 1
|
||||
pending[#pending + 1] = task
|
||||
end,
|
||||
getError = function() return nil end,
|
||||
}
|
||||
end,
|
||||
},
|
||||
}
|
||||
local ex = setmetatable({ romData = "ROM fixture", plan = plan }, Extractor)
|
||||
local ok, err = ex:runParallel("fixture sha1")
|
||||
local label = case[1] .. " " .. plan.id
|
||||
T.eq(ok, failure == nil, label .. " scheduler result")
|
||||
T.eq(maximum, math.min(#plan.tasks, case[5]), label .. " maximum concurrent nested workers")
|
||||
T.eq(live, 0, label .. " completes all workers")
|
||||
for _, task in ipairs(plan.tasks) do
|
||||
T.eq(starts[task.id], 1, label .. " starts " .. task.id .. " exactly once")
|
||||
T.eq(completions[task.id], 1, label .. " completes " .. task.id .. " exactly once")
|
||||
end
|
||||
if failure then
|
||||
T.check(tostring(err):find("fixture failure", 1, true) ~= nil, label .. " propagates worker failure")
|
||||
T.eq(next(writes), nil, label .. " failed tasks cannot publish success output")
|
||||
else
|
||||
T.check(next(writes) ~= nil, label .. " successful pool publishes maps inventory")
|
||||
end
|
||||
end
|
||||
for _, plan in pairs(plans) do
|
||||
for _, case in ipairs(cases) do run(plan, case) end
|
||||
run(plan, { "failed handheld task", "Linux", 8, { PORTMASTER = "1" }, 1 }, plan.tasks[1].id)
|
||||
end
|
||||
os.getenv = getenv
|
||||
T.finish("game3_import_worker_policy_2636")
|
||||
@@ -0,0 +1,207 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
local Extract = require("src.import.gba.battle_chrome_extract")
|
||||
local Lz77 = require("src.import.gba.lz77")
|
||||
local V = require("src.import.gba.games.emerald")
|
||||
local cfg = V.BATTLE_UI
|
||||
local originals = { decompress = Lz77.decompress, terrainTable = Extract.requireTerrainTable }
|
||||
local data, written = {}, {}
|
||||
local terrainRows = {}
|
||||
local gfx = string.rep("\x11", 32) .. string.rep("\x22", 32)
|
||||
local palette = "\0\0\x1f\0\xe0\3" .. string.rep("\0", 26)
|
||||
local map = string.rep("\0\x20", 1024) .. string.rep("\1\x20", 1024)
|
||||
local function terrain(tr)
|
||||
data[tr.tiles], data[tr.pal], data[tr.tilemap] = gfx, palette, map
|
||||
end
|
||||
for id = 0, cfg.terrain_count - 1 do
|
||||
local tr = { tiles = 100 + id * 3, pal = 101 + id * 3, tilemap = 102 + id * 3 }
|
||||
terrain(tr)
|
||||
terrainRows[#terrainRows + 1] = { key = Extract.TERRAIN_KEYS[id], id = id, cfg = tr }
|
||||
end
|
||||
for _, scene in ipairs(cfg.scenes) do terrain(scene.cfg) end
|
||||
Extract.requireTerrainTable = function() return terrainRows end
|
||||
Lz77.decompress = function(_, offset) return data[offset] or string.rep("\0", 32) end
|
||||
local rom = { get = function() return 0 end, u16 = function() return 0 end }
|
||||
local cache = { write = function(_, path, bytes) written[path] = bytes; return true end }
|
||||
local root = "fixture/pokemon/battle/"
|
||||
local function bake()
|
||||
Extract.runRse(rom, cache, { cacheRoot = "fixture" }, cfg)
|
||||
return assert(loadstring(written[root .. "manifest.lua"]))()
|
||||
end
|
||||
local manifest = bake()
|
||||
local green = string.char(0, 255, 0, 255)
|
||||
local red = string.char(255, 0, 0, 255)
|
||||
for key, info in pairs(manifest.terrains) do
|
||||
T.eq(info.postDexFile, "terrain_" .. key .. "_post_dex.rgba", key .. " manifest retains right screen")
|
||||
local pixels = written[root .. (info.postDexFile or "missing")]
|
||||
T.eq(pixels and #pixels, 256 * 256 * 4, key .. " right screen dimensions")
|
||||
T.eq(pixels and pixels:sub(1, 4), green, key .. " right screen uses second map block and palette bank2")
|
||||
T.eq(written[root .. info.file]:sub(1, 4), red, key .. " ordinary left screen unchanged")
|
||||
end
|
||||
local tr = terrainRows[1].cfg
|
||||
local savedMap = data[tr.tilemap]
|
||||
data[tr.tilemap] = savedMap:sub(1, 2048)
|
||||
T.raises(bake, "post-dex terrain map", "truncated ROM map cannot invent right screen")
|
||||
data[tr.tilemap] = savedMap
|
||||
local Versions = require("src.import.gba.versions")
|
||||
local frlgManifests = {}
|
||||
Extract.requireTerrainTable = originals.terrainTable
|
||||
local profiles = {
|
||||
{ "fr10", "41cb23d8dccc8ebd7c649cd8fbb58eeace6e2fdc" },
|
||||
{ "fr11", "dd5945db9b930750cb39d00c84da8571feebf417" },
|
||||
{ "lg10", "574fa542ffebb14be69902d1d36f1ec0a4afd71e" },
|
||||
{ "lg11", "7862c67bdecbe21d1d69ce082ce34327e1c6ed5e" },
|
||||
}
|
||||
for _, edition in ipairs(profiles) do
|
||||
Versions.select(edition[2])
|
||||
local frCfg, bytes, rows = Versions.BATTLE_UI, {}, {}
|
||||
local tableBase = frCfg.terrain_table or Versions.address(Extract.TERRAIN_TABLE)
|
||||
local function pointer(off, value)
|
||||
for i = 0, 3 do bytes[off + i] = math.floor((value + 0x08000000) / 256 ^ i) % 256 end
|
||||
end
|
||||
for id = 0, 19 do
|
||||
local row = id == 0 and frCfg.terrain_grass
|
||||
or { tiles = 1000 + id * 3, pal = 1001 + id * 3, tilemap = 1002 + id * 3 }
|
||||
terrain(row)
|
||||
pointer(tableBase + id * 20, row.tiles)
|
||||
pointer(tableBase + id * 20 + 4, row.tilemap)
|
||||
pointer(tableBase + id * 20 + 16, row.pal)
|
||||
rows[#rows + 1] = row
|
||||
end
|
||||
local frRom = { get = function(_, off) return bytes[off] or 0 end }
|
||||
local function bakeFr()
|
||||
Extract.run(frRom, cache, { cacheRoot = "fixture" })
|
||||
return assert(loadstring(written[root .. "manifest.lua"]))()
|
||||
end
|
||||
local decoded = Extract.requireTerrainTable(function(off) return frRom:get(off) end, frCfg)
|
||||
T.eq(#decoded, 20, edition[1] .. " actual ROM terrain table decodes20 rows")
|
||||
local frManifest, count = bakeFr(), 0
|
||||
frlgManifests[edition[1]] = frManifest
|
||||
for key, info in pairs(frManifest.terrains) do
|
||||
count = count + 1
|
||||
T.eq(info.postDexFile, "terrain_" .. key .. "_post_dex.rgba", edition[1] .. " " .. key .. " manifest retains right screen")
|
||||
local pixels = written[root .. (info.postDexFile or "missing")]
|
||||
T.eq(pixels and #pixels, 256 * 256 * 4, edition[1] .. " " .. key .. " right screen dimensions")
|
||||
T.eq(pixels and pixels:sub(1, 4), green, edition[1] .. " " .. key .. " right screen uses second map block and palette bank2")
|
||||
T.eq(written[root .. info.file]:sub(1, 4), red, edition[1] .. " " .. key .. " ordinary left screen unchanged")
|
||||
end
|
||||
T.eq(count, 20, edition[1] .. " all20 terrains preserved")
|
||||
data[rows[1].tilemap] = map:sub(1, 2048)
|
||||
T.raises(bakeFr, "post-dex terrain map", edition[1] .. " truncated ROM map cannot invent right screen")
|
||||
data[rows[1].tilemap] = map
|
||||
end
|
||||
Versions.select("firered")
|
||||
Lz77.decompress, Extract.requireTerrainTable = originals.decompress, originals.terrainTable
|
||||
|
||||
local draws, imageReads = {}, {}
|
||||
_G.love = require("tests.love_stub")
|
||||
love.image.newImageData = function(w, h, _, bytes) return { w = w, h = h, bytes = bytes } end
|
||||
love.graphics.newImage = function(id)
|
||||
return { data = id, setFilter = function() end, getDimensions = function() return id.w, id.h end }
|
||||
end
|
||||
love.graphics.newQuad = function(...) return { ... } end
|
||||
love.graphics.draw = function(...) draws[#draws + 1] = { ... } end
|
||||
local Chrome = require("src.ui.game3.battle_chrome")
|
||||
local function install(m, bytes)
|
||||
draws = {}
|
||||
Chrome.install({ read = function(_, path)
|
||||
if path:match("/manifest.lua$") then return m end
|
||||
local file = path:match("/([^/]+)$")
|
||||
imageReads[file] = (imageReads[file] or 0) + 1
|
||||
return bytes[file]
|
||||
end })
|
||||
end
|
||||
local rgba = string.rep(green, 256 * 256)
|
||||
local assets = { ["terrain_grass.rgba"] = string.rep(red, 256 * 256), ["terrain_grass_post_dex.rgba"] = rgba }
|
||||
local metadata = 'return { layout="rse", terrains={ grass={ file="terrain_grass.rgba", postDexFile="terrain_grass_post_dex.rgba", w=256, h=256 } } }'
|
||||
install(metadata, assets)
|
||||
T.check(type(Chrome.drawPostDexBg) == "function", "renderer exposes post-dex ROM scene")
|
||||
if Chrome.drawPostDexBg then
|
||||
T.eq(Chrome.drawPostDexBg("grass"), true, "draw succeeds with required ROM data")
|
||||
T.eq(#draws, 1, "post-dex draw has one background")
|
||||
T.check(draws[1][1].data.bytes == rgba, "draw selects right screen image")
|
||||
T.same(draws[1][2], { 0, 0, 240, 112, 256, 256 }, "post-dex crop protects lower textbox")
|
||||
T.eq(draws[1][3], 0, "draw starts at screen x0")
|
||||
T.eq(draws[1][4], 0, "draw starts at screen y0")
|
||||
T.raises(function() Chrome.drawPostDexBg("missing") end, "post-dex", "unknown RSE terrain cannot substitute grass")
|
||||
assets["terrain_grass_post_dex.rgba"] = nil
|
||||
install(metadata, assets)
|
||||
T.raises(function() Chrome.drawPostDexBg("grass") end, "post-dex", "missing asset cannot substitute ordinary background")
|
||||
install('return { layout="rse", terrains={ grass={file="terrain_grass.rgba",w=256,h=256} } }', assets)
|
||||
T.raises(function() Chrome.drawPostDexBg("grass") end, "post-dex", "old manifest cannot degrade post-dex scene")
|
||||
assets["terrain_grass_post_dex.rgba"] = rgba
|
||||
for _, family in ipairs({ "firered", "leafgreen" }) do
|
||||
install('return { terrains={grass={file="terrain_grass.rgba",postDexFile="terrain_grass_post_dex.rgba",w=256,h=256}} }', assets)
|
||||
T.eq(Chrome.drawPostDexBg("grass"), true, family .. " draws its required post-dex ROM image")
|
||||
T.eq(#draws, 1, family .. " post-dex API draws one background")
|
||||
local draw = draws[1]
|
||||
T.check(draw and draw[1].data.bytes == rgba, family .. " draw selects second screen")
|
||||
T.same(draw and draw[2], { 0, 0, 240, 112, 256, 256 }, family .. " crop protects textbox")
|
||||
T.raises(function() Chrome.drawPostDexBg("missing") end, "post-dex", family .. " cannot substitute grass for unknown terrain")
|
||||
assets["terrain_grass_post_dex.rgba"] = nil
|
||||
install('return { terrains={grass={file="terrain_grass.rgba",postDexFile="terrain_grass_post_dex.rgba",w=256,h=256}} }', assets)
|
||||
T.raises(function() Chrome.drawPostDexBg("grass") end, "post-dex", family .. " missing asset cannot substitute ordinary background")
|
||||
assets["terrain_grass_post_dex.rgba"] = rgba
|
||||
install('return { terrains={grass={file="terrain_grass.rgba",w=256,h=256}} }', assets)
|
||||
T.raises(function() Chrome.drawPostDexBg("grass") end, "post-dex", family .. " old manifest cannot degrade post-dex scene")
|
||||
end
|
||||
end
|
||||
local Contract = require("src.import.CacheContract")
|
||||
local required = Contract.requiredFiles("emerald")
|
||||
local complete, requiredSet = {}, {}
|
||||
for _, path in ipairs(required) do complete["emerald/" .. path] = "fixture"; requiredSet[path] = true end
|
||||
local fs = { prefix = "" }
|
||||
fs.exists = function(path) return complete[fs.prefix .. path] ~= nil end
|
||||
T.eq(Contract.allRequiredFilesExist("emerald", fs), true, "complete Emerald contract is valid")
|
||||
for key, info in pairs(manifest.terrains) do
|
||||
local path = "data/generated/gba/pokemon/battle/" .. (info.postDexFile or "missing")
|
||||
T.check(requiredSet[path], key .. " post-dex output is required")
|
||||
local saved = complete["emerald/" .. path]
|
||||
complete["emerald/" .. path] = nil
|
||||
local ok, missing = Contract.allRequiredFilesExist("emerald", fs)
|
||||
T.eq(ok, false, key .. " missing post-dex data blocks readiness")
|
||||
T.eq(missing, path, key .. " contract identifies missing post-dex output")
|
||||
complete["emerald/" .. path] = saved
|
||||
end
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
for _, profile in ipairs(profiles) do
|
||||
local version = GameVersion.forSha1(profile[2])
|
||||
Versions.select(profile[2])
|
||||
T.eq(Versions.CACHE_VERSION, 128, profile[1] .. " active edition retains shared128 cache stamp")
|
||||
local files, set = {}, {}
|
||||
for _, path in ipairs(Contract.requiredFiles(version)) do
|
||||
files[version .. "/" .. path] = "fixture"
|
||||
set[path] = true
|
||||
end
|
||||
local meta = version .. "/data/generated/gba/meta.json"
|
||||
files[version .. "/" .. Contract.MARKER_PATH] = Contract.markerFor(version, profile[2])
|
||||
local profileFs = { prefix = "" }
|
||||
profileFs.exists = function(path) return files[profileFs.prefix .. path] ~= nil end
|
||||
profileFs.read = function(path) return files[profileFs.prefix .. path] end
|
||||
files[meta] = '{"cache_version":127}'
|
||||
T.eq(Contract.isReady(version, profileFs), false, profile[1] .. " old127 stamp requires reimport")
|
||||
files[meta] = '{"cache_version":128}'
|
||||
T.eq(Contract.isReady(version, profileFs), true, profile[1] .. " complete128 cache is ready")
|
||||
local count = 0
|
||||
for key, info in pairs(frlgManifests[profile[1]].terrains) do
|
||||
count = count + 1
|
||||
local path = "data/generated/gba/pokemon/battle/" .. info.postDexFile
|
||||
T.check(set[path], profile[1] .. " " .. key .. " post-dex output is required")
|
||||
local saved = files[version .. "/" .. path]
|
||||
files[version .. "/" .. path] = nil
|
||||
T.eq(Contract.isReady(version, profileFs), false, profile[1] .. " " .. key .. " missing post-dex data blocks readiness")
|
||||
local ok, missing = Contract.allRequiredFilesExist(version, profileFs)
|
||||
T.eq(ok, false, profile[1] .. " " .. key .. " contract rejects missing post-dex output")
|
||||
T.eq(missing, path, profile[1] .. " " .. key .. " contract identifies missing file")
|
||||
files[version .. "/" .. path] = saved
|
||||
end
|
||||
T.eq(count, 20, profile[1] .. " all20 required outputs tested")
|
||||
for _, key in ipairs({ "frontier", "groudon", "kyogre", "rayquaza", "magma", "aqua", "sidney", "phoebe", "glacia", "drake" }) do
|
||||
T.eq(set["data/generated/gba/pokemon/battle/terrain_" .. key .. "_post_dex.rgba"], nil,
|
||||
profile[1] .. " does not require Emerald-only " .. key)
|
||||
end
|
||||
end
|
||||
Versions.select("firered")
|
||||
T.eq(V.CACHE_VERSION, 14, "Emerald cache version advanced once to14")
|
||||
T.eq(require("src.import.gba.versions_frlg").CACHE_VERSION, 128, "FRLG cache version advanced once to128")
|
||||
T.finish("game3_post_dex_background_2637")
|
||||
@@ -0,0 +1 @@
|
||||
dofile("tests/emerald_tent_save_reload_2638_test.lua")
|
||||
@@ -0,0 +1,153 @@
|
||||
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
|
||||
|
||||
local Fixture = {}
|
||||
|
||||
function Fixture.load(version, opts)
|
||||
opts = opts or {}
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set(version)
|
||||
local generation = GameVersion.generation(version)
|
||||
local Merge = require("src.mods.Merge")
|
||||
local Gen = require("Gen")
|
||||
local baseId = generation == 3 and "FR_EDITOR_BASE" or "EDITOR_BASE"
|
||||
local newId = generation == 3 and "FR_EDITOR_NEW" or "EDITOR_NEW"
|
||||
local lazyId = "FR_EDITOR_LAZY"
|
||||
local missingId = generation == 3 and "FR_EDITOR_MISSING" or "EDITOR_MISSING"
|
||||
local pair, mid = opts.pair or "editor_fixture", opts.mid or 7
|
||||
local atlas = opts.atlas or "mods/editor_map_fixture/atlas.png"
|
||||
local base = opts.base or {
|
||||
id = baseId, width = 3, height = 2, tileset = "EDITOR_TILES", blocks = { 1, 1, 1, 1, 1, 1 },
|
||||
warps = {}, midLayout = generation == 3 and {
|
||||
width = 3, height = 2, pair = pair, midAt = function() return 0 end,
|
||||
} or nil,
|
||||
}
|
||||
local data = { maps = { [baseId] = Merge.deepCopy(base) }, tilesets = {} }
|
||||
if generation == 3 then data.game3Maps = { [baseId] = base }
|
||||
elseif generation == 2 then Gen.bindGoldData(data) end
|
||||
local source
|
||||
if generation == 3 then
|
||||
source = ([[return function(mod)
|
||||
local function layout(pair)
|
||||
return { width = 6, height = 4, pair = pair, midAt = function() return %d end }
|
||||
end
|
||||
mod.content.maps:patch(%q, { width = 6, height = 4, midLayout = layout(%q), pair = %q, weather = 2 })
|
||||
mod.content.maps:register(%q, { id = %q, width = 6, height = 4, midLayout = layout(%q), pair = %q, warps = {} })
|
||||
mod.content.maps:register(%q, { id = %q, width = 6, height = 4, pair = %q, warps = {} })
|
||||
mod.content.maps:register(%q, { id = %q, width = 6, height = 4, warps = {} })
|
||||
end]]):format(mid, baseId, pair, pair, newId, newId, pair, pair, lazyId, lazyId, pair, missingId, missingId)
|
||||
else
|
||||
source = ([[return function(mod)
|
||||
local row = {}; for i = 1, 16 do row[i] = 0 end
|
||||
mod.content.tilesets:register("EDITOR_TILES", {
|
||||
id = "EDITOR_TILES", image = %q, tilesPerRow = 1, blocks = { row, row },
|
||||
walkable = { 0 }, collision = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }, trueColor = true,
|
||||
})
|
||||
mod.content.maps:patch(%q, { width = 4, height = 3, blocks = { 1,1,1,1,1,1,1,1,1,1,1,1 } })
|
||||
mod.content.maps:register(%q, {
|
||||
id = %q, tileset = "EDITOR_TILES", width = 4, height = 3,
|
||||
blocks = { 1,1,1,1,1,1,1,1,1,1,1,1 }, warps = {}, objects = {}, signs = {},
|
||||
})
|
||||
mod.content.tilesets:register("EDITOR_MISSING_TILES", { image = "mods/editor_map_fixture/missing.png", blocks = { row }, walkable = {} })
|
||||
mod.content.maps:register(%q, { id = %q, tileset = "EDITOR_MISSING_TILES", width = 2, height = 2, blocks = { 0,0,0,0 }, warps = {} })
|
||||
end]]):format(atlas, baseId, newId, newId, missingId, missingId)
|
||||
end
|
||||
local files = {
|
||||
["mods/editor_map_fixture/manifest.json"] = ([[{"id":"editor_map_fixture","name":"Editor map fixture","version":"1.0.0","api":2,"entry":"main.lua","games":[%q]}]]):format(version),
|
||||
["mods/editor_map_fixture/main.lua"] = source,
|
||||
}
|
||||
local fs = {}
|
||||
function fs.read(path) return files[path] end
|
||||
function fs.write(path, bytes) files[path] = bytes; return true end
|
||||
function fs.getInfo(path)
|
||||
if files[path] then return { type = "file" } end
|
||||
for key in pairs(files) do if key:sub(1, #path + 1) == path .. "/" then return { type = "directory" } end end
|
||||
end
|
||||
function fs.load(path) return loadstring(assert(files[path]), "@" .. path) end
|
||||
function fs.getDirectoryItems(path)
|
||||
local found, items = {}, {}
|
||||
for key in pairs(files) do
|
||||
if key:sub(1, #path + 1) == path .. "/" then
|
||||
local name = key:sub(#path + 2):match("^[^/]+")
|
||||
if not found[name] then found[name], items[#items + 1] = true, name end
|
||||
end
|
||||
end
|
||||
table.sort(items)
|
||||
return items
|
||||
end
|
||||
local loader = require("src.mods.Loader").new({ fs = fs, generation = generation, version = version })
|
||||
assert(loader:load(data), table.concat(loader.errors, "; "))
|
||||
assert(#loader:status().loaded == 1, "fixture mod did not load")
|
||||
return data, loader, { base = baseId, new = newId, lazy = lazyId, missing = missingId, pair = pair, mid = mid }
|
||||
end
|
||||
|
||||
if ... == "tests.engine.save_editor_custom_maps" then return Fixture end
|
||||
|
||||
love = require("tests.love_stub")
|
||||
local T = require("tests.harness")
|
||||
local Gen = require("Gen")
|
||||
local Browser = require("MapBrowser")
|
||||
local State = require("State")
|
||||
local Kit = require("Kit")
|
||||
local realImage = love.graphics.newImage
|
||||
love.graphics.newImage = function(path)
|
||||
if type(path) == "string" and path:find("missing.png", 1, true) then error("missing fixture image") end
|
||||
return realImage(path)
|
||||
end
|
||||
local function state(data, version, id)
|
||||
local s = State.new()
|
||||
s.data, s.version, s.tab, s.mapId = data, version, "map", id
|
||||
s.save = { version = version, generation = require("src.core.GameVersion").generation(version), player = { map = id, x = 1, y = 1 } }
|
||||
return s
|
||||
end
|
||||
local function draw(s)
|
||||
Kit.beginFrame(-1, -1, false, 0)
|
||||
Browser.draw(s, Kit, 0, 0, 1200, 760)
|
||||
Kit.endFrame()
|
||||
end
|
||||
|
||||
for _, version in ipairs({ "red", "gold", "crystal", "firered", "emerald" }) do
|
||||
local data, loader, ids = Fixture.load(version)
|
||||
local maps = Gen.maps(data)
|
||||
local generation = require("src.core.GameVersion").generation(version)
|
||||
T.eq(maps[ids.base].width, generation == 3 and 6 or 4, version .. " mod patch reaches preview map lookup")
|
||||
T.check(maps[ids.new] ~= nil, version .. " loaded mod's new map is discoverable")
|
||||
if generation == 3 then
|
||||
T.eq(maps[ids.base].weather, 2, version .. " existing mod patch is authoritative")
|
||||
T.eq(maps[ids.base].midLayout:midAt(0, 0), 7, version .. " mod-supplied layout survives base binding")
|
||||
end
|
||||
local missing = state(data, version, ids.missing)
|
||||
draw(missing)
|
||||
T.check(type(missing.mapPreviewReason) == "string" and missing.mapPreviewReason:find("unavailable", 1, true), version .. " missing artwork is explained in the actual map panel")
|
||||
if Browser.preview then
|
||||
local s = state(data, version, ids.new)
|
||||
if generation < 3 then
|
||||
local ok, map, reason = Browser.preview(s)
|
||||
T.check(ok and map.renderer and not reason, version .. " loaded custom atlas builds a terrain renderer")
|
||||
T.eq(map.widthCells, 8, version .. " block dimensions use walking cells")
|
||||
else
|
||||
local Dataset = require("src.core.game3.dataset")
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
local root = "editor-custom-map-fixture"
|
||||
Extract.NATIVE_ROOT = root
|
||||
Dataset.invalidateManifestCache()
|
||||
local cells = {}; for i = 1, 24 do cells[i] = { mid = 7, coll = 0, elev = 0 } end
|
||||
love.filesystem.write(root .. "/layouts/" .. ids.lazy .. ".mid", require("src.import.gba.native_pack").encodeMidLayout({ width = 6, height = 4, cells = cells }))
|
||||
local original = data.maps[ids.new].midLayout
|
||||
Browser.preview(s)
|
||||
T.eq(data.maps[ids.new].midLayout, original, version .. " preview preserves supplied layout identity")
|
||||
s.mapId = ids.lazy
|
||||
local ok, map = Browser.preview(s)
|
||||
T.check(ok and data.maps[ids.lazy].midLayout and data.maps[ids.lazy].midLayout:midAt(2, 2) == 7, version .. " gameplay API lazily attaches a mod-added native MID file")
|
||||
T.check(map:inBounds(5, 3) and not map:inBounds(6, 3), version .. " native grid preserves placement bounds")
|
||||
s.mapId = ids.missing
|
||||
local _, grid, reason = Browser.preview(s)
|
||||
T.check(grid:inBounds(5, 3) and reason:find("missing native map layout", 1, true), version .. " missing layout retains the coordinate grid")
|
||||
data.maps[ids.missing].midLayout = { midAt = function() return 0 end }
|
||||
local _, _, pairReason = Browser.preview(s)
|
||||
T.check(pairReason:find("missing native tileset pair", 1, true), version .. " missing pair is distinguished from missing layout")
|
||||
end
|
||||
end
|
||||
require("src.mods.Runtime").reset()
|
||||
end
|
||||
|
||||
T.finish()
|
||||
@@ -0,0 +1,142 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness")
|
||||
local Font = require("src.render.Font")
|
||||
Font.load({ font = require("tests.fixture_data.font") })
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Version = require("src.core.GameVersion")
|
||||
local Timing = require("src.core.Timing")
|
||||
package.loaded["src.core.Sound"] = { play = function() end, playPress = function() end }
|
||||
|
||||
local function make(text, opts, save)
|
||||
local pressed, pops, calls = false, 0, 0
|
||||
local game = {
|
||||
data = {}, save = save or { options = { textSpeed = 1 } },
|
||||
input = { wasPressed = function(_, key) return pressed == key end,
|
||||
isDown = function() return false end },
|
||||
stack = { pop = function() pops = pops + 1 end },
|
||||
}
|
||||
local box = TextBox.new(game, text, function() calls = calls + 1 end, opts)
|
||||
return box, function(key)
|
||||
pressed = key or false
|
||||
box:update(1 / 60)
|
||||
pressed = false
|
||||
end, function() return pops, calls end
|
||||
end
|
||||
|
||||
local function prompt(box, tick)
|
||||
for _ = 1, 200 do
|
||||
if box.waiting or box.done then return true end
|
||||
tick()
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function advance(box, tick, key)
|
||||
for _ = 1, Timing.TEXT_PRE_ADVANCE do tick() end
|
||||
tick(key or "a")
|
||||
end
|
||||
|
||||
local function rows(box, expected, label)
|
||||
T.eq(#box.shown, #expected, label .. " physical row count")
|
||||
for i, text in ipairs(expected) do
|
||||
T.same(box.shown[i], Font.encode(text), label .. " glyph row " .. i)
|
||||
end
|
||||
T.same(box:visibleText(), expected, label .. " visibleText")
|
||||
local drawCode, drawBox = Font.drawCode, Font.drawBox
|
||||
local drawn = {}
|
||||
Font.drawCode = function(code, x, y)
|
||||
drawn[#drawn + 1] = { code = code, x = x, y = y }
|
||||
end
|
||||
Font.drawBox = function() end
|
||||
local blink = box.blink
|
||||
box.blink = 30
|
||||
box:draw()
|
||||
box.blink = blink
|
||||
Font.drawCode, Font.drawBox = drawCode, drawBox
|
||||
local want = {}
|
||||
for i, text in ipairs(expected) do
|
||||
local x = box.textX
|
||||
for _, code in ipairs(Font.encode(text)) do
|
||||
want[#want + 1] = { code = code, x = x, y = i == 1 and box.line1Y or box.line2Y }
|
||||
x = x + Font.advanceOf(code)
|
||||
end
|
||||
end
|
||||
T.same(drawn, want, label .. " drawn glyph positions")
|
||||
end
|
||||
|
||||
-- pokered/home/text.asm:262
|
||||
for _, edition in ipairs({ "red", "blue", "yellow" }) do
|
||||
Version.set(edition)
|
||||
local box, tick, counts = make("A\vB{DONE}")
|
||||
T.check(prompt(box, tick), edition .. " reaches cont")
|
||||
T.check(box.waiting and box.contAdvance, edition .. " waits before scrolling")
|
||||
rows(box, { "A" }, edition .. " before cont")
|
||||
tick("a")
|
||||
T.check(box.waiting, edition .. " protected wait rejects early A")
|
||||
advance(box, tick, "b")
|
||||
T.eq(box.holdFrames, Timing.TEXT_SCROLL_PAIR, edition .. " scroll timing")
|
||||
T.same(box.shown, { {}, {} }, edition .. " old upper row cleared before typing")
|
||||
for _ = 1, Timing.TEXT_SCROLL_PAIR do tick() end
|
||||
T.eq(box.charIndex, 0, edition .. " scroll hold suppresses typing")
|
||||
T.check(prompt(box, tick), edition .. " completes after one cont press")
|
||||
T.check(box.done and not box.waiting, edition .. " has no extra cont prompt")
|
||||
rows(box, { "", "B" }, edition .. " single-row cont")
|
||||
tick("a")
|
||||
local pops, calls = counts()
|
||||
T.eq(pops, 1, edition .. " closes once")
|
||||
T.eq(calls, 1, edition .. " resumes callback once")
|
||||
rows(make("A\vB{DONE}", { instant = true }), { "", "B" }, edition .. " instant cont")
|
||||
local plain, plainTick = make("A\nB{DONE}")
|
||||
T.check(prompt(plain, plainTick) and plain.done, edition .. " ordinary line has no cont wait")
|
||||
rows(plain, { "A", "B" }, edition .. " ordinary line")
|
||||
end
|
||||
|
||||
Version.set("red")
|
||||
for _, case in ipairs({
|
||||
{ "A\nB\vC", { "B", "C" }, "two-row cont" },
|
||||
{ "\vB", { "", "B" }, "leading cont" },
|
||||
{ "A\v\vB", { "", "B" }, "empty cont" },
|
||||
{ "A\vB\vC", { "B", "C" }, "successive cont" },
|
||||
}) do
|
||||
local box, tick = make(case[1])
|
||||
for _ = 1, 4 do
|
||||
T.check(prompt(box, tick), case[3] .. " reaches next prompt")
|
||||
if box.done then break end
|
||||
advance(box, tick)
|
||||
end
|
||||
T.check(box.done, case[3] .. " completes")
|
||||
rows(box, case[2], case[3])
|
||||
rows(make(case[1], { instant = true }), case[2], case[3] .. " instant")
|
||||
end
|
||||
|
||||
local para, tick = make("X\fA\vB")
|
||||
T.check(prompt(para, tick) and not para.contAdvance, "para waits before clearing")
|
||||
advance(para, tick)
|
||||
T.eq(para.holdFrames, Timing.TEXT_PAGE_CLEAR, "para clear timing")
|
||||
for _ = 1, Timing.TEXT_PAGE_CLEAR do tick() end
|
||||
T.check(prompt(para, tick) and para.contAdvance, "para restarts at a one-row cont")
|
||||
rows(para, { "A" }, "para restarts at top")
|
||||
advance(para, tick)
|
||||
T.check(prompt(para, tick) and para.done, "para then cont completes")
|
||||
rows(para, { "", "B" }, "para then cont")
|
||||
|
||||
for _, edition in ipairs({ "gold", "firered" }) do
|
||||
Version.set(edition)
|
||||
local box, tick = make("A\vB")
|
||||
T.check(prompt(box, tick) and box.contAdvance, edition .. " reaches cont")
|
||||
advance(box, tick)
|
||||
T.check(prompt(box, tick) and box.done, edition .. " completes cont")
|
||||
rows(box, { "A", "B" }, edition .. " existing row semantics")
|
||||
rows(make("A\vB", { instant = true }), { "A", "B" }, edition .. " instant unchanged")
|
||||
end
|
||||
|
||||
Version.set("red")
|
||||
local save2, tick2 = make("A\vB", nil, { generation = 2, options = { textSpeed = 1 } })
|
||||
T.check(prompt(save2, tick2), "Gen 2 save reaches cont under default version")
|
||||
advance(save2, tick2)
|
||||
T.check(prompt(save2, tick2), "Gen 2 save completes cont")
|
||||
rows(save2, { "A", "B" }, "Gen 2 save unchanged")
|
||||
|
||||
T.finish("text_cont_single_row_bug2634")
|
||||
@@ -23,7 +23,7 @@ require("src.core.GameVersion").set("firered")
|
||||
local gfx = setmetatable({}, { __index = function() return function() end end })
|
||||
gfx.newQuad = function() return {} end
|
||||
gfx.newImage = function() return { setFilter = function() end, getDimensions = function() return 8, 8 end } end
|
||||
_G.love = { graphics = gfx }
|
||||
_G.love = { graphics = gfx, image = require("tests.love_stub").image }
|
||||
|
||||
local Storage = require("src.core.game3.storage")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
-- pokered/scripts/ViridianCity.asm:150
|
||||
-- pokeyellow/scripts/ViridianCity_2.asm:10
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local S = require("tests.harness").suite("parity Viridian gambler #2632")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local bit = require("bit")
|
||||
local oldVersion = GameVersion.get()
|
||||
local savedModules = {}
|
||||
for name, value in pairs(package.loaded) do
|
||||
if name:match("^data%.scripts%.") or name == "src.script.MapScripts" then
|
||||
savedModules[name] = value
|
||||
end
|
||||
end
|
||||
|
||||
local badges = {
|
||||
"BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE",
|
||||
"SOULBADGE", "MARSHBADGE", "VOLCANOBADGE", "EARTHBADGE",
|
||||
}
|
||||
|
||||
local function clearRegistry()
|
||||
local names = {}
|
||||
for name in pairs(package.loaded) do
|
||||
if name:match("^data%.scripts%.") or name == "src.script.MapScripts" then
|
||||
names[#names + 1] = name
|
||||
end
|
||||
end
|
||||
for _, name in ipairs(names) do package.loaded[name] = nil end
|
||||
end
|
||||
|
||||
local function exercise(handler, version, mask, flagName)
|
||||
local inventory = {}
|
||||
for i, badge in ipairs(badges) do
|
||||
if bit.band(mask, bit.lshift(1, i - 1)) ~= 0 then inventory[badge] = true end
|
||||
end
|
||||
local stack = setmetatable({}, { __index = StateStack })
|
||||
stack:init()
|
||||
local pressed = false
|
||||
local game = {
|
||||
data = { text = {
|
||||
_ViridianCityGambler1GymLeaderReturnedText = "RETURNED{DONE}",
|
||||
_ViridianCityGambler1GymAlwaysClosedText = "CLOSED{DONE}",
|
||||
} },
|
||||
save = { inventory = inventory, flags = {}, options = { textSpeed = 1 } },
|
||||
stack = stack,
|
||||
input = {
|
||||
wasPressed = function(_, key) return pressed and key == "a" end,
|
||||
isDown = function() return false end,
|
||||
},
|
||||
}
|
||||
if flagName then game.save.flags[flagName] = true end
|
||||
local completed = 0
|
||||
local label = version .. " mask=" .. mask .. " event=" .. (flagName or "none")
|
||||
handler(game, nil, nil, function() completed = completed + 1 end)
|
||||
local box = stack:top()
|
||||
S.check(getmetatable(box) == TextBox, label .. " opens the actual TextBox")
|
||||
if getmetatable(box) ~= TextBox then return end
|
||||
local expected = (mask == 127 or flagName ~= nil) and "RETURNED" or "CLOSED"
|
||||
S.eq(table.concat(box.pages[1], "\n"), expected, label .. " cartridge branch")
|
||||
S.eq(completed, 0, label .. " callback waits for dismissal")
|
||||
for _ = 1, 100 do
|
||||
if box.done then break end
|
||||
stack:update(1 / 60)
|
||||
end
|
||||
S.check(box.done, label .. " finishes revealing")
|
||||
pressed = true
|
||||
stack:update(1 / 60)
|
||||
S.eq(stack:top(), nil, label .. " dismisses the TextBox")
|
||||
S.eq(completed, 1, label .. " completes callback exactly once")
|
||||
stack:update(1 / 60)
|
||||
S.eq(completed, 1, label .. " callback stays completed")
|
||||
end
|
||||
|
||||
for _, version in ipairs({ "red", "blue", "yellow" }) do
|
||||
GameVersion.set(version)
|
||||
clearRegistry()
|
||||
local scripts = require("data.scripts.init")
|
||||
local handler = scripts.talkScript("VIRIDIAN_CITY", "TEXT_VIRIDIANCITY_GAMBLER1")
|
||||
S.check(type(handler) == "function", version .. " resolves the actual registry handler")
|
||||
if type(handler) == "function" then
|
||||
for mask = 0, 255 do
|
||||
exercise(handler, version, mask, nil)
|
||||
exercise(handler, version, mask, "EVENT_BEAT_GIOVANNI")
|
||||
exercise(handler, version, mask, "EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
clearRegistry()
|
||||
for name, value in pairs(savedModules) do package.loaded[name] = value end
|
||||
GameVersion.set(oldVersion)
|
||||
S.finish()
|
||||
@@ -97,8 +97,9 @@ end
|
||||
|
||||
function Gen.maps(data)
|
||||
if type(data) ~= "table" then return {} end
|
||||
if data.game3Maps then return data.maps or data.game3Maps end
|
||||
local m = overlayRecords(data.maps, data.gen2Maps)
|
||||
return overlayRecords(m, data.game3Maps)
|
||||
return m
|
||||
end
|
||||
|
||||
function Gen.tilesets(data)
|
||||
|
||||
@@ -307,6 +307,111 @@ local function drawOverlays(S, map)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function gridMap(def, id, generation)
|
||||
local factor = generation == 3 and 1 or 2
|
||||
local map = {
|
||||
id = id, def = def, width = def.width, height = def.height,
|
||||
widthCells = def.width * factor, heightCells = def.height * factor,
|
||||
warps = def.warps or {},
|
||||
}
|
||||
function map:inBounds(cx, cy)
|
||||
return cx >= 0 and cy >= 0 and cx < self.widthCells and cy < self.heightCells
|
||||
end
|
||||
function map:warpAtCell(cx, cy)
|
||||
for i, warp in ipairs(self.warps) do
|
||||
if warp.x == cx and warp.y == cy then return { index = i, def = warp } end
|
||||
end
|
||||
end
|
||||
return map
|
||||
end
|
||||
|
||||
function MapBrowser.preview(S)
|
||||
local generation = Gen.of(S.save, S.version)
|
||||
local def = Gen.maps(S.data)[S.mapId]
|
||||
if not def then return false, "unknown map" end
|
||||
if generation == 3 and not def.midLayout then
|
||||
S._mapNativeTried = S._mapNativeTried or {}
|
||||
if not S._mapNativeTried[def] then
|
||||
S._mapNativeTried[def] = true
|
||||
pcall(function()
|
||||
require("src.core.game3.map").ensureMidLayout({ data = S.data }, S.mapId, def)
|
||||
end)
|
||||
end
|
||||
end
|
||||
if type(def.width) ~= "number" or type(def.height) ~= "number" then
|
||||
return false, "incomplete map record (missing width/height)"
|
||||
end
|
||||
local map = gridMap(def, S.mapId, generation)
|
||||
local reason
|
||||
if generation == 3 then
|
||||
local layout = def.midLayout
|
||||
local pair = def.pair or (layout and layout.pair)
|
||||
if not layout or type(layout.midAt) ~= "function" then
|
||||
reason = "missing native map layout"
|
||||
elseif not pair then
|
||||
reason = "missing native tileset pair"
|
||||
else
|
||||
local NativeTileset = require("src.core.game3.tileset_native")
|
||||
local loaded, ts = pcall(NativeTileset.get, pair)
|
||||
if not loaded or not ts or not ts.image then
|
||||
reason = "native tileset image unavailable: " .. tostring(pair)
|
||||
else
|
||||
function map:tileAtCell(cx, cy) return layout:midAt(cx, cy) or 0 end
|
||||
map.renderer = {
|
||||
draw = function(_, camX, camY)
|
||||
local zoom = S.mapZoom or 1
|
||||
local startCx = math.max(0, math.floor(camX / CELL) - 1)
|
||||
local endCx = math.min(def.width - 1, math.ceil((camX + (S._mapViewW or 480) / zoom) / CELL) + 1)
|
||||
local startCy = math.max(0, math.floor(camY / CELL) - 1)
|
||||
local endCy = math.min(def.height - 1, math.ceil((camY + (S._mapViewH or 432) / zoom) / CELL) + 1)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for cy = startCy, endCy do
|
||||
for cx = startCx, endCx do
|
||||
local mid = layout:midAt(cx, cy)
|
||||
if mid and mid >= 0 then
|
||||
local slot = NativeTileset.slotFor(ts, mid)
|
||||
local quad = NativeTileset.quad(ts, slot)
|
||||
if quad then love.graphics.draw(ts.image, quad, cx * CELL - camX, cy * CELL - camY) end
|
||||
if ts.layered and ts.overImage then
|
||||
local over = NativeTileset.overQuad(ts, slot)
|
||||
if over then love.graphics.draw(ts.overImage, over, cx * CELL - camX, cy * CELL - camY) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
else
|
||||
local tileset = Gen.tilesets(S.data)[def.tileset]
|
||||
if not tileset then
|
||||
reason = "missing tileset: " .. tostring(def.tileset)
|
||||
elseif not tileset.image then
|
||||
reason = "missing tileset image: " .. tostring(def.tileset)
|
||||
elseif not tileset.blocks then
|
||||
reason = "missing tileset blocks: " .. tostring(def.tileset)
|
||||
elseif generation == 2 then
|
||||
local loadedMap, built = pcall(require("src.world.gen2.Map").new, def, tileset)
|
||||
if not loadedMap then return false, built end
|
||||
map = built
|
||||
local MapPreview = require("src.world.gen2.MapPreview")
|
||||
S._g2MapBaker = S._g2MapBaker or MapPreview.baker({
|
||||
tilesets = Gen.tilesets(S.data), gen2Roofs = S.data.gen2Roofs,
|
||||
roofs = S.data.roofs, gen2Palettes = S.data.gen2Palettes, palettes = S.data.palettes,
|
||||
})
|
||||
local loaded, renderer = pcall(MapPreview.renderer, S._g2MapBaker, map)
|
||||
if loaded and renderer then map.renderer = renderer
|
||||
else reason = "tileset image unavailable: " .. tostring(tileset.image) end
|
||||
else
|
||||
local loaded, rendered = pcall(MapLoader.load, S.data, S.mapId)
|
||||
if loaded then map = rendered
|
||||
else reason = "tileset image unavailable: " .. tostring(tileset.image) end
|
||||
end
|
||||
end
|
||||
return true, map, reason and ("Preview unavailable: " .. reason .. ". Coordinate grid remains available.")
|
||||
end
|
||||
|
||||
local function drawSection(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
@@ -496,110 +601,8 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
)
|
||||
end
|
||||
|
||||
local ok, map
|
||||
if Gen.of(S.save) == 3 then
|
||||
local def = Gen.maps(S.data)[S.mapId]
|
||||
if def then
|
||||
local mw = def.width or 20
|
||||
local mh = def.height or 18
|
||||
local midLayout = def.midLayout
|
||||
local pair = def.pair or (midLayout and midLayout.pair)
|
||||
map = {
|
||||
id = S.mapId,
|
||||
def = def,
|
||||
width = mw,
|
||||
height = mh,
|
||||
widthCells = mw,
|
||||
heightCells = mh,
|
||||
warps = def.warps or {},
|
||||
inBounds = function(self, cx, cy)
|
||||
return cx >= 0 and cx < self.width and cy >= 0 and cy < self.height
|
||||
end,
|
||||
warpAtCell = function(self, cx, cy)
|
||||
for _, w in ipairs(self.warps or {}) do
|
||||
if w.x == cx and w.y == cy then
|
||||
return { def = w }
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
tileAtCell = function(self, cx, cy)
|
||||
if midLayout and midLayout.midAt then
|
||||
return midLayout:midAt(cx, cy) or 0
|
||||
end
|
||||
return 0
|
||||
end,
|
||||
}
|
||||
|
||||
local okN, NativeTileset = pcall(require, "src.core.game3.tileset_native")
|
||||
if okN and NativeTileset and pair and midLayout and midLayout.midAt then
|
||||
local ts = NativeTileset.get(pair)
|
||||
if ts and ts.image then
|
||||
map.renderer = {
|
||||
draw = function(self, camX, camY)
|
||||
local viewW = S._mapViewW or 480
|
||||
local viewH = S._mapViewH or 432
|
||||
local zoom = S.mapZoom or 1
|
||||
local startCx = math.max(0, math.floor(camX / CELL) - 1)
|
||||
local endCx = math.min(mw - 1, math.ceil((camX + viewW / zoom) / CELL) + 1)
|
||||
local startCy = math.max(0, math.floor(camY / CELL) - 1)
|
||||
local endCy = math.min(mh - 1, math.ceil((camY + viewH / zoom) / CELL) + 1)
|
||||
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for cy = startCy, endCy do
|
||||
for cx = startCx, endCx do
|
||||
local mid = midLayout:midAt(cx, cy)
|
||||
if mid and mid >= 0 then
|
||||
local slot = NativeTileset.slotFor(ts, mid)
|
||||
local q = NativeTileset.quad(ts, slot)
|
||||
if q then
|
||||
love.graphics.draw(ts.image, q, cx * CELL - camX, cy * CELL - camY)
|
||||
end
|
||||
if ts.layered and ts.overImage then
|
||||
local oq = NativeTileset.overQuad(ts, slot)
|
||||
if oq then
|
||||
love.graphics.draw(ts.overImage, oq, cx * CELL - camX, cy * CELL - camY)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
ok = true
|
||||
else
|
||||
ok, map = false, "unknown map"
|
||||
end
|
||||
elseif Gen.of(S.save) == 2 then
|
||||
local def = Gen.maps(S.data)[S.mapId]
|
||||
if def then
|
||||
local Map2 = require("src.world.gen2.Map")
|
||||
if type(def.width) ~= "number" or type(def.height) ~= "number" then
|
||||
ok, map = false, "incomplete map record (missing width/height)"
|
||||
else
|
||||
local tileset = Gen.tilesets(S.data)[def.tileset]
|
||||
ok, map = pcall(Map2.new, def, tileset or {})
|
||||
if ok and map and not map.renderer then
|
||||
local MapPreview = require("src.world.gen2.MapPreview")
|
||||
S._g2MapBaker = S._g2MapBaker
|
||||
or MapPreview.baker({
|
||||
tilesets = Gen.tilesets(S.data),
|
||||
gen2Roofs = S.data.gen2Roofs,
|
||||
roofs = S.data.roofs,
|
||||
gen2Palettes = S.data.gen2Palettes,
|
||||
palettes = S.data.palettes,
|
||||
})
|
||||
map.renderer = MapPreview.renderer(S._g2MapBaker, map)
|
||||
end
|
||||
end
|
||||
else
|
||||
ok, map = false, "unknown map"
|
||||
end
|
||||
else
|
||||
ok, map = pcall(MapLoader.load, S.data, S.mapId)
|
||||
end
|
||||
local ok, map, previewReason = MapBrowser.preview(S)
|
||||
S.mapPreviewReason = previewReason
|
||||
if not ok then
|
||||
Kit.text(
|
||||
"mono",
|
||||
@@ -777,6 +780,9 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
end
|
||||
drawOverlays(S, map)
|
||||
love.graphics.pop()
|
||||
if previewReason then
|
||||
Kit.textWrapped("tiny", previewReason, vx0 + 8 * s, vy0 + 8 * s, math.max(0, vinner - 16 * s), PAL.yellow)
|
||||
end
|
||||
Kit.popClip()
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user