mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #1080 from MaxTomahawk/feat/device-date-time
feat(mods): add shared local date and time formatting
This commit is contained in:
@@ -449,3 +449,23 @@ platform-bridge mod bundled only with that build's launcher, for example).
|
||||
Neither hook needs a `Runtime.wantsHook` guard before calling it: `Hooks:call`
|
||||
already falls straight through to the vanilla function when no mod has
|
||||
wrapped the name, at negligible cost.
|
||||
|
||||
## Shared date and time presentation
|
||||
|
||||
The global Options menu owns `DATE FORMAT` (`DEVICE`, `DD-MM-YYYY`,
|
||||
`MM-DD-YYYY`, `YYYY-MM-DD`) and `TIME FORMAT` (`DEVICE`, `24 HOUR`, `12 HOUR`).
|
||||
These preferences live in `options.lua`, so checkpoint restore never rewinds
|
||||
them. `DEVICE` uses the process time locale when the platform provides one;
|
||||
the portable fallback is `DD-MM-YYYY` plus 24-hour time.
|
||||
|
||||
Mods format captured timestamps through the read-only public facade:
|
||||
|
||||
```lua
|
||||
local date = mod.datetime:date(game, createdAt)
|
||||
local time = mod.datetime:time(game, createdAt)
|
||||
local both = mod.datetime:dateTime(game, createdAt)
|
||||
```
|
||||
|
||||
The live `game` supplies only the current option context. Formatting never
|
||||
mutates the save, options, or timestamp, and invalid timestamps return
|
||||
`"----"`.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
-- Shared local date/time presentation for engine UI and mods. Preferences
|
||||
-- live in options.lua, never checkpoint progress. "device" uses the process
|
||||
-- time locale when the platform supplies one and otherwise falls back to the
|
||||
-- deterministic DD-MM-YYYY / 24-hour convention.
|
||||
|
||||
local DateTime = {}
|
||||
|
||||
local DATE_FORMATS = {
|
||||
dmy = "%d-%m-%Y",
|
||||
mdy = "%m-%d-%Y",
|
||||
ymd = "%Y-%m-%d",
|
||||
}
|
||||
local TIME_FORMATS = {
|
||||
["24h"] = "%H:%M",
|
||||
["12h"] = "%I:%M %p",
|
||||
}
|
||||
|
||||
local function validTimestamp(value)
|
||||
return type(value) == "number" and value == value and value >= 0
|
||||
and value ~= math.huge and value ~= -math.huge
|
||||
end
|
||||
|
||||
local function render(timestamp, pattern)
|
||||
local ok, value = pcall(os.date, pattern, math.floor(timestamp))
|
||||
if not ok or type(value) ~= "string" or value == "" then return nil end
|
||||
return value
|
||||
end
|
||||
|
||||
local function currentLocale()
|
||||
if not os.setlocale then return nil end
|
||||
local ok, value = pcall(os.setlocale, nil, "time")
|
||||
if not ok or type(value) ~= "string" then return nil end
|
||||
return value
|
||||
end
|
||||
|
||||
local function deviceAvailable(localeName)
|
||||
return type(localeName) == "string" and localeName ~= ""
|
||||
and localeName ~= "C" and localeName ~= "POSIX"
|
||||
end
|
||||
|
||||
function DateTime.formatWithLocale(timestamp, datePreference, timePreference, localeName)
|
||||
if not validTimestamp(timestamp) then return { date = "----", time = "----" } end
|
||||
local datePattern = DATE_FORMATS[datePreference]
|
||||
local timePattern = TIME_FORMATS[timePreference]
|
||||
if not datePattern then
|
||||
datePattern = deviceAvailable(localeName) and "%x" or DATE_FORMATS.dmy
|
||||
end
|
||||
if not timePattern then
|
||||
if deviceAvailable(localeName) then
|
||||
local sample = render(timestamp, "%X") or ""
|
||||
local marker = render(timestamp, "%p") or ""
|
||||
timePattern = marker ~= "" and sample:find(marker, 1, true)
|
||||
and TIME_FORMATS["12h"] or TIME_FORMATS["24h"]
|
||||
else
|
||||
timePattern = TIME_FORMATS["24h"]
|
||||
end
|
||||
end
|
||||
return {
|
||||
date = render(timestamp, datePattern) or "----",
|
||||
time = render(timestamp, timePattern) or "----",
|
||||
}
|
||||
end
|
||||
|
||||
local function preferences(game)
|
||||
local options = game and game.save and game.save.options
|
||||
options = type(options) == "table" and options or {}
|
||||
return options.dateFormat or "device", options.timeFormat or "device"
|
||||
end
|
||||
|
||||
function DateTime.date(game, timestamp)
|
||||
local datePreference, timePreference = preferences(game)
|
||||
return DateTime.formatWithLocale(timestamp, datePreference, timePreference,
|
||||
currentLocale()).date
|
||||
end
|
||||
|
||||
function DateTime.time(game, timestamp)
|
||||
local datePreference, timePreference = preferences(game)
|
||||
return DateTime.formatWithLocale(timestamp, datePreference, timePreference,
|
||||
currentLocale()).time
|
||||
end
|
||||
|
||||
function DateTime.dateTime(game, timestamp)
|
||||
local datePreference, timePreference = preferences(game)
|
||||
local value = DateTime.formatWithLocale(timestamp, datePreference, timePreference,
|
||||
currentLocale())
|
||||
if value.date == "----" or value.time == "----" then return "----" end
|
||||
return value.date .. " " .. value.time
|
||||
end
|
||||
|
||||
return DateTime
|
||||
@@ -341,6 +341,12 @@ function SaveData.defaultOptions()
|
||||
-- gets the tick without going looking for the row. Inert wherever the
|
||||
-- overlay never appears (desktop) or LOVE has no vibrator.
|
||||
haptics = "light",
|
||||
-- Shared UI/mod timestamp presentation. DEVICE follows the process time
|
||||
-- locale where the platform exposes one; otherwise DateTime falls back to
|
||||
-- DD-MM-YYYY and 24-hour time. Kept in options.lua so checkpoints never
|
||||
-- rewind presentation preferences.
|
||||
dateFormat = "device", -- device | dmy | mdy | ymd
|
||||
timeFormat = "device", -- device | 24h | 12h
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ local GameVersion = require("src.core.GameVersion")
|
||||
local Version = require("src.core.Version")
|
||||
local Assets = require("src.render.Assets")
|
||||
local ModUI = require("src.ui.ModUI")
|
||||
local DateTime = require("src.core.DateTime")
|
||||
local AssetTransform = require("src.mods.AssetTransform")
|
||||
local Manifest = require("src.mods.Manifest")
|
||||
local Merge = require("src.mods.Merge")
|
||||
@@ -917,6 +918,16 @@ function Loader:_api(mod)
|
||||
-- the widget toolkit facade (12 4.5) is one shared surface, not
|
||||
-- per-mod state; each widget inside it loads on first touch
|
||||
ui = ModUI,
|
||||
-- Read-only shared timestamp presentation using current options.lua
|
||||
-- preferences. The live game supplies only the current option context;
|
||||
-- checkpoint/save data never changes as a side effect.
|
||||
datetime = {
|
||||
date = function(_, game, timestamp) return DateTime.date(game, timestamp) end,
|
||||
time = function(_, game, timestamp) return DateTime.time(game, timestamp) end,
|
||||
dateTime = function(_, game, timestamp)
|
||||
return DateTime.dateTime(game, timestamp)
|
||||
end,
|
||||
},
|
||||
-- namespaced per mod; M11 backs these with save.modData /
|
||||
-- options.modOptions, the shape mods compile against is already final
|
||||
save = {
|
||||
|
||||
@@ -49,6 +49,30 @@ local Rulesets = {
|
||||
modern_clean = require("src.battle.rulesets.modern_clean"),
|
||||
}
|
||||
local FILTERS = { "OFF", "1X", "2X", "3X" }
|
||||
local DATE_FORMATS = {
|
||||
{ "device", "DEVICE" }, { "dmy", "DD-MM-YYYY" },
|
||||
{ "mdy", "MM-DD-YYYY" }, { "ymd", "YYYY-MM-DD" },
|
||||
}
|
||||
local TIME_FORMATS = {
|
||||
{ "device", "DEVICE" }, { "24h", "24 HOUR" }, { "12h", "12 HOUR" },
|
||||
}
|
||||
|
||||
local function preferenceIndex(rows, value)
|
||||
for index, row in ipairs(rows) do
|
||||
if row[1] == value then return index end
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
local function preferenceStep(rows, value, direction)
|
||||
local index = preferenceIndex(rows, value)
|
||||
direction = direction and direction < 0 and -1 or 1
|
||||
return rows[((index - 1 + direction) % #rows) + 1][1]
|
||||
end
|
||||
|
||||
local function preferenceLabel(rows, value)
|
||||
return rows[preferenceIndex(rows, value)][2]
|
||||
end
|
||||
|
||||
local function speedIndex(game)
|
||||
-- default matches InitOptions' TEXT_DELAY_MEDIUM in wOptions
|
||||
@@ -439,6 +463,24 @@ local function buildRows(game)
|
||||
activate = function(g)
|
||||
require("src.ui.Screens").push(g, "BindingsMenu")
|
||||
end },
|
||||
{ id = "dateFormat", label = Strings("DATE FORMAT"),
|
||||
value = function(g)
|
||||
return Strings(preferenceLabel(DATE_FORMATS, g.save.options.dateFormat))
|
||||
end,
|
||||
step = function(g, dir)
|
||||
g.save.options.dateFormat = preferenceStep(
|
||||
DATE_FORMATS, g.save.options.dateFormat, dir)
|
||||
return true
|
||||
end },
|
||||
{ id = "timeFormat", label = Strings("TIME FORMAT"),
|
||||
value = function(g)
|
||||
return Strings(preferenceLabel(TIME_FORMATS, g.save.options.timeFormat))
|
||||
end,
|
||||
step = function(g, dir)
|
||||
g.save.options.timeFormat = preferenceStep(
|
||||
TIME_FORMATS, g.save.options.timeFormat, dir)
|
||||
return true
|
||||
end },
|
||||
-- permanent on-screen pad toggle (#327); layout editing stays in the
|
||||
-- launcher. Hidden where the overlay never appears (desktop without
|
||||
-- POKEPORT_TOUCH), so the row costs a non-mobile install nothing.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local DateTime = require("src.core.DateTime")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
local stamp = os.time({ year = 2026, month = 8, day = 11, hour = 17, min = 5, sec = 0 })
|
||||
|
||||
local defaults = SaveData.defaultOptions()
|
||||
T.eq(defaults.dateFormat, "device", "date format defaults to device locale")
|
||||
T.eq(defaults.timeFormat, "device", "time format defaults to device locale")
|
||||
|
||||
local game = { save = { options = { dateFormat = "dmy", timeFormat = "24h" } } }
|
||||
T.eq(DateTime.date(game, stamp), os.date("%d-%m-%Y", stamp),
|
||||
"explicit DMY uses day-month-year")
|
||||
T.eq(DateTime.time(game, stamp), os.date("%H:%M", stamp),
|
||||
"explicit 24-hour time omits seconds")
|
||||
T.eq(DateTime.dateTime(game, stamp),
|
||||
os.date("%d-%m-%Y %H:%M", stamp),
|
||||
"combined formatter composes exact date and time preferences")
|
||||
|
||||
game.save.options.dateFormat = "mdy"
|
||||
T.eq(DateTime.date(game, stamp), os.date("%m-%d-%Y", stamp),
|
||||
"explicit MDY is available")
|
||||
game.save.options.dateFormat = "ymd"
|
||||
T.eq(DateTime.date(game, stamp), os.date("%Y-%m-%d", stamp),
|
||||
"explicit YMD is available")
|
||||
game.save.options.timeFormat = "12h"
|
||||
T.eq(DateTime.time(game, stamp), os.date("%I:%M %p", stamp),
|
||||
"explicit 12-hour time is available")
|
||||
|
||||
local fallback = DateTime.formatWithLocale(stamp, "device", "device", "C")
|
||||
T.eq(fallback.date, os.date("%d-%m-%Y", stamp),
|
||||
"missing device locale falls back to requested DMY")
|
||||
T.eq(fallback.time, os.date("%H:%M", stamp),
|
||||
"missing device locale falls back to requested 24-hour time")
|
||||
|
||||
local invalid = DateTime.date({}, -1)
|
||||
T.eq(invalid, "----", "invalid timestamps fail closed")
|
||||
|
||||
T.finish("date_time")
|
||||
+18
-1
@@ -288,7 +288,7 @@ local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout",
|
||||
"tilt", "gbcfx", "zoom", "voidFill", "videoMode",
|
||||
"faithfulRes", "fpsCap",
|
||||
"speedOverworld", "speedBattle", "speedMenu",
|
||||
"mods", "controls" }
|
||||
"mods", "controls", "dateFormat", "timeFormat" }
|
||||
check(#om.rows == #WANT_IDS, "vanilla options row count (plus MODS/CONTROLS)")
|
||||
for i, id in ipairs(WANT_IDS) do
|
||||
check(om.rows[i].id == id, "options row order: " .. id)
|
||||
@@ -408,6 +408,23 @@ check(bm.items[1].label == "UP" and bm.items[1].right == "UP/D-UP"
|
||||
"with no rebind the rows mirror the fixed map, key and pad both (#589)")
|
||||
check(cbGame.save.options.bindings == nil,
|
||||
"opening the screen alone writes nothing")
|
||||
|
||||
-- shared date/time presentation stays in options.lua and is available to
|
||||
-- engine UI and mods without becoming checkpoint progress
|
||||
om.game.save.options.dateFormat = "device"
|
||||
om.game.save.options.timeFormat = "device"
|
||||
check(om.rows[26].value(om.game) == "DEVICE",
|
||||
"DATE FORMAT defaults to device locale")
|
||||
om.rows[26].step(om.game, 1)
|
||||
check(om.game.save.options.dateFormat == "dmy"
|
||||
and om.rows[26].value(om.game) == "DD-MM-YYYY",
|
||||
"DATE FORMAT exposes deterministic DMY override")
|
||||
check(om.rows[27].value(om.game) == "DEVICE",
|
||||
"TIME FORMAT defaults to device locale")
|
||||
om.rows[27].step(om.game, 1)
|
||||
check(om.game.save.options.timeFormat == "24h"
|
||||
and om.rows[27].value(om.game) == "24 HOUR",
|
||||
"TIME FORMAT exposes deterministic 24-hour override")
|
||||
check(bm.onKeyPressed == nil and bm.onGamepadPressed == nil,
|
||||
"no raw-input claim until a capture is armed")
|
||||
press(bm, "a")
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
|
||||
local FIXTURE = {
|
||||
["mods/date_probe/manifest.json"] = [[{
|
||||
"id": "date_probe",
|
||||
"name": "Date Probe",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"api": 2
|
||||
}]],
|
||||
["mods/date_probe/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.exports.datetime = mod.datetime
|
||||
]],
|
||||
}
|
||||
|
||||
local run = T.sdk.loadMods({ "mods/date_probe" }, { fs = T.sdk.memfs(FIXTURE) })
|
||||
T.eq(#run.errors, 0, "fixture mod loads cleanly")
|
||||
local datetime = run.loader.exports.date_probe.datetime
|
||||
T.eq(type(datetime), "table", "mod object exposes datetime formatting")
|
||||
T.eq(type(datetime.date), "function", "public formatter exposes date")
|
||||
T.eq(type(datetime.time), "function", "public formatter exposes time")
|
||||
T.eq(type(datetime.dateTime), "function", "public formatter exposes date-time")
|
||||
|
||||
local stamp = os.time({ year = 2026, month = 8, day = 11, hour = 17, min = 5, sec = 0 })
|
||||
local game = { save = { options = { dateFormat = "dmy", timeFormat = "24h" } } }
|
||||
T.eq(datetime:date(game, stamp), os.date("%d-%m-%Y", stamp),
|
||||
"mod date follows current global engine preference")
|
||||
T.eq(datetime:time(game, stamp), os.date("%H:%M", stamp),
|
||||
"mod time follows current global engine preference")
|
||||
T.eq(datetime:dateTime(game, stamp), os.date("%d-%m-%Y %H:%M", stamp),
|
||||
"mod date-time composes the same preference")
|
||||
|
||||
run.release()
|
||||
T.finish("date_time")
|
||||
Reference in New Issue
Block a user