This commit is contained in:
bryanthaboi
2026-08-03 16:38:07 -04:00
parent 45575a1fd8
commit b831076839
8 changed files with 244 additions and 0 deletions
+4
View File
@@ -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
+11
View File
@@ -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
+7
View File
@@ -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
+2
View File
@@ -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)
+113
View File
@@ -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
+19
View File
@@ -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"),
+22
View File
@@ -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
+66
View File
@@ -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