LuaDigitalWatch exposes a Lua API for replacing/extending the vanilla digital watch overlay.
local Watch = require("LuaDigitalWatch/LuaDigitalWatch")
Watch.init()The mod adds a Lua Digital Watch page to sandbox settings.
| Option | Values | Default | Effect |
|---|---|---|---|
| Minute Display Resolution | Mod Default / 1 Minute / 10 Minutes | Mod Default | How often the minute digit updates. |
Mod Default defers to whatever the mod author set via Watch.setDefaultMinuteResolution(...).
If no mod author changed it, "Mod Default" is 1-minute resolution.
Players can always force "1 Minute" or "10 Minutes" regardless of the mod author's preference.
All APIs live in 42/media/lua/client/LuaDigitalWatch/LuaDigitalWatch.lua.
Watch.init() -> nilWatch.getInstance() -> LuaDigitalWatchUI|nil
Watch.registerSkin(skinId, descriptor) -> booleanWatch.unregisterSkin(skinId) -> booleanWatch.setActiveSkin(skinId) -> booleanWatch.cloneSkin(source) -> LuaDigitalWatchSkin|nil(sourcecan be skin id or descriptor table)Watch.getActiveSkin() -> LuaDigitalWatchSkin|nilWatch.getSkins() -> table<string, LuaDigitalWatchSkin>Watch.getRegisteredSkins() -> table<string, LuaDigitalWatchSkin>
Notes:
unregisterSkinreturnsfalsewhen the skin does not exist.unregisterSkinreturnsfalsefor the active skin; switch skins first.
Watch.registerStateProvider(providerId, priority, resolveFn) -> booleanWatch.unregisterStateProvider(providerId) -> booleanWatch.getRegisteredProviders() -> table<string, LuaDigitalWatchProvider>
Provider callback:
---@param context table
---@return LuaDigitalWatchState|nil
function resolveFn(context)
endcontext fields:
player— the player being checkedwornItems— the player's worn item listinventory— the player's inventory containerui— the watch UI instance
Return a LuaDigitalWatchState table to claim the watch, or nil to pass:
{
player = IsoPlayer, -- which player owns this state
digital = boolean, -- true if the watch has a digital display
isAlarmSet = boolean, -- true if the alarm is set
isAlarmRinging = boolean, -- true if the alarm is currently ringing
item = AlarmClock|AlarmClockClothing, -- primary alarm item (used for right-click dialog)
items = { ... }, -- all alarm items (used for toggle/stop operations)
}When items contains multiple alarm clock items, clicking the bell icon toggles or stops all of them.
The item field is kept for backward compatibility and is used by the right-click alarm time dialog.
Watch.registerSlot(slotId, slotDef) -> booleanWatch.unregisterSlot(slotId) -> booleanWatch.setSlotOrder(slotIdList) -> booleanWatch.getRegisteredSlots() -> table<string, LuaDigitalWatchSlot>
Slot shape:
{
order = 60, -- optional, default 100
isVisible = function(context) return true end, -- optional
render = function(context) end, -- required
}Watch.on(eventName, callback) -> nilWatch.off(eventName, callback) -> nil
Use Watch.on(...) to subscribe and Watch.off(...) with the same callback function to unsubscribe.
Event timing and usage:
-
watch:state_resolved(payload, ui)- Fires once per update after the framework resolves watch state.
- Use for logic that depends on resolved state (digital/alarm/player/provider).
- Do not draw here.
-
watch:visibility_changed(visible, ui)- Fires only when watch visibility flips
false -> trueortrue -> false. - Use for reacting to show/hide transitions.
- Fires only when watch visibility flips
-
watch:render_pre(context, ui)- Fires at the start of each render pass (ghost and normal passes).
- Use to adjust render color/flags before slots draw.
- Example: tint display red by mutating
context.colour.
-
watch:slot_render(slotId, context, ui)- Fires immediately after each slot is rendered.
- Use for slot-specific post-processing/diagnostics.
-
watch:render_post(context, ui)- Fires after all slots finish rendering.
- Use for final render-pass logic.
watch:state_resolved payload fields:
player: resolved player (IsoPlayer|nil)digital: whether digital display should be usedisAlarmSet: whether alarm is currently setisAlarmRinging: whether alarm is currently ringingsource: provider state table returned by the winning provider, ornilui: watch UI instance
Minimal subscribe/unsubscribe pattern:
local function onRenderPre(context, ui)
if context.ghost then
return
end
context.colour.r = 1.0
context.colour.g = 0.2
context.colour.b = 0.2
end
Watch.on("watch:render_pre", onRenderPre)
-- later:
Watch.off("watch:render_pre", onRenderPre)Watch.setDefaultMinuteResolution(minutes) -> booleanWatch.getMinuteResolution() -> integer
Controls how often the minute digit on the watch display changes.
| Resolution | Behavior |
|---|---|
1 |
Minute display updates every in-game minute (framework default). |
10 |
Minute display updates every 10 in-game minutes (vanilla behavior). |
setDefaultMinuteResolution only accepts 1 or 10. Returns false for any other value.
Resolution precedence (highest wins):
- Sandbox setting "1 Minute" or "10 Minutes" (player override, always wins)
Watch.setDefaultMinuteResolution(...)(mod author preference)- Framework default (
1)
For modders that want vanilla 10-minute resolution as the default for their mod:
local Watch = require("LuaDigitalWatch/LuaDigitalWatch")
Watch.init()
Watch.setDefaultMinuteResolution(10)Players who prefer 1-minute resolution can still override this in sandbox settings by choosing "1 Minute" instead of "Mod Default".
Watch.setTemperatureResolver(resolver) -> nilWatch.getTemperatureResolver() -> function|nil
The resolver callback receives (player, ui) and returns a temperature number or nil.
Watch.setOverride(key, value) -> booleanWatch.clearOverride(key) -> booleanWatch.getOverrides() -> table<string, any>
Override keys:
timeOfDaytemperaturedatedigitalvisiblealarmSetalarmRinging
Slot render/isVisible receives:
{
ui = LuaDigitalWatchUI,
ghost = boolean,
colour = { r = number, g = number, b = number, a = number },
textures = {
digitsLarge = table<integer, Texture>,
digitsSmall = table<integer, Texture>,
colon = Texture,
slash = Texture,
minus = Texture,
dot = Texture,
tempC = Texture,
tempF = Texture,
tempE = Texture,
texAM = Texture,
texPM = Texture,
alarmOn = Texture,
alarmRinging = Texture,
},
slots = table,
digits = {
time = integer[],
temp = integer[],
date = integer[],
},
state = {
digital = boolean,
isAlarmSet = boolean,
isAlarmRinging = boolean,
isAM = boolean,
clockPlayer = IsoPlayer|nil,
},
}Default skin descriptor location:
42/media/lua/client/LuaDigitalWatch/LuaDigitalWatchDefaultSkinData.lua
Asset paths use media/textures/ as the base directory.
{
id = "my-skin-id",
label = "Optional Label",
sizes = {
small = {
assets = { ... },
slots = { ... },
},
large = {
assets = { ... },
slots = { ... },
},
},
}A standalone example mod is provided at: Spiffo Watch UI
It demonstrates a minimal framework integration:
- clones default skin with
Watch.cloneSkin("luawatch-default") - registers and activates a custom skin
- tints display digits green via
watch:render_pre - swaps both watch backgrounds to custom files
Entry file:
media/lua/client/LuaDigitalWatchSpiffoExample/LuaDigitalWatchSpiffoExample.lua
Custom background placeholder files:
mods/LuaDigitalWatchSpiffoExample/42/media/textures/ClockAssets/SpiffoExampleBackgroundSmall.pngmods/LuaDigitalWatchSpiffoExample/42/media/textures/ClockAssets/SpiffoExampleBackgroundLarge.png
Replace those two .png files with your final custom backgrounds.
Minimal skin-clone pattern:
local Watch = require("LuaDigitalWatch/LuaDigitalWatch")
Watch.init()
local skin = Watch.cloneSkin("luawatch-default")
skin.id = "my-custom-skin"
skin.sizes.small.assets.background = "media/textures/ClockAssets/MySmallBg.png"
skin.sizes.large.assets.background = "media/textures/ClockAssets/MyLargeBg.png"
Watch.registerSkin(skin.id, skin)
Watch.setActiveSkin(skin.id)- Always call
Watch.init()before registration APIs. registerSlotfails unlessslotDef.renderis a function.registerStateProviderfails unlessresolveFnis a function andpriorityis numeric.unregisterSkincannot remove the active skin.- Use slash path requires:
require("LuaDigitalWatch/..."). - Asset paths must start with
media/textures/, notmedia/ui/.
Use mods/LuaDigitalWatch/TESTING.md for static and in-game validation checks.