Merge pull request #1080 from MaxTomahawk/feat/device-date-time

feat(mods): add shared local date and time formatting
This commit is contained in:
bryanthaboi
2026-08-11 21:21:51 -04:00
committed by GitHub
8 changed files with 265 additions and 1 deletions
+90
View File
@@ -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
+6
View File
@@ -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
+11
View File
@@ -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 = {
+42
View File
@@ -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.