mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
309 lines
12 KiB
Lua
309 lines
12 KiB
Lua
#!/usr/bin/env luajit
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
require("tests.game3_cache").stubSpeciesNames()
|
|
|
|
package.loaded["src.core.game3.audio"] = setmetatable({}, {
|
|
__index = function() return function() end end,
|
|
})
|
|
|
|
local failed, passed = 0, 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
passed = passed + 1
|
|
print("[ok] " .. msg)
|
|
else
|
|
failed = failed + 1
|
|
print("[FAIL] " .. msg)
|
|
end
|
|
end
|
|
|
|
local function eq(a, b, msg)
|
|
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
|
end
|
|
|
|
local Schema = require("src.core.game3.save_schema_firered")
|
|
local Rng = require("src.core.game3.rng")
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
|
|
local FLAG_SYS_SAFARI_MODE = 0x800
|
|
local VAR_SAFARI_ENTRANCE = 0x406E
|
|
|
|
print("[test] 1. New Game rolls a secret id in pret's order")
|
|
do
|
|
-- pokefirered/src/new_game.c:103 SeedWildEncounterRng, then :123 InitPlayerTrainerId
|
|
Rng.SeedRng(0x2468)
|
|
Rng.Random()
|
|
local expected = Rng.Random()
|
|
|
|
local session = Schema.newGame({ rngSeed = 0x2468 })
|
|
eq(session.trainerId, 0x2468, "trainerId is the timer seed")
|
|
check(type(session.secretId) == "number", "newGame stamps a numeric secretId")
|
|
check(type(session.secretId) == "number"
|
|
and session.secretId >= 0 and session.secretId < 0x10000, "secretId is 16 bit")
|
|
eq(session.secretId, expected, "secretId is the Random() after the wild seed")
|
|
|
|
local other = Schema.newGame({ rngSeed = 0x1357 })
|
|
check(other.secretId ~= session.secretId, "a different seed rolls a different secret id")
|
|
end
|
|
|
|
print("[test] 2. secretId survives save and load")
|
|
do
|
|
local session = Schema.newGame({ rngSeed = 0x4242 })
|
|
local secret = session.secretId
|
|
local save = Schema.toSaveTable(session)
|
|
eq(save.secretId, secret, "toSaveTable writes secretId")
|
|
local loaded = Schema.fromSaveTable(save)
|
|
eq(loaded.secretId, secret, "fromSaveTable restores secretId")
|
|
eq(loaded.trainerId, session.trainerId, "and trainerId beside it")
|
|
|
|
local Catching = require("src.core.game3.battle.catching")
|
|
loaded.party = {}
|
|
loaded.storage = nil
|
|
eq(Catching.playerSecretId(loaded), secret,
|
|
"an empty party still reports the saved secret id")
|
|
end
|
|
|
|
print("[test] 3. An old save with none of the three fields loads clean")
|
|
do
|
|
local old = {
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
name = "RED", rivalName = "BLUE", gender = 0, money = 3000, coins = 0,
|
|
trainerId = 31337,
|
|
party = { { species = 1, speciesId = 1, level = 7, hp = 22, maxHp = 22,
|
|
otId = 31337, otSecretId = 4242 } },
|
|
dex = { seen = {}, owned = {} },
|
|
map = "FR_PALLET_TOWN", x = 5, y = 6, facing = "down",
|
|
flags = {}, vars = {},
|
|
}
|
|
local loaded = Schema.fromSaveTable(old)
|
|
eq(loaded.secretId, nil, "no secretId in, no secretId out")
|
|
eq(loaded.safari, nil, "no safari block")
|
|
eq(loaded.mail, nil, "no mail pool")
|
|
eq(#loaded.party, 1, "the party is not quarantined")
|
|
eq(loaded.party[1].otSecretId, 4242, "the mon keeps its own secret id")
|
|
eq(loaded.orphaned, nil, "nothing was orphaned")
|
|
|
|
local Catching = require("src.core.game3.battle.catching")
|
|
eq(Catching.playerSecretId(loaded), 4242,
|
|
"the old-save recovery path still reads it off an owned mon")
|
|
|
|
local again = Schema.fromSaveTable(Schema.toSaveTable(loaded))
|
|
eq(again.secretId, 4242, "and the next save persists what was recovered")
|
|
end
|
|
|
|
print("[test] 4. Continue clears safari mode, whatever spelling the save used")
|
|
do
|
|
local save = {
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
party = {}, dex = { seen = {}, owned = {} },
|
|
map = "FR_FUCHSIA_CITY", x = 4, y = 4,
|
|
flags = {
|
|
[FLAG_SYS_SAFARI_MODE] = true,
|
|
[tostring(FLAG_SYS_SAFARI_MODE)] = true,
|
|
["FLAG_SYS_SAFARI_MODE"] = true,
|
|
[tostring(0x828)] = true,
|
|
},
|
|
vars = {
|
|
[tostring(VAR_SAFARI_ENTRANCE)] = 1,
|
|
["VAR_MAP_SCENE_FUCHSIA_CITY_SAFARI_ZONE_ENTRANCE"] = 1,
|
|
[tostring(0x4001)] = 3,
|
|
},
|
|
safari = { balls = 17, steps = 412 },
|
|
}
|
|
local loaded = Schema.fromSaveTable(save)
|
|
-- pokefirered/src/overworld.c:1695 CB2_ContinueSavedGame
|
|
check(Flags.getFlag(loaded, nil, FLAG_SYS_SAFARI_MODE) == false,
|
|
"ResetSafariZoneFlag_ clears FLAG_SYS_SAFARI_MODE on Continue")
|
|
eq(loaded.flags[FLAG_SYS_SAFARI_MODE], nil, "the numeric key is gone")
|
|
eq(loaded.flags[tostring(FLAG_SYS_SAFARI_MODE)], nil, "the decimal key is gone")
|
|
eq(loaded.flags["FLAG_SYS_SAFARI_MODE"], nil, "the name key is gone")
|
|
eq(loaded.flags[tostring(0x828)], true, "an unrelated flag is untouched")
|
|
|
|
eq(Flags.getVar(loaded, nil, VAR_SAFARI_ENTRANCE), 0,
|
|
"Overworld_ResetStateOnContinue zeroes the entrance scene var")
|
|
eq(loaded.vars[tostring(VAR_SAFARI_ENTRANCE)], nil, "no stale decimal var key")
|
|
eq(loaded.vars["VAR_MAP_SCENE_FUCHSIA_CITY_SAFARI_ZONE_ENTRANCE"], nil,
|
|
"no stale named var key")
|
|
eq(loaded.vars[tostring(0x4001)], 3, "an unrelated var is untouched")
|
|
|
|
eq(loaded.safari, nil, "gNumSafariBalls and the step counter do not come back")
|
|
|
|
local session = Schema.newGame({ rngSeed = 7 })
|
|
session.safari = { balls = 30, steps = 600 }
|
|
eq(Schema.toSaveTable(session).safari, nil,
|
|
"and they are EWRAM, so no save block ever carries them")
|
|
|
|
local stranded = Schema.fromSaveTable({
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
party = {}, dex = { seen = {}, owned = {} },
|
|
map = "FR_SAFARI_ZONE_CENTER", x = 26, y = 30, facing = "up",
|
|
flags = {}, vars = {},
|
|
})
|
|
-- pokefirered/data/scripts/safari_zone.inc:7 SafariZone_EventScript_Exit
|
|
eq(stranded.map, "FR_FUCHSIA_CITY_SAFARI_ZONE_ENTRANCE", "a save left in the zone resumes at the gate")
|
|
eq(stranded.x, 4, "gate x")
|
|
eq(stranded.y, 1, "gate y")
|
|
eq(Flags.getVar(stranded, nil, VAR_SAFARI_ENTRANCE), 1, "entrance ExitWarpIn scene queued")
|
|
local again = Schema.fromSaveTable(Schema.toSaveTable(stranded))
|
|
eq(again.map, "FR_FUCHSIA_CITY_SAFARI_ZONE_ENTRANCE", "second Continue stays at the gate")
|
|
end
|
|
|
|
print("[test] 5. The mail pool rides the save through mail.lua")
|
|
do
|
|
local KEY = "src.core.game3.mail"
|
|
local prev = package.loaded[KEY]
|
|
|
|
local exported, restored = nil, nil
|
|
package.loaded[KEY] = {
|
|
export = function(session)
|
|
exported = session
|
|
return { pool = { [1] = { item = 121, words = { 1, 2, 3 } } } }
|
|
end,
|
|
restore = function(saved)
|
|
restored = saved
|
|
return saved and { pool = saved.pool, live = true } or nil
|
|
end,
|
|
}
|
|
|
|
local session = Schema.newGame({ rngSeed = 9 })
|
|
local save = Schema.toSaveTable(session)
|
|
check(exported == session, "toSaveTable hands the session to Mail.export")
|
|
check(type(save.mail) == "table" and save.mail.pool[1].item == 121,
|
|
"the exported pool lands in the save table")
|
|
|
|
local loaded = Schema.fromSaveTable(save)
|
|
check(restored == save.mail, "fromSaveTable hands the saved pool to Mail.restore")
|
|
check(type(loaded.mail) == "table" and loaded.mail.live == true,
|
|
"and the restored pool lands on the session")
|
|
|
|
package.loaded[KEY] = { GIVE_MAIL = 1 }
|
|
local bare = Schema.newGame({ rngSeed = 11 })
|
|
bare.mail = { pool = {} }
|
|
local bareSave = Schema.toSaveTable(bare)
|
|
check(bareSave.mail == bare.mail,
|
|
"a mail module without export is a no-op, the raw field passes through")
|
|
eq(Schema.fromSaveTable(bareSave).mail, bare.mail,
|
|
"and so is one without restore")
|
|
|
|
package.loaded[KEY] = prev
|
|
end
|
|
|
|
print("[test] 6. A script gift mon carries the player's secret id")
|
|
do
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
Pokemon.install(nil)
|
|
local Party = require("src.core.game3.party")
|
|
local session = Schema.newGame({ rngSeed = 0x0F0F })
|
|
-- pokefirered/src/script_pokemon_util.c:48 ScriptGiveMon
|
|
local code, mon = Party.giveMonToPlayer(session, 1, 5, "BULBASAUR")
|
|
eq(code, Party.MON_GIVEN_TO_PARTY, "the gift went to the party")
|
|
eq(mon.otId, session.trainerId, "stamped with the visible trainer id")
|
|
check(type(mon.otSecretId) == "number", "and with a numeric secret half")
|
|
eq(mon.otSecretId, session.secretId, "which is the player's own")
|
|
end
|
|
|
|
print("[test] 7. the SaveBlock1 warp slots ride the save")
|
|
do
|
|
-- pokefirered/include/global.h:764
|
|
local session = Schema.newGame({ rngSeed = 0x77 })
|
|
eq(session.dynamicWarp, nil, "a New Game has no dynamic warp")
|
|
eq(session.escapeWarp, nil, "and no escape warp")
|
|
|
|
session.map = "FR_SILPH_CO_ELEVATOR"
|
|
-- pokefirered/src/overworld.c:600 SetDynamicWarp
|
|
session.dynamicWarp = { map = "FR_SILPH_CO_5F", warpId = 255, x = 22, y = 3 }
|
|
-- pokefirered/src/overworld.c:651 SetEscapeWarp
|
|
session.escapeWarp = { map = "FR_PEWTER_CITY", warpId = 255, x = 14, y = 8 }
|
|
|
|
local save = Schema.toSaveTable(session)
|
|
eq((save.dynamicWarp or {}).map, "FR_SILPH_CO_5F", "toSaveTable writes the dynamic warp")
|
|
eq((save.escapeWarp or {}).map, "FR_PEWTER_CITY", "and the escape warp")
|
|
|
|
local loaded = Schema.fromSaveTable(save)
|
|
eq((loaded.dynamicWarp or {}).map, "FR_SILPH_CO_5F", "the lift exit survives the reload")
|
|
eq((loaded.dynamicWarp or {}).x, 22, "with its recorded x")
|
|
eq((loaded.dynamicWarp or {}).y, 3, "and its y")
|
|
eq((loaded.escapeWarp or {}).map, "FR_PEWTER_CITY", "so does the escape warp")
|
|
eq((loaded.escapeWarp or {}).y, 8, "with its doorstep y")
|
|
|
|
local old = Schema.fromSaveTable({
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
party = {}, dex = { seen = {}, owned = {} },
|
|
map = "FR_PALLET_TOWN", x = 5, y = 6, facing = "down", flags = {}, vars = {},
|
|
})
|
|
eq(old.dynamicWarp, nil, "an old save without the slots loads with none")
|
|
eq(old.escapeWarp, nil, "neither half is invented")
|
|
|
|
local junk = Schema.fromSaveTable({
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
party = {}, dex = { seen = {}, owned = {} },
|
|
map = "FR_PALLET_TOWN", x = 5, y = 6, facing = "down", flags = {}, vars = {},
|
|
dynamicWarp = "FR_SILPH_CO_5F", escapeWarp = 7,
|
|
})
|
|
eq(junk.dynamicWarp, nil, "a non-table dynamic warp is dropped")
|
|
eq(junk.escapeWarp, nil, "and so is a non-table escape warp")
|
|
end
|
|
|
|
print("[test] 8. the flash level is saved with the block it lives in")
|
|
do
|
|
-- pokefirered/include/global.h:770
|
|
local Runtime = require("src.core.game3.runtime")
|
|
local FieldView = require("src.core.game3.field_view")
|
|
local prev = Runtime.session
|
|
|
|
local session = Schema.newGame({ rngSeed = 0x78 })
|
|
eq(session.flashLevel, 0, "a New Game starts with no flash level")
|
|
|
|
Runtime.session = session
|
|
-- pokefirered/src/overworld.c:966 SetFlashLevel
|
|
FieldView.setFlashLevel(FieldView.MAX_FLASH_LEVEL)
|
|
eq(session.flashLevel, FieldView.MAX_FLASH_LEVEL,
|
|
"SetFlashLevel writes the level into the save block")
|
|
|
|
local loaded = Schema.fromSaveTable(Schema.toSaveTable(session))
|
|
eq(loaded.flashLevel, FieldView.MAX_FLASH_LEVEL,
|
|
"saving in a dark cave and reloading keeps the level")
|
|
|
|
local old = Schema.fromSaveTable({
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
party = {}, dex = { seen = {}, owned = {} },
|
|
map = "FR_PALLET_TOWN", x = 5, y = 6, facing = "down", flags = {}, vars = {},
|
|
})
|
|
eq(old.flashLevel, nil, "a save with no flashLevel key loads without one")
|
|
-- pokefirered/src/overworld.c:956 SetDefaultFlashLevel
|
|
Runtime.session = old
|
|
eq(FieldView.setDefaultFlashLevel(nil, "FR_PALLET_TOWN"), 0,
|
|
"so the map load falls back to the default for the map")
|
|
eq(old.flashLevel, 0, "and that default is what the next save carries")
|
|
|
|
Runtime.session = prev
|
|
end
|
|
|
|
print("[test] berryPowder round-trips through the schema")
|
|
do
|
|
-- include/global.h:354, src/berry_powder.c:50
|
|
local session = Schema.newGame({ rngSeed = 0x99 })
|
|
eq(session.berryPowder, 0, "a New Game starts with 0 berry powder")
|
|
|
|
session.berryPowder = 40
|
|
local save = Schema.toSaveTable(session)
|
|
eq(save.berryPowder, 40, "toSaveTable writes berryPowder")
|
|
local loaded = Schema.fromSaveTable(save)
|
|
eq(loaded.berryPowder, 40, "fromSaveTable restores berryPowder")
|
|
|
|
local old = Schema.fromSaveTable({
|
|
schemaVersion = 1, engine = "game3", version = "firered",
|
|
party = {}, dex = { seen = {}, owned = {} },
|
|
map = "FR_PALLET_TOWN", x = 5, y = 6, facing = "down", flags = {}, vars = {},
|
|
})
|
|
eq(old.berryPowder, 0, "a save with no berryPowder key loads 0")
|
|
end
|
|
|
|
print(string.format("[test] %d passed, %d failed", passed, failed))
|
|
if failed > 0 then
|
|
print("[test] FAILED " .. failed)
|
|
os.exit(1)
|
|
end
|
|
print("[test] all passed")
|