mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
CLOSES #592
This commit is contained in:
@@ -70,6 +70,10 @@ function love.conf(t)
|
||||
-- just work. FULL_SENSOR ignores the device's rotation lock, so
|
||||
-- GameActivity.setOrientationBis remaps it to FULL_USER after SDL has
|
||||
-- run: same orientations allowed, but auto-rotate being off now wins.
|
||||
-- A persisted ORIENTATION lock (#592) overrides all of this after boot:
|
||||
-- src/core/Orientation.lua sets SDL_HINT_ORIENTATIONS over the FFI and
|
||||
-- re-triggers the request, from main.lua for the launcher and from
|
||||
-- Game:applyOptions in game.
|
||||
-- iOS follows the Info.plist orientations
|
||||
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
|
||||
t.window.resizable = true
|
||||
|
||||
@@ -332,6 +332,17 @@ one used sideways. An `options.lua` from before this split keeps its single
|
||||
layout in both orientations until one of them is edited. In-game, Options →
|
||||
**TOUCH PAD** toggles the same on/off flag without leaving a play session.
|
||||
|
||||
## Screen orientation lock (Android)
|
||||
|
||||
Options → **ORIENTATION** (also in the launcher's gear menu) locks the
|
||||
screen to **PORTRAIT**, **LANDSCAPE** (either landscape, following the
|
||||
device), or **REVERSE LANDSCAPE**, or leaves it on **AUTO** (#592). AUTO
|
||||
allows every orientation but defers to the system: with auto-rotate turned
|
||||
off in Android's quick settings, the game stays put instead of following
|
||||
the sensor (#716). Changes apply immediately -- the screen rotates as the
|
||||
row is stepped -- and persist in `options.lua`. Android only: iOS follows
|
||||
the app's fixed orientation list, and desktop windows rotate nothing.
|
||||
|
||||
## Translation support
|
||||
|
||||
Every string the player can read is now reachable from a mod, so a
|
||||
|
||||
@@ -209,6 +209,13 @@ function love.load(args)
|
||||
end
|
||||
love.graphics.setDefaultFilter("nearest", "nearest")
|
||||
|
||||
-- Apply the persisted Android orientation lock (#592) before the launcher
|
||||
-- shows: SDL created the window with no orientation hint, so without this
|
||||
-- the launcher would rotate freely until Game:applyOptions runs at boot.
|
||||
-- No-op on desktop / iOS / when options.lua does not exist yet.
|
||||
require("src.core.Orientation").applyOptions(
|
||||
require("src.core.SaveData").loadOptions())
|
||||
|
||||
-- Standalone editor. A bare `--editor` run has no launcher behind it, so
|
||||
-- Close quits; --save points it at a specific file, otherwise it opens the
|
||||
-- default save path for POKEPORT_VERSION (Red unless overridden), whose
|
||||
|
||||
@@ -839,6 +839,8 @@ function Game:applyOptions(opts)
|
||||
-- returns true when a persisted GBC FX level was cleared on mobile
|
||||
local gbcCleared = require("src.render.GBCFX").applyOptions(opts)
|
||||
require("src.core.VideoMode").applyOptions(opts)
|
||||
-- Android orientation lock (#592); no-op everywhere else
|
||||
require("src.core.Orientation").applyOptions(opts)
|
||||
-- after VideoMode: a faithful-resolution lock is an exact window size, so
|
||||
-- it has to be the last word on the window (it drops fullscreen to hold)
|
||||
require("src.core.FaithfulRes").applyOptions(opts)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Screen orientation lock, Android only (#592, #716).
|
||||
--
|
||||
-- Persisted as options.orientation: "auto" | "portrait" | "landscape" |
|
||||
-- "reverseLandscape". The lock travels through SDL_HINT_ORIENTATIONS:
|
||||
-- SDLActivity.setOrientationBis parses the hint's space-separated names
|
||||
-- into a setRequestedOrientation call, and GameActivity's override then
|
||||
-- remaps any *_SENSOR result onto the matching *_USER constant, so a device
|
||||
-- with auto-rotate off stays put (#716). AUTO leaves the hint empty, which
|
||||
-- with a resizable window means "any orientation, deferring to the system
|
||||
-- rotation lock"; LANDSCAPE allows both landscapes (SENSOR_LANDSCAPE ->
|
||||
-- USER_LANDSCAPE); REVERSE LANDSCAPE is SDL's LandscapeRight alone.
|
||||
--
|
||||
-- SDL only re-reads the hint when the window is created or its resizable
|
||||
-- flag changes (SDL_androidwindow.c: Android_CreateWindow /
|
||||
-- Android_SetWindowResizable both call Android_JNI_SetOrientation). LOVE
|
||||
-- 11.5 exposes neither hints nor a resizable setter, so apply() goes through
|
||||
-- the FFI to SDL's C API: set the hint, then pulse the window's resizable
|
||||
-- flag off and back on -- each edge makes the Android backend recompute the
|
||||
-- requested orientation, so a change from the launcher or the OPTION menu
|
||||
-- takes hold immediately, and the flag ends where it started (conf.lua sets
|
||||
-- resizable on mobile). Everything is pcall-guarded: desktop, iOS (the
|
||||
-- Info.plist governs there) and headless stubs make this a no-op.
|
||||
|
||||
local Orientation = {}
|
||||
|
||||
Orientation.MODES = { "auto", "portrait", "landscape", "reverseLandscape" }
|
||||
Orientation.DEFAULT = "auto"
|
||||
|
||||
local LABELS = {
|
||||
auto = "AUTO",
|
||||
portrait = "PORTRAIT",
|
||||
landscape = "LANDSCAPE",
|
||||
reverseLandscape = "REVERSE LANDSCAPE",
|
||||
}
|
||||
|
||||
-- SDL_HINT_ORIENTATIONS values, exactly the names SDLActivity parses
|
||||
-- (SDLActivity.java setOrientationBis): "Portrait", "PortraitUpsideDown",
|
||||
-- "LandscapeLeft", "LandscapeRight". Both landscapes together promote to
|
||||
-- SENSOR_LANDSCAPE; LandscapeRight alone maps to REVERSE_LANDSCAPE.
|
||||
local HINTS = {
|
||||
auto = "",
|
||||
portrait = "Portrait",
|
||||
landscape = "LandscapeLeft LandscapeRight",
|
||||
reverseLandscape = "LandscapeRight",
|
||||
}
|
||||
|
||||
function Orientation.normalize(mode)
|
||||
if HINTS[mode] then return mode end
|
||||
return Orientation.DEFAULT
|
||||
end
|
||||
|
||||
function Orientation.modeLabel(mode)
|
||||
return LABELS[Orientation.normalize(mode)]
|
||||
end
|
||||
|
||||
function Orientation.isAndroid()
|
||||
if not love or not love.system or not love.system.getOS then return false end
|
||||
return love.system.getOS() == "Android"
|
||||
end
|
||||
|
||||
function Orientation.cycle(mode, dir)
|
||||
local cur, idx = Orientation.normalize(mode), 1
|
||||
for i, m in ipairs(Orientation.MODES) do
|
||||
if m == cur then idx = i break end
|
||||
end
|
||||
local n = #Orientation.MODES
|
||||
return Orientation.MODES[(idx - 1 + (dir or 1)) % n + 1]
|
||||
end
|
||||
|
||||
-- The SDL2 C API this module needs. cdef errors on redefinition, so run it
|
||||
-- once and remember whether it took; ffi itself may be absent (plain Lua
|
||||
-- test interpreters), hence the pcall'd require.
|
||||
local cdefOk = nil
|
||||
local function sdlFfi()
|
||||
local okFfi, ffi = pcall(require, "ffi")
|
||||
if not okFfi then return nil end
|
||||
if cdefOk == nil then
|
||||
cdefOk = pcall(ffi.cdef, [[
|
||||
typedef struct SDL_Window SDL_Window;
|
||||
int SDL_SetHint(const char *name, const char *value);
|
||||
SDL_Window *SDL_GL_GetCurrentWindow(void);
|
||||
void SDL_SetWindowResizable(SDL_Window *window, int resizable);
|
||||
]])
|
||||
end
|
||||
if not cdefOk then return nil end
|
||||
return ffi
|
||||
end
|
||||
|
||||
-- Push the mode into the live activity. Returns true when the hint reached
|
||||
-- SDL (the symbols resolved), false on any non-Android / stubbed platform.
|
||||
function Orientation.apply(mode)
|
||||
if not Orientation.isAndroid() then return false end
|
||||
local ffi = sdlFfi()
|
||||
if not ffi then return false end
|
||||
mode = Orientation.normalize(mode)
|
||||
local ok = pcall(function()
|
||||
-- "SDL_IOS_ORIENTATIONS" is SDL_HINT_ORIENTATIONS's name (SDL_hints.h);
|
||||
-- despite the IOS in the string, the Android backend reads it too.
|
||||
ffi.C.SDL_SetHint("SDL_IOS_ORIENTATIONS", HINTS[mode])
|
||||
local win = ffi.C.SDL_GL_GetCurrentWindow()
|
||||
if win ~= nil then
|
||||
ffi.C.SDL_SetWindowResizable(win, 0)
|
||||
ffi.C.SDL_SetWindowResizable(win, 1)
|
||||
end
|
||||
end)
|
||||
return ok
|
||||
end
|
||||
|
||||
function Orientation.applyOptions(opts)
|
||||
return Orientation.apply(opts and opts.orientation)
|
||||
end
|
||||
|
||||
return Orientation
|
||||
@@ -173,6 +173,25 @@ local function coreRows(opts)
|
||||
end)
|
||||
end
|
||||
|
||||
-- ORIENTATION (#592): Android only -- the lock rides SDL's orientation
|
||||
-- hint, which iOS reads only at startup (the Info.plist governs there) and
|
||||
-- desktop ignores. 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 osName = love.system and love.system.getOS and love.system.getOS()
|
||||
local okOr, Orientation = pcall(require, "src.core.Orientation")
|
||||
if okOr and osName == "Android" 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 okFr, FaithfulRes = pcall(require, "src.core.FaithfulRes")
|
||||
if okFr then
|
||||
add(Strings("FAITHFUL RATIO"),
|
||||
|
||||
@@ -19,6 +19,7 @@ local TileRenderer = require("src.render.TileRenderer")
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local VideoMode = require("src.core.VideoMode")
|
||||
local Orientation = require("src.core.Orientation")
|
||||
local FaithfulRes = require("src.core.FaithfulRes")
|
||||
local FrameCap = require("src.core.FrameCap")
|
||||
local Performance = require("src.core.Performance")
|
||||
@@ -350,6 +351,19 @@ local function buildRows(game)
|
||||
VideoMode.apply(o.videoMode)
|
||||
return true
|
||||
end },
|
||||
-- Android orientation lock (#592): AUTO / PORTRAIT / LANDSCAPE /
|
||||
-- REVERSE LANDSCAPE, live-applied through SDL's orientation hint.
|
||||
-- Filtered out below on everything that is not Android.
|
||||
{ id = "orientation", label = Strings("ORIENTATION"),
|
||||
value = function(g)
|
||||
return Strings(Orientation.modeLabel(g.save.options.orientation))
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
o.orientation = Orientation.cycle(o.orientation, dir)
|
||||
Orientation.apply(o.orientation)
|
||||
return true
|
||||
end },
|
||||
-- Lock the window to an exact 160x144 multiple, so the surface IS the
|
||||
-- Game Boy screen with no letterbox at all. Sits next to VIDEO MODE
|
||||
-- because it overrides it: holding an exact size means dropping
|
||||
@@ -432,6 +446,14 @@ local function buildRows(game)
|
||||
end
|
||||
rows = filtered
|
||||
end
|
||||
-- ORIENTATION only on Android, the one platform Orientation.apply reaches.
|
||||
if not Orientation.isAndroid() then
|
||||
local filtered = {}
|
||||
for _, row in ipairs(rows) do
|
||||
if row.id ~= "orientation" then filtered[#filtered + 1] = row end
|
||||
end
|
||||
rows = filtered
|
||||
end
|
||||
-- TOUCH PAD only where the overlay can appear (mobile, or desktop with
|
||||
-- POKEPORT_TOUCH=1). POKEPORT_TOUCH=0 forces it off everywhere.
|
||||
do
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
-- ORIENTATION lock (#592, #716): the option model.
|
||||
--
|
||||
-- The Android side (SDL hint parsing, GameActivity's *_SENSOR -> *_USER
|
||||
-- remap) can only be exercised on a device; what this tier pins down is the
|
||||
-- Lua contract every UI row leans on: the mode set, normalization of stale
|
||||
-- or garbage saves, the cycle order in both directions, and that apply() is
|
||||
-- a safe no-op anywhere that is not Android -- including here, where love
|
||||
-- is a headless stub and no SDL library is loaded.
|
||||
-- luajit tests/engine/orientation_option.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Orientation = require("src.core.Orientation")
|
||||
|
||||
local realOS = love.system and love.system.getOS
|
||||
love.system = love.system or {}
|
||||
|
||||
-- ------------------------------------------------------------- normalize
|
||||
|
||||
T.eq(Orientation.DEFAULT, "auto", "AUTO is the default")
|
||||
T.eq(Orientation.normalize(nil), "auto", "missing option reads as AUTO")
|
||||
T.eq(Orientation.normalize("sideways"), "auto", "garbage reads as AUTO")
|
||||
T.eq(Orientation.normalize("portrait"), "portrait", "valid modes pass through")
|
||||
T.eq(Orientation.normalize("reverseLandscape"), "reverseLandscape",
|
||||
"reverse landscape is a real mode")
|
||||
|
||||
-- ------------------------------------------------------------------ cycle
|
||||
|
||||
T.eq(Orientation.cycle("auto", 1), "portrait", "cycle forward from AUTO")
|
||||
T.eq(Orientation.cycle("reverseLandscape", 1), "auto", "cycle wraps forward")
|
||||
T.eq(Orientation.cycle("auto", -1), "reverseLandscape", "cycle wraps back")
|
||||
T.eq(Orientation.cycle(nil, 1), "portrait", "cycling a fresh save starts at AUTO")
|
||||
|
||||
-- one full lap forward touches every mode exactly once
|
||||
local seen, mode = {}, "auto"
|
||||
for _ = 1, #Orientation.MODES do
|
||||
seen[mode] = true
|
||||
mode = Orientation.cycle(mode, 1)
|
||||
end
|
||||
T.eq(mode, "auto", "a full lap returns to the start")
|
||||
for _, m in ipairs(Orientation.MODES) do
|
||||
T.eq(seen[m], true, "lap visits " .. m)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------- labels
|
||||
|
||||
for _, m in ipairs(Orientation.MODES) do
|
||||
T.eq(type(Orientation.modeLabel(m)), "string", m .. " has a label")
|
||||
T.eq(#Orientation.modeLabel(m) <= 17, true,
|
||||
m .. "'s label fits the OPTION box value line (17 cells at x=24)")
|
||||
end
|
||||
|
||||
-- -------------------------------------------------- apply() stays harmless
|
||||
|
||||
love.system.getOS = function() return "OS X" end
|
||||
T.eq(Orientation.apply("portrait"), false, "desktop apply is a refused no-op")
|
||||
love.system.getOS = function() return "iOS" end
|
||||
T.eq(Orientation.apply("portrait"), false, "iOS defers to the Info.plist")
|
||||
-- Android posed but no SDL loaded in this process: the FFI path must fail
|
||||
-- closed inside its pcall, never throw.
|
||||
love.system.getOS = function() return "Android" end
|
||||
local ok, err = pcall(Orientation.applyOptions, { orientation = "landscape" })
|
||||
T.eq(ok, true, "posed-Android apply never raises (" .. tostring(err) .. ")")
|
||||
|
||||
love.system.getOS = realOS
|
||||
Reference in New Issue
Block a user