Developer API
On this page(10)
SnStaff exposes a public developer API for other plugins: cancellable Bukkit events and a read-only
query service. The API lives in the com.sn.staff.api package inside the plugin jar. There is no
separate artifact.
Only com.sn.staff.api is a stable, kept contract. Everything else in the jar is obfuscated and
internal. Do not call it.
Getting the jar
-
Download the latest SnStaff jar from SnDevelopment.
-
Install it into your local Maven repository:
mvn install:install-file -Dfile=SnStaff-<version>.jar -DgroupId=com.sn \ -DartifactId=SnStaff -Dversion=<version> -Dpackaging=jar -
Depend on it with
providedscope. Never shade it.
<dependency>
<groupId>com.sn</groupId>
<artifactId>SnStaff</artifactId>
<version><!-- the version you installed --></version>
<scope>provided</scope>
</dependency>Quick start
Declare the dependency in your plugin.yml:
depend: [SnStaff] # or softdepend if optionalResolve the API when you need it:
import com.sn.staff.api.SnStaffAPI;
import com.sn.staff.api.SnStaffProvider;
SnStaffAPI api = SnStaffProvider.get();
if (api != null && api.isVanished(player.getUniqueId())) {
// skip the vanished staff member
}SnStaffProvider.isAvailable() answers the same question without handing back the service. Both
methods are safe from any thread.
SnStaff registers the facade as the last step of its startup and unregisters it first on disable.
get() returns null while SnStaff is absent, not enabled yet or already disabled.
Resolve the reference when you need it. Do not cache it across a SnStaff disable or a server reload.
Master switch
API events can be disabled by the server owner with api-events.enabled: false in config.yml.
The default is true.
While the switch is off, no event is dispatched at all. The cancellable hooks report "not cancelled", so every staff action proceeds as if no listener existed. The query service stays available either way.
The switch is read on every dispatch, so /staff reload applies it at once. No event fires while
SnStaff itself is not enabled.
The server owner, not you, decides whether your listeners run. Behavior that must never be switched off cannot rely on an SnStaff event.
Events
All events live in com.sn.staff.api.event, extend org.bukkit.event.Event and implement
Cancellable. Each one carries the usual static getHandlerList().
Cancellable events fire before the action. Cancelling aborts it. Every event fires synchronously on the main thread, before SnStaff changes any state.
| Event | Fired when | Cancel effect |
|---|---|---|
StaffModeEnterEvent | A player is about to enter staff mode, through /staff or the automatic re-entry on join | The player is untouched: no inventory is closed, saved or cleared |
StaffVanishToggleEvent | A staff member toggles their own vanish with /vanish or the vanish tool | The current vanish state is kept |
PlayerFreezeEvent | A player is about to be frozen with /freeze or the freeze tool | The target stays unfrozen and untouched |
PlayerUnfreezeEvent | A staff member is about to unfreeze a player with /freeze on a frozen player or the freeze tool | The player stays frozen |
StaffChatSendEvent | A staff chat or admin chat message passed SnStaff's checks and is about to be published | The message is dropped: not delivered, not shown in public chat, not logged |
StaffRankChangeEvent | A /staff admin rank command is about to change a registered rank | Nothing is stored, synced, applied to LuckPerms or logged |
SnStaff has no notification events. Every event above is a pre-event you can cancel.
The automatic re-entry on join only happens with staff-mode.never-on-join: false.
StaffChatSendEvent fires for /sc, /ac, the chat prefixes and a channel mode toggled on.
StaffRankChangeEvent fires for add, promote, demote, remove and set.
Cancelling politely
Five of the six events send no message on cancel. SnStaff stays silent, so your plugin must tell the player why.
StaffRankChangeEvent is the exception. The actor gets the messages.ranks.cancelled line, "The
rank change was cancelled by another plugin".
A staff chat line typed in normal chat has its chat event cancelled before StaffChatSendEvent
fires. Cancelling it therefore drops the message entirely. It never falls back to public chat.
@EventHandler(ignoreCancelled = true)
public void onFreeze(PlayerFreezeEvent event) {
if (!event.getTarget().getWorld().getName().equals("event_arena")) {
return;
}
event.setCancelled(true);
Player staff = event.getStaff(); // null when the console freezes
if (staff != null) {
staff.sendMessage("Players in the event arena cannot be frozen.");
}
}@EventHandler(ignoreCancelled = true)
public void onRankChange(StaffRankChangeEvent event) {
if ("remove".equals(event.getAction())) {
getLogger().info(event.getTargetName() + " leaves the staff, was " + event.getFromRank());
}
}Payloads
| Event | Payload |
|---|---|
StaffModeEnterEvent | getStaff() |
StaffVanishToggleEvent | getStaff(), isVanishing() |
PlayerFreezeEvent | getStaff(), getTarget() |
PlayerUnfreezeEvent | getStaff(), getTarget() |
StaffChatSendEvent | getSender(), getChannel(), getMessage() |
StaffRankChangeEvent | getActor(), getTarget(), getTargetName(), getFromRank(), getToRank(), getAction() |
isVanishing()istruewhen the player is about to vanish, andfalsewhen they are about to become visible.- On both freeze events,
getStaff()isnullwhen the console acts. A console freeze has no owner. - The staff member of
PlayerFreezeEventbecomes the owner of the freeze session. OnPlayerUnfreezeEventit may be someone else, because any holder ofsnstaff.freezecan unfreeze any frozen player. getChannel()is"staff"or"admin".getMessage()is the text as typed, without the channel prefix and surrounding whitespace. It is never blank and may still carry color codes.- On
StaffRankChangeEvent,getActor()isnullfor the console.getTarget()is a UUID, because the player may be offline or on another server. getTargetName()isnullwhen unknown.getFromRank()isnullwhen the player is not registered, andgetToRank()isnullon a removal.getAction()is"add","promote","demote","remove"or"set".
getFromRank() comes from a database read made off the main thread just before the event. Two
rank changes of the same player at the same time, here or on another server, can make it stale.
Event payloads are read-only. Cancellation is the only mutation a listener gets: there is no way to rewrite a staff chat message.
SnStaff checks the player again right after StaffModeEnterEvent and PlayerFreezeEvent. When a
listener disconnected the player or already applied that state itself, the action is dropped.
What fires nothing
- Leaving staff mode fires no event.
StaffVanishToggleEventcovers player-driven toggles only. Internal transitions fire nothing: staff mode entry or exit, vanish on join, a reload turning vanish off, quit cleanup and plugin disable.PlayerUnfreezeEventcovers staff-driven unfreezes only. Forced releases are not reported and cannot be cancelled: the frozen player quitting, a reload turning freeze off, and plugin disable.StaffRankChangeEventnever fires for a rejected command, such as an unknown player or an unchanged rank. The safety sync, the LuckPerms protection and a change from another server fire nothing either.
Query service
The facade is read-only. No method changes SnStaff state, so observe changes through the events above.
Synchronous methods read in-memory state and never block. Most are safe from any thread. The Notes column flags the two that are main thread only.
| Method | Returns | Notes |
|---|---|---|
isInStaffMode(UUID) | boolean | Also true while staff mode is being entered or left. Any thread |
isVanished(UUID) | boolean | SnStaff vanish only, never another plugin's vanish. Any thread |
isFrozen(UUID) | boolean | Any thread |
getFreeze(UUID) | Optional<FreezeView> | The freeze session and its owner. Empty when the player is not frozen. Any thread |
isStaff(Player) | boolean | A registered staff rank or the snstaff.logged permission. Main thread only |
getStaffRank(UUID) | Optional<String> | The registered rank, else the LuckPerms primary group. Not colored. Any thread |
isPinUnlocked(Player) | boolean | Whether the player passed the staff PIN check. Main thread only |
getServerName() | String | The network.server-name label, shown as {server}. Any thread |
isNetworked() | boolean | Whether SnStaff shares its state through a shared MySQL database. Any thread |
getApiVersion() | String | The API contract version. Any thread |
The staff mode, vanish and freeze queries describe this server only.
While the module behind a query is switched off, the query returns its neutral value, false or
empty, instead of failing. The toggles are staff-mode.enabled, vanish.enabled and
freeze.enabled. They are read at call time, so a /staff reload applies on the next query.
getStaffRank falls back to the LuckPerms primary group when the ranks module has no entry or is
disabled. It never touches the database. A player who is offline or on another backend usually
reads as empty.
isPinUnlocked is always true for a player the PIN module does not bind. It is also true for
everyone while pin.enabled is false. The answer changes as the player unlocks
or gets locked out, so do not cache it.
getServerName is a label only. It may repeat across servers, so it never identifies an instance.
isNetworked is false with SQLite, and when the network layer was forced off.
SnStaffAPI api = SnStaffProvider.get();
if (api != null) {
api.getFreeze(target.getUniqueId()).ifPresent(freeze -> viewer.sendMessage(freeze.hasOwner()
? "Frozen by " + freeze.ownerName()
: "Frozen, no owner"));
}Asynchronous methods return CompletableFuture and read the database.
| Method | Returns | Notes |
|---|---|---|
getStaffStats(UUID, String) | CompletableFuture<Optional<StaffStatsView>> | Activity statistics of one staff member for one window. Callable from any thread |
The period is "total" for the lifetime, "week" for the current ISO week or "month" for the
current calendar month. It is case-insensitive. The week and the month follow stats.timezone.
The database read runs off the main thread, and the future completes on the main thread. Its continuations may use the Bukkit API directly.
| Outcome | Future |
|---|---|
| An unknown period | Fails with an IllegalArgumentException |
stats.enabled: false | Completes with an empty value |
| Nothing stored for that window | Completes with an empty value |
| A database failure | Fails with the cause |
An unknown period and a disabled statistics module need no database read. Their future is already completed, so its continuations run on your calling thread.
The values are what was flushed to the database. Counters of a staff member online right now may lag
by up to one network.heartbeat-seconds interval. If SnStaff disables before the read finishes, the
future may complete on another thread, or never.
api.getStaffStats(staff.getUniqueId(), "week")
.thenAccept(stats -> stats.ifPresentOrElse(
view -> viewer.sendMessage("Logins this week: " + view.logins()),
() -> viewer.sendMessage("No statistics this week.")))
.exceptionally(failure -> {
getLogger().warning("SnStaff statistics read failed: " + failure);
return null;
});Views
Every returned object is an immutable record in com.sn.staff.api.model. A snapshot does not
follow later changes.
| View | Fields |
|---|---|
FreezeView | player, owner, ownerName, plus hasOwner() |
StaffStatsView | player, period, logins, playtimeMillis, activeMillis, messages, commands |
The owner of a freeze is the staff member who froze the player and talks with them through the
freeze chat. A session has no owner when the console froze the player, or when the owner left the
server. The player then stays frozen, and owner and ownerName are null. ownerName is the
name the owner had when they froze the player.
In a StaffStatsView, period is total, week or month. Logins count network joins, so a
server switch is not a login. Playtime is the time tracked online, in milliseconds. Active time is
the part of it in which the staff member moved or looked around within
stats.idle-threshold-seconds. It never exceeds the playtime. messages and commands count the
uncancelled chat lines and commands.
Versioning
Call getApiVersion() for the API contract version. It is independent of the plugin version.
Additions bump the minor component. Existing members are never removed or changed; deprecated
members keep working.