mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
344 lines
13 KiB
Lua
344 lines
13 KiB
Lua
#!/usr/bin/env luajit
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
require("tests.game3_cache").stubSpeciesNames()
|
|
|
|
local failed = 0
|
|
local function check(cond, msg)
|
|
if cond then
|
|
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 store = { flags = {}, vars = {} }
|
|
local session = { store = store, map = "FR_FOUR_ISLAND_ICEFALL_CAVE_1F", party = {} }
|
|
package.loaded["src.core.game3.runtime"] = {
|
|
getSession = function() return session end,
|
|
isActive = function() return true end,
|
|
}
|
|
|
|
local Std = require("src.core.game3.scripting.stdscripts")
|
|
local Natives = require("src.core.game3.scripting.natives")
|
|
local Events = require("src.core.game3.scripting.natives_events")
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
|
|
local function newCtx()
|
|
return { specialVars = {} }
|
|
end
|
|
|
|
local function getVar(ctx, id) return Flags.getVar(store, ctx, id) end
|
|
local function setVar(ctx, id, v) Flags.setVar(store, ctx, id, v) end
|
|
|
|
print("[test] 1. natives_*.lua auto-discovery")
|
|
local names = Natives.MODULE_NAMES
|
|
check(type(names) == "table" and #names >= 3, "MODULE_NAMES lists the topic modules")
|
|
local sorted = true
|
|
for i = 2, #names do
|
|
if names[i - 1] > names[i] then sorted = false end
|
|
end
|
|
check(sorted, "MODULE_NAMES is sorted so a later module overrides an earlier id")
|
|
local seen = {}
|
|
for _, n in ipairs(names) do seen[n] = true end
|
|
check(seen["natives_events"], "discovery found natives_events")
|
|
check(seen["natives_queries"], "discovery found natives_queries")
|
|
check(seen["natives_seagallop"], "discovery found natives_seagallop")
|
|
local merged = true
|
|
for _, n in ipairs(names) do
|
|
local mod = Natives.MODULES[n]
|
|
for id in pairs((mod and mod.HANDLERS) or {}) do
|
|
if Natives.ALLOW["special:" .. id] == nil then
|
|
merged = false
|
|
print(" missing merged id " .. string.format("0x%X", id) .. " from " .. n)
|
|
end
|
|
end
|
|
end
|
|
check(merged, "every discovered module's HANDLERS are merged into Natives.ALLOW")
|
|
check(Natives.Queries ~= nil and Natives.Seagallop ~= nil,
|
|
"the legacy Natives.Queries / Natives.Seagallop handles still resolve")
|
|
|
|
local roster = {}
|
|
for _, n in ipairs(Natives.KNOWN_MODULES or {}) do roster[n] = true end
|
|
local onDisk, listed = {}, false
|
|
local pipe = io.popen('ls -1 "' .. Natives.MODULE_DIR .. '" 2>/dev/null')
|
|
if pipe then
|
|
for line in pipe:lines() do
|
|
local base = line:match("^(natives_[%w_]+)%.lua$")
|
|
if base then
|
|
listed = true
|
|
onDisk[#onDisk + 1] = base
|
|
end
|
|
end
|
|
pipe:close()
|
|
end
|
|
check(listed, "the scripting directory listing found natives_*.lua modules to check")
|
|
local complete = true
|
|
for _, base in ipairs(onDisk) do
|
|
if not roster[base] then
|
|
complete = false
|
|
print(" " .. base .. " is missing from Natives.KNOWN_MODULES")
|
|
end
|
|
end
|
|
check(complete, "every natives_*.lua on disk is in the known-name fallback roster")
|
|
local bound = true
|
|
for _, base in ipairs(Natives.KNOWN_MODULES or {}) do
|
|
local okMod, mod = pcall(require, "src.core.game3.scripting." .. base)
|
|
if okMod and type(mod) == "table" then
|
|
for id in pairs(mod.HANDLERS or {}) do
|
|
if Natives.ALLOW["special:" .. id] == nil then
|
|
bound = false
|
|
print(" " .. base .. " id " .. string.format("0x%X", id) .. " is unbound")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
check(bound, "every roster module's handlers are bound, listing or no listing")
|
|
|
|
print("[test] 2. ids match the 0-based def_special index of pokefirered/data/specials.inc")
|
|
local EXPECTED = {
|
|
ShowFieldMessageStringVar4 = 0x8D,
|
|
DrawWholeMapView = 0x8E,
|
|
RockSmashWildEncounter = 0xAB,
|
|
EnterSafariMode = 0xCD,
|
|
ExitSafariMode = 0xCE,
|
|
InitRoamer = 0x129,
|
|
SetIcefallCaveCrackedIceMetatiles = 0x135,
|
|
ShakeScreen = 0x136,
|
|
SetPostgameFlagsUnusedSlot = 0x155,
|
|
ForcePlayerOntoBike = 0x157,
|
|
SampleResortGorgeousMonAndReward = 0x15D,
|
|
ForcePlayerToStartSurfing = 0x161,
|
|
DisableMsgBoxWalkaway = 0x171,
|
|
SetPostgameFlags = 0x19A,
|
|
DoDeoxysTriangleInteraction = 0x1AB,
|
|
SetDeoxysTrianglePalette = 0x1AC,
|
|
UpdateLoreleiDollCollection = 0x1B9,
|
|
CreateEnemyEventMon = 0x1BB,
|
|
}
|
|
for name, id in pairs(EXPECTED) do
|
|
eq(Std.SPECIAL[name], id, "Std.SPECIAL." .. name)
|
|
check(Events.HANDLERS[id] ~= nil, name .. " has a handler")
|
|
end
|
|
|
|
print("[test] 3. no id collides with another Std.SPECIAL name")
|
|
local byId = {}
|
|
local collisions = 0
|
|
for name, id in pairs(Std.SPECIAL) do
|
|
if byId[id] then
|
|
collisions = collisions + 1
|
|
print(" 0x" .. string.format("%X", id) .. " claimed by " .. byId[id] .. " and " .. name)
|
|
end
|
|
byId[id] = name
|
|
end
|
|
eq(collisions, 0, "distinct id per special name")
|
|
|
|
print("[test] 4. no registered event special logs as unknown")
|
|
Natives.resetLog()
|
|
local logs = {}
|
|
local quietAdapters = { log = function(m) logs[#logs + 1] = m end }
|
|
for id in pairs(Events.HANDLERS) do
|
|
local ctx = newCtx()
|
|
local _, _, known = Natives.special(ctx, id, quietAdapters)
|
|
check(known, string.format("special 0x%X dispatches to a handler", id))
|
|
end
|
|
eq(#logs, 0, "nothing reached the unknown-special log")
|
|
|
|
print("[test] 5. DrawWholeMapView forces the field redraw")
|
|
local FieldView = package.loaded["src.core.game3.field_view"]
|
|
if not FieldView then
|
|
FieldView = { _nativeDirty = false }
|
|
package.loaded["src.core.game3.field_view"] = FieldView
|
|
end
|
|
FieldView._nativeDirty = false
|
|
Events.HANDLERS[Std.SPECIAL.DrawWholeMapView](newCtx(), {})
|
|
check(FieldView._nativeDirty == true, "DrawWholeMapView marks the view dirty")
|
|
|
|
print("[test] 6. CreateEnemyEventMon stages the event mon for StartLegendaryBattle")
|
|
local Encounters = require("src.core.game3.encounters")
|
|
Encounters.takePendingWild()
|
|
local ctx6 = newCtx()
|
|
-- pokefirered/include/constants/species.h:256
|
|
setVar(ctx6, 0x8004, 249)
|
|
setVar(ctx6, 0x8005, 70)
|
|
setVar(ctx6, 0x8006, 0)
|
|
eq(Events.HANDLERS[Std.SPECIAL.CreateEnemyEventMon](ctx6, {}), false,
|
|
"CreateEnemyEventMon does not yield")
|
|
local staged = Encounters.takePendingWild()
|
|
check(staged ~= nil, "a wild is pending after CreateEnemyEventMon")
|
|
eq(staged and staged.species, 249, "staged species")
|
|
eq(staged and staged.level, 70, "staged level")
|
|
eq(staged and staged.item, nil, "ITEM_NONE stages no held item")
|
|
check(staged and staged.fatefulEncounter == true, "the event mon is a fateful encounter")
|
|
|
|
local ctx6b = newCtx()
|
|
-- pokefirered/include/constants/species.h:419
|
|
setVar(ctx6b, 0x8004, 410)
|
|
setVar(ctx6b, 0x8005, 30)
|
|
setVar(ctx6b, 0x8006, 175)
|
|
Events.HANDLERS[Std.SPECIAL.CreateEnemyEventMon](ctx6b, {})
|
|
local staged2 = Encounters.takePendingWild()
|
|
eq(staged2 and staged2.item, 175, "a non-zero item is staged as the held item")
|
|
|
|
print("[test] 7. StartLegendaryBattle now finds the event mon")
|
|
local ctx7 = newCtx()
|
|
setVar(ctx7, 0x8004, 249)
|
|
setVar(ctx7, 0x8005, 70)
|
|
setVar(ctx7, 0x8006, 0)
|
|
Events.HANDLERS[Std.SPECIAL.CreateEnemyEventMon](ctx7, {})
|
|
local startedWith
|
|
local adapters7 = {
|
|
startWildBattle = function(foe, cb, opts)
|
|
startedWith = { foe = foe, opts = opts }
|
|
cb("win")
|
|
end,
|
|
}
|
|
Natives.special(ctx7, Std.SPECIAL.StartLegendaryBattle, adapters7)
|
|
check(startedWith ~= nil, "StartLegendaryBattle started a battle")
|
|
eq(startedWith and startedWith.foe.species, 249, "the legendary battle uses the event mon")
|
|
check(startedWith and startedWith.opts.legendary == true, "battle flagged legendary")
|
|
|
|
print("[test] 8. SetIcefallCaveCrackedIceMetatiles restores only cracked tiles")
|
|
local Field = require("src.core.game3.field")
|
|
check(type(Field.setMetatile) == "function", "Field.setMetatile is the real entry point")
|
|
local realSetMetatile = Field.setMetatile
|
|
local writes = {}
|
|
Field.setMetatile = function(x, y, mid, impassable)
|
|
writes[#writes + 1] = { x = x, y = y, mid = mid, impassable = impassable }
|
|
end
|
|
store.flags = {}
|
|
local ctx8 = newCtx()
|
|
Events.HANDLERS[Std.SPECIAL.SetIcefallCaveCrackedIceMetatiles](ctx8, {})
|
|
eq(#writes, 0, "no cracked ice with no temp flags set")
|
|
Flags.setFlag(store, ctx8, 1, true)
|
|
Flags.setFlag(store, ctx8, 4, true)
|
|
Events.HANDLERS[Std.SPECIAL.SetIcefallCaveCrackedIceMetatiles](ctx8, {})
|
|
eq(#writes, 2, "two cracked ice tiles restored")
|
|
eq(writes[1] and writes[1].x, 8, "first cracked tile x")
|
|
eq(writes[1] and writes[1].y, 3, "first cracked tile y")
|
|
eq(writes[2] and writes[2].x, 8, "fourth cracked tile x")
|
|
eq(writes[2] and writes[2].y, 9, "fourth cracked tile y")
|
|
eq(writes[1] and writes[1].mid, 0x35A, "METATILE_SeafoamIslands_CrackedIce")
|
|
eq(writes[1] and writes[1].impassable, false, "cracked ice is not written impassable")
|
|
Field.setMetatile = realSetMetatile
|
|
store.flags = {}
|
|
|
|
print("[test] 9. ForcePlayerOntoBike / ForcePlayerToStartSurfing set avatar state")
|
|
local Player = require("src.core.game3.player")
|
|
Player.biking, Player.surfing, Player.surfHopping = false, false, false
|
|
Events.HANDLERS[Std.SPECIAL.ForcePlayerOntoBike](newCtx(), {})
|
|
check(Player.biking == true, "ForcePlayerOntoBike mounts the bike")
|
|
Events.HANDLERS[Std.SPECIAL.ForcePlayerToStartSurfing](newCtx(), {})
|
|
check(Player.surfing == true, "ForcePlayerToStartSurfing puts the player on the water")
|
|
check(Player.biking == false, "surfing clears the bike")
|
|
check(Player.surfHopping == false, "the forced transition skips the hop")
|
|
Player.biking, Player.surfing = false, false
|
|
Player.surfing = true
|
|
Events.HANDLERS[Std.SPECIAL.ForcePlayerOntoBike](newCtx(), {})
|
|
check(Player.biking == false, "a surfing player is not forced onto the bike")
|
|
Player.surfing = false
|
|
|
|
print("[test] 10. RockSmashWildEncounter")
|
|
local ctx10 = newCtx()
|
|
local savedRollRocks = Encounters.rollRocks
|
|
Encounters.rollRocks = nil
|
|
Events.HANDLERS[Std.SPECIAL.RockSmashWildEncounter](ctx10, {})
|
|
eq(getVar(ctx10, 0x800D), 0, "no rollRocks means VAR_RESULT stays FALSE")
|
|
local askedMap
|
|
Encounters.rollRocks = function(mapId)
|
|
askedMap = mapId
|
|
return { species = 74, level = 15 }
|
|
end
|
|
local rockFoe
|
|
local adapters10 = {
|
|
startWildBattle = function(foe, cb) rockFoe = foe; cb("win") end,
|
|
}
|
|
local ctx10b = newCtx()
|
|
Events.HANDLERS[Std.SPECIAL.RockSmashWildEncounter](ctx10b, adapters10)
|
|
eq(askedMap, session.map, "rollRocks is asked about the current map")
|
|
eq(getVar(ctx10b, 0x800D), 1, "a rolled encounter sets VAR_RESULT to TRUE")
|
|
check(rockFoe ~= nil and rockFoe.wildScripted == true, "the rock smash battle is a scripted wild")
|
|
local ctx10c = newCtx()
|
|
Encounters.rollRocks = function() return nil end
|
|
Events.HANDLERS[Std.SPECIAL.RockSmashWildEncounter](ctx10c, adapters10)
|
|
eq(getVar(ctx10c, 0x800D), 0, "an empty roll sets VAR_RESULT to FALSE")
|
|
Encounters.rollRocks = savedRollRocks
|
|
|
|
print("[test] 11. Safari specials drive game3/safari.lua when it exists")
|
|
local savedSafari = package.loaded["src.core.game3.safari"]
|
|
local calls = {}
|
|
package.loaded["src.core.game3.safari"] = {
|
|
enter = function() calls[#calls + 1] = "enter" end,
|
|
exit = function() calls[#calls + 1] = "exit" end,
|
|
}
|
|
Events.HANDLERS[Std.SPECIAL.EnterSafariMode](newCtx(), {})
|
|
Events.HANDLERS[Std.SPECIAL.ExitSafariMode](newCtx(), {})
|
|
eq(table.concat(calls, ","), "enter,exit", "EnterSafariMode / ExitSafariMode call Safari")
|
|
package.loaded["src.core.game3.safari"] = { }
|
|
local okNoApi = pcall(function()
|
|
Events.HANDLERS[Std.SPECIAL.EnterSafariMode](newCtx(), {})
|
|
Events.HANDLERS[Std.SPECIAL.ExitSafariMode](newCtx(), {})
|
|
end)
|
|
check(okNoApi, "the safari seam is safe while game3/safari.lua has no enter/exit")
|
|
package.loaded["src.core.game3.safari"] = savedSafari
|
|
|
|
print("[test] 12. SetPostgameFlags")
|
|
session.gcnLinkFlags = 0
|
|
session.specialSaveWarpFlags = 0
|
|
Events.HANDLERS[Std.SPECIAL.SetPostgameFlags](newCtx(), {})
|
|
eq(session.gcnLinkFlags, 0x800E, "gcnLinkFlags bits 1,2,3,15")
|
|
eq(session.specialSaveWarpFlags, 0x80, "CHAMPION_SAVEWARP")
|
|
Events.HANDLERS[Std.SPECIAL.SetPostgameFlagsUnusedSlot](newCtx(), {})
|
|
eq(session.gcnLinkFlags, 0x800E, "the duplicate def_special slot runs the same handler")
|
|
|
|
print("[test] 13. cited no-ops return without yielding")
|
|
local NOOPS = {
|
|
"ShowFieldMessageStringVar4", "ShakeScreen", "InitRoamer",
|
|
"SampleResortGorgeousMonAndReward", "DisableMsgBoxWalkaway",
|
|
"UpdateLoreleiDollCollection",
|
|
}
|
|
for _, name in ipairs(NOOPS) do
|
|
local ctx = newCtx()
|
|
local yield = Events.HANDLERS[Std.SPECIAL[name]](ctx, {})
|
|
eq(yield, false, name .. " does not yield")
|
|
eq(getVar(ctx, 0x800D), 0, name .. " leaves VAR_RESULT at 0")
|
|
end
|
|
|
|
print("[test] 14. SampleResortGorgeousMonAndReward buffers the sampled species into STR_VAR_1")
|
|
local savedPokemon = package.loaded["src.core.game3.pokemon"]
|
|
package.loaded["src.core.game3.pokemon"] = {
|
|
name = function(sp) return ({ [25] = "PIKACHU", [150] = "MEWTWO" })[sp] or ("SP" .. tostring(sp)) end,
|
|
}
|
|
local savedDex = session.dex
|
|
session.dex = { owned = { [25] = true } }
|
|
local VAR_REQ = 0x4036
|
|
store.vars = {}
|
|
local ctx14 = newCtx()
|
|
ctx14.stringVars = {}
|
|
local buffered = {}
|
|
Events.HANDLERS[Std.SPECIAL.SampleResortGorgeousMonAndReward](ctx14, {
|
|
setStringVar = function(i, text) buffered[i] = text end,
|
|
})
|
|
eq(getVar(ctx14, VAR_REQ), 25, "an empty request samples the only owned species")
|
|
eq(ctx14.stringVars[1], "PIKACHU", "STR_VAR_1 names the freshly sampled species")
|
|
eq(buffered[1], "PIKACHU", "the host adapter receives STR_VAR_1")
|
|
local ctx14b = newCtx()
|
|
ctx14b.stringVars = {}
|
|
setVar(ctx14b, VAR_REQ, 150)
|
|
Events.HANDLERS[Std.SPECIAL.SampleResortGorgeousMonAndReward](ctx14b, {})
|
|
eq(getVar(ctx14b, VAR_REQ), 150, "a pending request is kept")
|
|
eq(ctx14b.stringVars[1], "MEWTWO", "STR_VAR_1 names the pending request")
|
|
session.dex = savedDex
|
|
store.vars = {}
|
|
package.loaded["src.core.game3.pokemon"] = savedPokemon
|
|
|
|
if failed > 0 then
|
|
print("[test] FAILED " .. failed)
|
|
os.exit(1)
|
|
end
|
|
print("[test] all passed")
|