CLOSES #2759, CLOSES #2761, CLOSES #2762, CLOSES #2763, CLOSES #2764, CLOSES #2765, CLOSES #2766, CLOSES #2767

CLOSES #2759, CLOSES #2761, CLOSES #2762, CLOSES #2763, CLOSES #2764, CLOSES #2765, CLOSES #2766, CLOSES #2767
This commit is contained in:
bryanthaboi
2026-10-08 10:20:26 -04:00
parent 3a6987c588
commit c819846a3d
40 changed files with 2465 additions and 46 deletions
+5 -1
View File
@@ -1641,6 +1641,11 @@ function love.quit()
require("src.import.LauncherWindow").observe(0)
require("src.import.LauncherWindow").flush()
end
local osName = love.system and love.system.getOS and love.system.getOS()
if osName == "NX" then
endProcessOnce()
return
end
if editorMode and EditorApp.quit then
-- true blocks the quit (unsaved-changes prompt). A quit that proceeds
-- must fall through to the worker shutdowns below instead of returning:
@@ -1667,7 +1672,6 @@ function love.quit()
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
-- claims the hook) is exactly the condition below.
local osName = love.system and love.system.getOS and love.system.getOS()
local mobile = (osName == "Android" or osName == "iOS")
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
return Game and not Importer and not quitToLauncher and not scripted
+8 -1
View File
@@ -1043,7 +1043,14 @@ function Game3:saveGame()
end)
if FieldModules.enabled("questLog", self.session) then QuestRecorder.save(self) end
local P = package.loaded["src.core.game3.player"]
if P and P.facing and not P.moving and P.cellX == self.session.x and P.cellY == self.session.y then self.session.facing = P.facing end
if P and P.facing and not P.moving and P.cellX == self.session.x and P.cellY == self.session.y then
self.session.facing = P.facing
self.session.surfing = P.surfing == true
self.session.underwater = P.underwater == true
self.session.biking = P.biking == true
self.session.bikeType = P.bikeType
self.session.elevation = P.elevation
end
local save = Schema.toSaveTable(self.session)
local rules = Schema.rulesFor(self.session.version)
if rules.saveLocation then
+11 -5
View File
@@ -63,6 +63,7 @@ Ui._pendingYesNo = nil
Ui._session = nil
Ui._active = 0
Ui._actionCursor = {}
Ui._actionCursorBattler = {}
Ui._moveCursor = {}
Ui._moveCursorMon = {}
Ui._target = nil
@@ -228,6 +229,7 @@ function Ui.reset(opts)
Ui._pendingYesNo = nil
Ui._active = 0
Ui._actionCursor = {}
Ui._actionCursorBattler = {}
Ui._moveCursor = {}
Ui._moveCursorMon = {}
Ui._swap = nil
@@ -715,12 +717,14 @@ function Ui.openMenu(battlerId, opts)
Ui._target = nil
Ui._active = tonumber(battlerId) or 0
Ui._partnerAction = opts and opts.partnerAction or nil
if is_double() then
-- pokefirered/src/battle_controller_player.c:2421
Ui._menuIndex = Ui._actionCursor[Ui._active] or 1
else
Ui._menuIndex = 1
local battler = Ui._st and State.battler(Ui._st, Ui._active)
-- pokefirered/src/battle_controller_player.c:2099
if Ui._actionCursorBattler[Ui._active] ~= battler then
Ui._actionCursorBattler[Ui._active] = battler
Ui._actionCursor[Ui._active] = 1
end
-- pokefirered/src/battle_controller_player.c:2421
Ui._menuIndex = Ui._actionCursor[Ui._active] or 1
Ui._pendingCommand = nil
Ui._wally = nil
local Wally = require("src.core.game3.battle.tutorial_wally")
@@ -1641,11 +1645,13 @@ function Ui.handleInput(input)
local idx, moved = grid_nav(Ui._menuIndex, input, 4)
if moved then
Ui._menuIndex = idx
Ui._actionCursor[Ui._active] = idx
play_select()
return true
end
if input:wasPressed("a") then
play_select()
Ui._actionCursor[Ui._active] = Ui._menuIndex
if Ui._st and Ui._st.safari then
-- pokefirered/src/battle_controller_safari.c:162
local act = Commands.playerAction(Ui._st, Ui._menuIndex, nil)
+1 -3
View File
@@ -851,8 +851,6 @@ function Collision.tryConnection(game, fromX, fromY, dir, run)
-- Park one cell before landing and keep the step running so the seam does
-- not hitch (same world pixels the neighbor strip already showed).
local CELL = 16
local WALK_FRAMES = 16
local RUN_FRAMES = 8
Player.cellX, Player.cellY = lx - d[1], ly - d[2]
Player.px, Player.py = Player.cellX * CELL, Player.cellY * CELL
Player.facing = dir
@@ -865,7 +863,7 @@ function Collision.tryConnection(game, fromX, fromY, dir, run)
Player.dismounting = Player.surfing and not landingWater or false
if Player.dismounting then lazyReq("src.core.game3.audio").stopSurfMusic() end
Player.spriteYOffset = 0
Player.stepFrames = run and RUN_FRAMES or WALK_FRAMES
Player.stepFrames, Player.running = Player.ordinaryStepFrames(run)
Player.syncSavePosition(g)
if Collision.isGrass and Collision.isGrass(lx, ly) then
+4 -1
View File
@@ -253,7 +253,10 @@ local function drawFieldPlane(game, vw, vh, Renderer)
if Tilt.active() and not transitioning and Renderer and Renderer.beginUprightPass then
FieldView.draw(game, vw, vh, { skipActors = true, exchangeCanvas = exchange })
Renderer:beginUprightPass()
FieldView.draw(game, vw, vh, { actorsOnly = true, billboard = true })
FieldView.draw(game, vw, vh, {
actorsOnly = true, billboard = true,
endUnderActors = function(drawOverhead) Renderer:occludeUprightActors(drawOverhead) end,
})
Renderer:endUprightPass()
else
FieldView.draw(game, vw, vh, { exchangeCanvas = exchange })
+14 -3
View File
@@ -1417,9 +1417,20 @@ function Field.executeFieldMove(payload)
Flags.setFlag(Space.store, nil, payload.flag, true)
end
end
FieldEffects.startFlash(function()
Field.locked = false
end)
local profile = lazyReq("src.core.game3.profile").forSession(Field._session)
local script = profile.field and profile.field.flashScript
if script then
local Space = lazyReq("src.core.game3.scripting.space")
local key = assert(Space.scriptKey(script), "ROM Flash script is not in the script cache: " .. script)
assert(Space.vm, "Flash requires the field script VM")
-- pokeruby/src/script.c:230
Space.vm.ctx.fieldControlsLocked = true
assert(Space.startScript(key), "ROM Flash script could not start: " .. script)
else
FieldEffects.startFlash(function()
Field.locked = false
end)
end
end)
elseif act == "dig" or act == "braille_rs_dig" then
Field.locked = true
+4 -1
View File
@@ -992,7 +992,8 @@ function FxRse.collectActors(actors)
local eo = O._byId[lid]
if eo and eo.visible and not eo.hidden then
local d = eo.disguise
if d and not d.done then
-- pokeemerald/src/field_effect_helpers.c:1362
if d and not d.done and not eo.invisible then
local sheet = FE().loadSheet(d.sheet)
local q = sheet and d.a and sheet.quads[d.a.frame or 0]
if q then
@@ -1004,6 +1005,8 @@ function FxRse.collectActors(actors)
love.graphics.draw(sheet.image, q, math.floor(x) - camX, math.floor(y) - camY)
love.graphics.setColor(1, 1, 1, 1)
end)
-- pokeemerald/src/field_effect_helpers.c:1365
actors[#actors].disguiseObject = eo
end
end
local bt = eo.berryTree
+10
View File
@@ -513,14 +513,18 @@ local function sortActors(a, b)
return a.subpriority > b.subpriority
end
local drawOrderObjects = {}
-- event_object_movement.c:7739-7754, scrcmd.c:1130
local function applyDrawOrder(actors, underActors, overActors, camY)
underActors = underActors or {}
overActors = overActors or {}
for i = #underActors, 1, -1 do underActors[i] = nil end
for i = #overActors, 1, -1 do overActors[i] = nil end
for obj in pairs(drawOrderObjects) do drawOrderObjects[obj] = nil end
for _, a in ipairs(actors) do
local obj = a.eventObject
if obj then drawOrderObjects[obj] = a end
if (obj and obj.fixedPriority) or a.fixedPriority then
a.fixedPriority = true
if obj and obj.fixedClass == nil then obj.fixedClass = a.priority or actorPriority(a) end
@@ -542,6 +546,11 @@ local function applyDrawOrder(actors, underActors, overActors, camY)
underActors[#underActors + 1] = a
end
end
-- pokeemerald/src/field_effect_helpers.c:1365
for _, a in ipairs(actors) do
local linked = a.disguiseObject and drawOrderObjects[a.disguiseObject]
if linked then a.subpriority = (linked.subpriority - 1) % 256 end
end
table.sort(underActors, sortActors)
table.sort(overActors, sortActors)
return underActors, overActors
@@ -1787,6 +1796,7 @@ function FieldView.draw(game, canvasW, canvasH, opts)
for _, a in ipairs(underActors) do
drawSingleActor(game, mapDef, a, camX, camY)
end
if usedNative and opts.endUnderActors then opts.endUnderActors(drawNativeOverTiles) end
end
-- pret BG1: metatile top layer covers normal OW sprites (roofs, desk counters, trees).
+50 -4
View File
@@ -453,9 +453,40 @@ function Map.ensureMidLayout(game, mapId, def)
return def.midLayout
end
--- Load a Sevii map under game3 ownership (pret enter order).
-- 1) Bind game3 player + collision + EventObjects
-- 2) Objects.loadMap then Space.runEnterScripts (ON_TRANSITION → ON_FRAME)
-- pokeemerald/src/overworld.c:911
-- pokefirered/src/overworld.c:878
function Map.applyInitialAvatar(session, def, saved, previous)
if not session then return end
local Player = require("src.core.game3.player")
if saved and (saved.surfing ~= nil or saved.underwater ~= nil) then
Player.restoreAvatar(saved)
return
end
local Flags = require("src.core.game3.scripting.flags")
local version = session.version or require("src.core.game3.profile").active().id
local cruiseId = Flags.forVersion(version).IDS.FLAG_SYS_CRUISE_MODE
local cruise = cruiseId and Flags.getFlag(session, nil, cruiseId)
local mapType = def and tonumber(def.mapType)
local isRse = session and require("src.core.game3.profile").family(session) == "rse"
local Collision = require("src.core.game3.collision")
local surfable = Collision.isSurfable(Collision.behavior(Player.cellX, Player.cellY))
local mapId = session and session.map or ""
local seafoam = not isRse and (mapId == "FR_SEAFOAM_ISLANDS_B3F" or mapId == "FR_SEAFOAM_ISLANDS_B4F")
Player.surfing, Player.underwater = false, false
if cruise and mapType ~= 8 then
Player.biking = false
if mapType == 6 then Player.facing = "right" end
elseif mapType == 5 then
Player.underwater, Player.biking = true, false
elseif surfable and not seafoam then
Player.surfing, Player.biking = true, false
end
if previous and ((previous.underwater and Player.surfing)
or (previous.surfing and Player.underwater)) then
Player.facing = previous.facing
end
end
function Map.load(mod, game, mapId, opts)
opts = opts or {}
if not MapIds.isGame3Map(mapId) then
@@ -557,6 +588,13 @@ function Map.load(mod, game, mapId, opts)
local session = (Runtime.getSession and Runtime.getSession()) or (game and game.session)
local save = game and game.save
local Player = require("src.core.game3.player")
local previousAvatar = { surfing = Player.surfing, underwater = Player.underwater, facing = Player.facing }
local savedAvatar = session and session._savedAvatar
if (opts.enterVia or Map._nextEnterVia) ~= "continue" or not savedAvatar
or savedAvatar.map ~= mapId or savedAvatar.x ~= x or savedAvatar.y ~= y
or (session and session._continueWarpDeferred) then
savedAvatar = nil
end
local onCyclingRoad = Player.isOnCyclingRoad and Player.isOnCyclingRoad(session, x, y, def)
local wasBiking = (Player.biking == true)
if opts.initialLoad and not wasBiking then
@@ -613,7 +651,6 @@ function Map.load(mod, game, mapId, opts)
end
-- pokefirered/src/overworld.c:2145 SetPlayerAvatarTransitionFlags
Player.biking = keepBike
Player.syncSavePosition(game)
local okFv, FieldView = pcall(require, "src.core.game3.field_view")
if okFv and FieldView then FieldView._nativeDirty = true end
@@ -646,6 +683,15 @@ function Map.load(mod, game, mapId, opts)
Collision.clear()
end
if not opts.seamless then
Map.applyInitialAvatar(session, def, savedAvatar, previousAvatar)
if session then
session._savedAvatar = nil
session.facing = Player.facing
end
end
Player.syncSavePosition(game)
Map._warmPairs = not opts.seamless or nil
-- pret GroundEffect_SpawnOnTallGrass when warping onto grass.
if not opts.seamless then
+6 -4
View File
@@ -1173,11 +1173,11 @@ local function tickMotion(eo, game, ctx)
end
--- Scripted one-cell step (no collision — FRLG applymovement forces).
function Objects.scriptStep(eo, dir, run, slow, fast)
function Objects.scriptStep(eo, dir, run, slow, fast, frames)
if not eo then return false end
local P = Player()
if eo == P then
return P.scriptStep and P.scriptStep(dir, run, slow, fast)
return P.scriptStep and P.scriptStep(dir, run, slow, fast, frames)
end
if eo.moving then return false end
local d = DELTA[dir]
@@ -1188,6 +1188,7 @@ function Objects.scriptStep(eo, dir, run, slow, fast)
-- pokefirered/src/event_object_movement.c:5333 StartRunningAnim
if run then eo.stepFrames = slow and RUN_SLOW_FRAMES or RUN_FRAMES end
if fast then eo.stepFrames = RUN_FRAMES end
if frames then eo.stepFrames = frames end
eo.frozen = true
eo.scriptBusy = true
return true
@@ -1307,9 +1308,9 @@ local function advanceTrack(lid, tr, game)
if type(act) == "table" then
if act.kind == "step" then
if eo == Player() then
if Player().scriptStep then Player().scriptStep(act.dir, act.run, act.slow, act.fast) end
if Player().scriptStep then Player().scriptStep(act.dir, act.run, act.slow, act.fast, act.frames) end
elseif eo then
Objects.scriptStep(eo, act.dir, act.run, act.slow, act.fast)
Objects.scriptStep(eo, act.dir, act.run, act.slow, act.fast, act.frames)
end
elseif act.kind == "jump" then
if eo == Player() then
@@ -1355,6 +1356,7 @@ local function advanceTrack(lid, tr, game)
Objects.revealTrainer(eo)
elseif act.kind == "turn" then
Objects.scriptFace(eo, act.dir)
if act.frames then tr.sleep = act.frames end
elseif act.kind == "face_player" then
-- pokefirered/src/event_object_movement.c:6772 MovementAction_FacePlayer_Step0
if eo and eo ~= Player() then
+45 -9
View File
@@ -192,6 +192,7 @@ function Player.reset(x, y, facing)
-- whiteout out of the water must not leave surfing set -- Collision.canEnter
-- reads Player.surfing and would treat water as walkable on land.
Player.surfing = false
Player.underwater = false
Player.surfHopping = false
Player.dismounting = false
Player.prevCellX = Player.cellX
@@ -230,6 +231,18 @@ function Player.syncFromSession(session)
if session.bikeType ~= nil then
Player.bikeType = session.bikeType
end
Player.restoreAvatar(session)
end
-- pokeemerald/src/field_player_avatar.c:1373
function Player.restoreAvatar(state)
if not state then return end
Player.underwater = state.underwater == true
Player.surfing = not Player.underwater and state.surfing == true
Player.biking = not Player.underwater and not Player.surfing and state.biking == true
Player.bikeType = state.bikeType
if state.elevation ~= nil then Player.elevation = tonumber(state.elevation) or 3 end
Player.surfHopping, Player.dismounting = false, false
end
function Player.syncFromHost(game)
@@ -249,17 +262,27 @@ end
--- Write avatar coords into save.position (ferry / host save). No host entity mirror.
function Player.syncSavePosition(game)
local save = game and game.save
if not (save and save.position) then return end
save.position.x = Player.cellX
save.position.y = Player.cellY
save.position.facing = Player.facing
save.position.biking = (Player.biking == true)
save.biking = (Player.biking == true)
if save then
if save.position then
save.position.x = Player.cellX
save.position.y = Player.cellY
save.position.facing = Player.facing
save.position.biking = Player.biking == true
end
save.biking = Player.biking == true
save.surfing = Player.surfing == true
save.underwater = Player.underwater == true
save.elevation = Player.elevation
end
local session = package.loaded["src.core.game3.runtime"]
session = session and session.getSession and session.getSession()
if session then
session.biking = (Player.biking == true)
if session.map then
session.surfing = Player.surfing == true
session.underwater = Player.underwater == true
session.bikeType = Player.bikeType
session.elevation = Player.elevation
if session.map and save and save.position then
save.position.map = session.map
end
end
@@ -629,9 +652,21 @@ function Player.tryMove(dir, game, run)
end
beginStep(tx, ty, run, false)
if not Player.jumping and not Player.biking then
Player.stepFrames, Player.running = Player.ordinaryStepFrames(run)
end
return "step"
end
-- pokeemerald/src/field_player_avatar.c:651
function Player.ordinaryStepFrames(run)
if Player.surfHopping or Player.dismounting then return WALK_FRAMES, false end
if Player.surfing and not Player.underwater and not Player.biking and not Player.action then
return RUN_FRAMES, false
end
return run and RUN_FRAMES or WALK_FRAMES, run == true
end
-- pokefirered/src/metatile_behavior.c:668 MetatileBehavior_IsCyclingRoadPullDownTile
local function isCyclingRoadPullDown(beh)
if Collision.isCyclingRoadPullDown then
@@ -777,7 +812,7 @@ function Player.forcedStep(dir, frames, opts)
end
--- Forced script step (applymovement localId 0xFF) — skips collision.
function Player.scriptStep(dir, run, slow, fast)
function Player.scriptStep(dir, run, slow, fast, frames)
if Player.moving then return false end
local d = DELTA[dir or Player.facing]
if not d then return false end
@@ -804,6 +839,7 @@ function Player.scriptStep(dir, run, slow, fast)
-- pokefirered/src/event_object_movement.c:9029 UpdateRunSlowAnim
if run and slow then Player.stepFrames = RUN_SLOW_FRAMES end
if fast then Player.stepFrames = RUN_FRAMES end
if frames then Player.stepFrames = frames end
return true
end
@@ -942,7 +978,6 @@ local function finishStep(game)
Player.jumpType = nil
Player.spriteYOffset = 0
Player.updateElevation(Player.cellX, Player.cellY)
Player.syncSavePosition(game)
-- Surf landing / dismount state transitions
local wasSurfing = Player.surfing or Player.dismounting
@@ -969,6 +1004,7 @@ local function finishStep(game)
end)
end
end
Player.syncSavePosition(game)
if ModRuntime.wants("world.stepped") then
local Map = package.loaded["src.core.game3.map"]
@@ -53,6 +53,8 @@ return {
invalidGfx = "OBJ_EVENT_GFX_NINJA_BOY",
-- pokeemerald/data/scripts/field_move_scripts.inc:60
fieldMoveScripts = true,
-- pokeemerald/data/scripts/flash.inc:1
flashScript = "EventScript_UseFlash",
-- pokeemerald/include/constants/event_objects.h:89
fieldMoveGfx = {
CUT_TREE = "OBJ_EVENT_GFX_CUTTABLE_TREE",
+2
View File
@@ -49,6 +49,8 @@ return {
-- pokeruby/src/event_object_movement.c:1767
invalidGfx = "OBJ_EVENT_GFX_LITTLE_BOY_1",
fieldMoveScripts = true,
-- pokeruby/src/fldeff_flash.c:111
flashScript = "gUnknown_081B694A",
fieldMoveGfx = {
CUT_TREE = "OBJ_EVENT_GFX_CUTTABLE_TREE",
ROCK_SMASH_ROCK = "OBJ_EVENT_GFX_BREAKABLE_ROCK",
+2
View File
@@ -41,6 +41,8 @@ TextAliases.gText_NothingToCut = "OtherText_NothingToCut"
TextAliases.gText_CantSurfHere = "OtherText_CantSurf"
TextAliases.gText_AlreadySurfing = "OtherText_AlreadySurfing"
TextAliases.gText_CantUseHere = "OtherText_CantUseThatHere"
-- pokeruby/src/party_menu.c:250
TextAliases.gText_InUseAlready_PM = "OtherText_CantUseThatHere"
TextAliases.gText_NotEnoughHp = "OtherText_NotEnoughHP"
TextAliases.gText_CantUseUntilNewBadge = "gOtherText_CantBeUsedBadge"
-- pokeruby/data/field_move_scripts.inc:10
+16 -3
View File
@@ -219,10 +219,21 @@ function Runtime.start(mod, game, session, opts)
end
end
Player.biking = keepBike
if session then session.biking = keepBike end
local Collision = lazyReq("src.core.game3.collision")
if def then Collision.bindMap(game, session.map, def) end
local savedAvatar = session._savedAvatar
if savedAvatar and (savedAvatar.map ~= session.map or savedAvatar.x ~= session.x
or savedAvatar.y ~= session.y or session._continueWarpDeferred) then savedAvatar = nil end
local adoptedAvatar = (session.surfing ~= nil or session.underwater ~= nil) and {
surfing = session.surfing, underwater = session.underwater,
biking = keepBike, bikeType = session.bikeType, elevation = session.elevation,
} or nil
Map.applyInitialAvatar(session, def, savedAvatar or adoptedAvatar)
session._savedAvatar = nil
if session then session.biking = Player.biking end
if game and game.save then
game.save.biking = keepBike
if game.save.position then game.save.position.biking = keepBike end
game.save.biking = Player.biking
if game.save.position then game.save.position.biking = Player.biking end
end
Player.syncSavePosition(game)
-- Space.onMapEnter already ran (or will run) from afterMap — don't double.
@@ -233,6 +244,8 @@ function Runtime.start(mod, game, session, opts)
x = session.x,
y = session.y,
facing = session.facing,
initialLoad = true,
enterVia = opts.reason == "continue" and "continue" or nil,
depth1Connections = true,
})
log("warped via game3 map loader → " .. tostring(session.map))
+16 -4
View File
@@ -337,10 +337,10 @@ function Schema.toSaveTable(session)
x = session.x,
y = session.y,
facing = session.facing,
biking = (package.loaded["src.core.game3.player"] and package.loaded["src.core.game3.player"].biking ~= nil)
and (package.loaded["src.core.game3.player"].biking == true)
or (session and session.biking == true)
or false,
biking = session.biking == true,
surfing = session.surfing,
underwater = session.underwater,
elevation = session.elevation,
bikeType = session.bikeType,
healMap = session.healMap,
healX = session.healX,
@@ -460,6 +460,9 @@ function Schema.fromSaveTable(save)
y = save.y or MapIds.newGameStart(version).y,
facing = save.facing or "down",
biking = save.biking == true,
surfing = save.surfing,
underwater = save.underwater,
elevation = tonumber(save.elevation),
bikeType = save.bikeType,
healMap = save.healMap,
healX = save.healX,
@@ -515,7 +518,16 @@ function Schema.fromSaveTable(save)
require("src.core.game3.save_sections").restore(save, session, version)
require("src.core.game3.save_mon").each(session, require("src.core.game3.save_mon").normalize)
rules.resetStateOnContinue(session)
session._savedAvatar = {
map = session.map, x = session.x, y = session.y,
surfing = session.surfing, underwater = session.underwater,
biking = session.biking, bikeType = session.bikeType, elevation = session.elevation,
}
local warpFlags = session.specialSaveWarpFlags
rules.useContinueGameWarp(session)
if session.specialSaveWarpFlags ~= warpFlags or session._continueWarpDeferred then
session._savedAvatar = nil
end
Schema.ensureMonBalls(session)
local legacySecret = save.secretId == nil and session.secretId == nil
Schema.repairOwnMons(session)
+13 -5
View File
@@ -89,6 +89,7 @@ Movement.decodeRse = rseAction
function Movement.decodeAction(b)
b = tonumber(b) or 0
local rseFamily = require("src.core.game3.profile").family(nil) == "rse"
if b == Movement.STEP_END or b == 0xFF then
return { kind = "end" }
end
@@ -104,10 +105,10 @@ function Movement.decodeAction(b)
if b >= 0x14 and b <= 0x17 then
return { kind = "jump", dir = DIR[b - 0x14], distance = 2 }
end
-- Delay 1 / 2 / 4 / 8 / 16 frames (scaled up for host step rate).
-- pokeemerald/src/event_object_movement.c:5609
if b >= 0x18 and b <= 0x1C then
local frames = ({ [0x18] = 2, [0x19] = 4, [0x1A] = 8, [0x1B] = 16, [0x1C] = 32 })[b]
return { kind = "sleep", frames = frames or 8 }
local frames = 2 ^ (b - 0x18)
return { kind = "sleep", frames = rseFamily and frames or frames * 2 }
end
-- Walk fast / in-place / faster walks → step or turn-in-place.
if b >= 0x1D and b <= 0x20 then
@@ -116,13 +117,20 @@ function Movement.decodeAction(b)
return { kind = "step", dir = DIR[b - 0x1D], fast = fast }
end
if b >= 0x21 and b <= 0x30 then
return { kind = "turn", dir = DIR[(b - 0x21) % 4] }
-- pokeemerald/src/event_object_movement.c:5811
local frames = rseFamily and b >= 0x2D and 4 or nil
return { kind = "turn", dir = DIR[(b - 0x21) % 4], frames = frames }
end
-- pokeemerald/src/event_object_movement.c:5827
if rseFamily and b >= 0x31 and b <= 0x34 then
return { kind = "step", dir = DIR[b - 0x31], frames = 6 }
end
if b >= 0x35 and b <= 0x38 then
return { kind = "step", dir = DIR[b - 0x35] }
end
if b >= 0x39 and b <= 0x3C then
return { kind = "step", dir = DIR[b - 0x39] }
-- pokeemerald/src/event_object_movement.c:5956
return { kind = "step", dir = DIR[b - 0x39], frames = rseFamily and 2 or nil }
end
-- pokefirered/src/event_object_movement.c:5333 StartRunningAnim (MOVE_SPEED_FAST_1)
if b >= 0x3D and b <= 0x40 then
+54
View File
@@ -122,8 +122,10 @@ function Renderer:releaseCanvases()
releaseCanvas(self.battleHUDCanvas); self.battleHUDCanvas = nil
releaseCanvas(self.worldCanvas); self.worldCanvas = nil
releaseCanvas(self.uprightCanvas); self.uprightCanvas = nil
releaseCanvas(self.tiltOverheadCanvas); self.tiltOverheadCanvas = nil
self.worldActive = false
self.uprightActive = false
self.uprightOccluded = false
self.worldOverride = nil
end
@@ -347,6 +349,7 @@ function Renderer:beginFrame(transparent)
self.uiOpaque = not transparent
self.worldActive = false
self.uprightActive = false
self.uprightOccluded = false
self.worldOverride = nil
-- warp-fade overlay from Transition (issue #121); cleared each frame so
-- a popped transition cannot leave a sticky black veil
@@ -590,6 +593,51 @@ function Renderer:tiltMesh()
return self._tiltMesh or nil
end
function Renderer:occludeUprightActors(drawOverhead)
local mesh = self:tiltMesh()
if not mesh then return end
if self._tiltOcclusionShader == nil then
local source, replaced = TILT_SHADER:gsub("return Texel%(tex, tc / vScale%) %* color;", [[
float a = Texel(tex, tc / vScale).a;
if (a <= 0.0) discard;
return vec4(1.0 - a);
]])
assert(replaced == 1)
self._tiltOcclusionShader = love.graphics.newShader(source)
end
local shader = self._tiltOcclusionShader
local vw, vh = self.worldCanvas:getWidth(), self.worldCanvas:getHeight()
local mask = self.tiltOverheadCanvas
if not mask or mask:getWidth() ~= vw or mask:getHeight() ~= vh then
if mask and mask.release then mask:release() end
mask = PixelCanvas.new(vw, vh, "linear")
self.tiltOverheadCanvas = mask
end
love.graphics.push("all")
love.graphics.origin()
love.graphics.setCanvas(mask)
love.graphics.setShader()
love.graphics.setStencilTest()
love.graphics.setScissor()
love.graphics.setBlendMode("alpha", "alphamultiply")
love.graphics.clear(0, 0, 0, 0)
love.graphics.setColor(1, 1, 1, 1)
drawOverhead()
love.graphics.pop()
love.graphics.push("all")
love.graphics.setShader(shader)
love.graphics.setStencilTest()
love.graphics.setScissor()
love.graphics.setBlendMode("multiply", "premultiplied")
love.graphics.setColor(1, 1, 1, 1)
mesh:setTexture(mask)
mesh:setVertices(Tilt.meshCorners(vw, vh))
love.graphics.draw(mesh)
love.graphics.pop()
self.uprightOccluded = true
end
-- Draw the world pass through the tilt projection. Two steps: (1) a
-- canvas-to-canvas palette pre-pass that bakes the SGB world zones into a
-- colorized ground canvas in flat space (a perspective transform breaks
@@ -1157,7 +1205,13 @@ function Renderer:endFrame(zones, worldZones)
local M = self.UPRIGHT_MARGIN
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setScissor(vux, vuy, vuw, vuh)
local blend, alpha
if self.uprightOccluded then
blend, alpha = love.graphics.getBlendMode()
love.graphics.setBlendMode("alpha", "premultiplied")
end
love.graphics.draw(self.uprightCanvas, wox - M * sx, woy - M * sy, 0, sx, sy)
if self.uprightOccluded then love.graphics.setBlendMode(blend, alpha) end
love.graphics.setScissor()
end
-- Screen-space warp fade (Transition) over the full world composite so
@@ -0,0 +1,111 @@
local U = require("tests.drivers.util")
return function(game)
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
print("RESULT 2764 failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
end
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
if not check(game.phase == "boot", "2764 boot ready") then return finish() end
game:_handleBootAction({ action = "new_game", name = "TIMING" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local Player = require("src.core.game3.player")
local Objects = require("src.core.game3.objects")
local Space = require("src.core.game3.scripting.space")
local Field = require("src.core.game3.field")
local Message = require("src.ui.game3.message")
local shots = os.getenv("POKEPORT_SHOT_DIR")
require("src.core.game3.encounters").onStep = function() return nil end
require("src.core.game3.trainer_sight").check = function() return false end
local function enter(mapId, x, y)
if Space.vm and Space.vm:isRunning() then Space.vm:halt(false) end
Message.reset()
Map.load(Runtime._mod, game, mapId, { x = x, y = y, facing = "left" })
U.wait(90)
if Space.vm and Space.vm:isRunning() then Space.vm:halt(false) end
Message.reset()
Player.reset(x, y, "left")
Field.locked = true
end
local function shot(name)
return shots and U.still(game, shots .. "/" .. name)
end
enter("EM_ROUTE119_WEATHER_INSTITUTE_2F", 5, 6)
local grunt, scientist = assert(Objects.find(7)), assert(Objects.find(5))
scientist.cellX, scientist.cellY, scientist.px, scientist.py = 1, 6, 16, 96
check(Space.vm:start("g3:0826ffc8"), "2764 cached Shelly continuation starts")
local shove, contact, dialogue = false, false, false
for _ = 1, 1800 do
grunt, scientist = assert(Objects.find(7)), assert(Objects.find(5))
if not shove and Player.cellY == 5 and grunt.moving then
shove = check(true, "2764 Weather north shove precedes grunt completion")
check(shot("2764_01_weather_north_shove.png"), "2764 Weather shove frame captured")
end
if not contact and grunt.cellX == 5 then
contact = true
check(Player.cellY == 5, "2764 Weather player vacates grunt destination")
end
if scientist.cellX == 4 and not scientist.moving and Message.isOpen() then
dialogue = true
check(Player.cellX == 5 and Player.cellY == 6 and scientist.cellY == 6,
"2764 Weather dialogue player5,6 scientist4,6")
if Message.isTyping() then Message.skipReveal() end
check(shot("2764_02_weather_dialogue_positions.png"), "2764 Weather dialogue frame captured")
break
end
if Message.isTyping() then Message.skipReveal() end
if Message.isWaiting() then U.tap(game, "a") end
U.wait(1)
end
check(shove and contact and dialogue, "2764 Weather cached continuation reaches dialogue")
enter("EM_SLATEPORT_CITY_OCEANIC_MUSEUM_2F", 12, 6)
Objects.addObject(2)
Objects.addObject(4)
local archie, other = assert(Objects.find(2)), assert(Objects.find(4))
other.cellX, other.cellY, other.px, other.py = 10, 6, 160, 96
archie.hidden, other.hidden = false, false
local tail
for _, rows in pairs(Space.bundle.scripts) do
if type(rows) == "table" then
for i, row in ipairs(rows) do
if row.op == "applymovement" and row[1] == 2 and row[2] == "g3:0820bcd8" then
tail = {}
for j = i, #rows do tail[#tail + 1] = rows[j] end
break
end
end
end
if tail then break end
end
if not check(tail ~= nil, "2764 Museum cached Archie arrival found") then return finish() end
Space.vm.scripts["2764_museum_tail"] = tail
check(Space.vm:start("2764_museum_tail"), "2764 Museum cached continuation starts")
local vacates, arrived = false, false
for _ = 1, 600 do
if not vacates and archie.moving and other.moving and other.py > 96 then
vacates = check(true, "2764 Museum grunt vacates during Archie approach")
check(shot("2764_03_museum_grunt_shove.png"), "2764 Museum shove frame captured")
end
if archie.cellX == 10 and archie.cellY == 6 and not archie.moving and Message.isOpen() then
arrived = true
check(other.cellX == 10 and other.cellY == 7, "2764 Museum grunt destination10,7")
check(Player.cellX == 12 and Player.cellY == 6, "2764 Museum player remains12,6")
if Message.isTyping() then Message.skipReveal() end
check(shot("2764_04_museum_dialogue_positions.png"), "2764 Museum dialogue frame captured")
break
end
U.wait(1)
end
check(vacates and arrived, "2764 Museum cached continuation reaches dialogue")
finish()
end
@@ -0,0 +1,132 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR")
return function(game)
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
print("RESULT native_surf_reload failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
end
local phase = os.getenv("BSA2759_PHASE") or "setup"
local identity = os.getenv("POKEPORT_IDENTITY")
if not check(identity and identity ~= "pokemon-love2d", "dedicated identity") then return finish() end
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
if not check(game.phase == "boot", "boot reached") then return finish() end
local SaveData = require("src.core.SaveData")
local Runtime = require("src.core.game3.runtime")
local Player = require("src.core.game3.player")
local Collision = require("src.core.game3.collision")
local Field = require("src.core.game3.field")
local Map = require("src.core.game3.map")
local Space = require("src.core.game3.scripting.space")
local Sprites = require("src.core.game3.ow_sprites")
local version = game.version or require("src.core.GameVersion").get()
local rse = require("src.core.game3.profile").family(version) == "rse"
local function stopScript()
if Space.vm and Space.vm.stop then Space.vm:stop() end
Field.unlock()
end
local function trace(label)
print(string.format("STATE %s map=%s x=%s y=%s water=%s surfing=%s gfx=%s avatar=%s",
label, tostring(Map.current), tostring(Player.cellX), tostring(Player.cellY),
tostring(Collision.isWater(Player.cellX, Player.cellY)), tostring(Player.surfing),
tostring(Sprites.playerGraphicsId(game, Player)), tostring(Sprites.avatarState(Player))))
end
if phase == "setup" then
game:_handleBootAction({ action = "new_game", name = "RELOAD", gender = 0 })
U.wait(240)
local mapId
for key in pairs(game.data.maps) do
if key:find(rse and "SEAFLOOR_CAVERN_ENTRANCE" or "BERRY_FOREST", 1, true) then mapId = key break end
end
if not check(mapId ~= nil, "avatar water map present in ROM cache") then return finish() end
require("src.core.game3.encounters").onStep = function() return nil end
require("src.core.game3.trainer_sight").check = function() return false end
Map.load(Runtime._mod, game, mapId, { x = 0, y = 0, facing = "down" })
U.wait(90)
stopScript()
local layout = game.data.maps[mapId].midLayout
local shore, water
local dirs = { { "right", 1, 0 }, { "left", -1, 0 }, { "down", 0, 1 }, { "up", 0, -1 } }
for y = 1, layout.height - 2 do
for x = 1, layout.width - 2 do
if Collision.isWater(x, y) then
for _, dir in ipairs(dirs) do
local sx, sy = x - dir[2], y - dir[3]
if Collision.isWalkable(sx, sy) and not Collision.isWater(sx, sy) then
shore, water = { x = sx, y = sy, facing = dir[1] }, { x = x, y = y }
break
end
end
end
if shore then break end
end
if shore then break end
end
if not check(shore ~= nil, "water with adjacent walkable shore") then return finish() end
if rse then
Map.load(Runtime._mod, game, mapId, { x = water.x, y = water.y, facing = shore.facing })
U.wait(90)
stopScript()
check(Player.surfing and not Player.underwater, "RSE ordinary cave water arrival restores Surf")
check(U.still(game, DIR .. "/2761_01_cave_water_entry.png"), "cave-entry shot")
local nextDir
for _, dir in ipairs(dirs) do
if Collision.isWater(water.x + dir[2], water.y + dir[3]) then nextDir = dir[1] break end
end
if not check(nextDir ~= nil, "cave water has next Surf cell") then return finish() end
Player.facing, Player.turnArmed = nextDir, false
check(Player.tryMove(nextDir, game, false) == "step", "RSE cave arrival moves without another Surf prompt")
Player._onStepDone = function() end
U.wait(40)
check(Player.surfing, "RSE cave next step retains Surf")
Map.load(Runtime._mod, game, mapId, shore)
U.wait(90)
stopScript()
check(not Player.surfing and not Player.underwater, "RSE ordinary cave land arrival is on foot")
end
Map.load(Runtime._mod, game, mapId, shore)
U.wait(90)
stopScript()
if not check(Player.startSurfing(game), "real player Surf hop starts") then return finish() end
U.wait(90)
trace("before_save")
if not check(Player.surfing and Player.cellX == water.x and Player.cellY == water.y,
"finished Surf hop is on selected water") then return finish() end
check(U.still(game, DIR .. "/2759_01_before_save_surf.png"), "before-save shot")
if not check(game:saveGame() == true, "Game3 native save succeeds") then return finish() end
local saved = SaveData.load()
check(saved and saved.engine == "game3" and saved.map == mapId
and saved.x == water.x and saved.y == water.y, "native disk save retains exact water location")
print("SAVE surfing=" .. tostring(saved and saved.surfing) .. " biking=" .. tostring(saved and saved.biking))
check(saved and saved.surfing == true and saved.underwater == false, "native disk save captures completed Surf mode")
check(love.filesystem.write("2759_witness.lua", SaveData.encode({ map = mapId,
x = water.x, y = water.y, gfx = Sprites.playerGraphicsId(game, Player) })), "restart witness saved")
else
local witness = SaveData.decode(love.filesystem.read("2759_witness.lua"))
if not check(witness ~= nil, "previous process supplied witness") then return finish() end
game:_handleBootAction({ action = "continue" })
for _ = 1, 4000 do
if game.phase == "field" then break end
U.wait(1)
end
if not check(game.phase == "field", "fresh process Continue reaches field") then return finish() end
U.wait(240)
trace("after_continue")
check(Map.current == witness.map and Player.cellX == witness.x and Player.cellY == witness.y,
"native Continue restores saved location")
check(Collision.isWater(Player.cellX, Player.cellY), "saved location remains water")
check(U.still(game, DIR .. "/2759_02_after_continue_surf.png"), "after-Continue shot")
check(Player.surfing, "native Continue restores Surf flag")
check(Sprites.playerGraphicsId(game, Player) == witness.gfx, "native Continue restores Surf graphics")
end
finish()
end
@@ -0,0 +1,171 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/cursor2766"
return function(game)
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. "cursor2766_native " .. label)
if not ok then failures = failures + 1 end
return ok
end
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local Party = require("src.core.game3.party")
local Bag = require("src.core.game3.bag")
local Items = require("src.core.game3.items_data")
local Bridge = require("src.core.game3.battle_bridge")
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local BagMenu = require("src.ui.game3.bag_menu")
local PartyMenu = require("src.ui.game3.party_menu")
local Anim = require("src.core.game3.battle.anim")
local AnimSeq = require("src.core.game3.battle.anim_seq")
local Intro = require("src.core.game3.battle.intro_seq")
local session = Runtime.getSession()
session.party = {}
Party.giveMon(session, 1, 20)
Party.giveMon(session, 4, 20)
Party.giveMon(session, 7, 20)
for _, mon in ipairs(session.party) do mon.moves, mon.pp = { 150 }, { 40 } end
session.party[1].hp = math.max(1, session.party[1].hp - 15)
session.bag = Bag.new()
Bag.add(session.bag, 13, 3)
Bag.add(session.bag, 4, 3)
local tick, lastTap = 0, 0
local function busy()
local step = AnimSeq._steps and AnimSeq._steps[AnimSeq._i]
return (Anim.vm() and Anim.vm():busy()) or (step and step.kind ~= "msg") or Intro._waitingGen
end
local function untilState(predicate, limit)
for _ = 1, limit or 5000 do
tick = tick + 1
if predicate() then return true end
if not busy() and Ui.dialogPending() and tick - lastTap >= 14 then
lastTap = tick
U.tap(game, "a")
else
U.wait(1)
end
end
return false
end
local function menu()
return Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu"
end
local function waitFor(predicate)
for _ = 1, 600 do
if predicate() then return true end
U.wait(1)
end
return false
end
local function itemRow(item)
for pocket = 1, #Items.BAG_POCKET_ORDER do
BagMenu.pocketIdx = pocket
for row, entry in ipairs(BagMenu.list(BagMenu.currentPocket())) do
if Items.toNumericId(entry.id) == item then
BagMenu.cursor, BagMenu.scroll = row, 0
return true
end
end
end
return false
end
check(Bridge.startWild(Runtime._mod, game, { species = 150, level = 2 }, { fade = false }),
"wild_battle_started")
if not check(untilState(menu), "initial_command_menu") then love.event.quit(1) return end
local st = Battle.getState()
st.enemy.mon.moves, st.enemy.mon.pp = { 150 }, { 40 }
local old, turn = st.player, st.turn
U.tap(game, "right")
U.tap(game, "a")
if not check(waitFor(function() return BagMenu.open and BagMenu.mode == "list" and not BagMenu._open end),
"Potion_Bag_open") then love.event.quit(1) return end
assert(itemRow(13))
U.tap(game, "a")
U.wait(10)
U.tap(game, "a")
if not check(waitFor(function() return PartyMenu.open and PartyMenu.mode == "use" end),
"Potion_Party_target_open") then love.event.quit(1) return end
PartyMenu.cursor = 1
U.wait(20)
U.tap(game, "a")
for _ = 1, 1200 do
if not PartyMenu.open then break end
U.tap(game, "a")
U.wait(3)
end
if not check(untilState(function() return menu() and st.turn > turn end),
"Potion_next_turn") then love.event.quit(1) return end
check(st.player == old and Bag.get(session.bag, 13) == 2 and Ui._menuIndex == 2,
"Potion_next_turn_Bag_selected")
check(U.still(game, DIR .. "/2766_potion_next_turn_Bag_selected.png"), "Potion_Bag_shot")
U.tap(game, "a")
check(waitFor(function() return BagMenu.open and BagMenu.mode == "list" and not BagMenu._open end),
"Potion_next_A_reopens_Bag")
U.tap(game, "b")
check(waitFor(menu) and Ui._menuIndex == 2, "Bag_cancel_retains_Bag")
U.tap(game, "a")
if not check(waitFor(function() return BagMenu.open and BagMenu.mode == "list" and not BagMenu._open end),
"Ball_Bag_open") then love.event.quit(1) return end
assert(itemRow(4))
turn = st.turn
local Catching = require("src.core.game3.battle.catching")
local tryCatch = Catching.tryCatch
Catching.tryCatch = function() return false, 0 end
U.tap(game, "a")
U.wait(10)
U.tap(game, "a")
local returned = untilState(function() return menu() and st.turn > turn end)
Catching.tryCatch = tryCatch
if not check(returned, "failed_Ball_next_turn") then love.event.quit(1) return end
check(Bag.get(session.bag, 4) == 2 and Ui._menuIndex == 2, "failed_Ball_next_turn_Bag_selected")
check(U.still(game, DIR .. "/2766_failed_ball_next_turn_Bag_selected.png"), "failed_Ball_Bag_shot")
U.tap(game, "a")
check(waitFor(function() return BagMenu.open and BagMenu.mode == "list" and not BagMenu._open end),
"failed_Ball_next_A_reopens_Bag")
U.tap(game, "b")
if not waitFor(menu) then love.event.quit(1) return end
U.tap(game, "left")
U.tap(game, "down")
U.tap(game, "a")
if not check(waitFor(function() return PartyMenu.open end), "switch_Party_open") then
love.event.quit(1) return
end
old, turn = st.player, st.turn
PartyMenu.cursor = 2
U.wait(60)
U.tap(game, "a")
U.wait(10)
U.tap(game, "a")
check(untilState(function() return menu() and st.player ~= old and st.turn > turn end),
"real_switch_next_turn")
check(Ui._menuIndex == 1 and st.player.partyIndex == 2, "real_switch_resets_Fight")
check(U.still(game, DIR .. "/2766_switched_Pokemon_Fight_selected.png"), "switch_Fight_shot")
local State = require("src.core.game3.battle.state")
local Engine = require("src.core.game3.battle.engine")
st.double, st.absent = true, {}
st.battlers = { [0] = st.player, [1] = st.enemy,
[2] = State.makeBattler(st.playerParty[1], "player", { partyIndex = 1, id = 2 }),
[3] = State.makeBattler(st.enemy.mon, "enemy", { partyIndex = 1, id = 3 }) }
local function direct(key)
Ui.handleInput({ wasPressed = function(_, k) return k == key end })
end
Ui.openMenu(0)
direct("right")
Ui.openMenu(2)
direct("down")
Ui.openMenu(0)
check(Ui._menuIndex == 2, "double_slot0_Bag_independent")
assert(Engine.performSwitch(st, Battle._adapter, 2, 3, { reason = "switch" }))
Ui.openMenu(2)
check(Ui._menuIndex == 1, "real_double_slot2_switch_Fight")
Ui.openMenu(0)
check(Ui._menuIndex == 2, "double_partner_switch_preserves_Bag")
print((failures == 0 and "PASS " or "FAIL ") .. "cursor2766_native_complete")
love.event.quit(failures == 0 and 0 or 1)
end
@@ -0,0 +1,84 @@
local U = require("tests.drivers.util")
local F = require("tests.drivers.em_fc_util").new("game3_disguise_order_2763")
return function(game)
if not F.boot(game) then return F.finish() end
local Runtime = require("src.core.game3.runtime")
local session = Runtime.getSession()
local version = session.version
local prefix = ({ emerald = "EM", ruby = "RU", sapphire = "SA" })[version]
if not F.check(prefix ~= nil, "disguise2763 RSE version") then return F.finish() end
local Objects = require("src.core.game3.objects")
local Fx = require("src.core.game3.field_effects_rse")
local View = require("src.core.game3.field_view")
local Player = require("src.core.game3.player")
local Flags = require("src.core.game3.scripting.flags")
local Space = require("src.core.game3.scripting.space")
local Sight = require("src.core.game3.trainer_sight")
local Field = require("src.core.game3.field")
local function ordered(eo, tag)
local actors, ninja = {}, nil
for _, obj in ipairs(Objects.forDraw()) do
local actor = { kind = "npc", i = obj.localId, eventObject = obj,
x = obj.px, y = obj.py, sortY = obj.py, elevation = obj.elevation }
actors[#actors + 1] = actor
if obj == eo then ninja = actor end
end
Fx.collectActors(actors)
local under = View.applyDrawOrder(actors, {}, {}, Player.py - 72)
local ni, mi, mask
for i, actor in ipairs(under) do
if actor == ninja then ni = i end
if actor.disguiseObject == eo then mi, mask = i, actor end
end
return F.check(ni and mi and ni < mi and mask.subpriority == (ninja.subpriority - 1) % 256,
"disguise2763 " .. version .. " " .. tag .. " ninja_before_mask")
end
local function spot(route, lid, kind)
local tag = kind .. "_" .. route
local mapId = prefix .. "_" .. route
if not F.check(F.goTo(game, mapId, 5, 5, "down"), tag .. " map_loaded") then return end
for _, id in ipairs(Objects._order) do
local trainerId = Sight.getTrainerId(Objects._byId[id])
if trainerId then Flags.setFlag(Space.store, nil, Flags.trainerFlagId(trainerId), true) end
end
local eo = Objects.find(lid)
if not F.check(eo and eo.disguise and eo.disguise.sheet == kind,
"disguise2763 " .. version .. " " .. tag .. " cached_disguise") then return end
F.goTo(game, mapId, eo.cellX + 3, eo.cellY + 1, "left", { wait = 20 })
eo = Objects.find(lid)
Field.lock()
F.check(eo.disguise and eo.disguise.a and eo.disguise.a.frame == 0 and not eo.invisible,
"disguise2763 " .. version .. " " .. tag .. " idle_frame")
ordered(eo, tag .. "_hidden")
F.check(F.shot(game, "2763_" .. version .. "_" .. tag .. "_01_hidden.png", true),
"disguise2763 " .. version .. " " .. tag .. " hidden_capture")
Objects.revealTrainer(eo)
U.wait(10)
F.check(eo.disguise and eo.disguise.revealing and not eo.disguise.done
and eo.disguise.a.frame > 0 and not eo.invisible,
"disguise2763 " .. version .. " " .. tag .. " midtear_frame")
ordered(eo, tag .. "_midtear")
F.check(F.shot(game, "2763_" .. version .. "_" .. tag .. "_02_midtear.png", true),
"disguise2763 " .. version .. " " .. tag .. " midtear_capture")
U.wait(40)
local actors = {}
Fx.collectActors(actors)
local mask
for _, actor in ipairs(actors) do if actor.disguiseObject == eo then mask = actor end end
local drawn = false
for _, obj in ipairs(Objects.forDraw()) do if obj == eo then drawn = true end end
F.check(mask == nil and drawn and not eo.invisible,
"disguise2763 " .. version .. " " .. tag .. " revealed_ninja_only")
F.check(F.shot(game, "2763_" .. version .. "_" .. tag .. "_03_revealed.png", true),
"disguise2763 " .. version .. " " .. tag .. " revealed_capture")
Field.unlock()
F.haltVm()
end
spot("ROUTE119", 12, "tree_disguise")
spot("ROUTE120", 34, "mountain_disguise")
F.finish()
end
@@ -83,7 +83,11 @@ return function(game)
U.wait(1)
end
check(game.boot ~= nil, "boot reached")
local Runtime = require("src.mods.Runtime")
local safeMode = Runtime.safeMode
Runtime.safeMode = false
try("title", function() titleOptions(game) end)
Runtime.safeMode = safeMode
try("new_game", function() game:_handleBootAction({ action = "new_game", name = "RED" }) end)
for _ = 1, 600 do
if game.phase == "field" then break end
+110
View File
@@ -0,0 +1,110 @@
local U = require("tests.drivers.util")
local failures = 0
local function check(ok, label)
print((ok and "PASS " or "FAIL ") .. "flash2762 " .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
print((failures == 0 and "PASS " or "FAIL ") .. "flash2762_native failures=" .. failures)
love.event.quit(failures == 0 and 0 or 1)
end
return function(game)
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
if not check(game.phase == "boot" and game.boot ~= nil, "boot_ready") then return finish() end
game:_handleBootAction({ action = "new_game", name = "FLASH" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local session = Runtime.getSession()
if not check(session ~= nil, "field_session_ready") then return finish() end
local version = session.version
local Profile = require("src.core.game3.profile").of(version)
local rse = Profile.family == "rse"
local Map = require("src.core.game3.map")
local Field = require("src.core.game3.field")
local View = require("src.core.game3.field_view")
local Space = require("src.core.game3.scripting.space")
local Flags = require("src.core.game3.scripting.flags")
local Moves = require("src.core.game3.field_moves")
local ShowMon = require("src.core.game3.field_move_show_mon")
local Party = require("src.ui.game3.party_menu")
local Pokemon = require("src.core.game3.pokemon")
local Text = require("src.core.game3.rom_text")
require("src.core.game3.encounters").onStep = function() return nil end
require("src.core.game3.trainer_sight").check = function() return false end
local species = assert(Pokemon.speciesFromName("PIKACHU"))
local move = Moves.MOVES.FLASH
local mon = { species = species, nickname = "FLASH", level = 20, moves = { move }, pp = { 20 }, personality = 0 }
Pokemon.applyStats(mon, session)
mon.hp = mon.maxHp
session.party = { mon }
Flags.setFlag(Space.store, nil, Moves.badgeFlag("FLASH"), true)
local maps = game.data.maps
local function mapNamed(suffix)
for id in pairs(maps) do
if id:sub(-#suffix) == suffix then return id end
end
return nil
end
local first = assert(mapNamed(rse and "GRANITE_CAVE_B1F" or "ROCK_TUNNEL_1F"))
local second = assert(mapNamed(rse and "GRANITE_CAVE_B2F" or "ROCK_TUNNEL_B1F"))
local outdoors = assert(mapNamed(rse and "DEWFORD_TOWN" or "PALLET_TOWN"))
local shots = os.getenv("POKEPORT_SHOT_DIR")
local function load(id)
local def = maps[id]
local warp = def.warps and def.warps[1]
local x, y = warp and warp.x or 8, warp and warp.y or 8
Map.load(Runtime._mod, game, id, { x = x, y = y, facing = "down" })
U.wait(90)
return check(Map.current == id, "loaded_" .. id)
end
local function selectFlash()
Party.show(session.party, nil, { session = session })
U.wait(40)
U.tap(game, "a")
U.wait(3)
local index
for i, name in ipairs(Party.ACTIONS) do if name == "FLASH" then index = i end end
if not check(index ~= nil and Party.mode == "action", "known_flash_action_available") then return false end
Party.actionCursor = index
U.tap(game, "a")
return true
end
if not load(first) then return finish() end
check(tonumber(Map.currentDef().cave) == 1 and not Flags.getFlag(Space.store, nil, Moves.SYS_FLAGS.FLASH_ACTIVE),
"initial_dark_cave_without_flash")
if not selectFlash() then return finish() end
check(ShowMon.isActive() and Field.locked, "show_mon_then_animation_lock")
for _ = 1, 900 do
if not ShowMon.isActive() and not Field.locked and not Space.vm:isRunning() then break end
U.wait(1)
end
check(not Field.locked and not ShowMon.isActive() and not Space.vm:isRunning(), "animation_end_unlocks_field")
check(View.getFlashLevel() == (rse and 1 or 0), "first_use_cart_level")
check(View.flashRadius() == (rse and 72 or nil), "first_use_cart_radius")
if shots then check(U.still(game, shots .. "/2762_01_first_flash.png"), "first_flash_frame_captured") end
if not load(second) then return finish() end
check(Flags.getFlag(Space.store, nil, Moves.SYS_FLAGS.FLASH_ACTIVE)
and View.getFlashLevel() == (rse and 1 or 0), "floor_change_preserves_flash")
if not selectFlash() then return finish() end
local key = (version == "ruby" or version == "sapphire") and "OtherText_CantUseThatHere" or "gText_InUseAlready_PM"
check(Party.mode == "message" and Party._messageText == Text.ascii(key), "repeated_flash_exact_cached_prompt")
check(Flags.getFlag(Space.store, nil, Moves.SYS_FLAGS.FLASH_ACTIVE)
and View.getFlashLevel() == (rse and 1 or 0), "rejection_keeps_flash_state")
if shots then check(U.still(game, shots .. "/2762_02_repeated_flash_prompt.png"), "repeat_prompt_frame_captured") end
Party.close()
if not load(outdoors) then return finish() end
check(not Flags.getFlag(Space.store, nil, Moves.SYS_FLAGS.FLASH_ACTIVE), "outdoors_clears_flash")
if not load(first) then return finish() end
if not selectFlash() then return finish() end
for _ = 1, 900 do
if not ShowMon.isActive() and not Field.locked and not Space.vm:isRunning() then break end
U.wait(1)
end
check(Flags.getFlag(Space.store, nil, Moves.SYS_FLAGS.FLASH_ACTIVE) and not Field.locked
and View.getFlashLevel() == (rse and 1 or 0), "cave_return_flash_usable")
finish()
end
+78
View File
@@ -0,0 +1,78 @@
local U=require('tests.drivers.util')
local failures=0
local function check(ok,label)
print((ok and 'PASS ' or 'FAIL ')..label)
if not ok then failures=failures+1 end
return ok
end
local function finish()
print((failures==0 and 'PASS ' or 'FAIL ')..'2767 native Surf timing failures='..failures)
love.event.quit(failures==0 and 0 or 1)
end
return function(game)
for _=1,600 do
if game.phase=='boot' and game.boot then break end
U.wait(1)
end
if not check(game.phase=='boot' and game.boot~=nil,'2767 boot ready') then return finish() end
game:_handleBootAction({action='new_game',name='SURFER'})
U.wait(180)
local Runtime=require('src.core.game3.runtime')
local Map=require('src.core.game3.map')
local Player=require('src.core.game3.player')
local Collision=require('src.core.game3.collision')
local Space=require('src.core.game3.scripting.space')
local Flags=require('src.core.game3.scripting.flags')
local Field=require('src.core.game3.field')
require('src.core.game3.encounters').onStep=function() return nil end
require('src.core.game3.trainer_sight').check=function() return false end
local session=Runtime.getSession()
if not check(session~=nil,'2767 field session ready') then return finish() end
local loader=game.mods
print('MODS 2767 safeMode='..tostring(loader and loader.safeMode))
for _,id in ipairs(loader and loader.order or {}) do
print('MODS 2767 ordered='..tostring(type(id)=='table' and id.manifest and id.manifest.id or id))
end
local mapId='EM_ROUTE131'
Map.load(Runtime._mod,game,mapId,{x=17,y=2,facing='right'})
U.wait(90)
local shoeId=Flags.forVersion('emerald').IDS.FLAG_SYS_B_DASH
if not check(Collision.isWater(17,2) and Collision.isWater(18,2),'2767 real Route131 water fixture') then return finish() end
local function measure(label,b,shoes)
if Space.vm and Space.vm:isRunning() then Space.vm:halt(false) end
Field.locked=false
Player.reset(17,2,'right')
Player.surfing,Player.currentElevation,Player.elevation=true,1,1
Flags.setFlag(Space.store,nil,shoeId,shoes)
local input={isDown=function(_,key) return key=='right' or key=='b' and b end}
Player.update(game,input)
if not check(Player.moving,label..' step starts') then return end
local selected=Player.stepFrames
Player._onStepDone=function() end
local count=0
while Player.moving and count<24 do
U.wait(1)
count=count+1
end
print('MEASURE 2767 '..label..' selected='..selected..' duration='..count..' surf='..tostring(Player.surfing))
check(count==8 and selected==8 and Player.cellX==18 and Player.surfing,label..' completes in 8 frames')
end
measure('no_B_no_shoes',false,false)
measure('B_no_shoes',true,false)
measure('no_B_shoes',false,true)
measure('B_shoes',true,true)
local shots=os.getenv('POKEPORT_SHOT_DIR')
if shots then check(U.still(game,shots..'/2767_01_surfing.png'),'2767 Surf frame captured') end
Map.load(Runtime._mod,game,mapId,{x=59,y=2,facing='right'})
U.wait(90)
Player.surfing,Player.currentElevation,Player.elevation=true,1,1
Player._onStepDone=function() end
local status=Player.tryMove('right',game,false)
local selected=Player.stepFrames
local count=0
while Player.moving and count<24 do U.wait(1);count=count+1 end
print('MEASURE 2767 water_seam result='..tostring(status)..' duration='..count..' selected='..selected..' map='..tostring(Map.current))
check(status=='connection' and Map.current=='EM_ROUTE130' and count==8 and selected==8 and Player.surfing,'2767 water seam completes in 8 frames')
if shots then check(U.still(game,shots..'/2767_02_water_seam.png'),'2767 seam frame captured') end
finish()
end
+239
View File
@@ -0,0 +1,239 @@
local U = require("tests.drivers.util")
local F = require("tests.drivers.em_fc_util").new("game3_tilt_roof_2765")
local function up(fn, wanted, replacement)
for i = 1, 100 do
local name, value = debug.getupvalue(fn, i)
if name == wanted then
if replacement then debug.setupvalue(fn, i, replacement) end
return value
end
end
error("missing " .. wanted)
end
return function(game)
if not F.boot(game) then return F.finish() end
local View = require("src.core.game3.field_view")
local Display = require("src.core.game3.display")
local Renderer = require("src.render.Renderer")
local Tilt = require("src.render.Tilt")
local Player = require("src.core.game3.player")
local Runtime = require("src.core.game3.runtime")
local session = Runtime.getSession()
local version = session.version
local map = version == "emerald" and "EM_OLDALE_TOWN" or "FR_PALLET_TOWN"
local x, y = 6, version == "emerald" and 13 or 3
if not F.check(F.goTo(game, map, x, y, "down"), "tilt2765 cached_roof_map") then return F.finish() end
require("src.core.game3.field").lock()
local plane = up(up(Display.present, "presentPlanes"), "drawFieldPlane")
local overhead = up(View.draw, "drawNativeOverTiles")
local function capturePlane(g)
love.graphics.push("all")
love.graphics.origin()
love.graphics.setShader()
love.graphics.setBlendMode("alpha", "alphamultiply")
love.graphics.setColor(1, 1, 1, 1)
Renderer:beginFrame(true)
Renderer:beginWorldPass()
local vw, vh = Renderer:worldViewSize()
plane(g, vw, vh, Renderer)
Renderer:endWorldPass()
love.graphics.pop()
return Renderer.worldCanvas:newImageData(),
Renderer.uprightActive and Renderer.uprightCanvas:newImageData() or nil
end
local function projectedAlpha()
local vw, vh = Renderer.worldCanvas:getDimensions()
local source = love.graphics.newCanvas(vw, vh, { dpiscale = 1 })
source:setFilter("linear", "linear")
local m = Renderer.UPRIGHT_MARGIN
local target = love.graphics.newCanvas(vw + 2 * m, vh + 2 * m, { dpiscale = 1 })
love.graphics.push("all")
love.graphics.origin()
love.graphics.setCanvas(source)
love.graphics.clear(0, 0, 0, 0)
love.graphics.setShader()
love.graphics.setStencilTest()
love.graphics.setScissor()
love.graphics.setBlendMode("alpha", "alphamultiply")
overhead()
love.graphics.setCanvas(target)
love.graphics.clear(0, 0, 0, 0)
love.graphics.setBlendMode("replace", "premultiplied")
love.graphics.setShader(Renderer:tiltShader())
local mesh = Renderer:tiltMesh()
mesh:setTexture(source)
mesh:setVertices(Tilt.meshCorners(vw, vh))
love.graphics.translate(m, m)
love.graphics.draw(mesh)
love.graphics.pop()
local data = target:newImageData()
source:release()
target:release()
return data
end
local maskMethod = Renderer.occludeUprightActors
Tilt.applyOptions({ tilt = 0 })
local off = capturePlane(game)
Renderer.occludeUprightActors = function() end
local referenceOff = capturePlane(game)
Renderer.occludeUprightActors = maskMethod
F.check(off:getString() == referenceOff:getString(), "tilt2765 " .. version .. " OFF_byte_identical")
F.check(F.shot(game, "2765_" .. version .. "_00_off.png", true), "tilt2765 OFF_capture")
local priority = Player.oamPriority
for level = 1, 3 do
Tilt.applyOptions({ tilt = level })
Player.oamPriority = 0
local _, elevated = capturePlane(game)
Player.oamPriority = 2
local _, normal = capturePlane(game)
local alpha = projectedAlpha()
local hidden = 0
if normal and elevated then
local w, h = normal:getDimensions()
for py = math.floor(h / 2) - 30, math.floor(h / 2) + 20 do
for px = math.floor(w / 2) - 16, math.floor(w / 2) + 16 do
local _, _, _, ea = elevated:getPixel(px, py)
local _, _, _, na = normal:getPixel(px, py)
local _, _, _, ma = alpha:getPixel(px, py)
if ea > 0.99 and ma > 0.99 and na < 0.01 then hidden = hidden + 1 end
end
end
end
F.check(hidden > 0, "tilt2765 " .. version .. " " .. Tilt.levelLabel(level) .. " real_roof_normal_hidden_elevated_visible")
F.check(F.shot(game, "2765_" .. version .. "_" .. Tilt.levelLabel(level) .. "_normal.png", true), "tilt2765 normal_capture")
Player.oamPriority = 0
F.check(F.shot(game, "2765_" .. version .. "_" .. Tilt.levelLabel(level) .. "_elevated.png", true), "tilt2765 elevated_capture")
end
Player.oamPriority = priority
local weather = require("src.core.game3.field_weather_rse")
if version == "emerald" then
require("src.core.game3.weather").resume()
local exchanges, writes = 0, 0
local exchange, write = Renderer.exchangeWorldCanvas, weather.writeActorMask
Renderer.exchangeWorldCanvas = function(self, current, replacement)
local changed = exchange(self, current, replacement)
if changed then exchanges = exchanges + 1 end
return changed
end
weather.writeActorMask = function(code, draw)
local canvas, shader = love.graphics.getCanvas(), love.graphics.getShader()
write(code, draw)
F.check(love.graphics.getCanvas() == canvas and love.graphics.getShader() == shader,
"tilt2765 weather_actor_mask_canvas_shader_restored")
writes = writes + 1
end
for _, id in ipairs({ weather.WEATHER.SHADE, weather.WEATHER.RAIN, weather.WEATHER.FOG_HORIZONTAL }) do
weather.setCurrentAndNextWeatherNoDelay(id)
weather.readyForInit()
for _ = 1, 180 do weather.update() end
capturePlane(game)
F.check(Renderer.uprightActive and Renderer.uprightOccluded, "tilt2765 weather_keeps_tilt_mask")
end
F.check(exchanges > 0 and writes > 0,
"tilt2765 real_weather_canvas_exchange_and_actor_mask exchanges=" .. exchanges .. " writes=" .. writes)
Renderer.exchangeWorldCanvas, weather.writeActorMask = exchange, write
weather.setCurrentAndNextWeatherNoDelay(weather.WEATHER.NONE)
end
local vw, vh = 96, 80
local oldViewSize = Renderer.worldViewSize
Renderer.worldViewSize = function() return vw, vh end
local saved = {}
local function replace(name, fn) saved[name] = up(View.draw, name, fn) end
replace("currentMapId", function() return "fixture" end)
replace("playerPixels", function() return 40, 32, "down", 0, false, nil, 0, 0 end)
for _, name in ipairs({ "modSeagallop", "modShopMenu", "modFieldEffects", "modDoors", "modHeal", "modSSAnne", "modFieldWeather" }) do
replace(name, function() return nil end)
end
local function batch(w, h, ox, oy, pixel)
local data = love.image.newImageData(w, h)
for py = 0, h - 1 do for px = 0, w - 1 do data:setPixel(px, py, pixel(px, py)) end end
local image = love.graphics.newImage(data)
image:setFilter("nearest", "nearest")
local b = love.graphics.newSpriteBatch(image, 1)
b:add(ox, oy)
return b
end
local main = batch(32, 32, 24, 20, function(px, py)
return 1, 0, 0, (px >= 12 and px < 20 and py >= 12 and py < 20) and 0 or 1
end)
local neighbor = batch(16, 32, 64, 20, function(_, py) return 1, 1, 0, py < 16 and 1 or 0.5 end)
replace("drawNativeTiles", function()
View._nativeOverPair, View._nativeOverBatches = "main", { main = main, neighbor = neighbor }
View._nativeOverOx, View._nativeOverOy, View._voidFrom = 0, 0, nil
love.graphics.setColor(0, 0, 1, 1)
love.graphics.rectangle("fill", 0, 0, vw, vh)
return true
end)
local elevatedOn = false
replace("collectGame3Actors", function()
local actors = { { kind = "mod", elevation = 3, x = 40, y = 24, draw = function()
love.graphics.setColor(0, 1, 0, 1)
love.graphics.rectangle("fill", 0, 0, vw, vh)
end } }
if elevatedOn then actors[#actors + 1] = { kind = "mod", elevation = 13, x = 40, y = 24, draw = function()
love.graphics.setColor(0, 1, 1, 1)
love.graphics.rectangle("fill", 64, 20, 16, 32)
end } end
return View.applyDrawOrder(actors)
end)
local fixture = { data = { maps = { fixture = { width = 32, blocks = {} } } } }
View._flashMapId, View._flashRadius = "fixture", nil
for level = 1, 3 do
Tilt.applyOptions({ tilt = level })
elevatedOn = false
local _, normal = capturePlane(fixture)
local alpha = projectedAlpha()
local errors, opaque, holes, partial = 0, 0, 0, 0
local m = Renderer.UPRIGHT_MARGIN
for py = 1, vh - 2 do
for px = 1, vw - 2 do
local _, green, _, a = normal:getPixel(px + m, py + m)
local _, _, _, mask = alpha:getPixel(px + m, py + m)
if math.abs(a - (1 - mask)) > 0.01 or math.abs(green - (1 - mask)) > 0.01 then errors = errors + 1 end
if mask > 0.99 then opaque = opaque + 1 elseif mask < 0.01 then holes = holes + 1 else partial = partial + 1 end
end
end
F.check(errors == 0 and opaque > 0 and holes > 0 and partial > 0,
"tilt2765 GPU_" .. Tilt.levelLabel(level) .. " opaque_holes_fractional_connected_pair errors=" .. errors)
elevatedOn = true
local _, elevated = capturePlane(fixture)
local _, g, b, a = elevated:getPixel(70 + m, 30 + m)
F.check(g > 0.99 and b > 0.99 and a > 0.99, "tilt2765 GPU_elevated_above_mask")
end
local current = love.graphics.newCanvas(160, 120, { dpiscale = 1 })
local shader = love.graphics.newShader("vec4 effect(vec4 c, Image t, vec2 uv, vec2 p) { return Texel(t,uv)*c; }")
love.graphics.push("all")
love.graphics.setCanvas(current)
love.graphics.setShader(shader)
love.graphics.setBlendMode("add", "premultiplied")
love.graphics.setColor(0.2, 0.3, 0.4, 0.5)
love.graphics.setScissor(4, 5, 6, 7)
love.graphics.translate(3, 9)
local before = { love.graphics.transformPoint(0, 0) }
before[3], before[4] = love.graphics.transformPoint(1, 0)
before[5], before[6] = love.graphics.transformPoint(0, 1)
Renderer:occludeUprightActors(overhead)
local r, g, b, a = love.graphics.getColor()
local blend, mode = love.graphics.getBlendMode()
local sx, sy, sw, sh = love.graphics.getScissor()
local after = { love.graphics.transformPoint(0, 0) }
after[3], after[4] = love.graphics.transformPoint(1, 0)
after[5], after[6] = love.graphics.transformPoint(0, 1)
local sameTransform = true
for i, value in ipairs(before) do sameTransform = sameTransform and value == after[i] end
F.check(love.graphics.getCanvas() == current and love.graphics.getShader() == shader
and blend == "add" and mode == "premultiplied" and math.abs(r - 0.2) < 0.001
and math.abs(g - 0.3) < 0.001 and math.abs(b - 0.4) < 0.001 and a == 0.5
and sx == 4 and sy == 5 and sw == 6 and sh == 7 and sameTransform,
"tilt2765 GPU_canvas_shader_blend_color_scissor_transform_restored")
love.graphics.pop()
for name, fn in pairs(saved) do up(View.draw, name, fn) end
Renderer.worldViewSize = oldViewSize
View.invalidate()
Tilt.applyOptions({ tilt = 0 })
F.finish()
end
@@ -0,0 +1,208 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local Schema = require("src.core.game3.save_schema_firered")
local SaveData = require("src.core.SaveData")
local Collision = require("src.core.game3.collision")
local MB = require("src.core.game3.mb")
local Interaction = require("src.core.game3.scripting.interaction_scripts")
require("src.core.game3.save_sections").of("emerald")
require("src.core.game3.save_sections").of("ruby")
local s
local function stub(name, value) package.loaded["src.core.game3." .. name] = value end
stub("runtime", { isActive = function() return true end, getSession = function() return s end })
stub("scripting.space", { bundle = {}, ensureBundle = function() end, activate = function() end,
runEnterScripts = function() end, runOnWarpIntoMap = function() end })
stub("ghosts", { capture = function() end, adopt = function() end,
visibleIds = function() return {} end, openFadeWindow = function() end })
stub("field", { lock = function() end, unlock = function() end, clearMetatiles = function() end })
stub("audio", { setSavedSong = function() end, canOverrideMapMusic = function() return false end,
stopSurfMusic = function() end, bikeMusic = function() end })
stub("encounters", { resetRateModifiers = function() end })
stub("field_effects", { leaveTallGrass = function() end, tallGrassAt = function() end })
stub("field_view", { setCameraPanning = function() end })
stub("field_modules", { enabled = function() return false end })
stub("rse.init", { call = function() end })
stub("rs.rematch", { enabled = function() return false end })
stub("rse.rematch", { tryUpdateRandomTrainerRematchesForMap = function() end })
stub("time_events", { run = function() end })
stub("objects", { loadMap = function() end, restoreSnapshot = function() end, beginFadeIn = function() end })
local Player = require("src.core.game3.player")
local Map = require("src.core.game3.map")
local Sprites = require("src.core.game3.ow_sprites")
local Flags = require("src.core.game3.scripting.flags")
local game = { save = { position = {} }, data = { maps = {} } }
local mid, def
local behavior, elevation, coll
local function terrain(beh, elev, collision)
behavior, elevation, coll = beh, elev, collision
Interaction.behaviors[def.pair] = { [0] = beh }
end
local function setup(version)
GameVersion.set(version)
s = { version = version, flags = {}, vars = {}, party = {}, name = "SURFER" }
mid = require("src.core.game3.profile").of(version).map.enginePrefix .. "AVATAR_TEST"
s.map, s.x, s.y, s.facing = mid, 0, 0, "right"
def = { width = 2, height = 2, mapType = 4, objects = {}, connections = {}, bikingAllowed = 1, pair = "avatar_test" }
local layout = { width = 2, height = 2, pair = def.pair }
function layout:collAt() return coll end
function layout:elevAt() return elevation end
function layout:midAt() return 0 end
function layout:collArray() return { coll, coll, coll, coll } end
def.midLayout = layout
game.data.maps, game.session = { [mid] = def }, s
terrain(MB.OCEAN_WATER, 1, 0x29)
Player.reset(0, 0, "right")
end
local function load(opts)
opts = opts or {}
opts.x, opts.y, opts.facing = 0, 0, opts.facing or "right"
check(Map.load(nil, game, mid, opts) ~= nil, "real Map.load completes")
end
local function roundtrip(surfing, underwater, elev)
s.surfing, s.underwater, s.biking, s.elevation = surfing, underwater, false, elev
local disk = SaveData.decode(SaveData.encode(Schema.toSaveTable(s)))
s = Schema.fromSaveTable(disk)
game.session = s
Player.syncFromSession(s)
load({ enterVia = "continue", initialLoad = true })
end
for _, version in ipairs({ "firered", "leafgreen", "emerald", "ruby", "sapphire" }) do
for gender = 0, 1 do
setup(version)
s.gender = gender
roundtrip(true, false, 1)
check(Player.surfing and not Player.underwater and not Player.biking,
version .. " native_codec_final_reset_restores_surf gender=" .. gender)
eq(Sprites.avatarState(Player), "SURFING", version .. " saved Surf graphics")
check(not Player.surfHopping and not Player.dismounting, version .. " Continue has no Surf animation")
roundtrip(false, false, 4)
check(not Player.surfing and not Player.underwater, version .. " explicit_on_foot_water_continue")
eq(Player.elevation, 4, version .. " saved upper elevation")
end
setup(version)
s.surfing, s.underwater = nil, nil
s = Schema.fromSaveTable(SaveData.decode(SaveData.encode(Schema.toSaveTable(s))))
eq(s.surfing, nil, version .. " legacy absence retained")
Player.syncFromSession(s)
load({ enterVia = "continue", initialLoad = true })
check(Player.surfing, version .. " legacy_surfable_continue")
Player.surfing, Player.underwater = true, false
load({ seamless = true })
check(Player.surfing, version .. " seamless preserves Surf")
terrain(MB.NORMAL, 3, 0)
load()
check(not Player.surfing and not Player.underwater, version .. " ordinary_land_clears_avatar")
terrain(MB.OCEAN_WATER, 1, 0x29)
load()
check(Player.surfing, version .. " ordinary_surfable_entry")
local cruise = Flags.forVersion(version).IDS.FLAG_SYS_CRUISE_MODE
Flags.setFlag(s, nil, cruise, true)
def.mapType = 6
load()
check(not Player.surfing and not Player.underwater, version .. " cruise wins over water")
eq(Player.facing, "right", version .. " cruise direction")
Flags.setFlag(s, nil, cruise, false)
if version == "firered" or version == "leafgreen" then
mid = "FR_SEAFOAM_ISLANDS_B3F"
s.map = mid
game.data.maps[mid] = def
load()
check(not Player.surfing, version .. " seafoam_ordinary_on_foot")
roundtrip(true, false, 1)
check(Player.surfing, version .. " seafoam_saved_surf_continue")
else
def.mapType = 5
load()
check(Player.underwater and not Player.surfing, version .. " underwater_entry")
roundtrip(false, true, 1)
check(Player.underwater and not Player.surfing, version .. " native_underwater_continue")
def.mapType = 4
load()
check(Player.surfing and not Player.underwater, version .. " dive_surface_entry")
terrain(MB.BRIDGE_OVER_OCEAN, 4, 0)
s.surfing, s.underwater, s.elevation = nil, nil, nil
s = Schema.fromSaveTable(Schema.toSaveTable(s))
Player.syncFromSession(s)
load({ enterVia = "continue" })
check(not Player.surfing, version .. " legacy_upper_bridge_on_foot")
end
setup(version)
s.surfing, s.underwater = true, false
s.specialSaveWarpFlags = 1
s.continueGameWarp = { map = mid, x = 0, y = 0 }
s = Schema.fromSaveTable(Schema.toSaveTable(s))
terrain(MB.NORMAL, 3, 0)
Player.syncFromSession(s)
load({ enterVia = "continue" })
check(not Player.surfing, version .. " special_continue_relocation_clears_saved_surf")
end
Player.underwater = true
Player.reset(0, 0, "right")
check(not Player.underwater, "reset clears stale underwater")
local ModRuntime = require("src.mods.Runtime")
local wants, emit = ModRuntime.wants, ModRuntime.emit
local expected, label, observed, nativeShape
local function synchronized()
eq(s.surfing, expected.surfing, label .. " session Surf")
eq(game.save.surfing, expected.surfing, label .. " host Surf")
eq(s.biking, expected.biking, label .. " session bike")
eq(game.save.biking, expected.biking, label .. " host bike")
if nativeShape then
eq(game.save.position, nil, label .. " native save stays flat")
else
eq(game.save.position.biking, expected.biking, label .. " host position bike")
end
eq(s.underwater, false, label .. " session underwater")
eq(game.save.elevation, Player.elevation, label .. " host elevation")
if not nativeShape then eq(game.save.position.x, Player.cellX, label .. " host x") end
eq(s.x, Player.cellX, label .. " session x")
end
ModRuntime.wants = function(name) return name == "world.stepped" end
ModRuntime.emit = function(name)
eq(name, "world.stepped", label .. " observer event")
observed = observed + 1
synchronized()
end
local function completed(surfing, biking, name)
expected, label, observed = { surfing = surfing, biking = biking }, name, 0
local callback = 0
Player._onStepDone = function()
callback = callback + 1
synchronized()
end
local frames = Player.stepFrames
for _ = 1, frames do Player.tick(game) end
check(not Player.moving and Player.surfing == surfing and Player.biking == biking, label .. " live completed mode")
eq(observed, 1, label .. " observer runs once")
eq(callback, 1, label .. " callback runs once")
synchronized()
end
for _, version in ipairs({ "firered", "leafgreen", "emerald", "ruby", "sapphire" }) do
local road = Flags.forVersion(version).IDS.FLAG_SYS_ON_CYCLING_ROAD
for _, native in ipairs({ false, true }) do
nativeShape = native
for _, cycling in ipairs(road and { false, true } or { false }) do
setup(version)
terrain(MB.NORMAL, 3, 0)
load()
game.save = native and Schema.toSaveTable(s) or { position = {} }
terrain(MB.OCEAN_WATER, 1, 0x29)
Collision.bindMap(game, mid, def)
check(Player.startSurfing(game), version .. " mount starts")
local suffix = " native=" .. tostring(native) .. " cycling=" .. tostring(cycling)
completed(true, false, version .. " completed_mount_sync" .. suffix)
if native then game.save = Schema.toSaveTable(s) end
terrain(MB.NORMAL, 3, 0)
Collision.bindMap(game, mid, def)
if road then Flags.setFlag(s, nil, road, cycling) end
check(Player.forcedStep("left"), version .. " dismount starts")
check(Player.dismounting, version .. " dismount transition selected")
completed(false, cycling, version .. (cycling and " cycling_remount_sync" or " completed_dismount_sync") .. suffix)
end
end
end
ModRuntime.wants, ModRuntime.emit = wants, emit
T.finish("game3_avatar_restore_2759_2761")
@@ -0,0 +1,56 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.fixture_data.game3_map_sections").install()
local Ui = require("src.core.game3.battle.ui")
local function press(key)
Ui.handleInput({ wasPressed = function(_, k) return k == key end })
end
local function check(ok, label)
assert(ok, "FAIL cursor2766 " .. label)
print("PASS cursor2766 " .. label)
end
local st = { player = { mon = {} } }
Ui.reset({ headless = true })
Ui.bindState(st)
Ui.openMenu()
check(Ui._menuIndex == 1, "initial_Fight")
press("right")
Ui.openMenu()
check(Ui._menuIndex == 2, "single_navigation_reopen_Bag")
press("down")
Ui.openMenu()
check(Ui._menuIndex == 4, "single_navigation_reopen_Run")
st.player = { mon = {} }
Ui.openMenu()
check(Ui._menuIndex == 1, "single_new_battler_Fight")
st.double = true
st.battlers = { [0] = st.player, [2] = { mon = {} } }
Ui.openMenu(0)
press("right")
Ui.openMenu(2)
press("down")
Ui.openMenu(0)
check(Ui._menuIndex == 2, "double_slot0_Bag")
Ui.openMenu(2)
check(Ui._menuIndex == 3, "double_slot2_Pokemon")
st.battlers[0] = { mon = {} }
Ui.openMenu(0)
check(Ui._menuIndex == 1, "double_slot0_canonical_replacement_Fight")
Ui.openMenu(2)
check(Ui._menuIndex == 3, "double_partner_cursor_unchanged")
st.battlers[2] = { mon = {} }
Ui.openMenu(2)
check(Ui._menuIndex == 1, "double_slot2_replacement_Fight")
st.double, st.battlers, st.safari = false, nil, true
Ui.openMenu()
press("right")
Ui.openMenu()
check(Ui._menuIndex == 2, "Safari_cursor_persists")
st.safari, st.oldManTutorial = false, true
Ui.openMenu()
local command = Ui.takeCommand()
check(Ui._mode == "none" and command and command.kind == "bag", "OldMan_scripted_Bag")
Ui.reset({ headless = true })
check(next(Ui._actionCursor) == nil and next(Ui._actionCursorBattler) == nil,
"new_battle_clears_owners_and_cursors")
print("PASS cursor2766_ROM_free")
+128
View File
@@ -0,0 +1,128 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
local GameVersion = require("src.core.GameVersion")
local Movement = require("src.core.game3.scripting.movement")
local Player = require("src.core.game3.player")
local Objects = require("src.core.game3.objects")
local Collision = require("src.core.game3.collision")
local Runtime = require("src.core.game3.runtime")
local Field = require("src.core.game3.field")
local game = { data = {}, session = { version = "emerald", flags = {}, vars = {}, party = {} } }
Runtime.session = game.session
Field.locked = true
local dirs = { "down", "up", "left", "right" }
local delta = { down = { 0, 1 }, up = { 0, -1 }, left = { -1, 0 }, right = { 1, 0 } }
local function tick()
Player.tick(game)
Objects.update(game)
end
local function fixture()
Player.reset(5, 5, "down")
local eo = { localId = 1, cellX = 5, cellY = 5, px = 80, py = 80,
facing = "down", animClock = 0, progress = 0, frozen = true, scriptBusy = true }
Objects._byId, Objects._order, Objects._tracks = { [1] = eo }, { 1 }, {}
return eo
end
for _, version in ipairs({ "emerald", "ruby", "sapphire" }) do
GameVersion.set(version)
game.session.version = version
for i = 0, 4 do
local frames = 2 ^ i
fixture()
Objects.applyMovement(255, { 0x18 + i, 0xFE })
for n = 1, frames - 1 do
tick()
T.check(not Objects.pollMovement(255), version .. " delay remains active " .. n)
end
tick()
T.check(Objects.pollMovement(255), version .. " delay ends at " .. frames)
end
for i, dir in ipairs(dirs) do
for _, spec in ipairs({ { 0x31, 6 }, { 0x39, 2 } }) do
for _, lid in ipairs({ 1, 255 }) do
local eo = fixture()
local actor = lid == 255 and Player or eo
Objects.applyMovement(lid, { spec[1] + i - 1, 0xFE })
T.eq(actor.stepFrames, spec[2], version .. " " .. dir .. " selected duration")
for n = 1, spec[2] do
tick()
T.eq(actor.moving, n < spec[2], version .. " " .. dir .. " moving tick " .. n)
end
T.eq(actor.cellX, 5 + delta[dir][1], version .. " " .. dir .. " landing x")
T.eq(actor.cellY, 5 + delta[dir][2], version .. " " .. dir .. " landing y")
end
end
end
end
for _, version in ipairs({ "firered", "leafgreen" }) do
GameVersion.set(version)
game.session.version = version
T.eq(Movement.decodeAction(0x1C).frames, 32, version .. " delay behavior retained")
T.eq(Movement.decodeAction(0x32).kind, "nop", version .. " current behavior retained")
T.eq(Movement.decodeAction(0x39).frames, nil, version .. " slide behavior retained")
end
GameVersion.set("emerald")
game.session.version = "emerald"
require("src.import.gba.versions").select("emerald")
local Cache = require("tests.game3_cache")
local root = Cache.mount("scripts/movements.lua", { native = true })
if not root then
print("[skip] 2764 extracted Emerald scenes: " .. tostring(Cache.reason))
T.finish("game3_cutscene_push_2764")
end
local Dataset = require("src.core.game3.dataset")
Dataset.hydrate(game)
local bundle = require("src.import.gba.extract_scripts").loadBundle(Cache.cache(), root, { allowIncomplete = true })
local moves = bundle.movements
local function enter(id)
game.session.map = id
local def = assert(game.data.maps[id])
Collision.bindMap(game, id, def)
Objects.loadMap(game, id, def)
end
local function stepFor(n)
for _ = 1, n do tick() end
end
enter("EM_ROUTE119_WEATHER_INSTITUTE_2F")
Player.reset(5, 6, "left")
local grunt = assert(Objects.find(7))
grunt.hidden = false
Objects.applyMovement(7, assert(moves["g3:08270170"]))
Objects.applyMovement(255, assert(moves["g3:0827017c"]))
stepFor(80)
T.eq(Player.py, 96, "Weather player stays until shove")
stepFor(6)
T.eq(Player.cellY, 5, "Weather six-frame north shove complete")
T.check(grunt.moving, "Weather shove precedes grunt completion")
stepFor(2)
T.eq(grunt.cellX, 5, "Weather grunt reaches destination")
T.eq(Player.cellY, 5, "Weather player vacated grunt destination")
stepFor(4)
Objects.applyMovement(255, assert(moves["g3:08270184"]))
local scientist = assert(Objects.find(5))
scientist.cellX, scientist.cellY, scientist.px, scientist.py = 1, 6, 16, 96
scientist.hidden = false
Objects.applyMovement(5, assert(moves["g3:08270187"]))
stepFor(2)
T.eq(Player.cellY, 6, "Weather return slide lasts two frames")
stepFor(48)
T.eq(Player.cellX, 5, "Weather dialogue player x")
T.eq(Player.cellY, 6, "Weather dialogue player y")
T.eq(scientist.cellX, 4, "Weather dialogue scientist x")
T.eq(scientist.cellY, 6, "Weather dialogue scientist y")
enter("EM_SLATEPORT_CITY_OCEANIC_MUSEUM_2F")
Player.reset(12, 6, "left")
local archie, other = assert(Objects.find(2)), assert(Objects.find(4))
other.cellX, other.cellY, other.px, other.py = 10, 6, 160, 96
archie.hidden, other.hidden = false, false
Objects.applyMovement(2, assert(moves["g3:0820bcd8"]))
Objects.applyMovement(4, assert(moves["g3:0820bcfe"]))
stepFor(140)
T.check(archie.moving and other.moving and other.py > 96, "Museum grunt vacates during Archie approach")
stepFor(4)
T.eq(archie.cellX, 10, "Museum Archie destination x")
T.eq(archie.cellY, 6, "Museum Archie destination y")
T.eq(other.cellY, 7, "Museum grunt vacates destination on time")
T.eq(Player.cellY, 6, "Museum player was not shoved")
T.finish("game3_cutscene_push_2764")
+147
View File
@@ -0,0 +1,147 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local GameVersion = require("src.core.GameVersion")
local Versions = require("src.import.gba.versions")
local Objects = require("src.core.game3.objects")
local Prepare = require("src.core.game3.object_prepare")
local sheets, sprites = {}, {}
package.loaded["src.core.game3.ow_sprites"] = { get = function(id) return sprites[id] end }
package.loaded["src.core.game3.field_effects"] = {
loadSheet = function(name) return sheets[name] end,
manifestObject = function(name) return sheets[name] end,
}
local Fx = require("src.core.game3.field_effects_rse")
local View = require("src.core.game3.field_view")
local function check(ok, label)
assert(ok, "FAIL disguise2763 " .. label)
end
local function maskFor(eo)
local actors = {}
Fx.collectActors(actors)
for _, actor in ipairs(actors) do
if actor.disguiseObject == eo then return actor end
end
end
local function exercise(def, version, label)
local eo = Prepare.instance(def, {
version = version, rse = true, graphicsId = def.graphicsId, elevation = def.elevation,
})
eo.disguise.a = Fx.anim(Fx.animCmds(eo.disguise.sheet, 1))
Objects._order, Objects._byId = { eo.localId }, { [eo.localId] = eo }
Objects._bounds = nil
check(#Objects.forDraw() == 1, label .. " ninja_drawn")
for _, camY in ipairs({ 0, eo.py - 112, eo.py - 23, eo.py + 17 }) do
for _, fixed in ipairs({ false, 0, 1, 83, 255 }) do
eo.fixedPriority, eo.subpriority = fixed ~= false, fixed ~= false and fixed or nil
eo.fixedClass = nil
local ninja = { kind = "npc", i = eo.localId, eventObject = eo,
x = eo.px, y = eo.py, sortY = eo.py, elevation = eo.elevation }
local back = { kind = "npc", i = 201, y = eo.py - 32, elevation = 3 }
local front = { kind = "npc", i = 202, y = eo.py + 32, elevation = 3 }
local actors = { front, ninja, back }
Fx.collectActors(actors)
check(#actors == 4, label .. " mask_collected")
local mask = actors[4]
check(mask.x == eo.px and mask.y == eo.py - 16, label .. " bitmap_anchor")
check(mask.eventObject == nil, label .. " independent_sprite")
actors = { mask, front, ninja, back }
local under, over = View.applyDrawOrder(actors, {}, {}, camY)
check(#under == 4 and #over == 0, label .. " mask_priority_class")
check(mask.subpriority == (ninja.subpriority - 1) % 256, label .. " linked_subpriority")
check(mask.priority == 2 and not mask.fixedPriority, label .. " mask_fixed_state")
check(eo.fixedClass == (fixed ~= false and 2 or nil), label .. " linked_fixed_class")
local indices = {}
for i, actor in ipairs(under) do indices[actor] = i end
check(indices[back] < indices[front], label .. " unrelated_objects_order")
if fixed == 0 then
check(mask.subpriority == 255 and indices[mask] < indices[ninja], label .. " unsigned_wrap")
else
check(indices[ninja] < indices[mask], label .. " ninja_before_mask")
end
check(mask.x == eo.px and mask.y == eo.py - 16, label .. " sorting_preserves_position")
end
end
eo.fixedPriority, eo.subpriority, eo.fixedClass = false, nil, nil
eo.invisible = true
check(maskFor(eo) == nil and #Objects.forDraw() == 0, label .. " linked_invisible")
eo.invisible, eo.hidden = false, true
check(maskFor(eo) == nil, label .. " linked_hidden")
eo.hidden, eo.visible = false, false
check(maskFor(eo) == nil, label .. " linked_not_visible")
eo.visible = true
Objects.revealTrainer(eo)
Fx.stepObjects()
check(eo.disguise.revealing and maskFor(eo) ~= nil and #Objects.forDraw() == 1,
label .. " ninja_drawn_during_tear")
for _ = 1, 100 do Fx.stepObjects() end
check(eo.disguise.done and maskFor(eo) == nil and #Objects.forDraw() == 1,
label .. " reveal_removes_only_mask")
print("PASS disguise2763 " .. label .. " linked_order_visibility_reveal")
end
local version = arg and arg[1]
if version then
GameVersion.set(version)
Versions.select(version)
local Cache = require("tests.game3_cache")
local root = Cache.mount()
if not root then
print("[skip] disguise2763 cache integration: " .. tostring(Cache.reason))
os.exit(0)
end
local cache = Cache.cache()
local function data(rel)
return assert(loadstring(assert(cache:read(root .. "/" .. rel)), "@" .. rel))()
end
sprites = data("ow/manifest.lua").sprites
for _, row in ipairs(data("field_effects/objects.lua").objects) do
if row.name == "tree_disguise" or row.name == "mountain_disguise" then
row.image, row.quads = {}, {}
for frame = 0, row.frames - 1 do row.quads[frame] = {} end
sheets[row.name] = row
end
end
local ninja = assert(cache:read(root .. "/ow/5.rgba"))
for name in pairs(sheets) do
local mask = assert(cache:read(root .. "/field_effects/" .. name .. ".rgba"))
local opaque, covered = 0, 0
for pixel = 0, 255 do
if ninja:byte(pixel * 4 + 4) > 0 then
opaque = opaque + 1
if mask:byte((pixel + 256) * 4 + 4) > 0 then covered = covered + 1 end
end
end
check(opaque == 124 and covered == opaque, version .. " " .. name .. " opaque_occlusion")
print("PASS disguise2763 " .. version .. " " .. name .. " opaque_occlusion_124_of_124")
end
local count = 0
for mapId, events in pairs(data("scripts/events.lua")) do
for _, def in ipairs(events.objects or {}) do
if def.movementType == 0x39 or def.movementType == 0x3A then
exercise(def, version, version .. " " .. mapId .. " " .. def.localId)
count = count + 1
end
end
end
check(count == (version == "emerald" and 7 or 5), version .. " all_cached_disguises")
print("PASS disguise2763 " .. version .. " cached_disguises=" .. count)
else
sprites[5] = { width = 16, height = 16 }
for _, name in ipairs({ "tree_disguise", "mountain_disguise" }) do
sheets[name] = { fw = 16, fh = 32, image = {}, quads = { [0] = {}, [1] = {} },
anims = { { { "frame", 0, 2 }, { "end" } },
{ { "frame", 0, 2 }, { "frame", 1, 2 }, { "end" } } } }
end
for _, selected in ipairs({ "emerald", "ruby", "sapphire" }) do
GameVersion.set(selected)
Versions.select(selected)
for _, movement in ipairs({ 0x39, 0x3A }) do
exercise({ localId = 1, x = 12, y = 14, elevation = 3, graphicsId = 5,
movementType = movement }, selected, selected .. " movement=" .. movement)
end
end
end
print("PASS game3_disguise_order_2763")
+98
View File
@@ -0,0 +1,98 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
local Flags = require("src.core.game3.scripting.flags")
local Space = require("src.core.game3.scripting.space")
local Vm = require("src.core.game3.scripting.vm")
local TextIR = require("src.core.game3.scripting.text_ir")
local session, finishShowMon, sound
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
package.loaded["src.core.game3.field_move_show_mon"] = {
start = function(_, _, fn) finishShowMon = fn end,
}
package.loaded["src.core.game3.audio"] = { playSe = function(id) sound = id end }
local level, radius = 4, nil
local View = {
getFlashLevel = function() return level end,
radiusForLevel = function(v) return 120 - v * 20 end,
setFlashRadius = function(v) radius = v end,
setFlashLevel = function(v) level, radius = v, nil end,
}
package.loaded["src.core.game3.field_view"] = View
local Moves = require("src.core.game3.field_moves")
local Field = require("src.core.game3.field")
local Fx = require("src.core.game3.field_effects")
local Text = require("src.core.game3.rom_text")
local function rejection(ctx, label)
local ok, res = pcall(Moves.flashFromMenu, ctx)
T.check(ok and res.ok == false and res.text == "CACHED REJECTION", label)
end
for _, version in ipairs({ "ruby", "sapphire", "emerald", "firered", "leafgreen" }) do
GameVersion.set(version)
session = { version = version, flags = {}, vars = {} }
Field._session = session
Field._locks = {}
Space.store = Flags.newStore({ flags = session.flags })
session.flags = Space.store.flags
local rs = version == "ruby" or version == "sapphire"
local label = rs and "gUnknown_081B694A" or "EventScript_UseFlash"
local key = "fixture_flash"
Space.bundle = { text = {
[rs and "OtherText_CantUseThatHere" or "gText_InUseAlready_PM"] = TextIR.fromAscii("CACHED REJECTION"),
}, labels = { [label] = key }, scripts = {
[key] = { { op = "animateflash", 1 }, { op = "setflashlevel", 1 }, { op = "end" } },
} }
Space.vm = Vm.new({ scripts = Space.bundle.scripts, store = Space.store })
Flags.setFlag(Space.store, nil, Moves.badgeFlag("FLASH"), true)
local ctx = { session = session, store = Space.store, isCave = true }
local payload = Moves.flashFromMenu(ctx)
T.check(payload.ok and payload.action == "flash", version .. " first_use_available")
level, radius, sound = 4, nil, nil
Fx._anims = {}
Field.executeFieldMove(payload)
T.check(Field.locked and finishShowMon ~= nil and #Fx._anims == 0,
version .. " show_mon_precedes_flash")
T.check(not Flags.getFlag(Space.store, nil, payload.flag), version .. " flag_waits_for_show_mon")
finishShowMon()
T.eq(sound, payload.se, version .. " flash_sound_preserved")
T.check(Flags.getFlag(Space.store, nil, payload.flag) and Field.locked,
version .. " active_flag_and_animation_lock")
Fx.step()
Space.vm:tick()
T.check(Field.locked and radius ~= nil, version .. " lock_waits_for_radius_animation")
for _ = 1, 600 do Fx.step(); Space.vm:tick() end
local rse = Profile.of(version).family == "rse"
T.eq(level, rse and 1 or 0, version .. " first_use_cart_level")
T.check(not Field.locked and not Space.vm:isRunning(), version .. " animation_end_unlocks_field")
rejection(ctx, version .. " repeated_flash_cached_prompt")
local game = { session = session, data = { maps = {
floor = { mapType = 4, cave = 1 }, lit = { mapType = 4, cave = 0 },
outdoors = { mapType = 1, cave = 0 },
} } }
Field.clearTempFieldEventData(game, "floor")
rejection(ctx, version .. " dark_floor_retains_flash")
Field.clearTempFieldEventData(game, "lit")
rejection(ctx, version .. " lit_underground_retains_flash")
Field.clearTempFieldEventData(game, "outdoors")
T.check(not Flags.getFlag(Space.store, nil, payload.flag) and Moves.flashFromMenu(ctx).ok,
version .. " outdoors_enables_next_cave_flash")
if rs then
T.raises(function() Text.ir("uncached_prompt") end, "is not in the script cache",
version .. " missing_rom_text_stays_strict")
Space.bundle.scripts[key] = { { op = "animateflash", 2 }, { op = "setflashlevel", 2 }, { op = "end" } }
level, radius = 4, nil
Fx._anims = {}
Field.executeFieldMove(payload)
finishShowMon()
for _ = 1, 600 do Fx.step(); Space.vm:tick() end
T.eq(level, 2, version .. " extracted_script_controls_level")
Space.bundle.labels[label] = nil
Field.executeFieldMove(payload)
T.raises(finishShowMon, "ROM Flash script is not in the script cache",
version .. " missing_rom_script_stays_strict")
Field._locks = {}
end
end
T.finish("game3_rs_flash_2762")
+85
View File
@@ -0,0 +1,85 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local MB = require("src.core.game3.mb")
local Collision = require("src.core.game3.collision")
local Interaction = require("src.core.game3.scripting.interaction_scripts")
local session = { version = "emerald", map = "EM_SURF_TIMING", flags = {}, vars = {} }
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
package.loaded["src.core.game3.audio"] = { stopSurfMusic = function() end }
package.loaded["src.core.game3.bike"] = { rse = function() return nil end }
package.loaded["src.core.game3.field_effects"] = { leaveTallGrass = function() end, tallGrassAt = function() end }
package.loaded["src.core.game3.field"] = { locked = false }
package.loaded["src.core.game3.objects"] = { hasMap = function() return false end }
local Player = require("src.core.game3.player")
local Flags = require("src.core.game3.scripting.flags")
local game = { save = { position = {} } }
local function setup(version, surfing, underwater, shore)
GameVersion.set(version)
session.version = version
local layout = { width = 4, height = 1, pair = "surf_timing" }
function layout:collAt(x) return surfing and not (shore and x == 1) and 0x29 or 0 end
function layout:elevAt(x) return surfing and not (shore and x == 1) and 1 or 3 end
function layout:midAt(x) return x end
function layout:collArray() return { self:collAt(0), self:collAt(1), self:collAt(2), self:collAt(3) } end
Interaction.behaviors[layout.pair] = { [0] = MB.OCEAN_WATER, [1] = shore and MB.NORMAL or MB.OCEAN_WATER,
[2] = MB.OCEAN_WATER, [3] = MB.OCEAN_WATER }
Collision.bindMap(game, session.map, { midLayout = layout, pair = layout.pair, warps = {} })
Player.reset(0, 0, "right")
Player.surfing, Player.underwater = surfing, underwater
Player.elevation, Player.currentElevation = surfing and 1 or 3, surfing and 1 or 3
end
local function measure(frames, label)
local start = Player.px
Player._onStepDone = function() end
eq(Player.stepFrames, frames, label .. " selected duration")
for frame = 1, frames do
Player.tick(game)
eq(Player.px - start, math.floor(frame * 16 / frames), label .. " pixel " .. frame)
eq(Player.moving, frame < frames, label .. " completes exactly " .. frame)
end
eq(Player.cellX, 1, label .. " landing")
end
for _, version in ipairs({ "firered", "leafgreen", "emerald", "ruby", "sapphire" }) do
for _, b in ipairs({ false, true }) do
for _, shoes in ipairs({ false, true }) do
setup(version, true, false)
local id = Flags.forVersion(version).IDS.FLAG_SYS_B_DASH
Flags.setFlag(session, nil, id, shoes)
local input = { isDown = function(_, key) return key == "right" or key == "b" and b end }
Player.update(game, input)
check(Player.moving, version .. " Surf input starts")
measure(8, version .. " surf_B=" .. tostring(b) .. "_shoes=" .. tostring(shoes))
check(Player.surfing and not Player.running, version .. " fast Surf keeps walking animation")
end
end
setup(version, true, false)
eq(Player.tryMove("right", game, false), "step", version .. " direct Surf step")
measure(8, version .. " direct_surf_eight_frames")
setup(version, false, false)
eq(Player.tryMove("right", game, false), "step", version .. " foot walk")
measure(16, version .. " foot_walk")
setup(version, false, false)
eq(Player.tryMove("right", game, true), "step", version .. " foot run")
measure(8, version .. " foot_run")
setup(version, true, false)
check(Player.scriptStep("right", false), version .. " scripted Surf walk starts")
measure(16, version .. " scripted_surf_walk")
setup(version, true, true)
eq(Player.tryMove("right", game, false), "step", version .. " underwater step")
measure(16, version .. " underwater_walk")
setup(version, true, false, true)
eq(Player.tryMove("right", game, true), "step", version .. " Surf dismount")
eq(Player.stepFrames, 16, version .. " dismount hop duration")
check(Player.dismounting and Player.jumping, version .. " dismount semantics")
setup(version, true, false)
Player.surfHopping = true
eq(Player.tryMove("right", game, false), "step", version .. " Surf mount")
eq(Player.stepFrames, 16, version .. " mount hop duration")
setup(version, true, false)
check(Player.forcedStep("right", 4), version .. " explicit forced Surf step")
eq(Player.stepFrames, 4, version .. " forced duration remains explicit")
end
T.finish("game3_surf_speed_2767")
+145
View File
@@ -0,0 +1,145 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local noop = function() end
for _, name in ipairs({ "seagallop_ui", "shop_menu", "field_effects", "doors", "pokecenter_heal", "ss_anne", "field_weather", "ow_sprites" }) do
package.loaded["src.core.game3." .. name] = {}
end
package.loaded["src.core.game3.map"] = { refreshWorld = noop }
package.loaded["src.core.game3.bg"] = { hasVisible = function() return false end }
package.loaded["src.core.game3.oam"] = {
resetFrame = noop, setLayer = noop, animateSprites = noop, buildOamBuffer = noop, flush = noop,
}
local F = require("src.core.game3.field_view")
local D = require("src.core.game3.display")
local R = require("src.render.Renderer")
local Tilt = require("src.render.Tilt")
local function up(fn, wanted, replacement)
for i = 1, 100 do
local name, value = debug.getupvalue(fn, i)
if name == wanted then
if replacement then debug.setupvalue(fn, i, replacement) end
return value
end
end
error("missing " .. wanted)
end
local plane = up(up(D.present, "presentPlanes"), "drawFieldPlane")
local records = {}
local function record(label) records[#records + 1] = label end
local roof, neighbor = {}, {}
local mesh = { setTexture = function(self, texture) self.texture = texture end,
setVertices = function(self, vertices) self.vertices = vertices end }
love.graphics.newMesh = function() return mesh end
love.graphics.newShader = function() return {} end
love.graphics.setStencilTest = noop
love.graphics.draw = function(what)
if what == roof or what == neighbor then
record(love.graphics.getCanvas() == R.worldCanvas and "ground-roof" or "mask-roof")
elseif what == mesh then
T.eq(love.graphics.getCanvas(), R.uprightCanvas, "mask projects onto upright actors")
T.eq(love.graphics.getBlendMode(), "multiply", "mask removes actor coverage")
local shader = love.graphics.getShader()
T.check(shader ~= nil, "projected mask uses a shader")
record("occlude-normal")
end
end
up(F.draw, "drawNativeTiles", function()
F._nativeOverPair, F._nativeOverBatches = "main", { main = roof, neighbor = neighbor }
record("ground")
return true
end)
up(F.draw, "collectGame3Actors", function()
return F.applyDrawOrder({
{ kind = "mod", elevation = 3, x = 80, y = 192, draw = function() record("normal") end },
{ kind = "mod", elevation = 13, x = 80, y = 192, draw = function() record("elevated") end },
})
end)
local game = { mapId = "sample", data = { maps = { sample = { width = 32, blocks = {} } } },
world = { map = { id = "sample" }, player = { px = 80, py = 192 } } }
F._flashMapId = "sample"
local function index(label)
for i, item in ipairs(records) do if item == label then return i end end
end
local function render(level, transitioning)
records = {}
Tilt.applyOptions({ tilt = level })
package.loaded["src.core.game3.battle_transition"] = { isActive = function() return transitioning end }
R:init()
R:beginFrame(true)
R:beginWorldPass()
plane(game, 240, 160, R)
T.eq(love.graphics.getCanvas(), R.worldCanvas, "field plane restores the current world canvas")
if level > 0 and not transitioning then
T.check(index("occlude-normal") ~= nil, "tilt2765 normal actors receive roof occlusion")
local masked = index("occlude-normal")
T.check(masked and index("normal") < masked and masked < index("elevated"),
"tilt2765 elevated actors draw after roof occlusion")
local maskRoofs = 0
for _, item in ipairs(records) do if item == "mask-roof" then maskRoofs = maskRoofs + 1 end end
T.eq(maskRoofs, 2, "tilt2765 includes the connected-pair overhead batch")
local vw, vh = R.worldCanvas:getWidth(), R.worldCanvas:getHeight()
local expected = Tilt.meshCorners(vw, vh)
if mesh.vertices then
for i, vertex in ipairs(expected) do
for j, value in ipairs(vertex) do T.eq(mesh.vertices[i][j], value, "mask shares ground projection") end
end
end
T.eq(love.graphics.getShader(), nil, "mask restores shader")
T.eq(love.graphics.getBlendMode(), "alpha", "mask restores blend mode")
else
T.eq(index("occlude-normal"), nil, "tilt2765 OFF and transitions skip the mask")
T.check(index("normal") < index("ground-roof") and index("ground-roof") < index("elevated"),
"flat field keeps cart layer ordering")
T.eq(R.uprightActive, false, "flat field does not enter upright pass")
end
R:endWorldPass()
end
for level = 0, 1 do
Tilt.applyOptions({ tilt = level })
R:init()
R:beginFrame(true)
R:beginWorldPass()
if level > 0 then
R:beginUprightPass()
R:endUprightPass()
end
R:endWorldPass()
local draw, getBlend = love.graphics.draw, love.graphics.getBlendMode
local reads = 0
love.graphics.draw = noop
love.graphics.getBlendMode = function()
reads = reads + 1
return getBlend()
end
R:endFrame(nil, nil)
love.graphics.draw, love.graphics.getBlendMode = draw, getBlend
T.eq(reads, 0, "tilt2765 unmasked composite skips blend-state reads at level " .. level)
end
render(0, false)
for level = 1, 3 do render(level, false) end
render(1, true)
render(1, false)
local priorMask = R.tiltOverheadCanvas
T.check(priorMask ~= nil, "tilt2765 lifecycle starts with an allocated mask")
R:init()
T.check(priorMask and priorMask.released, "tilt2765 init releases the overhead mask")
T.eq(R.tiltOverheadCanvas, nil, "tilt2765 init drops the overhead mask")
T.eq(R.uprightOccluded, false, "tilt2765 init clears premultiplied upright compositing")
render(1, false)
local releasedMask = R.tiltOverheadCanvas
R:releaseCanvases()
T.check(releasedMask and releasedMask.released, "tilt2765 release frees the overhead mask")
T.eq(R.tiltOverheadCanvas, nil, "tilt2765 release drops the overhead mask")
T.eq(R.uprightOccluded, false, "tilt2765 release clears premultiplied upright compositing")
T.check(pcall(R.releaseCanvases, R), "tilt2765 repeated release is safe")
R._tiltOcclusionShader = nil
love.graphics.newShader = function() error("unsupported mask shader") end
local current = love.graphics.getCanvas()
local ok = pcall(R.occludeUprightActors, R, noop)
T.check(not ok, "tilt2765 shader compilation failure reaches the display error boundary")
T.eq(love.graphics.getCanvas(), current, "failed shader creation does not switch canvas")
R._tiltMesh = false
T.check(pcall(R.occludeUprightActors, R, noop), "tilt2765 unavailable perspective mesh uses the existing renderer route")
Tilt.reset()
T.finish("game3_tilt_roof_2765")
+123
View File
@@ -0,0 +1,123 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local function read(path)
local file = assert(io.open(path, "rb"))
local body = file:read("*a")
file:close()
return body
end
local main = read("main.lua")
local hostSource = read("src/core/HostShell.lua")
local quitSource = assert(main:match("\nfunction love%.quit%(%).-\nend\n"))
local returnSource = assert(main:match("local function returnToLauncher%([^)]*%).-\nend\n"))
local endSource = assert(main:match("local function endProcessOnce%(%).-\nend\n"))
local dispatchSource = assert(main:match('( if name == "quit" then.-)\n if WAKE'))
local function scenario(osName, state, options)
options = options or {}
local stats = { events = {}, joins = 0, presence = 0, markers = 0,
sessions = 0, editor = 0, platform = 0, splash = 0, theme = 0,
observe = 0, flush = 0 }
local env = setmetatable({ processEnded = false, quitToLauncher = false,
launchedIntoGame = options.shortcut or false, editorMode = state == "editor",
RELAUNCH_MARKER = "marker" }, { __index = _G })
env.os = setmetatable({ getenv = function() return nil end }, { __index = os })
env.Game = state == "game" and {} or nil
env.Importer = state ~= "game" and { _themeVideo = {
release = function() stats.theme = stats.theme + 1 end } } or nil
env.launcherSplash = { release = function() stats.splash = stats.splash + 1 end }
env.EditorApp = { quit = function() stats.editor = stats.editor + 1 return true end }
env.PlatformHooks = { quitToLauncher = function(fn)
stats.platform = stats.platform + 1
if options.platformVeto then return true end
return fn()
end }
env.love = {
system = { getOS = function() return osName end },
event = { quit = function(value) stats.events[#stats.events + 1] = value or 0 end },
filesystem = { write = function() stats.markers = stats.markers + 1 end },
}
env.SessionLifecycle = {
endProcess = function() stats.joins = stats.joins + 1 end,
endGameSession = function() stats.sessions = stats.sessions + 1 end,
}
local hostChunk = assert(loadstring(hostSource, "@src/core/HostShell.lua"))
setfenv(hostChunk, env)
local host = hostChunk()
env.require = function(name)
if name == "src.core.HostShell" then return host end
if name == "src.core.SessionLifecycle" then return env.SessionLifecycle end
if name == "src.core.DiscordPresence" then
return { shutdown = function() stats.presence = stats.presence + 1 end }
end
if name == "src.core.SaveSerializer" then return { encode = function() return "{}" end } end
if name == "src.import.LauncherWindow" then
return { observe = function() stats.observe = stats.observe + 1 end,
flush = function() stats.flush = stats.flush + 1 end }
end
error("unexpected module " .. name)
end
local chunk = assert(loadstring(endSource .. "\n" .. returnSource .. "\n"
.. quitSource .. '\nreturn { quit = love.quit, explicitReturn = returnToLauncher, dispatch = function(a)\n'
.. 'local name = "quit"\n' .. dispatchSource .. '\nreturn "veto"\nend }', "@main.lua:quit-policy"))
setfenv(chunk, env)
return chunk(), stats, env
end
for _, state in ipairs({ "game", "launcher", "editor" }) do
local callbacks, stats, env = scenario("NX", state, { platformVeto = true })
eq(callbacks.dispatch(), 0, "NX " .. state .. " accepts system quit")
eq(stats.joins, 1, "NX " .. state .. " tears down workers once")
eq(stats.presence, 1, "NX " .. state .. " tears down presence once")
eq(#stats.events, 0, "NX " .. state .. " queues no restart")
eq(stats.markers, 0, "NX " .. state .. " writes no launcher handoff")
eq(stats.platform, 0, "NX " .. state .. " bypasses platform veto")
eq(stats.editor, 0, "NX " .. state .. " bypasses editor veto")
eq(stats.splash, 1, "NX " .. state .. " releases splash")
if state ~= "game" then
eq(stats.theme, 1, "NX " .. state .. " releases launcher theme")
eq(stats.observe, 1, "NX " .. state .. " observes launcher window")
eq(stats.flush, 1, "NX " .. state .. " flushes launcher window")
eq(env.Importer._themeVideo, nil, "NX " .. state .. " drops theme reference")
end
eq(callbacks.dispatch(), 0, "NX " .. state .. " accepts repeated quit")
eq(stats.joins, 1, "NX " .. state .. " repeated quit keeps teardown idempotent")
end
local desktop, desktopStats = scenario("OS X", "game")
eq(desktop.dispatch(), "veto", "desktop game close returns to launcher")
eq(desktopStats.events[1], "restart", "desktop game close requests restart")
eq(desktopStats.markers, 1, "desktop game close writes launcher handoff")
eq(desktopStats.joins, 1, "desktop game close joins before restart")
eq(desktop.dispatch("restart"), "restart", "desktop queued restart is accepted")
eq(desktopStats.joins, 1, "desktop queued restart avoids duplicate joins")
local editor, editorStats = scenario("OS X", "editor")
eq(editor.dispatch(), "veto", "desktop editor preserves unsaved-change veto")
eq(editorStats.joins, 0, "desktop editor veto leaves workers running")
local shortcut, shortcutStats = scenario("OS X", "game", { shortcut = true })
eq(shortcut.dispatch(), 0, "desktop shortcut exits without returning to launcher")
eq(#shortcutStats.events, 0, "desktop shortcut queues no restart")
local restart, restartStats = scenario("NX", "game")
eq(restart.dispatch("restart"), "restart", "NX explicit restart keeps requested native action")
eq(restartStats.joins, 1, "NX explicit restart joins workers once")
eq(#restartStats.events, 0, "NX explicit restart adds no event")
eq(restartStats.markers, 0, "NX explicit restart adds no launcher handoff")
local inGame, inGameStats = scenario("NX", "game")
inGame.explicitReturn()
eq(inGameStats.events[1], "restart", "NX in-game EXIT GAME retains explicit restart")
eq(inGameStats.markers, 1, "NX in-game EXIT GAME retains launcher handoff")
eq(inGameStats.sessions, 1, "NX in-game EXIT GAME ends game session")
eq(inGameStats.joins, 1, "NX in-game EXIT GAME joins workers once")
eq(inGame.dispatch("restart"), "restart", "NX in-game queued restart retains action")
eq(inGameStats.joins, 1, "NX in-game queued restart avoids duplicate joins")
check(inGameStats.events[2] == nil, "NX in-game queued restart adds no event")
T.finish("nx_quit_policy_2720")
+7 -1
View File
@@ -217,7 +217,13 @@ check(source("src/net/Fetch.lua"):find("registerProcessShutdown(Fetch.shutdown)"
local returnBody = mainSrc:match("local function returnToLauncher%(opts%)(.-)\nend\n") or ""
check(quitHook:find("returnToLauncher()", 1, true) ~= nil,
"love.quit routes a game window close to returnToLauncher on every platform")
"love.quit retains the desktop game window returnToLauncher route")
local nxAt = quitHook:find('if osName == "NX" then', 1, true)
local editorAt = quitHook:find("if editorMode and EditorApp.quit then", 1, true)
local platformAt = quitHook:find("PlatformHooks.quitToLauncher", 1, true)
check(nxAt ~= nil and editorAt ~= nil and platformAt ~= nil
and nxAt < editorAt and nxAt < platformAt,
"NX application exit precedes editor and platform quit vetoes")
check(quitHook:find("inProcessReturn", 1, true) == nil,
"no platform keeps the old in-process launcher swap")
local joinAt = returnBody:find("endProcessOnce()", 1, true)
@@ -0,0 +1,176 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local version = os.getenv("POKEPORT_VERSION") or "firered"
require("src.core.GameVersion").set(version)
require("src.import.gba.versions").select(version)
local Cache = require("tests.game3_cache")
if not Cache.mount() then
print("[skip] cursor2766 integration: " .. tostring(Cache.reason))
os.exit(0)
end
if version == "ruby" or version == "sapphire" then
local Kit = require("src.ui.game3.rse.scene_kit")
local cache = require("src.core.game3.dataset").cache()
Kit.loadLua = function(path)
return assert(loadstring(assert(cache:read(path))))()
end
require("src.ui.game3.frlg_font").measure = function(text) return #tostring(text) * 6 end
end
local Bag = require("src.core.game3.bag")
local ItemsData = require("src.core.game3.items_data")
local Pokemon = require("src.core.game3.pokemon")
ItemsData.install(nil)
Pokemon.install(nil)
local Runtime = { session = nil }
function Runtime.getSession() return Runtime.session end
package.loaded["src.core.game3.runtime"] = Runtime
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local State = require("src.core.game3.battle.state")
local Engine = require("src.core.game3.battle.engine")
local BagMenu = require("src.ui.game3.bag_menu")
local PartyMenu = require("src.ui.game3.party_menu")
local function input(key)
return { wasPressed = function(_, k) return k == key end }
end
local function press(key) Ui.handleInput(input(key)) end
local function check(ok, label)
assert(ok, "FAIL cursor2766 " .. version .. " " .. label)
print("PASS cursor2766 " .. version .. " " .. label)
end
local function mon(species, hp)
return { species = species, level = 20, hp = hp or 1000, maxHp = 1000,
moves = { 150 }, pp = { 40 } }
end
local function setup()
Battle.abort()
BagMenu.close()
PartyMenu.close()
local session = { version = version, name = "PLAYER", bag = Bag.new(),
dex = { seen = {}, owned = {} }, party = { mon(1, 5), mon(4), mon(7) } }
Bag.add(session.bag, 13, 3)
Bag.add(session.bag, 4, 3)
Runtime.session = session
assert(Battle.start({ session = session, headless = true, autoFight = false,
playerParty = session.party, foe = mon(150), wild = true }))
for _ = 1, 50 do
Battle.update(1 / 60, nil)
if Ui._mode == "menu" then break end
end
assert(Ui._mode == "menu")
return session, Battle.getState()
end
local function openBag()
press("right")
assert(Ui._menuIndex == 2)
press("a")
BagMenu.settle()
assert(BagMenu.open)
end
local function choose(item)
for pocket = 1, #ItemsData.BAG_POCKET_ORDER do
BagMenu.pocketIdx = pocket
for row, entry in ipairs(BagMenu.list(BagMenu.currentPocket())) do
if ItemsData.toNumericId(entry.id) == item then
BagMenu.cursor, BagMenu.scroll, BagMenu.mode = row, 0, "list"
BagMenu.handleInput(input("a"))
BagMenu.actionCursor = 1
BagMenu.handleInput(input("a"))
BagMenu.settle()
return
end
end
end
error("missing item " .. item)
end
local function nextTurn(st)
local turn = st.turn
Battle._headless = false
for _ = 1, 2000 do
Battle.update(1 / 60, { input = input(nil) })
if Battle._phase == "command" and Ui._mode == "menu" and st.turn > turn then
Battle._headless = true
return
end
assert(not st.over, "battle ended")
end
error("next turn unavailable " .. tostring(Battle._phase))
end
local session, st = setup()
openBag()
BagMenu.handleInput(input("b"))
BagMenu.settle()
check(Ui._mode == "menu" and Ui._menuIndex == 2, "Bag_cancel_retains_Bag")
session, st = setup()
openBag()
choose(13)
assert(PartyMenu.open)
PartyMenu.cursor = 1
for _ = 1, 100 do
if not PartyMenu.open then break end
PartyMenu.update(1 / 60)
PartyMenu.handleInput(input("a"))
end
BagMenu.settle()
assert(Ui._pendingCommand and Ui._pendingCommand.usedInMenu)
local old = st.player
nextTurn(st)
check(Bag.get(session.bag, 13) == 2 and st.player == old and Ui._menuIndex == 2,
"Potion_real_callback_next_turn_Bag")
press("a")
BagMenu.settle()
check(BagMenu.open and Ui._mode == "bag", "Potion_next_A_reopens_Bag")
BagMenu.close()
session, st = setup()
openBag()
choose(4)
assert(Ui._pendingCommand and Ui._pendingCommand.itemId == 4)
local Catching = require("src.core.game3.battle.catching")
local tryCatch = Catching.tryCatch
Catching.tryCatch = function() return false, 0 end
nextTurn(st)
Catching.tryCatch = tryCatch
check(Bag.get(session.bag, 4) == 2 and Ui._menuIndex == 2,
"failed_Ball_real_callback_next_turn_Bag")
press("a")
BagMenu.settle()
check(BagMenu.open and Ui._mode == "bag", "failed_Ball_next_A_reopens_Bag")
BagMenu.close()
press("left")
press("down")
press("a")
assert(PartyMenu.open)
for _ = 1, 60 do PartyMenu.update(1 / 60) end
PartyMenu.handleInput(input("b"))
check(Ui._mode == "menu" and Ui._menuIndex == 3, "Pokemon_cancel_retains_Pokemon")
old = st.player
assert(Engine.performSwitch(st, Battle._adapter, 0, 2, { reason = "switch" }))
Ui.openMenu()
check(st.player ~= old and Ui._menuIndex == 1, "real_single_switch_resets_Fight")
session, st = setup()
st.double = true
st.absent = {}
st.battlers = { [0] = st.player, [1] = st.enemy,
[2] = State.makeBattler(st.playerParty[2], "player", { partyIndex = 2, id = 2 }),
[3] = State.makeBattler(st.enemy.mon, "enemy", { partyIndex = 1, id = 3 }) }
Ui.openMenu(0)
openBag()
choose(4)
local command = Ui.takeCommand()
check(command and command.battler == 0, "double_Bag_callback_owns_slot0")
Ui.openMenu(2, { partnerAction = command })
press("down")
Ui.openMenu(0)
check(Ui._menuIndex == 2, "double_slot0_retains_Bag")
assert(Engine.performSwitch(st, Battle._adapter, 2, 3, { reason = "switch" }))
Ui.openMenu(2)
check(Ui._menuIndex == 1, "real_double_slot2_switch_resets_Fight")
Ui.openMenu(0)
check(Ui._menuIndex == 2, "double_slot2_switch_preserves_slot0_Bag")
assert(Engine.performSwitch(st, Battle._adapter, 0, 2, { reason = "switch" }))
Ui.openMenu(0)
check(Ui._menuIndex == 1, "real_double_slot0_switch_resets_Fight")
Ui.reset({ headless = true })
check(next(Ui._actionCursor) == nil and next(Ui._actionCursorBattler) == nil,
"new_battle_clears_cursor_ownership")
Battle.abort()
print("PASS cursor2766_cache_integration " .. version)
@@ -26,6 +26,22 @@ package.loaded["src.core.game3.field_effects"] = {
startRockSmash = function(_, _, _, done) calls.rock = calls.rock + 1; done() end,
startFlash = function(done) calls.flash = calls.flash + 1; done() end,
}
local scriptVm = { ctx = {} }
package.loaded["src.core.game3.scripting.space"] = {
vm = scriptVm,
scriptKey = function(name)
T.eq(name, "EventScript_UseFlash", "ordinary Emerald Flash resolves its profile ROM label")
return "fixture_flash"
end,
startScript = function(key)
T.eq(key, "fixture_flash", "ordinary Emerald Flash dispatches the resolved cached script")
T.check(scriptVm.ctx.fieldControlsLocked, "ordinary Emerald Flash gives the VM the field lock")
calls.flash, calls.flashScript = calls.flash + 1, key
scriptVm.ctx.fieldControlsLocked = nil
package.loaded["src.core.game3.field"].unlock()
return true
end,
}
local GameVersion = require("src.core.GameVersion")
GameVersion.set("emerald")
@@ -82,6 +98,8 @@ local function testCase(case)
T.eq(calls.puzzle, shouldOpen and 1 or 0, case.name .. " calls the door effect exactly when valid")
T.eq(calls.rock, not case.puzzle and not steel and 1 or 0, case.name .. " preserves normal Rock Smash")
T.eq(calls.flash, not case.puzzle and steel and 1 or 0, case.name .. " preserves normal Flash")
T.eq(calls.flashScript, not case.puzzle and steel and session.version == "emerald" and "fixture_flash" or nil,
case.name .. " dispatches the Emerald cached Flash script only for ordinary Emerald Flash")
T.eq(Field._locks.default, nil, case.name .. " releases only the move lock")
T.eq(Field.locked, case.namedLock == true, case.name .. " preserves named lock ownership")
if shouldOpen then
+2 -1
View File
@@ -223,8 +223,9 @@ local rc, curFrames = walkAndSettle("right")
eq(rc, "step", "the surfer paddles one cell east into the current")
eq(Player.cellX, 16, "the eastward current runs them to the corner")
eq(Player.cellY, 1, "the northward current then lifts them up the column")
-- pokefirered/src/field_player_avatar.c:509
-- pokefirered/src/event_object_movement.c:8925 sStepTimes
eq(curFrames, 16 + 10 * 6, "one paddle plus ten current pushes")
eq(curFrames, 8 + 10 * 6, "one paddle plus ten current pushes")
eq(ForcedMovement.forced, false, "leaving the current clears the forced flag")
print("[test] 8. a forced step rolls no encounter and burns no step counter")
+70
View File
@@ -0,0 +1,70 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local version = os.getenv("POKEPORT_VERSION") or "sapphire"
require("src.core.GameVersion").set(version)
require("src.import.gba.versions").select(version)
local Cache = require("tests.game3_cache")
local root = Cache.mount()
if not root then
print("[skip] flash2762 cache integration: " .. tostring(Cache.reason))
os.exit(0)
end
local Dataset = require("src.core.game3.dataset")
local Profile = require("src.core.game3.profile").of(version)
local Space = require("src.core.game3.scripting.space")
local Extract = require("src.import.gba.extract_scripts")
Space.bundle = Extract.loadBundle(Dataset.cache(), root, { allowIncomplete = true })
if Profile.field and Profile.field.flashScript then
assert(Space.installLabels(Space.bundle, Dataset.cache(), root))
end
local session = { version = version, flags = {}, vars = {} }
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
local showDone
package.loaded["src.core.game3.field_move_show_mon"] = { start = function(_, _, fn) showDone = fn end }
local Flags = require("src.core.game3.scripting.flags")
local Moves = require("src.core.game3.field_moves")
local Field = require("src.core.game3.field")
local View = require("src.core.game3.field_view")
local Fx = require("src.core.game3.field_effects")
local Text = require("src.core.game3.rom_text")
Space.store = Flags.newStore({ flags = session.flags })
session.flags = Space.store.flags
Space.vm = require("src.core.game3.scripting.vm").new({ scripts = Space.bundle.scripts, store = Space.store })
Field._session = session
Fx._cache = Dataset.cache()
local function check(ok, label)
assert(ok, "FAIL flash2762 " .. version .. " " .. label)
print("PASS flash2762 " .. version .. " " .. label)
end
Flags.setFlag(Space.store, nil, Moves.badgeFlag("FLASH"), true)
local ctx = { session = session, store = Space.store, isCave = true }
local payload = Moves.flashFromMenu(ctx)
check(payload.ok, "first_use_available")
View.setFlashLevel(4)
Fx._anims = {}
Field.executeFieldMove(payload)
showDone()
check(Field.locked and Flags.getFlag(Space.store, nil, payload.flag), "animation_locked_and_active")
for _ = 1, 600 do Fx.step(); Space.vm:tick() end
local rse = Profile.family == "rse"
check(View.getFlashLevel() == (rse and 1 or 0), "first_use_cart_level")
check(View.flashRadius() == (rse and 72 or nil), "first_use_cart_radius")
check(not Field.locked and not Space.vm:isRunning(), "animation_end_unlocks_field")
local key = (version == "ruby" or version == "sapphire") and "OtherText_CantUseThatHere" or "gText_InUseAlready_PM"
local expected = Text.ascii(key)
local res = Moves.flashFromMenu(ctx)
check(not res.ok and res.text == expected, "repeated_flash_exact_cached_prompt")
local game = { session = session, data = { maps = Dataset.buildMaps() } }
local floors = 0
for mapId, def in pairs(game.data.maps) do
if tonumber(def.cave) == 1 then
Field.clearTempFieldEventData(game, mapId)
View.setDefaultFlashLevel(game, mapId)
check(Flags.getFlag(Space.store, nil, payload.flag) and View.getFlashLevel() == (rse and 1 or 0),
"dark_floor_persists_" .. mapId)
floors = floors + 1
if floors == 2 then break end
end
end
check(floors == 2, "two_actual_cached_dark_floors")
print("PASS flash2762_cache_integration " .. version)
+10
View File
@@ -113,6 +113,10 @@ tests/engine/fly_map_arrows_bug1892.lua
tests/engine/formulas.lua
tests/engine/frame_cap_display.lua
tests/engine/frame_cap_legacy_loop_2106.lua
tests/engine/game3_avatar_restore_2759_2761.lua
tests/engine/game3_battle_action_cursor_2766.lua
tests/engine/game3_cutscene_push_2764.lua
tests/engine/game3_disguise_order_2763.lua
tests/engine/game3_em_bike_doors_2637.lua
tests/engine/game3_em_catch_prompt_2637.lua
tests/engine/game3_frontier_obedience_2638.lua
@@ -120,11 +124,14 @@ tests/engine/game3_post_dex_background_2637.lua
tests/engine/game3_rs_anim_names_test.lua
tests/engine/game3_rs_bag_cursor_width_2709.lua
tests/engine/game3_rs_bag_row_layout_2727.lua
tests/engine/game3_rs_flash_2762.lua
tests/engine/game3_rs_item_use_text_2699_test.lua
tests/engine/game3_rs_starter_choose_scale_2703.lua
tests/engine/game3_rs_summary_memo_location_2707.lua
tests/engine/game3_speed_hotkey_test.lua
tests/engine/game3_surf_speed_2767.lua
tests/engine/game3_tent_save_reload_2638.lua
tests/engine/game3_tilt_roof_2765.lua
tests/engine/game3_wailmer_pail_2697_test.lua
tests/engine/game_display_chord_test.lua
tests/engine/game_speed_categories_test.lua
@@ -512,6 +519,7 @@ tests/engine/npc_stay_turns_bug2117.lua
tests/engine/npc_walk_cadence_bug1303.lua
tests/engine/nurse_bow_bug995.lua
tests/engine/nx_display_test.lua
tests/engine/nx_quit_policy_2720.lua
tests/engine/nx_yellow_boot_test.lua
tests/engine/oak_intro_cry_bug2481.lua
tests/engine/oak_shrink_timing_2685.lua
@@ -754,6 +762,7 @@ tests/engine/yellow_oak_back_pikapic.lua
tests/engine/yellow_viridian_old_man_bug2122.lua
tests/engine/zero_damage_miss_ruleset_bug2153.lua
tests/game3_back_pic_anims_2702_test.lua
tests/game3_battle_action_cursor_2766_test.lua
tests/game3_battle_anims_phase1_test.lua
tests/game3_battle_rendering_test.lua
tests/game3_battle_shiny_test.lua
@@ -810,6 +819,7 @@ tests/game3_quest_log_integration_test.lua
tests/game3_reflection_neighbor_test.lua
tests/game3_renewable_hidden_items_test.lua
tests/game3_resident_caches_2695_test.lua
tests/game3_rs_flash_2762_test.lua
tests/game3_rs_naming_kb_crop_2701_test.lua
tests/game3_rs_pokedex_palette_2710_test.lua
tests/game3_runtime_camera_object_test.lua