mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
CLOSES #806, CLOSES #809, CLOSES #853, CLOSES #854, CLOSES #860, CLOSES #862, CLOSES #865, CLOSES #866
This commit is contained in:
+53
-1
@@ -30,6 +30,15 @@ local SaveData = {}
|
||||
-- deliberately shared across versions (it holds global preferences and the
|
||||
-- mod enable-state, not per-playthrough data).
|
||||
local OPTIONS_FILENAME = "options.lua"
|
||||
-- #828: options.lua is rewritten whole on every write (see saveOptions), and
|
||||
-- unlike the progress files it had no staged copy, so a write interrupted
|
||||
-- between the truncate and the flush -- the process replaced by
|
||||
-- HostShell.restart on the way back to the launcher, an Android
|
||||
-- external-storage volume that never flushed -- left a truncated or empty
|
||||
-- file that loadOptions could only answer with defaults: every setting
|
||||
-- "reset" at once. Same .bak/.tmp witness names the save files use.
|
||||
local OPTIONS_BACKUP_FILENAME = OPTIONS_FILENAME .. ".bak"
|
||||
local OPTIONS_TMP_FILENAME = OPTIONS_FILENAME .. ".tmp"
|
||||
|
||||
-- Main / backup / staged-witness names for a version (defaults to the active
|
||||
-- one). The backup is a rolling copy and .tmp is the staged-write witness;
|
||||
@@ -305,6 +314,13 @@ function SaveData.defaultOptions()
|
||||
-- layout (#633). Pre-#633 files stored one top-level positions table;
|
||||
-- TouchControls.normalizeConfig folds it into both orientations on load.
|
||||
touchControls = { enabled = true },
|
||||
-- Haptic feedback level for on-screen pad presses (#806):
|
||||
-- off | light | medium | heavy, mapped to a love.system.vibrate
|
||||
-- duration in src/core/TouchControls.lua. LIGHT by default, like the
|
||||
-- overlay itself defaulting on, so an options.lua predating this key
|
||||
-- gets the tick without going looking for the row. Inert wherever the
|
||||
-- overlay never appears (desktop) or LOVE has no vibrator.
|
||||
haptics = "light",
|
||||
}
|
||||
end
|
||||
|
||||
@@ -369,7 +385,21 @@ function SaveData.saveOptions(opts, fs)
|
||||
opts.modOptions = merged
|
||||
end
|
||||
local encoded = SaveSerializer.encode(opts)
|
||||
local ok, err = fs.write(OPTIONS_FILENAME, encoded)
|
||||
-- Stage the new bytes and roll the last good file aside BEFORE the main
|
||||
-- write truncates it, the same tmp/bak dance SaveData.save uses for
|
||||
-- progress: whatever ends the process mid-write, one of the three copies
|
||||
-- is complete and loadOptions promotes it instead of falling back to
|
||||
-- defaults (#828).
|
||||
local ok, err = fs.write(OPTIONS_TMP_FILENAME, encoded)
|
||||
if not ok then
|
||||
Logger.error("options save failed: %s", tostring(err))
|
||||
return nil
|
||||
end
|
||||
local prev = fs.getInfo(OPTIONS_FILENAME) and fs.read(OPTIONS_FILENAME)
|
||||
if type(prev) == "string" and prev ~= "" and prev ~= encoded then
|
||||
fs.write(OPTIONS_BACKUP_FILENAME, prev)
|
||||
end
|
||||
ok, err = fs.write(OPTIONS_FILENAME, encoded)
|
||||
if not ok then
|
||||
Logger.error("options save failed: %s", tostring(err))
|
||||
return nil
|
||||
@@ -387,6 +417,8 @@ function SaveData.saveOptions(opts, fs)
|
||||
#encoded, type(wrote) == "string" and tostring(#wrote) or "nothing")
|
||||
return nil
|
||||
end
|
||||
-- the staged witness has served its purpose; the main file is verified
|
||||
remove(fs, OPTIONS_TMP_FILENAME)
|
||||
return opts
|
||||
end
|
||||
|
||||
@@ -397,6 +429,26 @@ function SaveData.loadOptions(fs)
|
||||
if fs.getInfo(OPTIONS_FILENAME) then
|
||||
Logger.error("options load failed: %s", tostring(err))
|
||||
end
|
||||
-- #828: answering defaults here is what "closing the game reset all my
|
||||
-- settings" looked like -- one interrupted whole-file rewrite and every
|
||||
-- preference, the mod enable-state and the slot registry were gone.
|
||||
-- Promote the staged copy, then the rolled-aside backup, exactly as
|
||||
-- SaveData.load does for progress, and heal the main file from whichever
|
||||
-- one parsed.
|
||||
local recovered = readTable(fs, OPTIONS_TMP_FILENAME)
|
||||
local from = "tmp"
|
||||
if not recovered then
|
||||
recovered = readTable(fs, OPTIONS_BACKUP_FILENAME)
|
||||
from = "bak"
|
||||
end
|
||||
if recovered then
|
||||
Logger.warn("options.lua %s; recovered from %s copy",
|
||||
fs.getInfo(OPTIONS_FILENAME) and "corrupt" or "missing", from)
|
||||
if fs.write then
|
||||
fs.write(OPTIONS_FILENAME, SaveSerializer.encode(recovered))
|
||||
end
|
||||
return SaveData.mergeOptions(recovered)
|
||||
end
|
||||
return SaveData.defaultOptions()
|
||||
end
|
||||
return SaveData.mergeOptions(data)
|
||||
|
||||
@@ -85,6 +85,55 @@ local function clampScale(v)
|
||||
return v
|
||||
end
|
||||
|
||||
-- Haptic feedback (#806): a short vibration the instant a control takes a GB
|
||||
-- button, the way every mobile emulator front-end does it -- the pad has no
|
||||
-- edges under a thumb, so the buzz is the only confirmation a press landed.
|
||||
-- Persisted as options.haptics (src/core/SaveData.lua defaultOptions), NOT
|
||||
-- under options.touchControls: TouchControls:config() is the launcher
|
||||
-- editor's save snapshot and only emits enabled + layouts, so a nested key
|
||||
-- would be dropped on every editor save.
|
||||
-- love.system.vibrate takes a duration and nothing else, so "intensity" is a
|
||||
-- duration preset: Android runs the platform vibrator for exactly that long,
|
||||
-- while iOS ignores the duration and fires the fixed system vibration, so
|
||||
-- there the three levels all read as simply on.
|
||||
TouchControls.HAPTICS = { "off", "light", "medium", "heavy" }
|
||||
TouchControls.HAPTIC_DEFAULT = "light"
|
||||
|
||||
local HAPTIC_SECONDS = { off = 0, light = 0.012, medium = 0.025, heavy = 0.045 }
|
||||
local HAPTIC_LABELS = {
|
||||
off = "OFF", light = "LIGHT", medium = "MEDIUM", heavy = "HEAVY",
|
||||
}
|
||||
|
||||
function TouchControls.normalizeHaptics(level)
|
||||
if HAPTIC_SECONDS[level] then return level end
|
||||
return TouchControls.HAPTIC_DEFAULT
|
||||
end
|
||||
|
||||
function TouchControls.hapticLabel(level)
|
||||
return HAPTIC_LABELS[TouchControls.normalizeHaptics(level)]
|
||||
end
|
||||
|
||||
function TouchControls.cycleHaptics(level, dir)
|
||||
local cur, idx = TouchControls.normalizeHaptics(level), 1
|
||||
for i, m in ipairs(TouchControls.HAPTICS) do
|
||||
if m == cur then idx = i break end
|
||||
end
|
||||
local n = #TouchControls.HAPTICS
|
||||
return TouchControls.HAPTICS[(idx - 1 + (dir or 1)) % n + 1]
|
||||
end
|
||||
|
||||
-- One pulse at the given level. Feature-guarded rather than platform-gated:
|
||||
-- love.system.vibrate is a no-op on desktop and absent from the headless love
|
||||
-- stubs, so the press path below stays identical everywhere and the tests
|
||||
-- never reach a vibrator.
|
||||
function TouchControls.buzz(level)
|
||||
local secs = HAPTIC_SECONDS[TouchControls.normalizeHaptics(level)]
|
||||
if not secs or secs <= 0 then return false end
|
||||
if not (love and love.system and love.system.vibrate) then return false end
|
||||
pcall(love.system.vibrate, secs)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Copy a persisted positions table, dropping unknown / non-numeric entries.
|
||||
-- Always a fresh table: two orientations seeded from the same pre-#633
|
||||
-- layout must not alias, or dragging one would still move the other.
|
||||
@@ -174,6 +223,10 @@ end
|
||||
function TouchControls:init()
|
||||
self.active = wantsOverlay()
|
||||
self.enabled = true
|
||||
-- vibration level for presses (#806); applyOptions overwrites it from
|
||||
-- options.haptics, this is the value a harness that never applies options
|
||||
-- runs with
|
||||
self.haptics = TouchControls.HAPTIC_DEFAULT
|
||||
-- per-orientation buckets (#633); self.positions / self.scale mirror the
|
||||
-- one currently on screen so layout(), the editor and the tests keep a
|
||||
-- single lookup
|
||||
@@ -210,6 +263,9 @@ end
|
||||
function TouchControls:applyOptions(opts)
|
||||
local cfg = TouchControls.normalizeConfig(opts and opts.touchControls)
|
||||
self.enabled = cfg.enabled
|
||||
-- haptics is a plain top-level option, not part of the layout config the
|
||||
-- launcher editor round-trips through config() (#806)
|
||||
self.haptics = TouchControls.normalizeHaptics(opts and opts.haptics)
|
||||
self.layouts = cfg.layouts
|
||||
self.layoutW, self.layoutH = nil, nil
|
||||
self.layoutOx, self.layoutOy = nil, nil
|
||||
@@ -401,7 +457,14 @@ end
|
||||
local function pressBtn(self, btn)
|
||||
local n = (self.held[btn] or 0) + 1
|
||||
self.held[btn] = n
|
||||
if n == 1 then Input:overlayPressed(btn) end
|
||||
-- Buzz only on the 0 -> 1 edge, the same edge that presses the GB button:
|
||||
-- a second finger landing on a button that is already held, and a d-pad
|
||||
-- finger resting inside one direction, must not retrigger it. Sliding the
|
||||
-- d-pad to a new direction does, which is the point (#806).
|
||||
if n == 1 then
|
||||
Input:overlayPressed(btn)
|
||||
TouchControls.buzz(self.haptics)
|
||||
end
|
||||
end
|
||||
|
||||
local function releaseBtn(self, btn)
|
||||
|
||||
Reference in New Issue
Block a user