From c819846a3d3e46e5011d18cebc1bb85503480bff Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Thu, 8 Oct 2026 10:20:26 -0400 Subject: [PATCH] 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 --- main.lua | 6 +- src/core/Game3.lua | 9 +- src/core/game3/battle/ui.lua | 16 +- src/core/game3/collision.lua | 4 +- src/core/game3/display.lua | 5 +- src/core/game3/field.lua | 17 +- src/core/game3/field_effects_rse.lua | 5 +- src/core/game3/field_view.lua | 10 + src/core/game3/map.lua | 54 +++- src/core/game3/objects.lua | 10 +- src/core/game3/player.lua | 54 +++- src/core/game3/profiles/emerald/field.lua | 2 + src/core/game3/profiles/rs/field.lua | 2 + src/core/game3/profiles/rs/ui.lua | 2 + src/core/game3/runtime.lua | 19 +- src/core/game3/save_schema_firered.lua | 20 +- src/core/game3/scripting/movement.lua | 18 +- src/render/Renderer.lua | 54 ++++ .../drivers/emerald_cutscene_push_bug2764.lua | 111 ++++++++ .../game3_avatar_restore_2759_2761.lua | 132 ++++++++++ .../game3_battle_action_cursor_2766.lua | 171 +++++++++++++ tests/drivers/game3_disguise_order_2763.lua | 84 ++++++ tests/drivers/game3_mods_restart_bug2691.lua | 4 + tests/drivers/game3_rs_flash_2762.lua | 110 ++++++++ tests/drivers/game3_surf_speed_2767.lua | 78 ++++++ tests/drivers/game3_tilt_roof_2765.lua | 239 ++++++++++++++++++ .../engine/game3_avatar_restore_2759_2761.lua | 208 +++++++++++++++ .../game3_battle_action_cursor_2766.lua | 56 ++++ tests/engine/game3_cutscene_push_2764.lua | 128 ++++++++++ tests/engine/game3_disguise_order_2763.lua | 147 +++++++++++ tests/engine/game3_rs_flash_2762.lua | 98 +++++++ tests/engine/game3_surf_speed_2767.lua | 85 +++++++ tests/engine/game3_tilt_roof_2765.lua | 145 +++++++++++ tests/engine/nx_quit_policy_2720.lua | 123 +++++++++ tests/engine/quit_thread_shutdown.lua | 8 +- .../game3_battle_action_cursor_2766_test.lua | 176 +++++++++++++ tests/game3_braille_regi_field_moves_test.lua | 18 ++ tests/game3_field_forced_movement_test.lua | 3 +- tests/game3_rs_flash_2762_test.lua | 70 +++++ tests/quick.list | 10 + 40 files changed, 2465 insertions(+), 46 deletions(-) create mode 100644 tests/drivers/emerald_cutscene_push_bug2764.lua create mode 100644 tests/drivers/game3_avatar_restore_2759_2761.lua create mode 100644 tests/drivers/game3_battle_action_cursor_2766.lua create mode 100644 tests/drivers/game3_disguise_order_2763.lua create mode 100644 tests/drivers/game3_rs_flash_2762.lua create mode 100644 tests/drivers/game3_surf_speed_2767.lua create mode 100644 tests/drivers/game3_tilt_roof_2765.lua create mode 100644 tests/engine/game3_avatar_restore_2759_2761.lua create mode 100644 tests/engine/game3_battle_action_cursor_2766.lua create mode 100644 tests/engine/game3_cutscene_push_2764.lua create mode 100644 tests/engine/game3_disguise_order_2763.lua create mode 100644 tests/engine/game3_rs_flash_2762.lua create mode 100644 tests/engine/game3_surf_speed_2767.lua create mode 100644 tests/engine/game3_tilt_roof_2765.lua create mode 100644 tests/engine/nx_quit_policy_2720.lua create mode 100644 tests/game3_battle_action_cursor_2766_test.lua create mode 100644 tests/game3_rs_flash_2762_test.lua diff --git a/main.lua b/main.lua index 26bf11eb..ff8d0187 100644 --- a/main.lua +++ b/main.lua @@ -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 diff --git a/src/core/Game3.lua b/src/core/Game3.lua index fce8f2e7..13339248 100644 --- a/src/core/Game3.lua +++ b/src/core/Game3.lua @@ -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 diff --git a/src/core/game3/battle/ui.lua b/src/core/game3/battle/ui.lua index cd8cd4f1..d8cc2512 100644 --- a/src/core/game3/battle/ui.lua +++ b/src/core/game3/battle/ui.lua @@ -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) diff --git a/src/core/game3/collision.lua b/src/core/game3/collision.lua index 2f39a178..c8ba5731 100644 --- a/src/core/game3/collision.lua +++ b/src/core/game3/collision.lua @@ -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 diff --git a/src/core/game3/display.lua b/src/core/game3/display.lua index 956b26fe..7cf1d80d 100644 --- a/src/core/game3/display.lua +++ b/src/core/game3/display.lua @@ -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 }) diff --git a/src/core/game3/field.lua b/src/core/game3/field.lua index cc93c67d..e80598e5 100644 --- a/src/core/game3/field.lua +++ b/src/core/game3/field.lua @@ -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 diff --git a/src/core/game3/field_effects_rse.lua b/src/core/game3/field_effects_rse.lua index 4d80f3fb..4ec0336e 100644 --- a/src/core/game3/field_effects_rse.lua +++ b/src/core/game3/field_effects_rse.lua @@ -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 diff --git a/src/core/game3/field_view.lua b/src/core/game3/field_view.lua index ce6823a4..71526fbb 100644 --- a/src/core/game3/field_view.lua +++ b/src/core/game3/field_view.lua @@ -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). diff --git a/src/core/game3/map.lua b/src/core/game3/map.lua index 6b9d273f..c9cd53a6 100644 --- a/src/core/game3/map.lua +++ b/src/core/game3/map.lua @@ -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 diff --git a/src/core/game3/objects.lua b/src/core/game3/objects.lua index 177641b1..8b9bcf9d 100644 --- a/src/core/game3/objects.lua +++ b/src/core/game3/objects.lua @@ -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 diff --git a/src/core/game3/player.lua b/src/core/game3/player.lua index 763939f8..90c94df4 100644 --- a/src/core/game3/player.lua +++ b/src/core/game3/player.lua @@ -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"] diff --git a/src/core/game3/profiles/emerald/field.lua b/src/core/game3/profiles/emerald/field.lua index 5e384a81..5b95be22 100644 --- a/src/core/game3/profiles/emerald/field.lua +++ b/src/core/game3/profiles/emerald/field.lua @@ -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", diff --git a/src/core/game3/profiles/rs/field.lua b/src/core/game3/profiles/rs/field.lua index 82e83681..8eec44a6 100644 --- a/src/core/game3/profiles/rs/field.lua +++ b/src/core/game3/profiles/rs/field.lua @@ -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", diff --git a/src/core/game3/profiles/rs/ui.lua b/src/core/game3/profiles/rs/ui.lua index ed0bb382..9af48b98 100644 --- a/src/core/game3/profiles/rs/ui.lua +++ b/src/core/game3/profiles/rs/ui.lua @@ -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 diff --git a/src/core/game3/runtime.lua b/src/core/game3/runtime.lua index 61fd9471..2cfdad74 100644 --- a/src/core/game3/runtime.lua +++ b/src/core/game3/runtime.lua @@ -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)) diff --git a/src/core/game3/save_schema_firered.lua b/src/core/game3/save_schema_firered.lua index eb5c5a7e..c8fbfe8e 100644 --- a/src/core/game3/save_schema_firered.lua +++ b/src/core/game3/save_schema_firered.lua @@ -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) diff --git a/src/core/game3/scripting/movement.lua b/src/core/game3/scripting/movement.lua index f42f3313..4cdc9acc 100644 --- a/src/core/game3/scripting/movement.lua +++ b/src/core/game3/scripting/movement.lua @@ -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 diff --git a/src/render/Renderer.lua b/src/render/Renderer.lua index 46890945..5d71a8fb 100644 --- a/src/render/Renderer.lua +++ b/src/render/Renderer.lua @@ -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 diff --git a/tests/drivers/emerald_cutscene_push_bug2764.lua b/tests/drivers/emerald_cutscene_push_bug2764.lua new file mode 100644 index 00000000..66669b66 --- /dev/null +++ b/tests/drivers/emerald_cutscene_push_bug2764.lua @@ -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 diff --git a/tests/drivers/game3_avatar_restore_2759_2761.lua b/tests/drivers/game3_avatar_restore_2759_2761.lua new file mode 100644 index 00000000..68a3d656 --- /dev/null +++ b/tests/drivers/game3_avatar_restore_2759_2761.lua @@ -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 diff --git a/tests/drivers/game3_battle_action_cursor_2766.lua b/tests/drivers/game3_battle_action_cursor_2766.lua new file mode 100644 index 00000000..79941aa6 --- /dev/null +++ b/tests/drivers/game3_battle_action_cursor_2766.lua @@ -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 diff --git a/tests/drivers/game3_disguise_order_2763.lua b/tests/drivers/game3_disguise_order_2763.lua new file mode 100644 index 00000000..21100fcf --- /dev/null +++ b/tests/drivers/game3_disguise_order_2763.lua @@ -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 diff --git a/tests/drivers/game3_mods_restart_bug2691.lua b/tests/drivers/game3_mods_restart_bug2691.lua index b26a5d87..de22514d 100644 --- a/tests/drivers/game3_mods_restart_bug2691.lua +++ b/tests/drivers/game3_mods_restart_bug2691.lua @@ -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 diff --git a/tests/drivers/game3_rs_flash_2762.lua b/tests/drivers/game3_rs_flash_2762.lua new file mode 100644 index 00000000..cc4f2a1f --- /dev/null +++ b/tests/drivers/game3_rs_flash_2762.lua @@ -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 diff --git a/tests/drivers/game3_surf_speed_2767.lua b/tests/drivers/game3_surf_speed_2767.lua new file mode 100644 index 00000000..31b8804e --- /dev/null +++ b/tests/drivers/game3_surf_speed_2767.lua @@ -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 diff --git a/tests/drivers/game3_tilt_roof_2765.lua b/tests/drivers/game3_tilt_roof_2765.lua new file mode 100644 index 00000000..16f7e999 --- /dev/null +++ b/tests/drivers/game3_tilt_roof_2765.lua @@ -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 diff --git a/tests/engine/game3_avatar_restore_2759_2761.lua b/tests/engine/game3_avatar_restore_2759_2761.lua new file mode 100644 index 00000000..7be602bc --- /dev/null +++ b/tests/engine/game3_avatar_restore_2759_2761.lua @@ -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") diff --git a/tests/engine/game3_battle_action_cursor_2766.lua b/tests/engine/game3_battle_action_cursor_2766.lua new file mode 100644 index 00000000..1db5e3af --- /dev/null +++ b/tests/engine/game3_battle_action_cursor_2766.lua @@ -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") + diff --git a/tests/engine/game3_cutscene_push_2764.lua b/tests/engine/game3_cutscene_push_2764.lua new file mode 100644 index 00000000..c21e8948 --- /dev/null +++ b/tests/engine/game3_cutscene_push_2764.lua @@ -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") diff --git a/tests/engine/game3_disguise_order_2763.lua b/tests/engine/game3_disguise_order_2763.lua new file mode 100644 index 00000000..305c0d65 --- /dev/null +++ b/tests/engine/game3_disguise_order_2763.lua @@ -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") diff --git a/tests/engine/game3_rs_flash_2762.lua b/tests/engine/game3_rs_flash_2762.lua new file mode 100644 index 00000000..bd76b185 --- /dev/null +++ b/tests/engine/game3_rs_flash_2762.lua @@ -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") diff --git a/tests/engine/game3_surf_speed_2767.lua b/tests/engine/game3_surf_speed_2767.lua new file mode 100644 index 00000000..0227d12c --- /dev/null +++ b/tests/engine/game3_surf_speed_2767.lua @@ -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") diff --git a/tests/engine/game3_tilt_roof_2765.lua b/tests/engine/game3_tilt_roof_2765.lua new file mode 100644 index 00000000..6525f0fb --- /dev/null +++ b/tests/engine/game3_tilt_roof_2765.lua @@ -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") diff --git a/tests/engine/nx_quit_policy_2720.lua b/tests/engine/nx_quit_policy_2720.lua new file mode 100644 index 00000000..cda6c489 --- /dev/null +++ b/tests/engine/nx_quit_policy_2720.lua @@ -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") diff --git a/tests/engine/quit_thread_shutdown.lua b/tests/engine/quit_thread_shutdown.lua index 3f0a2bf6..7757f116 100644 --- a/tests/engine/quit_thread_shutdown.lua +++ b/tests/engine/quit_thread_shutdown.lua @@ -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) diff --git a/tests/game3_battle_action_cursor_2766_test.lua b/tests/game3_battle_action_cursor_2766_test.lua new file mode 100644 index 00000000..2984b469 --- /dev/null +++ b/tests/game3_battle_action_cursor_2766_test.lua @@ -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) diff --git a/tests/game3_braille_regi_field_moves_test.lua b/tests/game3_braille_regi_field_moves_test.lua index 1507368e..0cd0cd53 100644 --- a/tests/game3_braille_regi_field_moves_test.lua +++ b/tests/game3_braille_regi_field_moves_test.lua @@ -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 diff --git a/tests/game3_field_forced_movement_test.lua b/tests/game3_field_forced_movement_test.lua index b56ef67f..5b419196 100644 --- a/tests/game3_field_forced_movement_test.lua +++ b/tests/game3_field_forced_movement_test.lua @@ -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") diff --git a/tests/game3_rs_flash_2762_test.lua b/tests/game3_rs_flash_2762_test.lua new file mode 100644 index 00000000..4ca9f3ee --- /dev/null +++ b/tests/game3_rs_flash_2762_test.lua @@ -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) diff --git a/tests/quick.list b/tests/quick.list index e44817a3..7af6af43 100644 --- a/tests/quick.list +++ b/tests/quick.list @@ -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