diff --git a/src/core/Game.lua b/src/core/Game.lua index 8addc853..8c35cb42 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -545,22 +545,11 @@ function Game.wideBattleInStack(stack) return nil end --- Which of "battle"/"overworld"/"menu" per-category GAME SPEED (RFC 0007) --- applies right now. Whole-stack, the same idiom as fillScaleInStack/ --- wideBattleInStack above: an overlay with neither marker (PartyMenu, --- ChoiceBox, a NamingScreen, a text box) is transparent to the walk and --- inherits whatever is under it, making the category a property of the --- STACK POSITION the overlay sits over, not of the overlay itself. A --- scripted sequence (script.started/ended) never pushes a state of its --- own either -- it runs through the owning overworld/battle state's own --- script runner or message queue -- so it inherits the same way. Nothing --- identifying as either (the title screen, credits, an intro cutscene --- with nothing under it) falls to "menu", the bucket every non-gameplay --- screen gets; see the RFC's Decisions section for the full reasoning. function Game.speedCategoryInStack(stack) local states = stack and stack.states for i = #(states or {}), 1, -1 do local state = states[i] + if state and state.isMenu then return "menu" end if state and state.isBattle then return "battle" end if state and state.isOverworld then return "overworld" end end diff --git a/src/core/Game2.lua b/src/core/Game2.lua index 1aaf118a..a4a151d9 100644 --- a/src/core/Game2.lua +++ b/src/core/Game2.lua @@ -2462,6 +2462,7 @@ function Game2:applyOptions() hotbar = options.hotbar, }) require("src.core.VideoMode").applyOptions(options) + require("src.core.Orientation").applyOptions(options) require("src.core.FaithfulRes").applyOptions(options) require("src.core.ScreenPosition").applyOptions(options) require("src.core.VSync").applyOptions(options) diff --git a/src/core/Game3.lua b/src/core/Game3.lua index cf65a78a..3efbcef2 100644 --- a/src/core/Game3.lua +++ b/src/core/Game3.lua @@ -604,6 +604,10 @@ function Game3:fixedUpdate(dt) end function Game3:speedCategory() + local Stack = lazyReq("src.ui.game3.stack") + for i = #(Stack._layers or {}), 1, -1 do + if Stack._layers[i].isMenu then return "menu" end + end local okB, Battle = pcall(lazyReq, "src.core.game3.battle") if okB and Battle and Battle.isActive and Battle.isActive() then return "battle" diff --git a/src/core/game3/collision.lua b/src/core/game3/collision.lua index f2e13f26..22be1cca 100644 --- a/src/core/game3/collision.lua +++ b/src/core/game3/collision.lua @@ -976,6 +976,13 @@ function Collision.isWater(cx, cy) return Collision.isWaterOn(Collision._mapDef, cx, cy, Collision.cell(cx, cy)) end +-- pokeemerald/src/field_player_avatar.c:693 +-- pokefirered/src/field_player_avatar.c:568 +function Collision.isSurfDismount(cx, cy, elevation) + return Collision.elevationAt(cx, cy) == 3 + and Collision.elevationMismatchOn(Collision._mapDef, elevation, cx, cy) +end + -- pokeemerald/src/rotating_gate.c:961 function Collision.rotatingGateCollision(game, dir, x, y) local RG = package.loaded["src.core.game3.rotating_gate"] diff --git a/src/core/game3/player.lua b/src/core/game3/player.lua index 4e5a8382..5c380111 100644 --- a/src/core/game3/player.lua +++ b/src/core/game3/player.lua @@ -604,7 +604,7 @@ function Player.tryMove(dir, game, run) return "blocked", "acro" end local isDismount = Player.surfing and not Player.underwater - and (not (Collision.isWater and Collision.isWater(tx, ty))) + and Collision.isSurfDismount(tx, ty, Player.currentElevation) if isDismount then Player.dismounting = true lazyReq("src.core.game3.audio").stopSurfMusic() @@ -747,8 +747,8 @@ function Player.forcedStep(dir, frames, opts) beginStep(opts.ledgeX, opts.ledgeY, false, true) else local tx, ty = Player.cellX + d[1], Player.cellY + d[2] - Player.dismounting = Player.surfing - and (not (Collision.isWater and Collision.isWater(tx, ty))) or false + Player.dismounting = Player.surfing and not Player.underwater + and Collision.isSurfDismount(tx, ty, Player.currentElevation) or false -- pokefirered/src/field_player_avatar.c:1609 if Player.dismounting then lazyReq("src.core.game3.audio").stopSurfMusic() end beginStep(tx, ty, false, false) @@ -931,11 +931,6 @@ local function finishStep(game) elseif Player.dismounting then Player.dismounting = false Player.surfing = false - elseif Player.surfing then - local onWater = Collision.isWater and Collision.isWater(Player.cellX, Player.cellY) - if not onWater then - Player.surfing = false - end end local session = package.loaded["src.core.game3.runtime"] diff --git a/src/core/gen2/Save.lua b/src/core/gen2/Save.lua index e8378b12..cff36494 100644 --- a/src/core/gen2/Save.lua +++ b/src/core/gen2/Save.lua @@ -353,6 +353,7 @@ Save.DEFAULT_OPTIONS = { haptics = "light", touchControls = { enabled = true }, screenPos = "center", + orientation = "auto", } function Save.defaultOptions() @@ -374,7 +375,7 @@ Save.OPTIONS_KEY = "gold" local SHARED_KEYS = { touchControls = true, haptics = true, screenPos = true, - videoMode = true, faithfulRes = true, + videoMode = true, faithfulRes = true, orientation = true, mods = true, modsByVersion = true, modsGen2 = true, modOptions = true, modProfiles = true, modProfilesSeeded = true, activeProfile = true, @@ -400,6 +401,7 @@ function Save.loadOptions(fs) end end end + options.orientation = require("src.core.Orientation").normalize(options.orientation) return options end @@ -413,7 +415,8 @@ function Save.saveOptions(options, fs) local block = {} for key, value in pairs(options) do if SHARED_KEYS[key] then - file[key] = value + file[key] = key == "orientation" + and require("src.core.Orientation").normalize(value) or value else block[key] = value end diff --git a/src/import/LauncherSettings.lua b/src/import/LauncherSettings.lua index 08696287..a6ec4c79 100644 --- a/src/import/LauncherSettings.lua +++ b/src/import/LauncherSettings.lua @@ -146,6 +146,19 @@ local function addTouchRows(rows, add, opts, hooks) end end +local function addOrientationRow(add, opts) + local okOr, Orientation = pcall(require, "src.core.Orientation") + if okOr and (Orientation.isAndroid() or Orientation.isIOS()) then + add(Strings("ORIENTATION"), + function() return Strings(Orientation.modeLabel(opts.orientation)) end, + function(dir) + opts.orientation = Orientation.cycle(opts.orientation, dir) + Orientation.apply(opts.orientation) + return true + end) + end +end + local function coreRows(opts, hooks) local rows = {} local function add(label, value, step) @@ -295,21 +308,7 @@ local function coreRows(opts, hooks) end) end - -- ORIENTATION (#592, #1638): mobile only. Unlike the other launcher rows - -- this one live-applies: the window exists here too, and rotating under - -- the player's finger is the only feedback that reads. - do - local okOr, Orientation = pcall(require, "src.core.Orientation") - if okOr and (Orientation.isAndroid() or Orientation.isIOS()) then - add(Strings("ORIENTATION"), - function() return Strings(Orientation.modeLabel(opts.orientation)) end, - function(dir) - opts.orientation = Orientation.cycle(opts.orientation, dir) - Orientation.apply(opts.orientation) - return true - end) - end - end + addOrientationRow(add, opts) local okFr, FaithfulRes = pcall(require, "src.core.FaithfulRes") if okFr then @@ -738,6 +737,7 @@ local function gen2Rows(opts, hooks, shared) end) addTouchRows(rows, add, shared, hooks) + addOrientationRow(add, shared) return rows end diff --git a/src/ui/BagMenu.lua b/src/ui/BagMenu.lua index f2321bec..6194d39a 100644 --- a/src/ui/BagMenu.lua +++ b/src/ui/BagMenu.lua @@ -8,7 +8,7 @@ local Runtime = require("src.mods.Runtime") local TextBox = require("src.render.TextBox") local romText = require("src.core.RomText") -local BagMenu = {} +local BagMenu = { isMenu = true } local Bag = require("src.inventory.Bag") local Strings = require("src.core.Strings") @@ -735,6 +735,7 @@ function BagMenu.new(game, opts) -- the item box overlaps the kept-open START menu box, so neither docks to -- a screen edge on its own (start_sub_menus.asm:302-329) #1745 list.holdsUIAnchors = true + list.isMenu = true return list end diff --git a/src/ui/BoxMenu.lua b/src/ui/BoxMenu.lua index 4f5b8748..c0e6ac43 100644 --- a/src/ui/BoxMenu.lua +++ b/src/ui/BoxMenu.lua @@ -11,7 +11,7 @@ local Stats = require("src.pokemon.Stats") local TextBox = require("src.render.TextBox") local Strings = require("src.core.Strings") -local BoxMenu = {} +local BoxMenu = { isMenu = true } -- engine/pokemon/bills_pc.asm:118 -- vChars2 $78; engine/menus/save.asm:497 @@ -396,6 +396,7 @@ function BoxMenu.new(game) baseDraw(self) drawChrome(game) end + menu.isMenu = true return menu end diff --git a/src/ui/DexEntryMenu.lua b/src/ui/DexEntryMenu.lua index 5671366e..3a3a3990 100644 --- a/src/ui/DexEntryMenu.lua +++ b/src/ui/DexEntryMenu.lua @@ -16,7 +16,7 @@ local Font = require("src.render.Font") local Strings = require("src.core.Strings") local Theme = require("src.ui.Theme") -local DexEntryMenu = {} +local DexEntryMenu = { isMenu = true } DexEntryMenu.__index = DexEntryMenu DexEntryMenu.isOpaque = true diff --git a/src/ui/FlyMenu.lua b/src/ui/FlyMenu.lua index c83e3276..33afac7b 100644 --- a/src/ui/FlyMenu.lua +++ b/src/ui/FlyMenu.lua @@ -5,7 +5,7 @@ local ListMenu = require("src.ui.ListMenu") local Map = require("src.world.Map") local Strings = require("src.core.Strings") -local FlyMenu = {} +local FlyMenu = { isMenu = true } function FlyMenu.new(game) local items = {} @@ -25,12 +25,14 @@ function FlyMenu.new(game) }) end end - return ListMenu.new(game, Strings.source("FLY TO?"), items, { + local menu = ListMenu.new(game, Strings.source("FLY TO?"), items, { onChoose = function(item, list) list:close() game.overworld:flyTo(item.value) end, }) + menu.isMenu = true + return menu end return FlyMenu diff --git a/src/ui/LeaguePC.lua b/src/ui/LeaguePC.lua index 250cce8b..f5c5413e 100644 --- a/src/ui/LeaguePC.lua +++ b/src/ui/LeaguePC.lua @@ -4,7 +4,7 @@ local Font = require("src.render.Font") local Strings = require("src.core.Strings") local HallOfFame = require("src.ui.HallOfFame") -local LeaguePC = {} +local LeaguePC = { isMenu = true } LeaguePC.__index = LeaguePC LeaguePC.isOpaque = true diff --git a/src/ui/NamingScreen.lua b/src/ui/NamingScreen.lua index 4ef017a6..d9a266be 100644 --- a/src/ui/NamingScreen.lua +++ b/src/ui/NamingScreen.lua @@ -17,7 +17,7 @@ local Sound = require("src.core.Sound") local Theme = require("src.ui.Theme") local Strings = require("src.core.Strings") -local NamingScreen = {} +local NamingScreen = { isMenu = true } NamingScreen.__index = NamingScreen NamingScreen.isOpaque = true diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index 6cbd98d9..7857f9dd 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -34,7 +34,7 @@ local OptionRows = require("src.ui.OptionRows") local Renderer = require("src.render.Renderer") local Strings = require("src.core.Strings") -local OptionsMenu = {} +local OptionsMenu = { isMenu = true } OptionsMenu.__index = OptionsMenu OptionsMenu.isOpaque = true diff --git a/src/ui/PartyMenu.lua b/src/ui/PartyMenu.lua index 91453247..bd29ae2e 100644 --- a/src/ui/PartyMenu.lua +++ b/src/ui/PartyMenu.lua @@ -21,7 +21,7 @@ local Map = require("src.world.Map") local Strings = require("src.core.Strings") local Status = require("src.battle.Status") -local PartyMenu = {} +local PartyMenu = { isMenu = true } PartyMenu.__index = PartyMenu PartyMenu.isOpaque = true diff --git a/src/ui/PlayerPC.lua b/src/ui/PlayerPC.lua index b8642544..0b76b2f1 100644 --- a/src/ui/PlayerPC.lua +++ b/src/ui/PlayerPC.lua @@ -16,7 +16,7 @@ local romText = require("src.core.RomText") local Font = require("src.render.Font") local Theme = require("src.ui.Theme") -local PlayerPC = {} +local PlayerPC = { isMenu = true } local function itemName(game, id) local def = game.data.items[id] @@ -308,6 +308,7 @@ function PlayerPC.new(game, opts) end love.graphics.setColor(1, 1, 1, 1) end + menu.isMenu = true return menu end diff --git a/src/ui/PokedexMenu.lua b/src/ui/PokedexMenu.lua index 9c3dab80..81517fa5 100644 --- a/src/ui/PokedexMenu.lua +++ b/src/ui/PokedexMenu.lua @@ -6,7 +6,7 @@ local Strings = require("src.core.Strings") local Theme = require("src.ui.Theme") local MenuRepeat = require("src.ui.MenuRepeat") -local PokedexMenu = {} +local PokedexMenu = { isMenu = true } PokedexMenu.__index = PokedexMenu PokedexMenu.isOpaque = true diff --git a/src/ui/PrizeCounter.lua b/src/ui/PrizeCounter.lua index 6e116b82..364248f6 100644 --- a/src/ui/PrizeCounter.lua +++ b/src/ui/PrizeCounter.lua @@ -4,7 +4,7 @@ local Font = require("src.render.Font") local Strings = require("src.core.Strings") local Theme = require("src.ui.Theme") -local PrizeCounter = {} +local PrizeCounter = { isMenu = true } PrizeCounter.__index = PrizeCounter local NAME_X, TOP_Y, ROW_STEP = 16, 32, 16 diff --git a/src/ui/Screens.lua b/src/ui/Screens.lua index 9270d2de..4523bc21 100644 --- a/src/ui/Screens.lua +++ b/src/ui/Screens.lua @@ -172,6 +172,7 @@ local function build(game, id, ...) inst = factory.new(game, ...) end inst.screenId = inst.screenId or id + if factory.isMenu and inst.isMenu == nil then inst.isMenu = true end -- Standardized opt-in marker for mod-created options/settings screens. -- A mod may declare `isModOptions = true` on its screen factory table or -- on the returned instance. Either way the flag is propagated so that other diff --git a/src/ui/ShopMenu.lua b/src/ui/ShopMenu.lua index fdafa897..c4bbeaa8 100644 --- a/src/ui/ShopMenu.lua +++ b/src/ui/ShopMenu.lua @@ -16,7 +16,7 @@ local Strings = require("src.core.Strings") local TextBox = require("src.render.TextBox") local romText = require("src.core.RomText") -local ShopMenu = {} +local ShopMenu = { isMenu = true } local function txt(game, key, fallback) return game.data.text[key] or fallback @@ -343,6 +343,7 @@ function ShopMenu.new(game, stock, onQuit) drawClerk(self) Menu.draw(self) end + menu.isMenu = true return menu end diff --git a/src/ui/StartMenu.lua b/src/ui/StartMenu.lua index 27ca32ae..bf08a1eb 100644 --- a/src/ui/StartMenu.lua +++ b/src/ui/StartMenu.lua @@ -13,7 +13,7 @@ local Screens = require("src.ui.Screens") local Strings = require("src.core.Strings") local Theme = require("src.ui.Theme") -local StartMenu = {} +local StartMenu = { isMenu = true } local function sameItems(_, items) return items end @@ -229,6 +229,7 @@ function StartMenu.new(game) love.graphics.setColor(1, 1, 1, 1) end end + menu.isMenu = true return menu end diff --git a/src/ui/SummaryMenu.lua b/src/ui/SummaryMenu.lua index 3d7ca84a..9ed0b99a 100644 --- a/src/ui/SummaryMenu.lua +++ b/src/ui/SummaryMenu.lua @@ -39,7 +39,7 @@ do end end -local SummaryMenu = {} +local SummaryMenu = { isMenu = true } SummaryMenu.__index = SummaryMenu SummaryMenu.isOpaque = true diff --git a/src/ui/TownMap.lua b/src/ui/TownMap.lua index b1164ab0..c84f6d4b 100644 --- a/src/ui/TownMap.lua +++ b/src/ui/TownMap.lua @@ -21,7 +21,7 @@ local SpriteRenderer = require("src.render.SpriteRenderer") local Strings = require("src.core.Strings") local Theme = require("src.ui.Theme") -local TownMap = {} +local TownMap = { isMenu = true } TownMap.__index = TownMap TownMap.isOpaque = true diff --git a/src/ui/TrainerCard.lua b/src/ui/TrainerCard.lua index 26534a20..06b2f14c 100644 --- a/src/ui/TrainerCard.lua +++ b/src/ui/TrainerCard.lua @@ -9,7 +9,7 @@ local Badges = require("src.inventory.Badges") local Font = require("src.render.Font") local Strings = require("src.core.Strings") -local TrainerCard = {} +local TrainerCard = { isMenu = true } TrainerCard.__index = TrainerCard TrainerCard.isOpaque = true diff --git a/src/ui/game3/bag_menu.lua b/src/ui/game3/bag_menu.lua index 4acc10b4..0579d218 100644 --- a/src/ui/game3/bag_menu.lua +++ b/src/ui/game3/bag_menu.lua @@ -13,7 +13,7 @@ local PartyView = require("src.core.game3.battle.party_view") local RomText = require("src.core.game3.rom_text") local TextIR = require("src.core.game3.scripting.text_ir") -local BagMenu = {} +local BagMenu = { isMenu = true } BagMenu.open = false BagMenu.cursor = 1 diff --git a/src/ui/game3/berry_pouch.lua b/src/ui/game3/berry_pouch.lua index 77fc99a9..6bb65a28 100644 --- a/src/ui/game3/berry_pouch.lua +++ b/src/ui/game3/berry_pouch.lua @@ -25,7 +25,7 @@ local ItemUse = require("src.core.game3.item_use") local PartyView = require("src.core.game3.battle.party_view") local RomText = require("src.core.game3.rom_text") -local BerryPouch = {} +local BerryPouch = { isMenu = true } BerryPouch.open = false BerryPouch.cursor = 1 diff --git a/src/ui/game3/box_storage_ui.lua b/src/ui/game3/box_storage_ui.lua index 2941e920..9ff09916 100644 --- a/src/ui/game3/box_storage_ui.lua +++ b/src/ui/game3/box_storage_ui.lua @@ -19,7 +19,7 @@ local SummaryMenu = require("src.ui.game3.summary_menu") local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") -local BoxStorageUI = {} +local BoxStorageUI = { isMenu = true } -- pokefirered/src/pokemon_storage_system_data.c:2027 local MENU_TEXT = { diff --git a/src/ui/game3/controls_menu.lua b/src/ui/game3/controls_menu.lua index ac9ccf9a..3ceb340b 100644 --- a/src/ui/game3/controls_menu.lua +++ b/src/ui/game3/controls_menu.lua @@ -6,7 +6,7 @@ local Strings = require("src.core.Strings") local BindingsMenu = require("src.ui.BindingsMenu") local ChoiceBox = require("src.ui.ChoiceBox") -local Controls = {} +local Controls = { isMenu = true } Controls.ID = "controls" Controls.open = false diff --git a/src/ui/game3/daycare_menu.lua b/src/ui/game3/daycare_menu.lua index 1ecaec06..73c6b8e2 100644 --- a/src/ui/game3/daycare_menu.lua +++ b/src/ui/game3/daycare_menu.lua @@ -3,7 +3,7 @@ local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") -local DaycareMenu = {} +local DaycareMenu = { isMenu = true } local DAYCARE_MON_COUNT = 2 -- pokefirered/include/constants/global.h:34 local DAYCARE_LEVEL_MENU_EXIT = 5 -- pokefirered/include/constants/daycare.h:20 diff --git a/src/ui/game3/easy_chat.lua b/src/ui/game3/easy_chat.lua index 53b61d97..c308eae5 100644 --- a/src/ui/game3/easy_chat.lua +++ b/src/ui/game3/easy_chat.lua @@ -10,7 +10,7 @@ local Chrome = require("src.ui.game3.chrome") local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") -local EasyChat = {} +local EasyChat = { isMenu = true } EasyChat.openFlag = false EasyChat._state = nil diff --git a/src/ui/game3/fame_checker.lua b/src/ui/game3/fame_checker.lua index 3e88aca6..dc31f23e 100644 --- a/src/ui/game3/fame_checker.lua +++ b/src/ui/game3/fame_checker.lua @@ -7,7 +7,7 @@ local FameChecker = require("src.core.game3.fame_checker") local TextIR = require("src.core.game3.scripting.text_ir") local RomText = require("src.core.game3.rom_text") -local FameCheckerUi = {} +local FameCheckerUi = { isMenu = true } local PERSON = FameChecker.PERSON local PICK = FameChecker.PICKSTATE diff --git a/src/ui/game3/hall_of_fame_pc.lua b/src/ui/game3/hall_of_fame_pc.lua index a2b9bc1e..eb9001b3 100644 --- a/src/ui/game3/hall_of_fame_pc.lua +++ b/src/ui/game3/hall_of_fame_pc.lua @@ -7,7 +7,7 @@ local Pokemon = require("src.core.game3.pokemon") local RomText = require("src.core.game3.rom_text") local HofGfx = require("src.ui.game3.hall_of_fame_gfx") -local HofPc = {} +local HofPc = { isMenu = true } HofPc.open = false HofPc._teams = {} diff --git a/src/ui/game3/item_pc.lua b/src/ui/game3/item_pc.lua index 270cd77a..1a335a35 100644 --- a/src/ui/game3/item_pc.lua +++ b/src/ui/game3/item_pc.lua @@ -7,7 +7,7 @@ local Storage = require("src.core.game3.storage") local RomText = require("src.core.game3.rom_text") local Trig = require("src.core.game3.trig") -local ItemPc = {} +local ItemPc = { isMenu = true } ItemPc.open = false ItemPc.mode = "list" diff --git a/src/ui/game3/mod_manager.lua b/src/ui/game3/mod_manager.lua index fbb4a7c9..e4e0463d 100644 --- a/src/ui/game3/mod_manager.lua +++ b/src/ui/game3/mod_manager.lua @@ -11,7 +11,7 @@ local FrlgFont = require("src.ui.game3.frlg_font") local Strings = require("src.core.Strings") local ManagerState = require("src.mods.ManagerState") -local ModManager = {} +local ModManager = { isMenu = true } ModManager.open = false diff --git a/src/ui/game3/move_relearner.lua b/src/ui/game3/move_relearner.lua index 94699659..99cbafd4 100644 --- a/src/ui/game3/move_relearner.lua +++ b/src/ui/game3/move_relearner.lua @@ -11,7 +11,7 @@ local SummaryData = require("src.core.game3.summary_data") local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") -local MoveRelearner = {} +local MoveRelearner = { isMenu = true } MoveRelearner.open = false MoveRelearner.state = "list" diff --git a/src/ui/game3/naming.lua b/src/ui/game3/naming.lua index dcfbcc81..d246021a 100644 --- a/src/ui/game3/naming.lua +++ b/src/ui/game3/naming.lua @@ -14,7 +14,7 @@ local Versions = require("src.import.gba.versions") local RomText = require("src.core.game3.rom_text") local TextIR = require("src.core.game3.scripting.text_ir") -local Naming = {} +local Naming = { isMenu = true } Naming.MAX_LEN = 7 Naming.openFlag = false diff --git a/src/ui/game3/option_menu.lua b/src/ui/game3/option_menu.lua index 420297ae..4771d3c3 100644 --- a/src/ui/game3/option_menu.lua +++ b/src/ui/game3/option_menu.lua @@ -4,7 +4,7 @@ local Options = require("src.core.game3.options") local Rows = require("src.ui.game3.option_rows") local RomText = require("src.core.game3.rom_text") -local OptionMenu = {} +local OptionMenu = { isMenu = true } OptionMenu.open = false OptionMenu.cursor = 1 diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index 45993fde..53f020b5 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -16,7 +16,7 @@ local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") local TextIR = require("src.core.game3.scripting.text_ir") -local PartyMenu = {} +local PartyMenu = { isMenu = true } PartyMenu.open = false PartyMenu.cursor = 1 diff --git a/src/ui/game3/pc_menu.lua b/src/ui/game3/pc_menu.lua index 14e9b424..67f0b56c 100644 --- a/src/ui/game3/pc_menu.lua +++ b/src/ui/game3/pc_menu.lua @@ -15,7 +15,7 @@ local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") local Profile = require("src.core.game3.profile") -local PcMenu = {} +local PcMenu = { isMenu = true } local function rse_pc() if Profile.family(PcMenu._session) ~= "rse" then return nil end diff --git a/src/ui/game3/pokedex.lua b/src/ui/game3/pokedex.lua index ab41b4d7..62e4ae1a 100644 --- a/src/ui/game3/pokedex.lua +++ b/src/ui/game3/pokedex.lua @@ -17,7 +17,7 @@ local PokedexChrome = require("src.ui.game3.pokedex_chrome") local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") -local Pokedex = {} +local Pokedex = { isMenu = true } Pokedex.open = false Pokedex.screen = "mode_select" diff --git a/src/ui/game3/prize_corner.lua b/src/ui/game3/prize_corner.lua index eafb6137..02def2dc 100644 --- a/src/ui/game3/prize_corner.lua +++ b/src/ui/game3/prize_corner.lua @@ -4,7 +4,7 @@ local Stack = require("src.ui.game3.stack") local Window = require("src.ui.game3.window") local FrlgFont = require("src.ui.game3.frlg_font") -local PrizeCorner = {} +local PrizeCorner = { isMenu = true } -- pokefirered/include/constants/menu.h:21 PrizeCorner.LIST_POKEMON_PRIZES = 14 diff --git a/src/ui/game3/region_map.lua b/src/ui/game3/region_map.lua index c9073caa..a84a3108 100644 --- a/src/ui/game3/region_map.lua +++ b/src/ui/game3/region_map.lua @@ -8,7 +8,7 @@ local Strings = require("src.core.Strings") local Gpu = require("src.ui.game3.region_map_gpu") local Position = require("src.ui.game3.region_map_position") -local RegionMap = {} +local RegionMap = { isMenu = true } RegionMap.open = false RegionMap.cursorX = 0 diff --git a/src/ui/game3/rse/berry_tag.lua b/src/ui/game3/rse/berry_tag.lua index 9c9bc380..b4090429 100644 --- a/src/ui/game3/rse/berry_tag.lua +++ b/src/ui/game3/rse/berry_tag.lua @@ -5,7 +5,7 @@ local Kit = require("src.ui.game3.rse.scene_kit") local Gfx = require("src.ui.game3.rse.pokeblock_gfx") local BerryTrees = require("src.core.game3.rse.berry_trees") -local Tag = {} +local Tag = { isMenu = true } Tag.ID = "rse_berry_tag" Tag.open = false diff --git a/src/ui/game3/rse/frontier_pass.lua b/src/ui/game3/rse/frontier_pass.lua index defd91db..3c1b963f 100644 --- a/src/ui/game3/rse/frontier_pass.lua +++ b/src/ui/game3/rse/frontier_pass.lua @@ -5,7 +5,7 @@ local Kit = require("src.ui.game3.rse.scene_kit") local Gfx = require("src.ui.game3.rse.pokeblock_gfx") local PalFade = require("src.core.game3.pal_fade") -local Pass = {} +local Pass = { isMenu = true } Pass.ID = "rse_frontier_pass" Pass.MAP_ID = "rse_frontier_map" diff --git a/src/ui/game3/rse/item_storage.lua b/src/ui/game3/rse/item_storage.lua index 6aed8c7a..55e6d74f 100644 --- a/src/ui/game3/rse/item_storage.lua +++ b/src/ui/game3/rse/item_storage.lua @@ -5,7 +5,7 @@ local RomText = require("src.core.game3.rom_text") local Storage = require("src.core.game3.storage") local Bag = require("src.core.game3.bag") -local ItemStorage = {} +local ItemStorage = { isMenu = true } ItemStorage.open = false diff --git a/src/ui/game3/rse/mailbox.lua b/src/ui/game3/rse/mailbox.lua index 5a6e1a3e..e33d19e3 100644 --- a/src/ui/game3/rse/mailbox.lua +++ b/src/ui/game3/rse/mailbox.lua @@ -4,7 +4,7 @@ local FrlgFont = require("src.ui.game3.frlg_font") local RomText = require("src.core.game3.rom_text") local Mail = require("src.core.game3.mail") -local Mailbox = {} +local Mailbox = { isMenu = true } Mailbox.open = false diff --git a/src/ui/game3/rse/move_relearner.lua b/src/ui/game3/rse/move_relearner.lua index 9e35971b..9d043c63 100644 --- a/src/ui/game3/rse/move_relearner.lua +++ b/src/ui/game3/rse/move_relearner.lua @@ -10,7 +10,7 @@ local SummaryData = require("src.core.game3.summary_data") local RomText = require("src.core.game3.rom_text") local Kit = require("src.ui.game3.rse.scene_kit") -local RseRelearner = {} +local RseRelearner = { isMenu = true } RseRelearner.SUB = "rse/move_relearner" RseRelearner.open = false diff --git a/src/ui/game3/rse/option_menu.lua b/src/ui/game3/rse/option_menu.lua index 2bb2f1ca..85983f0e 100644 --- a/src/ui/game3/rse/option_menu.lua +++ b/src/ui/game3/rse/option_menu.lua @@ -7,7 +7,7 @@ local Chrome = require("src.ui.game3.chrome") local Kit = require("src.ui.game3.rse.scene_kit") local Screens = require("src.ui.game3.screens") -local OptionMenu = {} +local OptionMenu = { isMenu = true } OptionMenu.ID = "option" OptionMenu.VISIBLE = 7 diff --git a/src/ui/game3/rse/pokeblock_case.lua b/src/ui/game3/rse/pokeblock_case.lua index 641f7b85..6ea559af 100644 --- a/src/ui/game3/rse/pokeblock_case.lua +++ b/src/ui/game3/rse/pokeblock_case.lua @@ -6,7 +6,7 @@ local Kit = require("src.ui.game3.rse.scene_kit") local Gfx = require("src.ui.game3.rse.pokeblock_gfx") local Pokeblock = require("src.core.game3.rse.pokeblock") -local Case = {} +local Case = { isMenu = true } Case.ID = "rse_pokeblock_case" Case.open = false diff --git a/src/ui/game3/rse/pokedex.lua b/src/ui/game3/rse/pokedex.lua index cafa5747..ca20c8ec 100644 --- a/src/ui/game3/rse/pokedex.lua +++ b/src/ui/game3/rse/pokedex.lua @@ -2358,7 +2358,7 @@ function Pokedex.draw(s) love.graphics.pop() end -local Host = {} +local Host = { isMenu = true } Pokedex.Host = Host Host._s = nil Host._step = nil diff --git a/src/ui/game3/rse/pokenav/init.lua b/src/ui/game3/rse/pokenav/init.lua index 4740ad67..3f11e028 100644 --- a/src/ui/game3/rse/pokenav/init.lua +++ b/src/ui/game3/rse/pokenav/init.lua @@ -50,7 +50,7 @@ Pokenav.SCREENS = SCREENS local Shell = {} Shell.__index = Shell -local Host = {} +local Host = { isMenu = true } Pokenav.Host = Host -- pokeemerald/src/pokenav.c:388 diff --git a/src/ui/game3/rse/pyramid_bag.lua b/src/ui/game3/rse/pyramid_bag.lua index 8374c601..6f59c6ac 100644 --- a/src/ui/game3/rse/pyramid_bag.lua +++ b/src/ui/game3/rse/pyramid_bag.lua @@ -6,7 +6,7 @@ local RomText = require("src.core.game3.rom_text") local ItemsData = require("src.core.game3.items_data") local Gfx = require("src.ui.game3.rse.pokeblock_gfx") -local PBag = {} +local PBag = { isMenu = true } PBag.ID = "rse_pyramid_bag" PBag.open = false diff --git a/src/ui/game3/rse/region_map.lua b/src/ui/game3/rse/region_map.lua index 72bd6e6d..0fd4d570 100644 --- a/src/ui/game3/rse/region_map.lua +++ b/src/ui/game3/rse/region_map.lua @@ -606,7 +606,7 @@ function RegionMap.draw(s) love.graphics.pop() end -local Host = {} +local Host = { isMenu = true } RegionMap.Host = Host Host._s = nil Host._step = nil diff --git a/src/ui/game3/save_menu.lua b/src/ui/game3/save_menu.lua index 59445ed1..6ee02739 100644 --- a/src/ui/game3/save_menu.lua +++ b/src/ui/game3/save_menu.lua @@ -14,7 +14,7 @@ local RomText = require("src.core.game3.rom_text") local Flags = require("src.core.game3.scripting.flags") local Dex = require("src.core.game3.dex") -local SaveMenu = {} +local SaveMenu = { isMenu = true } SaveMenu.open = false SaveMenu.cursor = 1 -- 1=YES 2=NO diff --git a/src/ui/game3/shop_menu.lua b/src/ui/game3/shop_menu.lua index 36254535..05626aa0 100644 --- a/src/ui/game3/shop_menu.lua +++ b/src/ui/game3/shop_menu.lua @@ -9,7 +9,7 @@ local Bag = require("src.core.game3.bag") local MoneyBox = require("src.ui.game3.money_box") local RomText = require("src.core.game3.rom_text") -local ShopMenu = {} +local ShopMenu = { isMenu = true } ShopMenu.open = false ShopMenu.mode = "root" diff --git a/src/ui/game3/stack.lua b/src/ui/game3/stack.lua index 9b8cbfcc..ba65b8b4 100644 --- a/src/ui/game3/stack.lua +++ b/src/ui/game3/stack.lua @@ -24,6 +24,8 @@ end -- opts.drawUnder: if true, still draw layers below this one. function Stack.push(id, mod, opts) opts = opts or {} + local isMenu = opts.isMenu + if isMenu == nil then isMenu = mod and mod.isMenu end if opts.hideBelow ~= false then local Message = package.loaded["src.ui.game3.message"] if Message and Message.closeStay then Message.closeStay() end @@ -37,6 +39,7 @@ function Stack.push(id, mod, opts) Stack._layers[#Stack._layers + 1] = { id = id, mod = mod, + isMenu = isMenu == true, drawUnder = opts.drawUnder and true or false, hideBelow = opts.hideBelow ~= false, -- default hide layers underneath fullscreen = opts.fullscreen, diff --git a/src/ui/game3/start_menu.lua b/src/ui/game3/start_menu.lua index 5a7fabf3..d32b5ea0 100644 --- a/src/ui/game3/start_menu.lua +++ b/src/ui/game3/start_menu.lua @@ -10,7 +10,7 @@ local RomText = require("src.core.game3.rom_text") local ModRuntime = require("src.mods.Runtime") local FrlgData = require("src.ui.game3.start_menu_frlg") -local StartMenu = {} +local StartMenu = { isMenu = true } local function se(id) pcall(function() require("src.core.game3.audio").playSe(require("src.core.game3.se_ids").resolve(id)) end) diff --git a/src/ui/game3/summary_menu.lua b/src/ui/game3/summary_menu.lua index 20997fd2..e5776c2e 100644 --- a/src/ui/game3/summary_menu.lua +++ b/src/ui/game3/summary_menu.lua @@ -17,7 +17,7 @@ local Strings = require("src.core.Strings") local RomText = require("src.core.game3.rom_text") local ItemsData = require("src.core.game3.items_data") -local SummaryMenu = {} +local SummaryMenu = { isMenu = true } -- pokefirered/src/pokemon_summary_screen.c:2139 function SummaryMenu.heldItemText(mon) diff --git a/src/ui/game3/tm_case.lua b/src/ui/game3/tm_case.lua index 7cb3ed6d..9b0cea98 100644 --- a/src/ui/game3/tm_case.lua +++ b/src/ui/game3/tm_case.lua @@ -18,7 +18,7 @@ local SummaryData = require("src.core.game3.summary_data") local SummaryChrome = require("src.ui.game3.summary_chrome") local RomText = require("src.core.game3.rom_text") -local TmCase = {} +local TmCase = { isMenu = true } TmCase.open = false TmCase.cursor = 1 diff --git a/src/ui/game3/trainer_card.lua b/src/ui/game3/trainer_card.lua index e4d5b984..6f931a8e 100644 --- a/src/ui/game3/trainer_card.lua +++ b/src/ui/game3/trainer_card.lua @@ -4,7 +4,7 @@ local Stack = require("src.ui.game3.stack") local FrlgFont = require("src.ui.game3.frlg_font") local RomText = require("src.core.game3.rom_text") -local TrainerCard = {} +local TrainerCard = { isMenu = true } TrainerCard.open = false TrainerCard._session = nil diff --git a/src/ui/gen2/OptionsMenu.lua b/src/ui/gen2/OptionsMenu.lua index a0ee11c2..b86c8fef 100644 --- a/src/ui/gen2/OptionsMenu.lua +++ b/src/ui/gen2/OptionsMenu.lua @@ -232,6 +232,15 @@ local ROWS = { if speed == 1 then return Strings("NORMAL") end return Strings("%dX", speed) end }, + { label = Strings.source("ORIENTATION"), key = "orientation", port = true, + cycle = function(options, delta) + local Orientation = require("src.core.Orientation") + options.orientation = Orientation.cycle(options.orientation, delta) + Orientation.apply(options.orientation) + end, + text = function(options) + return Strings(require("src.core.Orientation").modeLabel(options.orientation)) + end }, { label = Strings.source("ZOOM"), key = "zoom", port = true, cycle = function(options, delta, game) local Zoom = require("src.render.Zoom") @@ -500,7 +509,7 @@ local GROUPS = { { id = "group.speed", label = Strings.source("SPEED"), members = { "textSpeed", "speed" } }, { id = "group.video", label = Strings.source("VIDEO"), - members = { "videoMode", "faithfulRes", "screenPos", "fpsCap", "vsync", + members = { "videoMode", "orientation", "faithfulRes", "screenPos", "fpsCap", "vsync", "logicClock" } }, { id = "group.graphics", label = Strings.source("GRAPHICS"), members = { "color", "uiLetterbox", "shaderfx", "shaderfx2", "frame" } }, @@ -586,6 +595,7 @@ local function buildRows() -- The descriptor and the save key stay, so a build that grows a printer -- only has to drop this test. local hidden = row.key == "print" + or (row.key == "orientation" and osName ~= "Android" and osName ~= "iOS") or (isNX and row.key == "videoMode") or (not showTouch and (row.id == "touchControls" or row.id == "touchLayout" or row.id == "haptics" diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index b01d45b7..9666f96f 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -3121,8 +3121,10 @@ function OverworldState:billsHousePokemonList() end table.insert(items, { label = Strings("CANCEL") }) -- TextBoxBorder b=10,c=9 at (0,0) -> total tw=11, th=12 - Game.stack:push(Menu.new(Game, items, - { tx = 0, ty = 0, tw = 11, th = 12 })) + local menu = Menu.new(Game, items, + { tx = 0, ty = 0, tw = 11, th = 12 }) + menu.isMenu = true + Game.stack:push(menu) end Game.stack:push(TextBox.new(Game, t._BillsHousePokemonListText1 or Strings("BILL's favorite\nPOKéMON list!"), openList)) @@ -3693,6 +3695,7 @@ function OverworldState:openPC(onDone) menu = Menu.new(Game, items, { tx = 0, ty = 0, tw = 16, th = #items * 2 + 2, onCancel = logOff, noSound = true }) + menu.isMenu = true -- engine/menus/pc.asm:5 Game.stack:push(TextBox.new(Game, (Game.data.text or {})._TurnedOnPC1Text diff --git a/src/world/gen2/World.lua b/src/world/gen2/World.lua index 776f9847..e5c31af5 100644 --- a/src/world/gen2/World.lua +++ b/src/world/gen2/World.lua @@ -2703,7 +2703,8 @@ function World:mapMusicSong(mapId) local def = self.maps and self.maps[mapId] -- ENGINE_ROCKETS_IN_MAHOGANY / _RADIO_TOWER (data/events/engine_flags.asm:40,:36) return World.mapMusicLabel(audio, def and def.music, - self:engineFlag(22), self:engineFlag(18)) + self:engineFlag(self:engineFlagId("ENGINE_ROCKETS_IN_MAHOGANY", 22)), + self:engineFlag(self:engineFlagId("ENGINE_ROCKETS_IN_RADIO_TOWER", 18))) end function World:playMapMusic() diff --git a/tests/drivers/game3_surf_bridge_bug2617.lua b/tests/drivers/game3_surf_bridge_bug2617.lua new file mode 100644 index 00000000..93e8460a --- /dev/null +++ b/tests/drivers/game3_surf_bridge_bug2617.lua @@ -0,0 +1,98 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/BSA/10-02-26-00-userreported/shots/2617" + +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 route110_surf_bridge failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + 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" and game.boot, "boot_ready") then return finish() end + game:_handleBootAction({ action = "new_game", name = "NICK", gender = 0 }) + U.wait(240) + 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 Field = require("src.core.game3.field") + local Space = require("src.core.game3.scripting.space") + if not check(Runtime.getSession() ~= nil, "session_ready") 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, "EM_ROUTE110", { x = 15, y = 29, facing = "down" }) + U.wait(90) + if Space.vm and Space.vm.stop then Space.vm:stop() end + Field.unlock() + Player.surfing, Player.dismounting, Player.surfHopping = true, false, false + Player.elevation, Player.currentElevation = 1, 1 + Player.facing, Player.turnArmed, Player.turnTimer = "down", true, 0 + if not check(Map.current == "EM_ROUTE110" and Player.cellX == 15 and Player.cellY == 29 + and Collision.isWater(15, 29), "normal_water_setup") then return finish() end + + local function state(label) + print(string.format("STATE %s x=%d y=%d surfing=%s dismounting=%s elevation=%s current=%s behavior=%s coll=%s moving=%s locked=%s", + label, Player.cellX, Player.cellY, tostring(Player.surfing), tostring(Player.dismounting), + tostring(Player.elevation), tostring(Player.currentElevation), + tostring(Collision.behavior(Player.cellX, Player.cellY)), tostring(Collision.cell(Player.cellX, Player.cellY)), + tostring(Player.moving), tostring(Field.locked))) + end + local function step(dir) + local x, y = Player.cellX, Player.cellY + for _ = 1, 24 do + U.hold(game, dir, 1) + if Player.moving then break end + end + for _ = 1, 80 do + if not Player.moving then break end + U.wait(1) + end + state(dir) + return x ~= Player.cellX or y ~= Player.cellY + end + + local Popup = require("src.ui.game3.map_name_popup") + for _ = 1, 360 do + if not Popup.isActive() then break end + U.wait(1) + end + if not check(not Popup.isActive(), "map_popup_finished") then return finish() end + if not check(step("up") and Map.current == "EM_ROUTE110" and Player.cellX == 15 + and Player.cellY == 28 and Player.surfing and not Player.dismounting + and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, 28), + "before_bridge_clear_water_by_input") then return finish() end + state("before_bridge_clear_water") + check(U.still(game, DIR .. "/2617_before_bridge_on_water.png"), "shot_before") + if not check(step("down") and Map.current == "EM_ROUTE110" and Player.cellX == 15 + and Player.cellY == 29 and Player.surfing and not Player.dismounting + and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, 29), + "returned_to_causal_setup_by_input") then return finish() end + state("before_bridge") + if not check(step("down") and Player.cellY == 30, "entered_bridge_by_input") then return finish() end + if not check(Player.surfing and not Player.dismounting and Player.elevation == 1, + "retain_surf_under_bridge") then return finish() end + check(U.still(game, DIR .. "/2617_under_bridge_after_entry.png"), "shot_under_bridge") + check(step("down") and Player.cellY == 31 and Player.surfing, "bridge_second_row") + check(step("down") and Player.cellY == 32 and Player.surfing, "bridge_third_row") + if not check(step("down") and Map.current == "EM_ROUTE110" and Player.cellX == 15 + and Player.cellY == 33 and Player.surfing and not Player.dismounting + and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, 33), + "exit_bridge_to_south_water") then return finish() end + for _, y in ipairs({ 34, 35 }) do + if not check(step("down") and Map.current == "EM_ROUTE110" and Player.cellX == 15 + and Player.cellY == y and Player.surfing and not Player.dismounting + and Player.elevation == 1 and Player.currentElevation == 1 and Collision.isWater(15, y), + "south_water_row" .. y .. "_retains_surf") then return finish() end + end + state("south_water_clear_of_bridge") + check(U.still(game, DIR .. "/2617_south_water_clear_of_bridge.png"), "shot_exit") + finish() +end diff --git a/tests/drivers/game3_surf_connections.lua b/tests/drivers/game3_surf_connections.lua index c0f11318..9a5fe2d5 100644 --- a/tests/drivers/game3_surf_connections.lua +++ b/tests/drivers/game3_surf_connections.lua @@ -65,7 +65,17 @@ return function(game) end local function phases(name) U.wait(90) - local pairs = { "general__rom_082d4b54", "general__rom_082d4b6c" } + local pairs = {} + for index, mapId in ipairs({ "FR_ROUTE_19", "FR_ROUTE_20" }) do + local def = game.data and game.data.maps and game.data.maps[mapId] + local layout = def and Map.ensureMidLayout(game, mapId, def) + local pair = layout and layout.pair + if type(pair) ~= "string" or pair == "" or pair == pairs[1] then + result(false, name .. "_distinct_loaded_pair_" .. mapId) + return false + end + pairs[index] = pair + end for index, phase in ipairs({ 2, 4 }) do local target = phase * 16 + 3 local ready = false diff --git a/tests/drivers/gen2_radio_tower_music_bug2623.lua b/tests/drivers/gen2_radio_tower_music_bug2623.lua new file mode 100644 index 00000000..f5e07468 --- /dev/null +++ b/tests/drivers/gen2_radio_tower_music_bug2623.lua @@ -0,0 +1,70 @@ +local U = require("tests.drivers.util") +local Music = require("src.core.Music") +local FlagNames = require("src.core.gen2.FlagNames") + +return function(game) + U.wait(60) + local world = game.world + local failures = 0 + local function check(ok, label) + if not ok then failures = failures + 1 end + print((ok and "PASS " or "FAIL ") .. label) + end + if not (world and world.map) then + print("FAIL 2623 gen2 world did not boot") + return love.event.quit(1) + end + check(game.mods and game.mods.safeMode == true, "2623 mods disabled before boot") + world.checkTrainerBattle = function() return false end + local tower = world:engineFlagId("ENGINE_ROCKETS_IN_RADIO_TOWER", 18) + local mahogany = world:engineFlagId("ENGINE_ROCKETS_IN_MAHOGANY", 22) + check(type(tower) == "number" and type(mahogany) == "number", + "2623 Rocket engine flags resolve") + if not (tower and mahogany) then return love.event.quit(1) end + world.events:set(FlagNames.events.EVENT_RADIO_TOWER_ROCKET_TAKEOVER, false) + world.events:set(FlagNames.events.EVENT_RADIO_TOWER_CIVILIANS_AFTER, true) + world.mapScenes.RADIO_TOWER_5F = 2 + world.mapScenes.MAHOGANY_MART_1F = 0 + local dir = os.getenv("POKEPORT_SHOT_DIR") + local function enter(mapId, x, y) + if not world:warpToMapId(mapId, x, y, "up") then + check(false, "2623 load " .. mapId) + return false + end + U.wait(120) + check(world.map.id == mapId, "2623 entered " .. mapId) + return world.map.id == mapId + end + + world:setEngineFlag(tower, true) + if not enter("GOLDENROD_CITY", 5, 10) then return love.event.quit(1) end + check(Music.current() == "Music_GoldenrodCity", "2623 ordinary Goldenrod music") + for floor = 1, 5 do + local mapId = "RADIO_TOWER_" .. floor .. "F" + world:setEngineFlag(tower, true) + if not enter(mapId, 2, 6) then return love.event.quit(1) end + print("2623 " .. mapId .. " flag=" .. tower + .. " active=" .. tostring(world:engineFlag(tower)) + .. " resolved=" .. tostring(world:mapMusicSong(mapId)) + .. " playing=" .. tostring(Music.current())) + check(world:engineFlag(tower), "2623 takeover active " .. mapId) + check(Music.current() == "Music_RocketTheme", "2623 rocket_music_floor_" .. floor) + if dir then U.still(game, dir .. "/2623_radio_tower_" .. floor .. "f_takeover.png") end + world:setEngineFlag(tower, false) + world:playMapMusic() + U.wait(120) + check(Music.current() == "Music_GoldenrodCity", "2623 cleared_music_floor_" .. floor) + end + + world:setEngineFlag(mahogany, true) + if not enter("MAHOGANY_MART_1F", 2, 6) then return love.event.quit(1) end + check(world:engineFlag(mahogany), "2623 Mahogany Rockets active") + check(Music.current() == "Music_RocketHideout", "2623 mahogany_rocket_music") + if dir then U.still(game, dir .. "/2623_mahogany_mart_rocket_music.png") end + world:setEngineFlag(mahogany, false) + world:playMapMusic() + U.wait(120) + check(Music.current() == "Music_CherrygroveCity", "2623 mahogany_cleared_music") + print("RESULT 2623 failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) +end diff --git a/tests/drivers/menu_speed_bug2578.lua b/tests/drivers/menu_speed_bug2578.lua new file mode 100644 index 00000000..3216caa0 --- /dev/null +++ b/tests/drivers/menu_speed_bug2578.lua @@ -0,0 +1,151 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/menu_speed_bug2578" +local failed = 0 +local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failed = failed + 1 end + return ok +end + +local function gen1(game) + game.speedOverride = 200 + game.save.party = { require("src.pokemon.Pokemon").new(game.data, "PIKACHU", 20) } + U.teleport(game, "PALLET_TOWN", 10, 12, "down") + local Screens = require("src.ui.Screens") + local Game = require("src.core.Game") + local opts = game.save.options + opts.speedOverworld, opts.speedBattle, opts.speedMenu = 4, 10, 2 + game.speedOverride = nil + check(game:logicSpeed() == 4, "2578_gen1_field_speed") + for _, name in ipairs({ "StartMenu", "PartyMenu", "BagMenu" }) do + Screens.push(game, name) + U.wait(20) + check(Game.speedCategoryInStack(game.stack) == "menu" and game:logicSpeed() == 2, + "2578_gen1_field_" .. name .. "_menu_speed") + check(U.still(game, DIR .. "/2578_gen1_field_" .. name .. ".png"), "2578_gen1_" .. name .. "_shot") + game.stack:pop() + check(game:logicSpeed() == 4, "2578_gen1_" .. name .. "_pop_restores_field") + end + local Menu = require("src.ui.Menu") + local function reachPCMenu() + for _ = 1, 600 do + local top = game.stack:top() + if getmetatable(top) == Menu then return top end + U.tap(game, "a") + U.wait(2) + end + error("actual interactive PC menu was not reached") + end + game.overworld:openPC() + local pc = reachPCMenu() + check(#pc.items >= 3 and pc.noSound and Game.speedCategoryInStack(game.stack) == "menu" + and game:logicSpeed() == 2, "2578_gen1_pc_main_uses_MENU_SPEED") + check(U.still(game, DIR .. "/2578_gen1_pc_main.png"), "2578_gen1_pc_main_shot") + U.tap(game, "b") + U.wait(2) + check(game.stack:top() == game.overworld and game:logicSpeed() == 4, + "2578_gen1_pc_main_pop_restores_field") + + game.speedOverride = 200 + U.teleport(game, "BILLS_HOUSE", 2, 3, "up") + game.speedOverride = nil + game.overworld:billsHousePokemonList() + local bill = reachPCMenu() + check(#bill.items == 5 and bill.items[1].keepOpen and Game.speedCategoryInStack(game.stack) == "menu" + and game:logicSpeed() == 2, "2578_gen1_bill_pc_viewer_uses_MENU_SPEED") + check(U.still(game, DIR .. "/2578_gen1_bill_pc_viewer.png"), "2578_gen1_bill_pc_viewer_shot") + U.tap(game, "b") + U.wait(2) + check(game.stack:top() == game.overworld and game:logicSpeed() == 4, + "2578_gen1_bill_pc_viewer_pop_restores_field") + game.speedOverride = 200 + U.teleport(game, "PALLET_TOWN", 10, 12, "down") + game.speedOverride = nil + local battle = require("src.battle.BattleState").newWild(game, "RATTATA", 3) + game.overworld:pushBattle(battle) + game.speedOverride = 200 + for _ = 1, 1500 do + if game.stack:top() == battle and battle.phase == "menu" then break end + U.tap(game, "a") + U.wait(2) + end + game.speedOverride = nil + check(Game.speedCategoryInStack(game.stack) == "battle" and game:logicSpeed() == 10, + "2578_gen1_actual_battle_speed") + for _, name in ipairs({ "PartyMenu", "BagMenu" }) do + Screens.push(game, name, { battle = battle }) + U.wait(20) + check(game:logicSpeed() == 2, "2578_gen1_battle_" .. name .. "_menu_speed") + check(U.still(game, DIR .. "/2578_gen1_battle_" .. name .. ".png"), "2578_gen1_battle_" .. name .. "_shot") + game.stack:pop() + check(game:logicSpeed() == 10, "2578_gen1_battle_" .. name .. "_pop_restores_battle") + end +end + +local function gen3(game) + game.speedOverride = 200 + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "RED", gender = 0 }) + U.wait(240) + local Runtime = require("src.core.game3.runtime") + local session = Runtime.getSession() + assert(session, "new game did not reach field") + session.party = {} + require("src.core.game3.party").giveMon(session, 25, 20) + local Stack = require("src.ui.game3.stack") + local Start = require("src.ui.game3.start_menu") + local Party = require("src.ui.game3.party_menu") + local Bag = require("src.ui.game3.bag_menu") + game.options.speedOverworld, game.options.speedBattle, game.options.speedMenu = 4, 10, 2 + game.speedOverride = nil + check(game:logicSpeed() == 4, "2578_gen3_field_speed") + local menus = { + { id = "start", open = function() Start.show({ session = session, game = game }) end, close = Start.close }, + { id = "party", open = function() Party.show(session.party, nil, { session = session }) end, close = Party.close }, + { id = "bag", open = function() Bag.show(session, { session = session }) end, close = Bag.close }, + } + for _, menu in ipairs(menus) do + menu.open() + U.wait(120) + check(Stack.has(menu.id) and game:logicSpeed() == 2, "2578_gen3_field_" .. menu.id .. "_menu_speed") + check(U.still(game, DIR .. "/2578_gen3_field_" .. menu.id .. ".png"), "2578_gen3_" .. menu.id .. "_shot") + menu.close() + check(game:logicSpeed() == 4, "2578_gen3_" .. menu.id .. "_pop_restores_field") + end + local Battle = require("src.core.game3.battle") + local Bridge = require("src.core.game3.battle_bridge") + local started, err = Bridge.startWild(Runtime._mod, game, { species = 16, level = 3 }, { fade = false }) + assert(started, tostring(err)) + game.speedOverride = 200 + for _ = 1, 1800 do + if Battle._phase == "command" then break end + U.tap(game, "a") + U.wait(2) + end + game.speedOverride = nil + check(Battle.isActive() and game:logicSpeed() == 10, "2578_gen3_actual_battle_speed") + Party.show(session.party, nil, { session = session, battle = true, mode = "battle_switch" }) + U.wait(120) + check(game:logicSpeed() == 2, "2578_gen3_battle_party_menu_speed") + check(U.still(game, DIR .. "/2578_gen3_battle_party.png"), "2578_gen3_battle_party_shot") + Party.close() + check(game:logicSpeed() == 10, "2578_gen3_battle_party_pop_restores_battle") + Bag.show(session, { session = session, battle = true }) + U.wait(120) + check(game:logicSpeed() == 2 and Stack.top().fullscreen == false, "2578_gen3_nonfullscreen_battle_bag_menu_speed") + check(U.still(game, DIR .. "/2578_gen3_battle_bag.png"), "2578_gen3_battle_bag_shot") + Bag.close() + check(game:logicSpeed() == 10, "2578_gen3_battle_bag_pop_restores_battle") +end + +return function(game) + local ok, err = xpcall(function() + if game.phase ~= nil then gen3(game) else gen1(game) end + end, debug.traceback) + if not ok then failed = failed + 1; print("FAIL 2578_driver " .. tostring(err)) end + print((failed == 0 and "PASS " or "FAIL ") .. "2578_menu_speed failures=" .. failed) + love.event.quit(failed == 0 and 0 or 1) +end diff --git a/tests/engine/game3_rse_title_option_speed_test.lua b/tests/engine/game3_rse_title_option_speed_test.lua index 6b8dd045..391f6d81 100644 --- a/tests/engine/game3_rse_title_option_speed_test.lua +++ b/tests/engine/game3_rse_title_option_speed_test.lua @@ -70,12 +70,15 @@ do eq(stale.options.speedOverworld, 7, "a stale Runtime game is untouched") check(wrote() > 0, "the change is persisted") g.phase = "field" - eq(g:logicSpeed(), 2, "logicSpeed follows the menu change") + eq(g:speedCategory(), "menu", "the actual OPTION layer owns its speed while open") + eq(g:logicSpeed(), 1, "open OPTION keeps MENU SPEED after changing OVERWORLD SPEED") check(selectRow("textSpeed"), "TEXT SPEED row reachable") OptionMenu.handleInput(fakeInput("right")) eq(Options.block(g.options).textSpeed, 2, "TEXT SPEED lands on the game's cart block") OptionMenu.close() + eq(g:speedCategory(), "overworld", "closing OPTION restores the field category") + eq(g:logicSpeed(), 2, "the changed OVERWORLD SPEED applies after OPTION closes") menu.state = "options" menu:frame({ new = {}, held = {} }) eq(state.textSpeed, 2, "closing OPTION refreshes the boot text speed for NEW GAME") diff --git a/tests/engine/game3_speed_hotkey_test.lua b/tests/engine/game3_speed_hotkey_test.lua index 404ad6ea..cec94a43 100644 --- a/tests/engine/game3_speed_hotkey_test.lua +++ b/tests/engine/game3_speed_hotkey_test.lua @@ -11,8 +11,9 @@ local Game3 = require("src.core.Game3") local battleActive = false package.loaded["src.core.game3.battle"] = { isActive = function() return battleActive end } -local stackBusy = false -package.loaded["src.ui.game3.stack"] = { busy = function() return stackBusy end, clear = function() end } +local Stack = require("src.ui.game3.stack") +local StartMenu = require("src.ui.game3.start_menu") +Stack.clear() local function newGame3(phase) local g = setmetatable({ @@ -29,13 +30,13 @@ Input:applyBindings(nil) do local g = newGame3("field") - stackBusy = true - eq(g:speedCategory(), "overworld", "menu over the field inherits overworld") + Stack.push("start", StartMenu) + eq(g:speedCategory(), "menu", "an explicit field menu owns MENU SPEED") g:keypressed("1") - eq(g.options.speedOverworld, 2, "1 with a field menu open bumps OVERWORLD SPEED") + eq(g.options.speedOverworld, 2, "1 with a field menu open syncs OVERWORLD SPEED") eq(g.options.speedMenu, 2, "1 with a field menu open syncs MENU SPEED") eq(g.options.speedBattle, 2, "and syncs BATTLE SPEED") - stackBusy = false + Stack.pop("start") eq(g:logicSpeed(), 2, "walk speed sticks after the menu closes") battleActive = true eq(g:speedCategory(), "battle", "battle stays battle") diff --git a/tests/engine/game3_surf_bridge_bug2617_test.lua b/tests/engine/game3_surf_bridge_bug2617_test.lua new file mode 100644 index 00000000..c224b7d2 --- /dev/null +++ b/tests/engine/game3_surf_bridge_bug2617_test.lua @@ -0,0 +1,146 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq +love = love or require("tests.love_stub") + +local GameVersion = require("src.core.GameVersion") +local MB = require("src.core.game3.mb") +local Collision = require("src.core.game3.collision") +local InteractionScripts = require("src.core.game3.scripting.interaction_scripts") +local session = { version = "emerald", map = "SURF_BRIDGE_TEST" } +local stops = 0 +package.loaded["src.core.game3.runtime"] = { getSession = function() return session end } +package.loaded["src.core.game3.audio"] = { stopSurfMusic = function() stops = stops + 1 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 game = { save = { position = {} } } + +local function setup(version, cells, surfing, elevation) + GameVersion.set(version) + session.version = version + local layout = { width = #cells, height = 1, pair = "surf_bridge_test" } + function layout:collAt(x) return cells[x + 1][1] end + function layout:elevAt(x) return cells[x + 1][2] end + function layout:midAt(x) return x end + function layout:collArray() + local coll = {} + for i, cell in ipairs(cells) do coll[i] = cell[1] end + return coll + end + local behaviors = {} + for i, cell in ipairs(cells) do behaviors[i - 1] = cell[3] or MB.NORMAL end + InteractionScripts.behaviors[layout.pair] = behaviors + Collision.bindMap(game, session.map, { midLayout = layout, pair = layout.pair, warps = {} }) + game.world = nil + Player.reset(0, 0, "right") + Player.surfing = surfing == true + Player.underwater = false + Player.elevation = elevation or cells[1][2] + Player.currentElevation = Player.elevation + stops = 0 +end + +local function settle() + Player._onStepDone = function() end + for _ = 1, 40 do + if not Player.moving then break end + Player.tick(game) + end + check(not Player.moving, "step completes") +end + +local function step(label) + eq(Player.tryMove("right", game, false), "step", label .. " accepted") + settle() +end + +local water = { 0x29, 1, MB.OCEAN_WATER } +local bridge15 = { 0, 15, MB.BRIDGE_OVER_OCEAN } +local bridge1 = { 0, 1, MB.BRIDGE_OVER_OCEAN } +local shore = { 0, 3, MB.NORMAL } + +-- pokeemerald/src/field_player_avatar.c:693 +setup("emerald", { water, bridge15, bridge1, water, shore }, true, 1) +step("water to multilevel bridge") +check(Player.surfing and not Player.dismounting, "retain_surf_under_bridge") +eq(Player.currentElevation, 1, "multilevel bridge keeps lower layer") +check(not Collision.isWater(1, 0), "bridge collision stays land for upper deck") +step("multilevel bridge to lower bridge") +check(Player.surfing, "retain_surf_on_elevation1_bridge") +step("bridge to water") +check(Player.surfing and Player.cellX == 3, "exit_bridge_to_water") +eq(stops, 0, "bridge traversal keeps Surf music") +eq(Player.tryMove("right", game, false), "step", "elevation3 shore accepted") +check(Player.surfing and Player.dismounting and Player.jumping, "shore starts Surf dismount hop") +eq(stops, 1, "shore stops Surf music once") +settle() +check(not Player.surfing and not Player.dismounting, "shore finishes on foot") +eq(Player.currentElevation, 3, "shore reaches default elevation") + +setup("emerald", { { 0, 4 }, bridge15, { 0, 4 } }, false, 4) +step("upper deck to multilevel bridge") +check(not Player.surfing and not Player.jumping, "upper deck remains walking") +eq(Player.currentElevation, 4, "upper deck keeps upper layer") +step("multilevel bridge to upper deck") +check(not Player.surfing, "upper deck never enables Surf") + +setup("emerald", { water, { 0x07, 15, MB.BRIDGE_OVER_OCEAN } }, true, 1) +eq(Player.tryMove("right", game, false), "blocked", "bridge support blocks Surf") +check(Player.surfing and not Player.moving and Player.cellX == 0, "support bump preserves Surf") + +setup("emerald", { water, { 0, 4 } }, true, 1) +local result, why = Player.tryMove("right", game, false) +eq(result, "blocked", "wrong elevation land rejects dismount") +eq(why, "elevation", "wrong elevation collision reason") +check(Player.surfing, "wrong elevation bump preserves Surf") + +setup("emerald", { water, shore }, true, 1) +game.world = { npcs = { { cellX = 1, cellY = 0, passable = false } } } +result, why = Player.tryMove("right", game, false) +eq(result, "blocked", "occupied shore rejects dismount") +eq(why, "entity", "occupied shore collision reason") +eq(stops, 0, "occupied shore keeps Surf music") + +-- pokeemerald/src/field_player_avatar.c:443 +setup("emerald", { water, bridge15 }, true, 1) +check(Player.forcedStep("right", 8), "forced bridge step accepted") +check(not Player.dismounting and not Player.jumping, "forced bridge step keeps Surf movement") +eq(Player.stepFrames, 8, "forced bridge retains requested speed") +settle() +check(Player.surfing, "forced_step_retains_surf_under_bridge") +eq(stops, 0, "forced bridge keeps Surf music") + +setup("emerald", { water, shore }, true, 1) +check(Player.forcedStep("right", 8), "forced shore step accepted") +check(Player.dismounting and Player.jumping, "forced shore starts dismount") +eq(Player.stepFrames, 16, "forced shore retains dismount timing") +settle() +check(not Player.surfing, "forced shore finishes on foot") + +setup("emerald", { water, bridge15 }, true, 1) +check(Player.forceStep("right"), "step without dismount setup accepted") +check(not Player.dismounting, "forceStep has no dismount setup") +settle() +check(Player.surfing, "finish_step_retains_surf_without_dismount") + +for _, version in ipairs({ "firered", "leafgreen" }) do + -- pokefirered/src/field_player_avatar.c:568 + setup(version, { water, { 0, 1, MB.PUDDLE }, { 0, 1, MB.SHALLOW_WATER }, water, shore }, true, 1) + for i = 1, 3 do + step(version .. " shallow step " .. i) + check(Player.surfing and not Player.dismounting, version .. " shallow water retains Surf") + end + step(version .. " shore") + check(not Player.surfing and Player.currentElevation == 3, version .. " shore dismount preserved") + eq(stops, 1, version .. " shore stops Surf music once") +end + +GameVersion.set("firered") +T.finish("game3_surf_bridge_bug2617_test") diff --git a/tests/engine/game_speed_categories_test.lua b/tests/engine/game_speed_categories_test.lua index 0cfbb990..9f8de241 100644 --- a/tests/engine/game_speed_categories_test.lua +++ b/tests/engine/game_speed_categories_test.lua @@ -22,7 +22,8 @@ local function stack(...) return { states = { ... } } end local battle = { isBattle = true } local overworld = { isOverworld = true } -local overlay = {} -- a party menu/choice box/naming screen/text box: no marker +local overlay = {} +local menu = { isMenu = true } eq(Game.speedCategoryInStack(nil), "menu", "a nil stack falls to menu") eq(Game.speedCategoryInStack(stack()), "menu", "an empty stack falls to menu") @@ -35,16 +36,20 @@ eq(Game.speedCategoryInStack(stack(battle)), "battle", "a battle alone resolves to battle") eq(Game.speedCategoryInStack(stack(overworld, overlay)), "overworld", - "a menu opened while walking inherits overworld") + "unmarked dialogue while walking inherits overworld") eq(Game.speedCategoryInStack(stack(battle, overlay)), "battle", - "a menu opened mid-battle inherits battle, not menu") + "unmarked battle dialogue inherits battle") eq(Game.speedCategoryInStack(stack(overworld, overlay, overlay)), "overworld", "the inheritance walk sees through more than one stacked overlay") eq(Game.speedCategoryInStack(stack(overworld, battle)), "battle", "a battle opened over the overworld reads as battle, not the overworld underneath it") eq(Game.speedCategoryInStack(stack(overworld, battle, overlay)), "battle", - "and a menu on top of THAT still reads as battle") + "dialogue on top of battle still reads as battle") +eq(Game.speedCategoryInStack(stack(overworld, menu)), "menu", + "an explicit field menu owns MENU SPEED") +eq(Game.speedCategoryInStack(stack(overworld, battle, menu, overlay)), "menu", + "dialogue over an explicit battle menu inherits MENU SPEED") -- ------- Game:_resolveLogicSpeed: category -> save.options key -> clamp diff --git a/tests/engine/gen2_orientation_bug2586_test.lua b/tests/engine/gen2_orientation_bug2586_test.lua new file mode 100644 index 00000000..b5e1a3c9 --- /dev/null +++ b/tests/engine/gen2_orientation_bug2586_test.lua @@ -0,0 +1,102 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path +love = require("tests.love_stub") +local T = require("tests.harness") +local SaveData = require("src.core.SaveData") +local Save = require("src.core.gen2.Save") +local Orientation = require("src.core.Orientation") +local Settings = require("src.import.LauncherSettings") +local OptionsMenu = require("src.ui.gen2.OptionsMenu") +local Game2 = require("src.core.Game2") +local StateStack = require("src.core.StateStack") +local realLoad, realSave = SaveData.loadOptions, SaveData.saveOptions +local realApply = Orientation.apply +local applied +Orientation.apply = function(mode) applied = mode; return true end +local function memfs() + local files = {} + return { + write = function(path, content) files[path] = content; return true end, + read = function(path) return files[path] end, + remove = function(path) files[path] = nil; return true end, + getInfo = function(path) return files[path] and { type = "file" } end, + } +end +local function orientationRow(model) + for _, section in ipairs(model.sections) do + for _, row in ipairs(section.rows) do + if row.label == "ORIENTATION" then return row end + end + end +end +for _, osName in ipairs({ "Android", "iOS", "OS X" }) do + love.system.getOS = function() return osName end + for _, version in ipairs({ "red", "yellow", "gold", "silver", "crystal", "firered", "emerald" }) do + local fs = memfs() + local seed = SaveData.defaultOptions() + seed.orientation = "landscape" + seed.gold = { orientation = "portrait", textSpeed = "FAST" } + realSave(seed, fs) + SaveData.loadOptions = function() return realLoad(fs) end + SaveData.saveOptions = function(opts) return realSave(opts, fs) end + local model = Settings.open({}, version) + local row = orientationRow(model) + T.eq(row ~= nil, osName ~= "OS X", osName .. " " .. version .. " row visibility") + if row then + T.eq(row.value(), Orientation.modeLabel("landscape"), version .. " launcher reads flat orientation") + applied = nil + T.eq(row.step(1), true, version .. " orientation row steps") + T.eq(applied, "reverseLandscape", version .. " launcher applies selection live") + T.eq(model.opts.orientation, "reverseLandscape", version .. " launcher writes shared root") + T.eq(model.opts.gold.orientation, "portrait", version .. " launcher ignores stale local copy") + model.save() + T.eq(realLoad(fs).orientation, "reverseLandscape", version .. " selection persists flat") + T.eq(Save.loadOptions(fs).orientation, "reverseLandscape", version .. " shared value loads into Gen2") + end + SaveData.loadOptions, SaveData.saveOptions = realLoad, realSave + end + local options = Save.defaultOptions() + options.orientation = "landscape" + local stack = setmetatable({}, { __index = StateStack }) + stack:init() + local game = { options = options, stack = stack } + local menu = OptionsMenu.new(game, { options = options }) + stack:push(menu) + local row + for _, item in ipairs(menu.rows) do if item.id == "orientation" then row = item end end + T.eq(row ~= nil, osName ~= "OS X", osName .. " in-game orientation visibility") + if row then + local focused = menu:focusRow("orientation") + T.check(focused ~= nil and focused ~= menu, osName .. " orientation lives in VIDEO group") + T.eq(focused:row().id, "orientation", osName .. " group focuses orientation") + applied = nil + focused:cycle(row, 1) + T.eq(options.orientation, "reverseLandscape", osName .. " actual menu cycles shared options") + T.eq(applied, "reverseLandscape", osName .. " actual menu applies selection live") + end +end +SaveData.loadOptions, SaveData.saveOptions = realLoad, realSave +for _, value in ipairs({ "landscape", "reverseLandscape", "bogus" }) do + local fs = memfs() + local seed = SaveData.defaultOptions() + seed.orientation = value + seed.gold = { orientation = "portrait", textSpeed = "FAST" } + realSave(seed, fs) + local options = Save.loadOptions(fs) + T.eq(options.orientation, Orientation.normalize(value), "root " .. value .. " wins with normalization") + options.orientation = "portrait" + T.check(Save.saveOptions(options, fs), "Gen2 orientation persistence returns success") + local written = realLoad(fs) + T.eq(written.orientation, "portrait", "Gen2 writes host orientation flat") + T.eq(written.gold.orientation, nil, "Gen2 write removes stale local orientation") + T.eq(written.gold.textSpeed, "FAST", "Gen2 cartridge option remains local") + T.eq(Save.loadOptions(fs).orientation, "portrait", "Gen2 host orientation round-trips") +end +T.eq(Save.loadOptions(memfs()).orientation, "auto", "missing Gen2 orientation defaults to AUTO") +love.system.getOS = function() return "OS X" end +local game = setmetatable({ options = Save.defaultOptions() }, { __index = Game2 }) +game.options.orientation = "reverseLandscape" +applied = nil +game:applyOptions() +T.eq(applied, "reverseLandscape", "Game2 applies the loaded host orientation") +Orientation.apply = realApply +T.finish("Gen2 orientation bug 2586") diff --git a/tests/engine/gen2_rocket_map_music_bug1385.lua b/tests/engine/gen2_rocket_map_music_bug1385.lua index fb6d638b..088d4bff 100644 --- a/tests/engine/gen2_rocket_map_music_bug1385.lua +++ b/tests/engine/gen2_rocket_map_music_bug1385.lua @@ -63,6 +63,58 @@ eq(World.mapMusicLabel(audio, 72, true, true), nil, eq(World.mapMusicLabel(audio, nil, true, true), nil, "a missing music byte resolves to nothing") +local function newWorld(engineFlagOrder) + local maps = { MAHOGANY_MART_1F = { music = MUSIC_MAHOGANY_MART } } + for floor = 1, 5 do + maps["RADIO_TOWER_" .. floor .. "F"] = { music = RADIO_TOWER_SENTINEL } + end + return setmetatable({ + constants = { engineFlagOrder = engineFlagOrder }, + maps = maps, + game = { save = { engineFlags = {} }, data = { audio = audio } }, + }, { __index = World }) +end + +-- pokecrystal/constants/engine_flags.asm:25 +local crystalOrder = { + [19] = "ENGINE_SAFARI_ZONE", + [20] = "ENGINE_ROCKETS_IN_RADIO_TOWER", + [23] = "ENGINE_REACHED_GOLDENROD", + [24] = "ENGINE_ROCKETS_IN_MAHOGANY", +} +for _, version in ipairs({ + { name = "Gold", tower = 18, mahogany = 22 }, + { name = "Crystal", tower = 19, mahogany = 23, order = crystalOrder }, +}) do + local world = newWorld(version.order) + world.game.save.engineFlags[version.tower] = true + world.game.save.engineFlags[version.mahogany] = true + for floor = 1, 5 do + local mapId = "RADIO_TOWER_" .. floor .. "F" + eq(world:mapMusicSong(mapId), "Music_RocketTheme", + version.name .. " takeover uses its own engine flag on " .. mapId) + end + eq(world:mapMusicSong("MAHOGANY_MART_1F"), "Music_RocketHideout", + version.name .. " Mahogany Rocket state uses its own engine flag") + + world.game.save.engineFlags = {} + for floor = 1, 5 do + local mapId = "RADIO_TOWER_" .. floor .. "F" + eq(world:mapMusicSong(mapId), "Music_GoldenrodCity", + version.name .. " cleared takeover restores city music on " .. mapId) + end + eq(world:mapMusicSong("MAHOGANY_MART_1F"), "Music_CherrygroveCity", + version.name .. " cleared Mahogany restores Cherrygrove music") +end + +local crystal = newWorld(crystalOrder) +crystal.game.save.engineFlags[18] = true +crystal.game.save.engineFlags[22] = true +eq(crystal:mapMusicSong("RADIO_TOWER_1F"), "Music_GoldenrodCity", + "Crystal Safari flag does not start Radio Tower takeover music") +eq(crystal:mapMusicSong("MAHOGANY_MART_1F"), "Music_CherrygroveCity", + "Crystal reached-Goldenrod flag does not start Mahogany Rocket music") + local data = { audio = { songs = { Music_RocketHideout = { file = "rocket_hideout.wav" }, diff --git a/tests/engine/menu_speed_bug2578_test.lua b/tests/engine/menu_speed_bug2578_test.lua new file mode 100644 index 00000000..6c33f8ca --- /dev/null +++ b/tests/engine/menu_speed_bug2578_test.lua @@ -0,0 +1,222 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path +love = require("tests.love_stub") + +local T = require("tests.harness") +local Game = require("src.core.Game") +local Game2 = require("src.core.Game2") +local Game3 = require("src.core.Game3") +local StateStack = require("src.core.StateStack") +local FixedStep = require("src.core.FixedStep") +local Stack3 = require("src.ui.game3.stack") +local battleActive = false +package.loaded["src.core.game3.battle"] = { isActive = function() return battleActive end } + +local g = setmetatable({ + save = { party = {}, player = { name = "RED" }, inventory = {}, + flags = {}, pcItems = {}, pokedex = { seen = {}, owned = {} }, + options = { speedOverworld = 4, speedBattle = 10, speedMenu = 2 } }, + data = { pokemon = {}, items = {}, maps = {}, text = {}, field = {} }, + input = { wasPressed = function() return false end }, +}, { __index = Game }) +g.stack = setmetatable({}, { __index = StateStack }) +g.stack:init() +local overworld, battle = { isOverworld = true }, { isBattle = true } +g.stack:push(overworld) + +g.data.pokemon.PIKACHU = { id = "PIKACHU", dex = 25 } +local constructors = { + StartMenu = {}, PartyMenu = {}, BagMenu = {}, OptionsMenu = {}, + PokedexMenu = {}, TrainerCard = {}, BoxMenu = {}, PlayerPC = {}, + LeaguePC = {}, NamingScreen = {}, FlyMenu = {}, TownMap = {}, + DexEntryMenu = { "PIKACHU" }, SummaryMenu = { { species = "PIKACHU", moves = {} } }, + PrizeCounter = { {} }, +} +for name, args in pairs(constructors) do + local screen = require("src.ui." .. name).new(g, unpack(args)) + T.eq(screen.isMenu, true, name .. " returned instance is a menu owner") + g.stack:push(screen) + T.eq(g:logicSpeed(), 2, name .. " uses MENU SPEED over the overworld") + g.stack:push({}) + T.eq(g:logicSpeed(), 2, name .. " nested dialogue inherits MENU SPEED") + g.stack:pop() + g.stack:pop() + T.eq(g:logicSpeed(), 4, name .. " pop restores OVERWORLD SPEED") +end + +local shop = require("src.ui.ShopMenu").new(g, {}) +T.eq(shop.isMenu, true, "ShopMenu actual returned generic instance owns MENU SPEED") +T.eq(Game.speedCategoryInStack({ states = { overworld, shop, {} } }), "menu", + "shop greetings and nested item lists inherit the concrete menu owner") + +local Menu = require("src.ui.Menu") +local OW = require("src.world.OverworldController") +local owGameIndex, owGame +for i = 1, 100 do + local name, value = debug.getupvalue(OW.openPC, i) + if not name then break end + if name == "Game" then owGameIndex, owGame = i, value; break end +end +assert(owGameIndex, "openPC Game binding absent") +debug.setupvalue(OW.openPC, owGameIndex, g) +local baseInput, pressed = g.input, nil +g.input = { wasPressed = function(_, key) return pressed == key end, + isDown = function() return false end } +local function tap(screen, key) + pressed = key + screen:update(1 / 60) + pressed = nil +end +local function advanceToMenu() + for _ = 1, 600 do + local top = g.stack:top() + if getmetatable(top) == Menu then return top end + tap(top, "a") + end + error("actual interactive PC menu was not reached") +end + +OW:openPC() +T.eq(g:logicSpeed(), 4, "PC opening text retains OVERWORLD SPEED") +local pc = advanceToMenu() +T.eq(#pc.items >= 3 and pc.noSound, true, "actual main PC chooser is reached") +T.eq(pc.isMenu, true, "main PC chooser concrete instance owns MENU SPEED") +T.eq(Game.speedCategoryInStack(g.stack), "menu", "main PC chooser category is menu") +T.eq(g:logicSpeed(), 2, "actual main PC chooser uses MENU SPEED") +pc.index = 2 +tap(pc, "a") +T.eq(g.stack:top().isTextBox, true, "main PC chooser opens actual player PC access text") +T.eq(g:logicSpeed(), 2, "player PC access text inherits MENU SPEED") +local playerPC = advanceToMenu() +T.eq(playerPC ~= pc and #playerPC.items == 4, true, "actual player PC submenu is reached") +T.eq(g:logicSpeed(), 2, "player PC submenu uses MENU SPEED") +tap(playerPC, "b") +T.eq(g.stack:top(), pc, "player PC back returns the same main chooser") +T.eq(g:logicSpeed(), 2, "returned main PC chooser retains MENU SPEED") +tap(pc, "b") +T.eq(g.stack:top(), overworld, "main PC cancel returns to field") +T.eq(g:logicSpeed(), 4, "main PC cancel restores OVERWORLD SPEED") + +for i, id in ipairs({ "EEVEE", "FLAREON", "JOLTEON", "VAPOREON" }) do + g.data.pokemon[id] = { id = id, name = id, dex = 132 + i } +end +OW:billsHousePokemonList() +T.eq(g:logicSpeed(), 4, "Bill viewer opening text retains OVERWORLD SPEED") +local bill = advanceToMenu() +T.eq(#bill.items == 5 and bill.items[1].keepOpen, true, "actual Bill PC viewer list is reached") +T.eq(bill.isMenu, true, "Bill PC viewer concrete instance owns MENU SPEED") +T.eq(Game.speedCategoryInStack(g.stack), "menu", "Bill PC viewer category is menu") +T.eq(g:logicSpeed(), 2, "actual Bill PC viewer uses MENU SPEED") +tap(bill, "a") +T.eq(getmetatable(g.stack:top()), require("src.ui.DexEntryMenu"), + "Bill viewer selection opens the actual DexEntryMenu") +T.eq(g.save.pokedex.seen.EEVEE, true, "Bill viewer preserves the selected species seen flag") +T.eq(g:logicSpeed(), 2, "Bill viewer nested DexEntryMenu uses MENU SPEED") +for _ = 1, 600 do + if g.stack:top() == bill then break end + tap(g.stack:top(), "b") +end +T.eq(g.stack:top(), bill, "DexEntryMenu back returns the same Bill viewer") +T.eq(g:logicSpeed(), 2, "returned Bill PC viewer retains MENU SPEED") +tap(bill, "b") +T.eq(g.stack:top(), overworld, "Bill viewer cancel returns to field") +T.eq(g:logicSpeed(), 4, "Bill viewer cancel restores OVERWORLD SPEED") + +g.input = baseInput +debug.setupvalue(OW.openPC, owGameIndex, owGame) +local generic = Menu.new(g, { { label = "CHOICE" } }) +T.eq(generic.isMenu, nil, "generic Menu remains transparent") +g.stack:push(generic) +T.eq(g:logicSpeed(), 4, "unowned generic choice retains OVERWORLD SPEED") +g.stack:pop() + +for _, name in ipairs({ "PartyMenu", "BagMenu" }) do + g.stack:push(battle) + local screen = require("src.ui." .. name).new(g, { battle = { playerParty = {} } }) + g.stack:push(screen) + T.eq(g:logicSpeed(), 2, "battle " .. name .. " uses MENU SPEED") + g.stack:pop() + T.eq(g:logicSpeed(), 10, "battle " .. name .. " pop restores BATTLE SPEED") + g.stack:push({}) + T.eq(g:logicSpeed(), 10, "battle dialogue stays at BATTLE SPEED") + g.stack:pop() + g.stack:pop() +end + +local party = require("src.ui.PartyMenu").new(g) +g.stack:push(party) +local function ticks(speed) + g.save.options.speedMenu = speed + party.blink = 0 + FixedStep.clock = function() return 0 end + FixedStep.refreshPeriod = nil + FixedStep:init(function(dt) party:update(dt) end) + local multiplier = g:logicSpeed() + FixedStep.maxAccum = FixedStep.catchupLimit(multiplier, 1 / 60) + for _ = 1, 60 do FixedStep:update(1 / 60, multiplier) end + return party.blink +end +T.eq(ticks(1), 60, "real party advances 60 logic ticks per second at MENU SPEED 1") +T.eq(ticks(4), 240, "real party advances 240 logic ticks per second at MENU SPEED 4") +g.speedOverride = 20 +T.eq(g:logicSpeed(), 20, "run override still wins over a menu") +g.linkSession = true +T.eq(g:logicSpeed(), 1, "link lock still wins over menu and override") +g.linkSession = nil +g.stack:push({ isFixedSpeed = true }) +T.eq(g:logicSpeed(), 1, "minigame lock still wins over menu and override") +g.stack:pop() +g.stack:pop() + +local g3 = setmetatable({ phase = "field", + options = { speedOverworld = 4, speedBattle = 10, speedMenu = 2 } }, { __index = Game3 }) +Stack3.clear() +for _, name in ipairs({ "start_menu", "party_menu", "bag_menu", "option_menu", "pokedex", + "trainer_card", "summary_menu", "box_storage_ui", "pc_menu", "item_pc", + "hall_of_fame_pc", "naming", "region_map", "tm_case", "berry_pouch", "save_menu", + "fame_checker", "controls_menu", "mod_manager", "rse.option_menu", "rse.item_storage", + "rse.mailbox", "shop_menu", "daycare_menu", "prize_corner", "move_relearner", "easy_chat", + "rse.move_relearner", "rse.pokeblock_case", "rse.berry_tag", "rse.frontier_pass", "rse.pyramid_bag" }) do + local mod = require("src.ui.game3." .. name) + Stack3.push(name, mod, { fullscreen = false }) + T.eq(g3:speedCategory(), "menu", name .. " owns the actual non-fullscreen layer") + T.eq(g3:logicSpeed(), 2, name .. " uses MENU SPEED") + Stack3.push("dialogue", {}, { fullscreen = true }) + T.eq(g3:logicSpeed(), 2, name .. " nested text inherits menu speed") + Stack3.pop("dialogue") + Stack3.pop(name) + T.eq(g3:logicSpeed(), 4, name .. " pop restores field speed") +end +for _, name in ipairs({ "pokedex", "region_map", "pokenav.init" }) do + local mod = require("src.ui.game3.rse." .. name).Host + Stack3.push(name, mod) + T.eq(g3:logicSpeed(), 2, "RSE " .. name .. " actual delegated Host owns MENU SPEED") + Stack3.pop(name) +end +for _, name in ipairs({ "party_menu", "bag_menu" }) do + battleActive = true + Stack3.push(name, require("src.ui.game3." .. name), { fullscreen = false }) + T.eq(g3:logicSpeed(), 2, "Gen3 battle " .. name .. " uses MENU SPEED") + Stack3.pop(name) + T.eq(g3:logicSpeed(), 10, "Gen3 battle " .. name .. " pop restores BATTLE SPEED") +end +Stack3.push("battle_message", {}, { fullscreen = true }) +T.eq(g3:logicSpeed(), 10, "Gen3 unmarked fullscreen battle text stays at BATTLE SPEED") +Stack3.clear() +battleActive = false +Stack3.push("scene", {}, { fullscreen = true }) +T.eq(g3:logicSpeed(), 4, "Gen3 unmarked field scene stays at OVERWORLD SPEED") +Stack3.clear() +Stack3.push("party", require("src.ui.game3.party_menu")) +g3.speedOverride = 20 +T.eq(g3:logicSpeed(), 20, "Gen3 run override wins over menu") +package.loaded["src.core.game3.link"] = { link = {} } +T.eq(g3:logicSpeed(), 1, "Gen3 link lock wins over menu and override") +package.loaded["src.core.game3.link"] = nil +package.loaded["src.core.game3.minigames.common"] = { isActive = function() return true end } +T.eq(g3:logicSpeed(), 1, "Gen3 minigame lock wins over menu and override") +package.loaded["src.core.game3.minigames.common"] = nil +Stack3.clear() + +local g2 = setmetatable({ options = { speed = 4, speedOverworld = 1, speedMenu = 10 } }, { __index = Game2 }) +T.eq(g2:logicSpeed(), 4, "Gen2 retains legacy GAME SPEED") +T.finish("menu speed bug 2578") diff --git a/tests/gen2_menus_test.lua b/tests/gen2_menus_test.lua index 8873060f..be6b3608 100644 --- a/tests/gen2_menus_test.lua +++ b/tests/gen2_menus_test.lua @@ -366,7 +366,7 @@ local options = OptionsMenu.new(optionsGame, { }) -- The cart's seven rows, then the port's: CONTROLS, audio, PERFORMANCE, -- speed, display, SHADER FX + SHADER FX 2 (the second slot added alongside -check("thirty-five rows", #OptionsMenu.ROWS, 35) +check("thirty-six row descriptors", #OptionsMenu.ROWS, 36) check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame") check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls") check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol") @@ -378,6 +378,17 @@ local function hasRow(rows, key) for _, row in ipairs(rows) do if row.key == key then return true end end return false end +check("ORIENTATION is a descriptor", hasRow(OptionsMenu.ROWS, "orientation"), true) +do + local oldSystem = love.system + for _, osName in ipairs({ "Linux", "Android", "iOS" }) do + love.system = { getOS = function() return osName end } + local platformOptions = OptionsMenu.new(newGame(Save.newGame())) + check(osName .. " orientation row visibility", + hasRow(platformOptions.rows, "orientation"), osName ~= "Linux") + end + love.system = oldSystem +end check("PRINT is still a descriptor", hasRow(OptionsMenu.ROWS, "print"), true) check("but never reaches the screen", hasRow(options.rows, "print"), false) check("and the save keeps its value",