Notification scheduling (C++)
Schedule bounded notifications in a host-owned Core queue.
The shared scheduler and Store projection below are available for custom hosts. In Unreal, prefer the per-player Blueprint/C++ adapter on UCalmUiLocalPlayerSubsystem. Creating a queue does not open a notification View.
Create an owner-scoped queue
Keep one queue in the game-owned controller/service for the intended player or session. Do not recreate it whenever a View opens. The queue runs on its owner's thread; it is not a thread-safe event bus.
#include <calmui/Notifications.h>
#include <utility>
calmui::NotificationQueue notifications({3, 32, 128, 0.18});
calmui::NotificationRequest request;
request.Id = "save-operation-42";
request.GroupKey = "save-completed";
request.Content = calmui::Value::Object{{"title", "Game saved"}};
request.Priority = 0;
request.DurationSeconds = 4;
const auto result = notifications.Enqueue(std::move(request));Content is game-owned data. The raw queue accepts any Value; the optional Store
projection requires an object. The GameUI schema supplies the Toast content
members. This raw example does not perform localization or register a Store field.
Capacity and enqueue results
Options are VisibleLimit, PendingLimit, RememberedIdLimit, ExitSeconds.
Defaults are 3, 32, 128 and 0.18 seconds. Limits are normalized to 1–16 visible,
0–256 pending and 1–4096 remembered IDs. Exit duration is clamped to 0–10 seconds;
non-finite input uses 0.18. Read back Options() for the effective values.
Enqueue() returns a status and, for Added/Grouped, the retained EntryId:
| Status | Meaning |
|---|---|
Added | Accepted into visible or pending entries |
Grouped | Updated an existing entry with the same nonempty GroupKey |
Duplicate | This event ID was already accepted within the retained history, or remains an active entry ID |
Full | Both visible and pending capacity are exhausted |
Invalid | Empty/oversized ID, oversized group, or negative/non-finite lifetime |
IDs and group keys have a 256-byte maximum. Empty group keys disable grouping. Full and Invalid do not consume the event ID; the host can retry or report loss. No visible entry is silently evicted for a higher-priority message. Priority orders waiting entries, with FIFO order for ties.
Grouping keeps the original ID, replaces Content, raises priority to the larger value, increments Occurrences and resets the lifetime. It also revives an exiting entry. Replaying the same grouped event ID does not count it again.
Advance and dismiss
Call Advance(elapsedSeconds, paused) once per host frame using the clock your
game intends. A paused, zero, negative or non-finite delta does nothing.
DurationSeconds = 0 makes an entry persistent until explicitly dismissed.
Pending entries never consume their lifetime while waiting for display.
Expired entries become NotificationPhase::Exiting and retain their visible
slot for the configured exit duration. Publish that phase so the renderer can
animate it. New exit phases get their full duration, and newly promoted entries
begin aging on the next Advance call. One large delta does not age the backlog
before the host has had an opportunity to display it. A zero exit duration
removes an expired/dismissed entry immediately.
Dismiss(id) removes a pending entry or starts exit for a visible one. Repeated
dismissal of an already exiting/missing entry returns false.
UpdateContent(id, content) changes presentation data without extending lifetime,
for example after a language change.
Read Visible() and PendingCount() after a mutation. References returned by
Visible are only valid until the next queue mutation. Revision() changes on
structural/content operations, not on ordinary countdown ticks; use it to avoid
republishing unchanged Store data. Advance returns whether presentation changed.
Reset and replay policy
Clear() removes all entries but retains recent event IDs. Clear(true) also
forgets IDs and is intended for a deliberate session reset. The history is
bounded: sufficiently old IDs can be accepted again after their entries and
history records are gone. This is not network exactly-once delivery, a save-game
format, or a replacement for authoritative game event handling.
Publish to a typed Store
Keep a calmui::NotificationStoreProjection next to the queue. Call
Publish(queue, store, fieldId) after mutations and before advancing a frame.
Use the generated ID for the notifications.entries array. The target must be
per-player, keyed by its string id, with member fields phase (string),
occurrences (int) and content (object). Content members must be declared in
the same Store schema. Nested object values are validated recursively.
Published/Unchanged are successful results. Busy means another transaction or
publication is active; finish it and retry. InvalidField, InvalidContent and
Failed preserve the existing Store data; inspect LastError() and correct the
binding/payload. No failure consumes queue entries. Suspend the queue clock while
publication fails, so messages do not expire unseen during a Store error.
The adapter owns this array exclusively. It caches projected rows by queue
identity/revision, writes only changed values, and restores its projection after
an external Store reset. It never joins or rolls back someone else's transaction
and uses no JSON bridge. Do not replace the queue object in place while retaining
its projection cache; construct/reset both together. Find(id) can inspect a
visible or pending entry; its pointer is invalid after a queue mutation.