Skip to content

AlexVDefi/LuaDigitalWatch

Repository files navigation

Lua Digital Watch Framework

LuaDigitalWatch exposes a Lua API for replacing/extending the vanilla digital watch overlay.

Quick Start

local Watch = require("LuaDigitalWatch/LuaDigitalWatch")
Watch.init()

Sandbox Options

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.

Public API

All APIs live in 42/media/lua/client/LuaDigitalWatch/LuaDigitalWatch.lua.

Core Lifecycle

  • Watch.init() -> nil
  • Watch.getInstance() -> LuaDigitalWatchUI|nil

Skin APIs

  • Watch.registerSkin(skinId, descriptor) -> boolean
  • Watch.unregisterSkin(skinId) -> boolean
  • Watch.setActiveSkin(skinId) -> boolean
  • Watch.cloneSkin(source) -> LuaDigitalWatchSkin|nil (source can be skin id or descriptor table)
  • Watch.getActiveSkin() -> LuaDigitalWatchSkin|nil
  • Watch.getSkins() -> table<string, LuaDigitalWatchSkin>
  • Watch.getRegisteredSkins() -> table<string, LuaDigitalWatchSkin>

Notes:

  • unregisterSkin returns false when the skin does not exist.
  • unregisterSkin returns false for the active skin; switch skins first.

State Provider APIs

  • Watch.registerStateProvider(providerId, priority, resolveFn) -> boolean
  • Watch.unregisterStateProvider(providerId) -> boolean
  • Watch.getRegisteredProviders() -> table<string, LuaDigitalWatchProvider>

Provider callback:

---@param context table
---@return LuaDigitalWatchState|nil
function resolveFn(context)
end

context fields:

  • player — the player being checked
  • wornItems — the player's worn item list
  • inventory — the player's inventory container
  • ui — 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.

Slot APIs

  • Watch.registerSlot(slotId, slotDef) -> boolean
  • Watch.unregisterSlot(slotId) -> boolean
  • Watch.setSlotOrder(slotIdList) -> boolean
  • Watch.getRegisteredSlots() -> table<string, LuaDigitalWatchSlot>

Slot shape:

{
    order = 60, -- optional, default 100
    isVisible = function(context) return true end, -- optional
    render = function(context) end, -- required
}

Event APIs

  • Watch.on(eventName, callback) -> nil
  • Watch.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 -> true or true -> false.
    • Use for reacting to show/hide transitions.
  • 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 used
  • isAlarmSet: whether alarm is currently set
  • isAlarmRinging: whether alarm is currently ringing
  • source: provider state table returned by the winning provider, or nil
  • ui: 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)

Minute Resolution APIs

  • Watch.setDefaultMinuteResolution(minutes) -> boolean
  • Watch.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):

  1. Sandbox setting "1 Minute" or "10 Minutes" (player override, always wins)
  2. Watch.setDefaultMinuteResolution(...) (mod author preference)
  3. 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".

Temperature Resolver

  • Watch.setTemperatureResolver(resolver) -> nil
  • Watch.getTemperatureResolver() -> function|nil

The resolver callback receives (player, ui) and returns a temperature number or nil.

Override APIs

  • Watch.setOverride(key, value) -> boolean
  • Watch.clearOverride(key) -> boolean
  • Watch.getOverrides() -> table<string, any>

Override keys:

  • timeOfDay
  • temperature
  • date
  • digital
  • visible
  • alarmSet
  • alarmRinging

Render Context Schema

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,
    },
}

Skin Descriptor Schema

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 = { ... },
        },
    },
}

Standalone Example Mod

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.png
  • mods/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)

Common Pitfalls

  • Always call Watch.init() before registration APIs.
  • registerSlot fails unless slotDef.render is a function.
  • registerStateProvider fails unless resolveFn is a function and priority is numeric.
  • unregisterSkin cannot remove the active skin.
  • Use slash path requires: require("LuaDigitalWatch/...").
  • Asset paths must start with media/textures/, not media/ui/.

Validation Matrix

Use mods/LuaDigitalWatch/TESTING.md for static and in-game validation checks.

About

Framework for Project Zomboid to interact with the digital watch UI

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages