Skip to content

Rubric Events

Oliver Yasuna edited this page Jul 1, 2026 · 2 revisions

Rubric — Events

Rubric fires events on every reload and every programmatic set(...). Two subscription surfaces:

  • ConfigEventBus — per-path ChangeEvents. For code that cares about individual values.
  • ReloadListener<S> — pair of (previous, current) POJOs after a reload. For code that reconciles state wholesale.

ChangeEvent

Fired for every path whose value differs between snapshots. Delivered on the platform main thread via Platform#mainThreadExecutor().

manager.getEvents().subscribe(event -> {
    // event.path()     — dotted path of the changed value
    // event.oldValue()
    // event.newValue()
    // event.tier()     — Reload.Tier for this entry
});

Path-filtered convenience:

manager.getEvents().subscribe("display.opacity", event -> {
    setOverlayOpacity((Double)event.newValue());
});

Notes:

  • ChangeEvents are only fired when there's a diff between two snapshots. The initial load doesn't fire (there was no "previous").
  • Programmatic edits (manager.set(path, value)) go through the diff pipeline, so subscribers see them.
  • File-watch reloads (external editor saved the file) also fire the diff pipeline. Debounced by io.fileWatchDebounceMillis.
  • Remote sync applies go through the same diff. Check manager.originOf(path) if you need to distinguish LOCAL_EDIT vs FROM_REMOTE.

ReloadListener<S>

Fired after every non-initial load(), on the main thread. Signature:

public interface ReloadListener<S> {
    void onReload(S previous, S current);
}

Usage:

manager.addReloadListener((previous, current) -> {
    // Compare or wholesale-swap. Both instances are safe to read;
    // the manager already updated `current` to be the live state.
});

// Later, if you're tearing down:
manager.removeReloadListener(listener);

Reload listeners are useful when the config change requires a coordinated rebuild (rebuild a cached lookup table, tear down and re-open a GUI screen, reconcile in-flight requests). ChangeEvents are more precise; reload listeners are simpler when you don't care which value changed.

Ordering

  • dispatch(changes) schedules every ChangeEvent onto the main-thread executor as a separate task. Subscribers see events in path order per reload.
  • fireReloadListeners(previous, current) also runs on the main thread. Ordering between the ChangeEvent batch and the ReloadListener batch is deterministic: change events run first, then reload listeners.

If a listener throws, Rubric catches the RuntimeException and continues — a faulty listener does not break the reload pipeline.

ConfigValue<V>

For code that only cares about a single value, ConfigValue<V> bundles read + subscribe:

final ConfigValue<Double> opacity = handle.value("display.opacity", Double.class);
opacity.subscribe(v -> setOverlayOpacity(v));
final double current = opacity.get();

Under the hood, ConfigValue<V> reads the snapshot and attaches a path-filtered subscription to the event bus.

Ordering across mods

The event bus is per-manager, per-config. It does not cross configs, and it does not fire on other mods' configs. If two mods both subscribe to your config's event bus, they run independently and their order is by registration.

What next

  • Concepts — the terms above (ChangeEvent, ReloadListener, ConfigValue).
  • Sync — how remote-driven changes flow through the same event pipeline.

Clone this wiki locally