Skip to content

Notifications

Pétrus Pradella edited this page Oct 26, 2026 · 1 revision

Notifications

Two pieces put things on a player's screen besides chat, both portable across Bukkit and Hytale and both built on the platform's HUD port (IPlatform.getHud() -> notify.IHudAccess, see Platform Abstraction):

  • notify.Notifier - "tell the player": a Notification delivered through a chain of channels, the first step the platform supports.
  • waypoint.Waypoints - "go here": a Waypoint pointed at through every supported channel it asks for.

A plugin never asks "does this server have a boss bar?". It names what it wants and what to fall back to; the platform answers, and a chain or a waypoint that can show nothing on this platform fails at load, not in front of a player.


The Notifier

Notifier.get().notify(player, Notification.of(NotifyKind.PROGRESS)
        .text("§aLogs: 12/64")
        .build());                                  // the kind's chain: action bar, else chat

Notifier.get().notify(player, Notification.of(NotifyKind.COMPLETE)
        .title("§6Quest complete").subtitle("§eLumberjack")
        .sound(HudSounds.LEVEL_UP)
        .build());                                  // title + sound, else chat + sound, else chat

Notifier.get().notify(player, Notification.of(NotifyKind.TRACKED).id("lumberjack")
        .text("§eLumberjack").progress(0.19f)
        .trackerLines(Collections.singletonList(TrackerLine.of(FancyText.of("Logs"), 12, 64)))
        .build());                                  // boss bar, else tracker, else action bar, else chat
Notifier.get().clear(player, "lumberjack");        // un-track: hides the bar / tracker of that id

Notifier.get().notify(player, Notification.of(NotifyKind.INFO).text("§9Mana full")
        .channels("action-bar", "chat").build());   // an explicit chain, overriding the kind's

Notification.Builder: id (same id updates or replaces - a boss bar, a tracker, a queued action bar), text, title/subtitle, lines (sidebar), trackerLines, progress (0..1), duration / ticks(n), priority (queued channels: the highest wins), sound (a platform id or a HudSounds alias), category (travels for a future inbox channel; nothing reads it yet), channels(...).

Kinds and chains

A chain is a list of steps; a step is one or more channels joined by +, all of which must be supported. The first fully supported step delivers; the others are skipped.

NotifyKind Shipped chain Shipped duration
PROGRESS action-bar, chat 3s
COMPLETE title+sound, chat+sound, chat 5s
TRACKED boss-bar, tracker, action-bar, chat 0s (until cleared)
INFO chat 3s
DENY action-bar, chat 3s

The admin changes them in the core's config.yml:

Settings:
  Notify:
    chains:
      progress: ["action-bar", "chat"]
      tracked: ["sidebar", "chat"]       # the sidebar is opt-in: no shipped chain uses it
    durations:
      progress: 3s

A channel name nobody registered fails the boot naming the key, the value and the channels there are; a <plugin>:<name> of another plugin is accepted and resolves once that plugin registers it.

Failing loud at load

@Override
public void onECPluginEnable() {
    Notifier.get().validate(getPluginData(), "tracked", NotifyChain.parse("boss-bar", "tracker"));
}

validate throws NotifyException naming the chain, the platform and the channels it supports when no step is supported. At run time notify never throws: a chain with no supported step shows nothing and logs once per chain and platform.

Stock channels

Channel 1.7.10 1.12.2 1.16.5 1.21.x Hytale How it delivers
chat yes yes yes yes yes the chat line
action-bar NecroTempus only yes yes yes no queued per player (priority, same id replaces, expiry), one tick per 500 ms
title NecroTempus only yes yes yes yes queued like the action bar; Hytale's event title
sound yes yes yes yes yes playSound; HudSounds aliases map per era / to Hytale sound events
boss-bar no yes yes yes no one bar per id with progress; hidden after the duration
sidebar yes (16 chars a line) yes yes yes no the scoreboard sidebar, one board per player
toast no yes yes yes yes Minecraft: a hidden advancement (one per distinct toast); Hytale: a notification
tracker yes yes yes yes yes Bukkit: the sidebar slot with a fixed layout; Hytale: the objective panel

The sidebar and the tracker share Minecraft's sidebar slot: the last one shown is drawn, and clearing it draws the other again. A player who quits is forgotten by every channel (no call needed), and a plugin's own channels stop on its shutdown.

A channel of your own

public final class InboxChannel implements NotifyChannel {
    public String getKey() { return "finalquests:inbox"; }
    public boolean isSupported() { return true; }
    public void deliver(FPlayer player, Notification notification) { /* store it */ }
}

Notifier.get().register(getPluginData(), new InboxChannel(), "Keeps the notification for /inbox.");
// a chain names it "finalquests:inbox" (or "inbox" while the name is unique)

Channels are a TypeRegistry<NotifyChannel> (Notifier.get().registry()); the key's owner must be the registering plugin, and it leaves with that plugin. A channel with per-player state captures the IHudAccess it delivered on and writes every later tick to that one.


Waypoints

WaypointHandle handle = Waypoints.get().show(player,
        Waypoint.of(WaypointId.of(getPluginData(), "lumber-camp"), WaypointTarget.region(camp))
                .label("§aLumber camp")
                .duration(Duration.ofMinutes(10))          // optional; none = until hidden
                .build());                                  // channels: Settings.Waypoint.channels
handle.hide();                                              // or Waypoints.get().hide(player, id)
  • Targets: WaypointTarget.at(position), region(region) (the centre of its bounds; a region open in height is centred on the world's height), following(supplier) (an entity, an NPC: the supplier answers null once it is gone). The target is read again every Settings.Waypoint.refresh-ticks, on the player's world thread.
  • Hiding: locate() answering null, the duration ending, hide/hideAll, the player quitting, or the owner's shutdown (Waypoints.hideOwnedBy). The same id shown again replaces the old one. State lives in memory only: a waypoint does not survive a restart - the plugin that showed it shows it again.
  • Channels: every supported one the waypoint asks for is used; the unsupported ones are skipped. None supported -> WaypointException naming the channels and the platform. Waypoints.validate(plugin, what, channels) is the load-time check.
Channel Bukkit Hytale Per player How
compass yes no yes setCompassTarget; hiding points it back at spawn
particle-path yes yes yes up to particle-path-length points one block apart from the eyes towards the target, VisualStyle.PATH
hologram with DecentHolograms or FancyHolograms enabled no yes the label floating over the target, shown only to that player
web-map with BlueMap, dynmap or squaremap enabled no no a marker named by the label in the Waypoints set/layer - everyone who opens the map sees it

hologram and web-map reach those plugins by reflection and are soft-depends: nothing is required, and each channel answers isSupported() only while its plugin is enabled.

Settings:
  Waypoint:
    channels: ["compass", "particle-path"]   # hologram and web-map are opt-in
    refresh-ticks: 10
    particle-path-length: 16

A channel of your own implements WaypointChannel (getKey, isSupported, isPerPlayer, show/refresh/hide, all on the player's world thread) and registers with Waypoints.get().register(plugin, channel, description).


Testing

ECoreTestWorld world = Platforms.lenient().hudWithout(Capability.TITLE).recordingOutlines().install();
ECPluginData core = Plugins.fakeCore(dir.toFile());                // forget(Plugins.CORE) before world.close()
Notifier.get().registerStock(core);
Waypoints.get().registerStock(core);
...
world.platform().getTestHud().getSoundCalls();       // what reached the HUD
world.platform().getRenderedOutlines();               // particle-path frames

hudWithout(...) turns the double into "1.7.10" or "Hytale" for a chain. The waypoint ticker runs on the ECScheduler: TestClock.acquire() and TestClock.advance(...) / runDue() drive it. ECoreTestWorld.close() stops every notify queue and forgets every waypoint. See Testing Toolkit.


See also

Clone this wiki locally