mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
295 lines
11 KiB
Lua
295 lines
11 KiB
Lua
#!/usr/bin/env luajit
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
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 done()
|
|
if failed > 0 then
|
|
print("[test] FAILED " .. failed)
|
|
os.exit(1)
|
|
end
|
|
print("[test] all passed")
|
|
os.exit(0)
|
|
end
|
|
|
|
local Cache = require("tests.game3_cache")
|
|
local cacheRoot = Cache.mount("scripts/events.lua", { native = true })
|
|
if not cacheRoot then
|
|
print("[skip] game3_stitchfield_ground_test: " .. tostring(Cache.reason))
|
|
done()
|
|
end
|
|
print("[info] FireRed cache at " .. cacheRoot)
|
|
|
|
local Space = require("src.core.game3.scripting.space")
|
|
local ExtractScripts = require("src.import.gba.extract_scripts")
|
|
Space.bundle = ExtractScripts.loadBundle(Cache.cache(), cacheRoot, { allowIncomplete = true })
|
|
|
|
local Dataset = require("src.core.game3.dataset")
|
|
local Runtime = require("src.core.game3.runtime")
|
|
local Map = require("src.core.game3.map")
|
|
local Field = require("src.core.game3.field")
|
|
local Player = require("src.core.game3.player")
|
|
local Collision = require("src.core.game3.collision")
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
local FieldEffects = require("src.core.game3.field_effects")
|
|
local Audio = require("src.core.game3.audio")
|
|
|
|
local MB_PUDDLE = 0x16
|
|
local MB_SHALLOW_WATER = 0x17
|
|
local MB_HOT_SPRINGS = 0x28
|
|
local SE_PUDDLE = 63
|
|
|
|
local seCount = 0
|
|
local realPlaySe = Audio.playSe
|
|
Audio.playSe = function(id, opts)
|
|
if id == SE_PUDDLE then seCount = seCount + 1 end
|
|
return realPlaySe(id, opts)
|
|
end
|
|
local function sinceSe()
|
|
local n = seCount
|
|
seCount = 0
|
|
return n
|
|
end
|
|
|
|
local seen = {}
|
|
local started = {}
|
|
local function sample()
|
|
for _, anim in ipairs(FieldEffects._anims) do
|
|
if not seen[anim] then
|
|
seen[anim] = true
|
|
started[anim.kind] = (started[anim.kind] or 0) + 1
|
|
end
|
|
end
|
|
end
|
|
local function sinceStarted(kind)
|
|
local n = started[kind] or 0
|
|
started[kind] = 0
|
|
return n
|
|
end
|
|
local function clearStarted()
|
|
started = {}
|
|
sinceSe()
|
|
end
|
|
|
|
local game = { data = {} }
|
|
Dataset.hydrate(game)
|
|
local session = { map = "FR_PALLET_TOWN", x = 12, y = 20, facing = "down", flags = {}, vars = {} }
|
|
game.session = session
|
|
Runtime.start(nil, game, session, { reason = "new_game" })
|
|
|
|
local held = nil
|
|
game.input = {
|
|
isDown = function(_, d) return d == held end,
|
|
wasPressed = function() return false end,
|
|
}
|
|
|
|
local function frames(n)
|
|
for _ = 1, n do
|
|
Field.update(1 / 60)
|
|
sample()
|
|
end
|
|
end
|
|
|
|
local function settle()
|
|
for _ = 1, 60 do
|
|
if not Field.locked and not Space.vm:isRunning() then return true end
|
|
frames(10)
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function goTo(mapId, x, y, facing)
|
|
held = nil
|
|
Map.load(nil, game, mapId, { x = x, y = y, facing = facing or "down" })
|
|
local ok = settle()
|
|
frames(2)
|
|
return ok and Player.cellX == x and Player.cellY == y
|
|
end
|
|
|
|
local function count(kind)
|
|
local n = 0
|
|
for _, anim in ipairs(FieldEffects._anims) do
|
|
if anim.kind == kind then n = n + 1 end
|
|
end
|
|
return n
|
|
end
|
|
|
|
local function walk(dir, x, y)
|
|
held = dir
|
|
for _ = 1, 30 do
|
|
frames(1)
|
|
if Player.moving then break end
|
|
end
|
|
held = nil
|
|
for _ = 1, 40 do
|
|
if not Player.moving then break end
|
|
frames(1)
|
|
end
|
|
return Player.cellX == x and Player.cellY == y
|
|
end
|
|
|
|
print("[test] 1. Four Island's pond edge is a puddle block")
|
|
-- pokefirered/data/maps/FourIsland/scripts.inc:23 FourIsland_OnFrame
|
|
local VAR_MAP_SCENE_FOUR_ISLAND = 0x4086
|
|
Flags.setVar(Space.store, Space.vm and Space.vm.ctx, VAR_MAP_SCENE_FOUR_ISLAND, 1)
|
|
check(goTo("FR_FOUR_ISLAND", 20, 9, "left"),
|
|
"Four Island settled at (20,9) with the rival scene already played")
|
|
check(Collision.behavior(20, 9) ~= MB_PUDDLE, "(20,9) is dry, behavior "
|
|
.. string.format("0x%02X", Collision.behavior(20, 9) or 0))
|
|
for _, c in ipairs({ { 19, 9 }, { 18, 9 }, { 18, 10 } }) do
|
|
check(Collision.behavior(c[1], c[2]) == MB_PUDDLE,
|
|
string.format("(%d,%d) is MB_PUDDLE", c[1], c[2]))
|
|
end
|
|
|
|
print("[test] 2. stepping onto a puddle from dry land splashes once on landing")
|
|
-- pokefirered/src/event_object_movement.c:8163 GetGroundEffectFlags_Puddle
|
|
-- pokefirered/src/event_object_movement.c:5343 ShiftStillObjectEventCoords
|
|
clearStarted()
|
|
check(walk("left", 19, 9), "walked (20,9) to (19,9)")
|
|
check(sinceStarted("splash") == 1,
|
|
"one splash, the finish step reads previous == current == MB_PUDDLE")
|
|
check(sinceStarted("ripple") == 1,
|
|
"one ripple, MetatileBehavior_HasRipples covers MB_PUDDLE")
|
|
check(sinceSe() == 1, "and SE_PUDDLE played once")
|
|
|
|
print("[test] 3. puddle to puddle splashes on the begin step and again on the finish step")
|
|
-- pokefirered/src/event_object_movement.c:8035 and :8049
|
|
clearStarted()
|
|
check(walk("left", 18, 9), "walked (19,9) to (18,9)")
|
|
check(sinceStarted("splash") == 2, "two splashes for the one step")
|
|
check(sinceStarted("ripple") == 1, "and one ripple on landing")
|
|
check(sinceSe() == 2, "SE_PUDDLE played twice")
|
|
|
|
print("[test] 4. the splash runs sAnim_Splash_0 and stops")
|
|
-- pokefirered/src/data/field_effects/field_effect_objects.h:571
|
|
local frameLog = {}
|
|
for _ = 1, 10 do
|
|
local f = nil
|
|
for _, anim in ipairs(FieldEffects._anims) do
|
|
if anim.kind == "splash" then f = anim.frame end
|
|
end
|
|
frameLog[#frameLog + 1] = tostring(f)
|
|
frames(1)
|
|
end
|
|
check(table.concat(frameLog, ",") == "0,0,0,0,1,1,1,1,nil,nil",
|
|
"splash frames 0,0,0,0,1,1,1,1 then gone, got " .. table.concat(frameLog, ","))
|
|
|
|
print("[test] 5. the ripple lives the 79 ticks of sAnim_Ripple")
|
|
-- pokefirered/src/data/field_effects/field_effect_objects.h:108
|
|
check(goTo("FR_FOUR_ISLAND", 20, 9, "left"), "back on dry land at (20,9)")
|
|
check(count("ripple") == 0, "no ripples left over, got " .. count("ripple"))
|
|
check(walk("left", 19, 9), "walked onto the puddle again")
|
|
local rippleAnim
|
|
for _, anim in ipairs(FieldEffects._anims) do
|
|
if anim.kind == "ripple" then rippleAnim = anim end
|
|
end
|
|
check(rippleAnim ~= nil and rippleAnim.cx == 19 and rippleAnim.cy == 9,
|
|
"the ripple sits on the cell that was stepped onto")
|
|
frames(79 - (rippleAnim and rippleAnim.timer or 0))
|
|
check(count("ripple") == 1, "still rippling on the 79th tick, the last frame of the anim")
|
|
frames(1)
|
|
check(count("ripple") == 0, "gone on the next tick, got " .. count("ripple"))
|
|
|
|
print("[test] 6. Route 21's sandbar wades from the first cell")
|
|
-- pokefirered/src/event_object_movement.c:8143 GetGroundEffectFlags_ShallowFlowingWater
|
|
check(goTo("FR_ROUTE_21_NORTH", 17, 24, "right"), "stood beside the Route 21 North sandbar")
|
|
check(Collision.behavior(17, 24) ~= MB_SHALLOW_WATER
|
|
and Collision.behavior(18, 24) == MB_SHALLOW_WATER
|
|
and Collision.behavior(18, 25) == MB_SHALLOW_WATER
|
|
and Collision.behavior(17, 25) == MB_SHALLOW_WATER,
|
|
"(18,24), (18,25) and (17,25) are MB_SHALLOW_WATER and (17,24) is not")
|
|
check(FieldEffects._ground.inShallowFlowingWater == false, "inShallowFlowingWater is clear")
|
|
clearStarted()
|
|
check(walk("right", 18, 24), "waded (17,24) to (18,24)")
|
|
check(sinceStarted("feet_water") == 1,
|
|
"the finish step reads previous == current, so the first cell wades")
|
|
check(count("feet_water") == 1, "one effect is running, got " .. count("feet_water"))
|
|
check(FieldEffects._ground.inShallowFlowingWater == true, "the sticky flag is set")
|
|
frames(2)
|
|
-- pokefirered/src/field_effect_helpers.c:707 UpdateFeetInFlowingWaterFieldEffect
|
|
check(sinceSe() == 1, "SE_PUDDLE played once for the cell it started on")
|
|
|
|
print("[test] 7. wading keeps exactly one effect and replays SE_PUDDLE per cell")
|
|
clearStarted()
|
|
check(walk("down", 18, 25), "waded (18,24) to (18,25)")
|
|
check(sinceStarted("feet_water") == 0, "no second effect, the sticky flag never cleared")
|
|
check(count("feet_water") == 1, "and one is running, got " .. count("feet_water"))
|
|
check(FieldEffects._ground.inShallowFlowingWater == true, "the sticky flag stayed set")
|
|
check(sinceStarted("splash") == 0, "no splash, MB_SHALLOW_WATER is not MB_PUDDLE")
|
|
check(sinceSe() == 1, "SE_PUDDLE played once")
|
|
clearStarted()
|
|
check(walk("left", 17, 25), "waded on to (17,25)")
|
|
check(sinceStarted("feet_water") == 0, "no second effect")
|
|
check(count("feet_water") == 1, "still exactly one, got " .. count("feet_water"))
|
|
check(sinceSe() == 1, "SE_PUDDLE replayed once for the new cell")
|
|
|
|
print("[test] 8. leaving the shallow water stops it")
|
|
check(walk("left", 16, 25), "stepped off the sandbar onto (16,25)")
|
|
check(FieldEffects._ground.inShallowFlowingWater == false, "the sticky flag cleared")
|
|
check(count("feet_water") == 0, "the effect stopped, got " .. count("feet_water"))
|
|
|
|
print("[test] 9. spawning already in the water starts it, as OnSpawn does")
|
|
-- pokefirered/src/event_object_movement.c:8023 GetAllGroundEffectFlags_OnSpawn
|
|
clearStarted()
|
|
check(goTo("FR_ROUTE_21_NORTH", 18, 25, "down"), "warped straight onto a sandbar cell")
|
|
check(FieldEffects._ground.inShallowFlowingWater == true, "the sticky flag is set on arrival")
|
|
check(count("feet_water") == 1, "one effect on arrival, got " .. count("feet_water"))
|
|
check(sinceStarted("ripple") == 0, "and no ripple, that is a finish-step effect only")
|
|
|
|
print("[test] 10. Ember Spa steams from the cell the player lands on")
|
|
-- pokefirered/src/event_object_movement.c:8196 GetGroundEffectFlags_HotSprings
|
|
check(goTo("FR_ONE_ISLAND_KINDLE_ROAD_EMBER_SPA", 13, 10, "down"), "stood at the spa edge")
|
|
check(Collision.behavior(13, 10) ~= MB_HOT_SPRINGS
|
|
and Collision.behavior(13, 11) == MB_HOT_SPRINGS
|
|
and Collision.behavior(13, 12) == MB_HOT_SPRINGS,
|
|
"the pool cells are MB_HOT_SPRINGS and the edge is not")
|
|
clearStarted()
|
|
check(walk("down", 13, 11), "stepped into the pool")
|
|
-- pokefirered/src/event_object_movement.c:5343 ShiftStillObjectEventCoords
|
|
check(sinceStarted("hot_springs") == 1, "steam started on the first spring cell")
|
|
check(walk("down", 13, 12), "waded deeper")
|
|
check(sinceStarted("hot_springs") == 0, "no second steam effect")
|
|
check(FieldEffects._ground.inHotSprings == true, "the sticky flag is set")
|
|
check(Collision.behavior(12, 12) == MB_HOT_SPRINGS, "(12,12) is MB_HOT_SPRINGS too")
|
|
check(walk("left", 12, 12), "waded across the pool")
|
|
check(sinceStarted("hot_springs") == 0, "no second steam effect")
|
|
check(count("hot_springs") == 1, "one is running, got " .. count("hot_springs"))
|
|
check(walk("right", 13, 12) and walk("up", 13, 11), "waded back to the pool edge")
|
|
check(count("hot_springs") == 1, "still steaming, both cells are hot springs")
|
|
check(walk("up", 13, 10), "back on dry land")
|
|
check(FieldEffects._ground.inHotSprings == false, "the sticky flag cleared")
|
|
check(count("hot_springs") == 0, "the steam stopped, got " .. count("hot_springs"))
|
|
check(sinceSe() == 0, "and the spa is silent, hot springs plays no SE_PUDDLE")
|
|
|
|
print("[test] 11. a map change resets the ground effect state")
|
|
-- pokefirered/src/event_object_movement.c:1934 ResetObjectEventFldEffData
|
|
check(goTo("FR_ROUTE_21_NORTH", 18, 25, "down"), "back on the sandbar")
|
|
check(count("feet_water") == 1, "wading again")
|
|
check(goTo("FR_PALLET_TOWN", 12, 20, "down"), "warped to Pallet Town")
|
|
check(FieldEffects._ground.inShallowFlowingWater == false,
|
|
"inShallowFlowingWater cleared by the map change")
|
|
check(count("feet_water") == 0, "and the effect stopped, got " .. count("feet_water"))
|
|
|
|
print("[test] 12. standing still costs nothing")
|
|
local anims = #FieldEffects._anims
|
|
FieldEffects.groundEffects()
|
|
collectgarbage("collect")
|
|
local before = collectgarbage("count")
|
|
for _ = 1, 20000 do FieldEffects.groundEffects() end
|
|
local perPoll = (collectgarbage("count") - before) * 1024 / 20000
|
|
check(#FieldEffects._anims == anims, "20000 polls started nothing")
|
|
check(perPoll < 1,
|
|
"the standing poll allocates " .. string.format("%.4f", perPoll) .. " bytes per frame")
|
|
|
|
Audio.playSe = realPlaySe
|
|
done()
|