Tag-based messages, channel registry, tag-filter routing (OR-list, empty=catch-all), per-channel history ring-buffer, subscriber callbacks, severity sugar wrappers (info/warn/error), and engine.print mirror.
lib-core.notify
Headless player-facing message routing. Tag-based messages with a channel registry, tag-filter routing, subscriber callbacks, and an engine.print mirror. No render code; no UI.
Version: 0.1.0 Lib-ID: lib-core.notify Requires: engine.time.now (optional, for auto-timestamp), engine.print (optional, for log mirror) Tags: notify, message, channel, routing, hud
Topology
graph LR
this["lib-core.notify"]
engine_time["engine.time.now (optional)"]
engine_print["engine.print (optional)"]
this -.->|optional| engine_time
this -.->|optional| engine_print
Scope (v0.1.0)
v0.1 ships the data layer only:
- Channel registry (create, destroy, list)
- Tag-filter routing: OR-match on shared tags; empty filter = catch-all
- Per-channel history ring-buffer (max_history capped, oldest dropped)
- Subscribe / unsubscribe callbacks (returns opaque handle)
- Severity sugar wrappers:
info,warn,error - Engine-log mirror (calls
engine.printon every posted message; toggle viaset_engine_log_mirror)
Intentional non-goals (deferred):
- Glob/wildcard tag filters
- AND-combination or exclude-tag filters
- Persistent history (disk/savegame serialization)
- Display rendering (belongs in a future lib-core.notify-display)
Message Schema
| Field | Type | Set by | Notes |
|---|---|---|---|
tags |
string[] | caller (required) | Non-empty; drives routing |
text |
string | caller (required) | Human-readable message body |
severity |
string | caller / auto | "info" / "warn" / "error"; defaults to "info" |
id |
number | auto | Monotonically increasing |
timestamp |
number / nil | auto | engine.time.now() if available |
Channel Schema
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | Unique channel identifier |
filter |
string[] | yes | Tag OR-list; {} = catch-all |
display_mode |
string | yes | "log" / "toast" / "detail" |
max_history |
number | no | Default: log=200, toast=20, detail=1 |
title |
string | no | Human-readable channel name |
Filter Semantics
Routing uses an OR-list: a message is delivered to a channel if at least one of the message's tags appears in the channel's filter array.
An empty filter array ({}) is a catch-all — every message is delivered
regardless of its tags.
channel filter = {"combat", "loot"} message tags = {"loot", "vendor"}
→ match: "loot" in both lists
API
notify.create_channel(def)
Syntax: notify.create_channel(def: table) -> void
Example:
notify.create_channel{
id = "combat-log",
filter = {"combat", "damage"},
display_mode = "log",
max_history = 100,
title = "Combat Log",
}
Registers a new channel. Loud-error on: non-table def, missing or empty
def.id, duplicate id, non-table def.filter, unknown def.display_mode.
max_history defaults to display-mode default (log=200, toast=20, detail=1).
notify.destroy_channel(channel_id)
Syntax: notify.destroy_channel(channel_id: string) -> void
Removes the channel and all its subscribers. Idempotent (no-op if channel does not exist).
notify.list_channels()
Syntax: notify.list_channels() -> table[]
Returns a shallow-copy array of all registered channel definitions (without internal history). Order is unspecified.
notify.post(msg)
Syntax: notify.post(msg: table) -> void
Example:
notify.post{ tags = {"combat", "damage"}, text = "You hit goblin for 12 damage.", severity = "info" }
Posts a message. Auto-fills id, timestamp, and severity (default
"info"). Routes to all channels whose filter matches at least one tag.
Appends to channel history (ring-buffered at max_history), invokes all
registered subscriber callbacks in insertion order, and mirrors to
engine.print if engine_log_mirror is enabled.
Loud-error on: non-table msg, missing or non-array msg.tags, empty
msg.tags, non-string tag elements, non-string msg.text.
notify.info(tags, text)
Syntax: notify.info(tags: string[], text: string) -> void
Sugar for M.post{ tags=tags, text=text, severity="info" }.
notify.warn(tags, text)
Syntax: notify.warn(tags: string[], text: string) -> void
Sugar for M.post{ tags=tags, text=text, severity="warn" }.
notify.error(tags, text)
Syntax: notify.error(tags: string[], text: string) -> void
Sugar for M.post{ tags=tags, text=text, severity="error" }.
notify.get_history(channel_id, n)
Syntax: notify.get_history(channel_id: string, n?: number) -> table[]
Example:
local last5 = notify.get_history("combat-log", 5)
Returns a shallow-copy array of the last n messages delivered to the
channel, oldest-first within the returned slice. If n is nil or greater
than the number of stored messages, returns all stored messages. Returns
{} for unknown channels.
notify.subscribe(channel_id, callback)
Syntax: notify.subscribe(channel_id: string, callback: function) -> handle
Example:
local handle = notify.subscribe("combat-log", function(msg)
hud.append(msg.text)
end)
Registers callback to be invoked whenever a message is routed to
channel_id. Returns an opaque handle table required by unsubscribe.
Loud-error on unknown channel or non-function callback.
notify.unsubscribe(handle)
Syntax: notify.unsubscribe(handle: table) -> void
Removes the subscription identified by handle. Silent if the subscription
or channel no longer exists (idempotent).
notify.set_engine_log_mirror(enable)
Syntax: notify.set_engine_log_mirror(enable: bool) -> void
Enables or disables mirroring every posted message to engine.print.
Only true (boolean) enables the mirror; any other value disables it.
Enabled by default at module load.
Engine-Log Mirror
When engine_log_mirror is enabled (default), every call to notify.post
also calls engine.print(msg.text) if engine.print is available. This
provides zero-config visibility in the engine console during development.
Disable in production or when a channel subscriber handles its own output:
notify.set_engine_log_mirror(false)
Test Backdoors
The following functions are for test-only use. Do not call them from game code.
notify._test_clear_all()— resets all state to initial valuesnotify._test_get_subscribers(channel_id)— returns raw subscriber listnotify._test_get_log_mirror()— returns currentengine_log_mirrorbool