Every plugin and every SnLib version, newest first.
The owner menu is a SnLib menu now. SnLib 1.28.0 added menus that can receive an item
(input cells + an open player inventory + item offers), and this release moves the owner menu
onto it. The layout lives in guis/owner.yml, in the same dialect as the buyer menu; the old
owner-menu.yml at the plugin folder root is no longer read, and the plugin logs a pointed
warning if it finds one. "Your item is read, never consumed" is now enforced by SnLib itself.
What changed in how the menu behaves, each on purpose:
guis/owner.yml (SnLib menu dialect). The old
owner-menu.yml is ignored; delete it once restyled. To remove the withdraw button, delete
the w from the layout - there is no toggle key any more.The GUI module could show anything and receive nothing. Every click and drag over a library menu was cancelled, unconditionally - which is exactly what makes a declarative menu safe, and what made one shape of menu inexpressible: the one that asks the player for an item. A shop asking which item you are selling. A kit editor asking what goes in slot 4. A deposit cell.
Two plugins had already paid for that gap by dropping out of the module entirely: SnDisplayShops' owner menu and SnKits' KitItemsEditor are raw Bukkit inventories with hand-rolled listeners, layouts living outside guis/, and none of what the module gives you - no view requirements, no per-click matrix, no anti-theft marker, no tenant teardown.
1.28.0 closes it with two yml keys and one callback.
# guis/editor.yml
title: "&8Kit editor"
player-inventory: open # the viewer may use their own inventory
layout:
- "fffffffff"
- "ffffiffff"
- "fffffffff"
items:
slot:
key: i
input: true # THIS cell receives an item
material: LIGHT_GRAY_STAINED_GLASS_PANE
display-name: "&eDrop an item here"
click-actions: # still fires when the cursor is EMPTY
- "[message] &7Hold the item you want to place."input: true (item level, also on templates) marks the cell as an INPUT SLOT. A viewer who clicks it holding a stack, or drags a stack onto it, hands that stack to the plugin. A click with an empty cursor is not an offer and still runs the cell's click-actions, so one cell is a button and a drop target at once.player-inventory: locked | open (menu level, default locked) decides whether the viewer may use their own inventory at all. Under open their plain clicks, number keys, Q drops, F swaps and drags inside their own inventory work again - stack splitting used to fail silently - and a shift-click there becomes an offer too, because a shift-click aims INTO the menu and only the plugin can decide what that means.The two are orthogonal and both default to the old behaviour. The pair is checked at parse: an input cell with a locked player inventory WARNs, because a viewer who can never pick a stack up can never offer one.
GuiSession s = gui.session(player);
s.onOffer(offer -> {
kit.setIcon(offer.stack()); // a copy, with its real amount
s.bind(offer.slot(), gui.template("filled"), offer.stack());
});public record ItemOffer(Player viewer, Kind kind, int slot, int playerSlot,
ItemStack stack, ClickType click) {
public enum Kind { CURSOR, DRAG, SHIFT_CLICK }
}| Kind | slot() | playerSlot() | click() |
|---|---|---|---|
CURSOR | the input cell clicked | -1 | LEFT or RIGHT |
DRAG | the single input cell covered | -1 | RIGHT for a single-item drag, LEFT for an even spread |
SHIFT_CLICK | -1 | the player-inventory slot the stack came from | SHIFT_LEFT / SHIFT_RIGHT |
click() is carried so the vanilla convention stays expressible: right deposits one, left deposits the stack. stack() always carries its real amount, which is what a deposit needs.
Every event behind an offer is cancelled before the handler runs, and the offer carries a defensive clone. The cancel itself is what puts the stack back on the cursor or in the inventory. SnLib does not move it, shrink it, delete it, store it, or write it into the menu.
That line is deliberate. How much of a stack you accept, where it goes and what the cell then shows are consumer decisions, and a library that guessed them would own the money-shaped half of every deposit flow. playerSlot() is what lets the consumer write the remainder back itself:
s.onOffer(offer -> {
if (offer.kind() != ItemOffer.Kind.SHIFT_CLICK) {
return;
}
ItemStack offered = offer.stack();
int accepted = vault.deposit(player, offered);
if (accepted <= 0) {
sn.lang().send(player, "vault.full");
return;
}
ItemStack remainder = offered.getAmount() > accepted
? offered.asQuantity(offered.getAmount() - accepted)
: null;
player.getInventory().setItem(offer.playerSlot(), remainder);
player.updateInventory(); // the ghost-stack resend, see below
s.refreshMenu();
});Always follow a write-back with updateInventory(). The click was cancelled, so the client is still drawing the stack it had before the event; when you then change that slot server-side, the client keeps painting a stale stack the player can appear to click on. One resend fixes it. It is documented on the record, on the developer page and on the admin page, because it is the single most common way to get this wrong.
GuiClickListener is now a thin adapter over a new pure core, gui/internal/OfferRouting: zone + policy + action + click + three booleans in, one Decision out.
| Situation | Decision |
|---|---|
Action is COLLECT_TO_CURSOR, any zone, any policy, input cell or not | CANCEL_ONLY |
| TOP cell, input, cursor non-empty, click LEFT or RIGHT | OFFER_CURSOR |
| TOP cell, anything else | CANCEL_AND_CLICK |
| OUTSIDE the window, either policy | CANCEL_ONLY |
BOTTOM, policy LOCKED | CANCEL_ONLY |
BOTTOM, policy OPEN, shift over a non-empty stack | OFFER_SHIFT |
BOTTOM, policy OPEN, anything else (incl. a shift over an EMPTY slot) | PASS_THROUGH |
DRAG covering no menu cell, policy OPEN | PASS_THROUGH |
DRAG covering no menu cell, policy LOCKED | CANCEL_ONLY |
| DRAG covering exactly ONE input cell, stack non-empty | OFFER_DRAG |
| DRAG, anything else (2+ cells even if all input; one non-input cell; empty cursor) | CANCEL_ONLY |
Two invariants carry everything else, and both are asserted directly:
COLLECT_TO_CURSOR (the double-click gather) is cancelled first, unconditionally, under both policies, input cell or not. It is the one action that pulls stacks out of the TOP inventory while the click that fires it lands on the bottom one.Deliberate divergences worth knowing: a shift-click over an EMPTY bottom slot passes through rather than being cancelled (vanilla does nothing with it, and cancelling a no-op only costs a client desync); a drag spread over several input cells is cancelled rather than split, because inventing a split would be SnLib deciding how much each cell gets; and offers never pass through strict-clicks, which filters ACTIONS - an offer is not one.
GuiProtectionListener is untouched. Nothing in the new paths stamps, writes or moves a stack anywhere.
OfferRoutingTest asserts exactly that for every ClickType in every zone and for every InventoryAction, not as a sample. SnChat's SnapshotGui anti-dupe guarantee and SnCrates' RewardListView HIGHEST bottom-click handler both rest on that property and both keep working unchanged.PlayerInventoryPolicy, the ItemOffer record and its Kind enum, GuiItemDef.input(), GuiDef.playerInventory(), and GuiSession.onOffer / handleOffer / isInputSlot / playerInventory. No removals, no changed signatures.com.sn:snlib:1.0.0 baseline.SnApi.LEVEL 18 -> 19. Consumers built against LEVEL 18 keep running against this jar unchanged; a consumer that uses the new API needs SnLib 1.28.0 or newer installed, and the handshake tells it so at enable time instead of failing later.SnApi javadoc, which is the source of truth.docs/consumer-pom-template.xml moves off its stale 1.21.1 snlib pin.hologram: in config.yml:
bounce-amplitude (0.1 blocks, 0 turns it off) and bounce-period-ticks (80). The bob is
carried by the same push the spin already sends, so it costs nothing extra no matter how many
shops are loaded.1k, 1.5m, 2b, up to Qi, and
1.500.000 typed back exactly as the menu prints it. Numbers written out in full stay exact at
any size. A line with more than one separator, or one past the largest price there is, is
refused instead of guessed at.config.yml ships two example currencies instead of six - one command-backed, one
EdTools-backed, one of each shape. currencies: is marked extensible, so nothing an operator
already declared is added, removed or touched by the update.CFC00 is replaced by the fleet accent ₢f2 across both language files (prefix and the snlib.help header / entry / footer).₢f2&lRank Menu and ₢f2&lRank Ladder, item names use the accent, lore uses &8 separators, &7 body text and &f values, and the page buttons read ₢f2Previous Page / ₢f2Next Page. The single-menu rankup button now uses the same semantic &a&lCLICK TO RANK UP the paginated ladder already used.rankup.yml moved off the ad-hoc yellow/lime palette onto accent headers plus semantic colour codes.config.yml band banners no longer carry numbers. They read 1 MODEL, 3 LIMITS, 4 INTEGRATIONS, which looked like a section was missing - the plugin simply has no band-2 (features) keys.top moved from the LIMITS band to FEEDBACK, where the standard places leaderboards, and the redundant double banners were collapsed into one per band.No Java changed. Because SnLib never overwrites a value that already exists on disk, the new colours apply to fresh installs; an existing server keeps whatever is in its own lang/, guis/ and rankup.yml files. To adopt them, delete those files (or the individual values) and let SnLib re-seed them on the next boot.
Player-owned display shops: place a tagged item, set a price, and it trades on its own.
ItemDisplay spinning above a text line, with
DecentHolograms driving the text where it is installed and a native TextDisplay where it is not.%sndisplayshops_shops_count%, %sndisplayshops_shops_max%,
%sndisplayshops_shops_total%.Everything the plugin says and every number it uses lives in config.yml, lang/messages_en.yml,
guis/buyer.yml and owner-menu.yml. All four are managed: new keys are merged in on upgrade and
your edits are kept.
Two worth knowing before you tune them:
database.pool-size ships at 1 on purpose. Two writers can land a shop's stock updates out of
order, and the losing write is the one that survives a restart. The plugin logs a SEVERE if it
finds MySQL configured above 1.limits.max-pickup-stacks has no "off" value. Picking up a shop hands its whole stock back in
one tick, so an unbounded value is a hang rather than a preference.Daily playtime gifts for Paper servers. Players accrue playtime, tiers unlock as they hit their thresholds, and each tier pays a set of rewards drawn fresh every gift day.
This is not optional and it is not a soft warning. The required API level is compiled into this
jar, so SnGifts 2.0.0 refuses to enable on any server whose SnLib.jar is older than 1.27.0
and says so in the console. Update SnLib first.
time-needed-minutes. Order
is by threshold ascending, so inserting a tier never renumbers the ones players already know.guis/gifts.yml as a layout mask plus
named regions - move, add or remove cells without touching a slot number.rewards.yml holds the pool; each gift day draws from it and persists the
draw, so every player who claims a tier that day gets the same reward set. Supports console
commands with {player} and vault:<amount> for economy payouts.| Command | Permission |
|---|---|
/gifts (alias /regalos, renameable in config.yml) | sngifts.use |
/gifts reset <player> | sngifts.admin.reset |
/gifts resetall | sngifts.admin.resetall |
/gifts resetgifts | sngifts.admin.resetgifts |
/gifts bypass | sngifts.admin.bypass |
/gifts reload, /gifts debug | sngifts.admin.reload, sngifts.admin.debug |
sngifts.use gates the whole command tree, including the admin subcommands. If you restrict it,
grant it to staff as well.
Every player-facing string lives in lang/messages_en.yml and every value in config.yml. Both
are managed: new keys are merged in on update while your edits and comments are preserved, so
you never have to diff a config by hand. There is no config-version key to maintain.
claim.ip-limit-per-gift ships at 1. On a network behind a proxy without IP forwarding, or
for households and CGNAT, set it to 0 to disable the limit - config.yml documents the
trade-off inline.Nothing on SnFuture returned a new stage. thenSync, exceptionally and orDisablePlugin all end in return this, so a method ending in
return write(...).thenSync(publish);whose caller writes callee().thenSync(next) did not build a chain. It registered two dependents on ONE CompletableFuture. Sibling order is unspecified and OpenJDK pops dependents last-registered-first, so next ran before publish, and read exactly the state the publish was about to install. Hopping to the main thread did not rescue it: both tasks were then queued in that same inverted order.
"B after A's continuation" was inexpressible on this surface, so no amount of documentation could fix it.
SnFuture.chainSync(Consumer<T>)Runs the consumer on the main thread like thenSync, but returns a new future settled from inside that same task, after the consumer returned. Whatever the caller registers on it is a successor.
The fix belongs in the producer: swap its one thenSync and every existing caller becomes correct without being touched.
// before: the caller's step is a sibling of the publish
return writeState(KEY, date).thenSync(ignored -> lastResetDate = date);
// after: the caller's step is a successor
return writeState(KEY, date).chainSync(ignored -> lastResetDate = date);thenSyncThey all follow from the caller's handler now hanging off the derived future instead of the source.
exceptionally or orDisablePlugin. A chained failure nobody consumes is a failure nobody sees.While the plugin is running, only a main-thread task can complete it, so it carries the wrapMainCompleted marking and a main-thread join()/joinWithin throws instead of deadlocking the server. The marking is not applied when the plugin is already disabled or the context is already tearing down at the moment chainSync is called, because from then on the hop can never happen and the future settles on the completing thread; marking it anyway would make the guard fire during a teardown flush on a wait that would have returned.
A producer whose future a teardown flush joins must keep using thenSync. Joins belong on the future the database module returned.
One window does not settle and is documented rather than closed: a hop that was queued and then cancelled by a disable leaves the future pending. That is precisely what thenSync already did in the same window.
SnPapi.applyOnMain returned SnFuture.wrap for a future only its own main-thread task can complete. A future produced off the main thread and then waited on from it was a silent, permanent server deadlock with no log line. Both overloads now return wrapMainCompleted on the off-main branch, which turns the hang into an immediate throw. Shipped as its own commit, because it is a behaviour change rather than an addition.
Strictly additive. API level 17 -> 18. Every existing method is byte-for-byte unchanged, japicmp passes against the baseline, and every one of the 111 thenSync call sites across the consumer plugins keeps its exact current behaviour. An old consumer jar runs on 1.27.0 unchanged.
A consumer compiled against 1.27.0 inlines API level 18 into its bytecode and will refuse to enable against an older installed SnLib.jar, so update the library on the server before the plugin that needs it.
508 tests, 8 of them new, covering every branch of the sequencing core including the residual window above.
Every player-facing surface moves onto the Sn brand palette. The 2.0.0 rebuild adopted SnLib's structure but kept its own colors, so SnKits looked like a different product next to the rest of the pack.
₢f2,
replacing the ad-hoc 7FFFF and a stray 7FF55 on page 2 of the kits menu&6/&9/&d/&4/&b one-offs for the accent
color; destructive actions unify on &c; the back button reads the same in all
three menus that have one%snkits_onetime_used_<id>%Existing installs keep their current colors: SnLib never overwrites a value already
on disk. To adopt the new look, delete lang/messages_en.yml and guis/ and let
them re-seed, or restyle the keys by hand.
Rank ladder for Paper 1.20.x and 1.21.x. Every rank lives in one file, a rank is priced in whatever currency your server already runs, and the menu players open is a YAML file you lay out yourself.
Each rank is one entry in rankup.yml with an order and a display prefix. Orders do not need to be contiguous, so 10, 20, 30 leaves room to insert a rank later without renumbering anything. The lowest order is the starting rank: everyone begins there, and since nobody ever reaches it, its requirements are never charged and its rewards never fire.
requirements and rewards describe reaching a rank, not leaving it. A rankup checks and charges the price of the rank the player is going to, and fires that rank's rewards.
hours is always available and needs no setup: it is the player's playtime, and it is checked, never charged.
Every other price is a currency you declare in config.yml, and two types ship:
vault reads and charges the Vault economy, so any economy plugin works.placeholder reads a balance from any PlaceholderAPI placeholder and charges by running a console command: check, consume, and optionally deposit so refunds are possible. {amount} and %player% are substituted in both commands. That covers tokens, gems, points and anything else a plugin exposes as a placeholder.Requirements are checked and charged in the order you wrote them, and if a later charge is refused everything already taken is handed back. A currency whose backing plugin is not installed is skipped at load with a warning and every requirement naming it is dropped, which makes that rank cheaper rather than unreachable.
Any SnLib action tag works: [broadcast], [message], [console], [title], [actionbar], [sound] and the [chance=50] guard. A line with no tag runs as a console command. %player% and PlaceholderAPI resolve per line.
Two of them, and menu.mode in config.yml decides which one /rankup opens.
single shows one dynamic button for the next rank, styled by that rank's own menu-item, with the live leaderboard beside it. paginated lays the whole ladder out one slot per rank, opens on the page holding your next rank, and renders each tile in one of four states: claimed, ready, next, locked. Exactly one tile is ever ready or next.
Both are ordinary guis/*.yml files: a title, rows, a character grid for the layout and items you can move, restyle or delete. Rank lore takes hex colors, MiniMessage, PlaceholderAPI tokens, and the plugin's own {req_hours}, {req_<currency>}, {missing_hours}, {missing_<currency>} and {current_hours}, so a tile can show both the price and what the player still lacks without PlaceholderAPI installed.
A snapshot of the highest ranked players, rebuilt on the database thread every top.refresh-seconds and exposed as %snrankup_top_name_<N>% and %snrankup_top_value_<N>%. Those two answer even when there is no requesting player, so holograms, signs and Discord bridges get a real value instead of an empty string.
/rankup opens the menu. Its aliases are ru and rank, and they are re-read on reload.
Admin side: /rankup force <player> advances one rank and fires that rank's rewards, /rankup set <player> <rank> writes a rank with no charge and no rewards, /rankup reset <player> puts a player back on the starting rank, and /rankup bypass [player] toggles ignoring rank requirements for you or for someone else. /rankup reload, /rankup help and /rankup debug come with the framework. Every argument tab-completes.
Optional. %snrankup_prefix%, %snrankup_num%, %snrankup_next_prefix% and %snrankup_next_num% resolve anywhere, alongside the two leaderboard placeholders. The shipped menus use them, so on a server without PlaceholderAPI those lines render blank while everything else keeps working.
SQLite by default, with nothing to configure. MySQL by setting database.type and filling the connection block. Every read and write happens off the server thread.
The owner picks how a number reads.
A plugin decided at its call site whether a balance rendered 1500000, 1,500,000 or 1.5M. The server owner could not change it without the plugin being rebuilt, and across a fleet that meant three formatters with three different suffix ladders, plus plugins that formatted nothing at all and left raw digits sitting in chat.
SnText.applyLocals now understands a trailing format hint on a local token. The choice moves to the language file the owner already edits.
| Hint | 1500000 becomes |
|---|---|
{balance:short} | 1.5M |
{balance:grouped} | 1,500,000 |
{balance:raw} | 1500000 |
# Before
balance-msg: "&aYou have &f{balance} &acoins" # You have 1500000 coins
# After
balance-msg: "&aYou have &f{balance:short} &acoins" # You have 1.5M coinsIt works in messages, item names, item lore and menu titles, and it needs no change to the consumer plugin: update SnLib.jar, restart, and every plugin already installed honours it.
Inert unless it is asked for. Five rules keep it from touching text that never opted in:
raw, short and grouped count. A Discord timestamp like <t:1700000000:R>, or a PAPI token that takes an argument, passes through verbatim;{player:short} still renders the name;parseFormatted accepts 1e400 and BigDecimal.valueOf would throw on it.short and grouped round HALF_UP to two decimals; raw never rounds, because exactness is the only reason to reach for it.
One caveat worth knowing: the value is re-parsed from its rendered form, which is precisely what lets an unmodified plugin honour the hint. So a caller that already abbreviated hands over 1.23M, and :raw answers 1230000 rather than the exact figure. Only a caller that passes unformatted digits can be re-rendered losslessly.
No public API added, so SnApi.LEVEL stays 17 and no consumer needs recompiling to benefit. 500 tests, 12 of them new.
Placeholders that need no player.
Not every placeholder describes a player. A leaderboard row, a server-wide counter, an event countdown - that is global data that merely happens to be exposed through PlaceholderAPI's per-player surface. And the callers that ask for it routinely supply no player at all: a global hologram, a Discord bridge, a console task, /papi parse --null.
BuiltExpansion.onRequest rejected a null requester before it had even looked at which resolver was being asked for, so a token that needed no player was dropped along with the ones that did. The consumer could not opt out: sn.papi().expansion(...) is the only registration path, and taking the raw PlaceholderExpansion back would take back registration, persistence and unregistration with it.
ExpansionBuilder.global(String param, Supplier<String>) and globalPrefixed(String prefix, Function<String, String>) - bind a resolver that takes NO OfflinePlayer, so it cannot dereference one and it answers a null requester like any other. Same keyspace and precedence as placeholder / prefixed; the later declaration for one key wins outright.onRequest now locates the resolver FIRST and rejects the null requester SECOND. Everything bound through placeholder / prefixed keeps the short-circuit exactly as before, so a resolver written against the non-null contract its binder documents never starts seeing nulls.LeaderboardCache.exposePlaceholders(id) binds its top_ tokens global, so every board exposed through it renders in a global hologram without the consumer changing a line. pos_<id> asks about the requester by definition and stays player-bound.sn.papi().expansion("shop")
.placeholder("balance", player -> money(player)) // needs a player
.global("open_shops", () -> String.valueOf(registry.openCount()))
.globalPrefixed("top_", position -> topSellerAt(position))
.register();Strictly additive: two new public methods, zero public signatures changed, and the only behaviour change on an existing path is exposePlaceholders, which answers strictly more than it did. SnApi.LEVEL 16 -> 17.
Kits plugin for Paper 1.20.x and 1.21.x, built to be run from the menus and edited in-game. Every kit lives in its own file, every menu is a YAML file you can add to, and nothing about a kit needs a text editor: /kit edit <kit> opens the whole thing.
One kit is one file, kits/<id>.yml. Items are stored with Bukkit's own serialization, so enchantments, custom names, lore, custom model data, leather colors, skull textures and shulker contents all survive a round trip untouched.
Each kit carries a cooldown and six flags, all of them toggles in /kit edit: enabled, requires-permission, one-time, auto-armor, auto-offhand and drop-items-when-full.
A claim never overwrites what a player is wearing or holding. If the helmet slot is taken, the kit's helmet goes to the inventory instead. With drop-items-when-full: false a claim into a full inventory is refused rather than dropped on the ground, and refusing costs the player nothing: no cooldown, no spent one-time.
/kit edit <kit> then the items button opens a 6-row grid. Slots 0 to 52 are the kit contents at the exact position the preview will show them; drag anything in from your own inventory. Closing the grid saves, including with Escape. Any item can be the kit's off-hand item: shift-right-click it. Shields and totems still auto-equip by material.
Slot 53 adds a command item: an item that is not given but runs commands when the kit is claimed. Right-click it to set its display item, add commands, and choose whether each runs as the console or as the player. A command item with no display is invisible in the preview and still runs.
guis/kits-main.yml and guis/kits-more.yml ship as examples, and any other file you drop in guis/ becomes a menu you can open by name. Left-click claims, right-click previews. Each kit declares four icons, one per state (available, on cooldown, one-time used, disabled), and the plugin picks the one that matches the viewer.
Kit items resolve placeholders: put %player_name% or any PlaceholderAPI token, and & or &#RRGGBB colors, in an item's name or lore and it renders per player, both in the preview and on the item that lands in the inventory.
Lets a player claim the same kit more than once before its cooldown is over, one extra claim per extra copy they own. It is built for selling kits: the amount lives in a permission (snkits.uses.<kit>.<n>), so your shop only ever runs a LuckPerms command. The highest amount wins, so you never have to unset the previous node, and each claim carries its own cooldown so copies recharge one by one rather than all at once. Off by default.
Hands configured kits to players when they join, in first-join or every-join mode, through the same claim path as everything else. A denied give is always silent, so nobody is told about a cooldown every time they log in. Off by default.
Players claim from the menus. /kit opens one and that is the intended path; /kit claim <kit> exists for command blocks, shops and anyone who prefers typing.
Admin side: /kit create builds a kit from your current inventory, /kit edit, /kit delete, /kit enable and /kit disable, /kit give <player> <kit> bypasses every claim rule, /kit reset clears stored usage for one player, one kit, or everyone, and /kit gui <gui> [player] opens a specific menu. /kit import converts the kits of PlayerKits2. Every argument tab-completes for real, including kit ids filtered to what you may actually claim.
Optional. Without it everything works; with it, %snkits_cooldown_<id>%, %snkits_available_<id>%, %snkits_onetime_used_<id>%, %snkits_uses_left_<id>% and %snkits_uses_max_<id>% resolve anywhere. Inside the plugin's own menus and messages, {cooldown}, {uses_left} and {uses_max} do the same job without PlaceholderAPI installed.
config.yml ships lang: en, and the lobby and statistics menus (guis/lobby.yml, guis/stats.yml) are English.lang/messages_es.yml still ships complete, so lang: es plus /coinflip reload switches the messages back.lang never translated the menus, and now the files say so. Setting lang: en previously gave you English chat with Spanish menus; menu text is edited directly in guis/*.yml.Nothing to do, and nothing changes on a server that already ran this plugin. SnLib preserves the values already on disk, so your config.yml keeps its lang and your menus keep their current wording. Only a fresh install starts out in English.
/crates key wipe [crate] [confirm]Deletes the virtual key balances of every player on the server, offline players included. Without a crate id it clears every crate; with one it clears only that crate. Physical key items are untouched - they live in inventories and no balance command reaches them.
It cannot be undone, so it runs in two halves. Without the confirmation word the command only counts: it reports how many balances and how many keys are at stake and quotes back the exact line that would destroy them. Only a second invocation carrying that word deletes anything.
/crates key wipe
> WARNING This deletes 4,120 virtual key(s) across 1,284 balance(s), for EVERY player
> and EVERY crate. It cannot be undone. Run /crates key wipe confirm to go through with it.
/crates key wipe confirm
> Wiped 1,284 virtual key balance(s), for every player and every crate.messages.keys.wipe-confirm-word,
default confirm), so it translates with the rest of the plugin. It is never
tab-completed - having to type it is the safety.sncrates.admin.wipekeys (default op, child of sncrates.admin). Required on top of
sncrates.admin.keys, so the ordinary key commands can be delegated to a junior admin
without handing them the one that empties the key economy.
messages.keys.wipe-confirm-word, wipe-warning, wipe-warning-crate, wiped,
wiped-crate, wipe-nothing, wipe-nothing-crate, wipe-failed. They merge into an
existing lang/messages_en.yml on boot; your edits are kept.
plugin.yml declares depend: [SnLib, Vault]. A server without Vault.jar will not load
SnCoinFlip at all. Install Vault before updating.
vault: true is
served by whatever economy plugin registered its provider with Vault (EssentialsX, CMI,
XConomy, ...). It needs no other field - no id, no commands, no placeholder - so a fresh
install can wager with zero editing.vault: true currency registers either way and starts working the moment a provider appears -
no restart, no /coinflip reload. Until then its wagers are refused as unverifiable and the
players are told exactly that, not told they are broke.money currency first, one command-backed
example and one EdTools example. The five EdTools examples are down to one: copying the block
is all the other four ever were.Nothing in an existing config.yml changes meaning. vault: is a new key that means false when
absent, so every currency you already have keeps the backend it already had.
currencies: is a section this plugin never edits (# sn:extensible), which also means the new
money currency is not inserted into a config.yml that already exists. To pick it up, add it
by hand:
money:
display-name: "&aMONEY"
vault: trueor delete the whole currencies: block and let the plugin write the new defaults back on the next
boot.
The editor tells you how to get out of a chat prompt. Every prompt now
sends the cancel word and the seconds left underneath it. One language key
(messages.editor.cancel-hint), so it covers every prompt including new ones.
Preview Layout is a click, not a chat prompt. It cycles the layouts listed
under the new editor.preview-layouts in config.yml, skipping any id that no
file in guis/ backs, so a crate can never end up pointing at a preview that
will not open.
Accepted Keys offers physical, virtual and both. PERMISSION is off the
button. It still parses out of a crate file and still opens crates - clicking
the button on such a crate restarts the cycle at physical.
A reward's win commands are listed on its icon, numbered, so you can see
which ones are there. A line is marked in red when it will not run as written:
it is empty, it uses a placeholder that does not exist (only {player},
{amount} and {crate} are filled in), or it uses a %papi% token on a server
with no PlaceholderAPI.
Fixed: opening a crate panel logged Invalid value in config.yml -> 'effects.complete-particle-count': received '40', using default '40' once per
render. Nothing was wrong with the value - the panel was reading a number as
text. It is read as a number now.
Tidier defaults. The five three-state controls no longer share two dyes
between them; each state of each control has its own item. Every "Back" is the
same door and the arrows are only ever page arrows. The menu and message text
says what to write - FLAME, BLOCK_CHEST_OPEN 1 1.2 - instead of naming
types like "a Bukkit particle" or "DateTimeFormatter syntax".
CHAIN was avoided on purpose: it is IRON_CHAIN on 1.21.9 and newer. Every
material shipped here resolves on 1.20.2, 1.21.1 and 1.21.11.
The economy dependency contract was inverted. Vault is now the mandatory backend and EdTools is optional.
depend: [SnLib, Vault] - Vault backs the reserved economy.vault-economy-id, so it is the one provider the plugin can never run without. It was a softdepend until 1.0.1.softdepend: [PlaceholderAPI, EdTools] - EdTools is now one currency provider among the several this plugin will grow, instead of the hard dependency the whole plugin was built around.No configuration change is required, and no economy call site changed: EconomyService already routed the reserved id to Vault and every other id to EdTools.
Vault is now required. Install Vault plus an economy provider (EssentialsX, CMI, ...) before updating, or the server will refuse to enable the plugin. EdTools can now be removed if you only use the vault economy.
UnknownDependencyException: Unknown/missing dependency plugins: [SnSuperiorSkyblock]). It is a
soft dependency now: install it and every island rule applies exactly as before, leave it out and
the same plugin runs as a plain chunk loader.config.yml, lang/messages_en.yml and plugin.yml now state which rules need the island
plugin. The shipped item lore is island-neutral; existing values are preserved on update.Requires SnLib. SuperiorSkyblock, DecentHolograms and PlaceholderAPI are optional.
PvP coinflip gambling for Paper 1.20.x / 1.21.x, with as many currencies as you care to configure.
A player stakes an amount, the wager goes up in a public lobby menu, someone accepts it, both stakes are debited, a fair 50/50 winner is drawn and an animation plays before the winner takes the pot. Lifetime totals are kept per player per currency.
guis/*.yml.lang/ merged on update so your edits and comments survive.SnLib.jar must be present in plugins/ (com.sn:snlib 1.24.1 or newer).plugins/.Sn-License/license.yml. One key covers every bundled Sn plugin.config.yml, lang/ and guis/ generate on first boot. Replace the shipped currencies: block with your own before letting players use it.edtoolapi: true.Two settings are worth reading the docs for, because getting either wrong costs players money and neither looks like a misconfiguration from the outside: balance-placeholder must render an exact, unabbreviated number, and verification.confirm-look-ticks must out-wait whatever refreshes that number on a proxied setup.
Requirements: Java 21+, Paper 1.20.x or 1.21.x
Full documentation: https://github.com/ValentinTarnovsky/Sn-Releases/tree/main/docs/gitbook/sncoinflip
/chunkloader give now reports exactly one outcome to each side. When the target's inventory is full the loader is queued, and both the admin and the player used to be told that immediately afterwards, contradicting the notice they had just received. The success lines are now on the delivered branch only, and the queued line carries the same size and remaining time the success line does.Neither issue could destroy, duplicate or lose a loader. An owed loader was always durable and is still handed over on the next join.
SnLib.jar must be present in plugins/ (com.sn:snlib 1.24.1 or newer).SnSuperiorSkyblock is required.config.yml, lang/messages_en.yml and guis/loader.yml are preserved. The reworded messages.given-queued line applies to fresh installs; an existing language file keeps your value, which stays correct without the new {size} and {time} placeholders.Requirements: Java 21+, Paper 1.21.8
Full documentation: https://github.com/ValentinTarnovsky/Sn-Releases/tree/main/docs/gitbook/snchunkloader
Placeable chunk loaders for SnSuperiorSkyblock islands. Each loader keeps an odd N x N square of chunks loaded and ticking, centered on its own chunk, and carries its own stored lifetime. Size 1 keeps 1 chunk, 3 keeps 9, 5 keeps 25, 7 keeps 49.
The lifetime counts down only while a loader is on and actually holding its chunks. At zero the loader turns itself off and stays where it is: nothing is deleted, and a player can feed it more time and switch it back on. A loader minted with the duration -1 never expires at all.
Chunk loading uses Paper plugin chunk tickets with the plugin's own per-chunk reference count, so two overlapping loaders share their chunks and a chunk is only released when the last loader covering it is gone. Loaded chunks tick redstone, hoppers, crops and generators without a player nearby. They do not spawn mobs, which is a vanilla rule rather than a plugin one.
Root is /chunkloader, aliases /scl and /snchunkloader. Every subcommand is administrative: players never type a command, they place, break and right-click the loader itself.
| Command | Description | Permission |
|---|---|---|
/chunkloader give <player> <size> <duration> | Gives one loader item. Duration accepts 30m, 12h, 1d12h and -1 for infinite | snchunkloader.admin.give |
/chunkloader list [player] | Lists the loaders a player has placed, with position, size, status and time left | snchunkloader.admin.list |
/chunkloader chunks | Shows how many chunks are kept loaded, by how many loaders, and how many of those are on | snchunkloader.admin.chunks |
/chunkloader reload | Reloads the configuration, the language file and the menu, and restarts the task intervals | snchunkloader.admin.reload |
/chunkloader help [page] | Shows the command list | snchunkloader.admin |
/chunkloader debug | Toggles runtime debug output | snchunkloader.admin.debug |
snchunkloader.use (default true) is the player-facing gate: placing a loader, breaking one, and right-clicking one to add time or open its menu.
| Key | Default | Effect |
|---|---|---|
chunk-loader.material | BEACON | Block placed as a loader, and the material of its item. Decide it before any loader exists and then leave it alone: a placed loader is recognised by its block, and loader items already handed out carry the old material and stop being placeable. |
chunk-loader.size.min / .max | 1 / 7 | The odd sizes give may mint. A hard ceiling of 31 applies whatever you set. |
chunk-loader.time.max-stack-seconds | 2592000 | Ceiling on the lifetime one loader can accumulate by stacking. 0 is unlimited. |
chunk-loader.limits.per-player | 3 | Placed loaders per owner. 0 is unlimited. |
chunk-loader.limits.per-island | 5 | Placed loaders per island, counted as number of loaders. 0 is unlimited. |
chunk-loader.island.require-own-island | true | Membership gate on placing, breaking, feeding and toggling. Turning it off lets anyone take someone else's loader. |
chunk-loader.island.protected-area-only | false | Restricts placement to the island's protected range instead of its full claimed area. |
chunk-loader.island.return-on-membership-loss | true | Returns a loader when its owner stops being a member of the island it stands on. |
chunk-loader.tasks.reconciliation-interval-seconds | 120 | Sweep that recovers loaders removed with no event, and re-checks island membership. 0 disables both. |
hologram.provider | snlib | Display backend: snlib or decentholograms. Falls back to snlib when DecentHolograms is absent. |
database.type | sqlite | sqlite needs nothing else. mysql reads the connection block below it. |
Aliases for /chunkloader live in command.aliases and are re-read on /chunkloader reload.
max-stack-seconds says.limits keys work the same way: they are checked at placement and never again, so lowering one blocks new placements and leaves standing loaders alone.hologram.provider and hologram.offset-y apply to displays created after the change. A display keeps the backend and the height it was created with, so those two keys need a restart rather than a reload.Nd Nh Nm. Zero components are omitted and seconds are never shown.SnLib.jar must be present in plugins/ (com.sn:snlib 1.24.1 or newer).SnSuperiorSkyblock is required: it owns the island model the plugin is built on.config.yml, lang/messages_en.yml and guis/loader.yml on first boot.plugins/.Sn-License/license.yml. One key unlocks every bundled Sn plugin on the server. The key is validated at every startup through an outbound HTTPS call to sn-license-server.okimc-dev.workers.dev, so allow that host through your firewall. The jar hash is part of the check, and a repacked jar fails it.Requirements: Java 21+, Paper 1.21.8
Full documentation: https://github.com/ValentinTarnovsky/Sn-Releases/tree/main/docs/gitbook/snchunkloader
keepInventory now really keeps them. On a keep-inventory death the grid and cursor stacks
are returned to the player's own inventory instead of being scattered on the ground at respawn.No configuration or permission changes. Nothing to migrate.
Item drops are blocked for every player by default. /drop opens a personal, time-boxed window during which that player's drops go through; when it expires the block returns on its own, with no scheduled task involved.
On death, the contents of the player's 2x2 crafting grid are force-dropped. Vanilla preserves those four slots across respawn, so without this they are a pocket that defeats the restriction entirely.
| Command | Description | Permission |
|---|---|---|
/drop | Opens your drop window | sndrop.use |
/drop reload | Reloads the configuration files | sndrop.admin.reload |
/drop help | Shows the command list | sndrop.use |
/drop debug | Toggles runtime debug output | sndrop.admin.debug |
| Key | Default | Effect |
|---|---|---|
drop.duration-seconds | 30 | Length of the window /drop opens. Values below 1 are raised to 1. |
drop.refresh-window-if-active | false | When true, /drop during a live window restarts the timer from now. It never adds time. |
messages.blocked-cooldown-millis | 1000 | Minimum gap between two "you cannot drop" warnings to the same player. 0 warns on every attempt. |
Aliases for /drop live in command.aliases and are re-read on /drop reload.
PlayerDropItemEvent is guarded./drop did to the window, and what the death sweep confiscated.SnLib.jar must be present in plugins/ (com.sn:snlib 1.24.1 or newer).config.yml and lang/messages_en.yml on first boot.Requirements: Java 21+, Paper 1.21+
Violation alerts are now opt-in. After this update your existing staff stop
receiving them until each one runs /snchat alerts once. That choice is then
saved and never has to be repeated.
snchat.notify now decides who is eligible for chat-control violation alerts, not who
receives them. Alerts start off for everyone./snchat alerts turns your own alerts on or off, and the choice survives relog and
restart. Previously it only lasted the session and alerts came back on at the next login.snchat.notify still silences a player immediately, even if their opt-in is on file.data.yml in the plugin folder, written by the plugin, holding who opted in. It is never
seeded or merged, and is not meant to be edited by hand.SnYml.flush() no longer blocks on a write it can land itself.
flush() joined the scheduled async write with a 10-second Future.get on the calling thread. On
a live 1.21.8 Paper server, a consumer that paired save(); flush(); on the primary thread saw
that join expire its full budget on every single call - ten seconds of frozen server per admin
command, with a watchdog thread dump each time. The data still landed, because flush() fell
through to an inline write once the timeout expired; only the clock was lost.
flush() now takes the staged snapshot over and writes it itself. The scheduled drain then finds
nothing staged and exits.
Two waits are involved and only one is bounded, which is the design:
ioLock became a
ReentrantLock for this - an intrinsic monitor cannot be acquired with a timeout, and a
version that used synchronized would hang teardown forever on a wedged disk.save() staged is not a courtesy. It always happens, blocking
uninterruptibly if the disk is wedged, exactly as the previous code did after its timeout.drainPendingWrites() now holds ioLock across the take as well as the write, so a concurrent
flush() cannot return while the drain is holding the only copy of a snapshot.
No public surface changes. SnApi.LEVEL is unchanged and japicmp reports no incompatibility, so
consumers pinned at any earlier 1.x need no recompile.
Consumers need no change. save() alone was always the complete persist path at runtime;
flush() is a teardown primitive the context already calls for every mounted file. Pairing
save(); flush(); in a command or listener was never necessary and is what triggered the freeze.
/customcrafting set no longer freezes the server. Every set parked the main thread for
ten seconds and tripped Paper's watchdog ("The server has not responded for 10 seconds"),
followed by Can't keep up!. /customcrafting remove did the same, and a set onto a block
that already held a workstation paid it twice.
The workstation write handed the file to the async scheduler and then waited for it on the main thread - the same thread that has to dispatch the task. The wait could only expire, never succeed. Both commands now return within a tick, and the workstation is still persisted immediately.
No data was ever at risk: the file was always written, just ten seconds late.
config.yml no longer ships the update-check block. The releases feed is public, so
anonymous polling already works and the token was never needed. Existing servers can leave the
key in place - it is ignored.Group-based chat formatting for Paper, with inline showcase tokens, chat moderation, automatic announcements and a per-group command whitelist.
Chat formatting - one format per LuckPerms primary group, falling back to a global chat-format. Every line can carry a hover tooltip and a click action, and every string supports & codes, &#RRGGBB hex and PlaceholderAPI. Placeholders are {prefix}, {suffix}, {displayname}, {name} and {message}.
Showcase tokens - [item] puts the item a player is holding into their message, tooltip and all. [inv] and [ec] post a clickable tag that opens a frozen replica of the sender's inventory or ender chest, viewable by anyone who clicks it. Each family has its own material blacklist.
Chat moderation - caps, character repetition, message cooldown, a plugin:command syntax block and a global chat mute. Each module either corrects the message or denies it, has its own snchat.bypass.* node and its own staff-alert switch.
Announcements - a rotating or random list on its own interval, each entry with its own hover and click.
Command whitelist - a per-LuckPerms-group allowlist of the commands a player may run, with unlisted groups falling back to default.
Put SnChat.jar, SnLib.jar and LuckPerms into plugins/ and start the server. SnChat generates its own config.yml, lang/messages_en.yml and both guis/ files on first boot, and announcements.yml and blockcommands.yml once it enables.
The command blocker is on out of the box. blockcommands.yml ships with a populated default group and every unlisted group falls back to it, so players are restricted from the first boot. Add your groups to that file or set command-blocker.enabled: false. This is the one default that surprises people.
Licensing, permissions and the full config reference are in the README.
The four line tags - [center], [rgb], [small], [noprefix] - are gated by their own snchat.prefixtags node, separate from snchat.color. They affect the whole line rather than a span of it, so selling coloured chat to a donor rank does not also sell line centring.
%sntempblocks_*% placeholder worked under /papi parse but rendered as a raw %token% in DecentHolograms and DeluxeMenus: the expansion answered null off the main thread, which PlaceholderAPI reads as "this expansion does not handle that placeholder". Both of those refresh asynchronously, so the first frame showed the value and every refresh after it showed the token. The main thread now publishes a snapshot on the expiry sweep and the resolvers read it from any thread, so /papi parse, a hologram and a menu all show exactly the same thing. Countdowns keep the absolute wipe instant, so they stay exact between publishes.%sntempblocks_mode%, %sntempblocks_bypass% and the n/a of a PER_BLOCK zone reached PlaceholderAPI as ₢f2Interval / &7n/a, because PAPI output never enters SnLib's render pass. They are rendered before they leave now.messages.wipe-done-empty line when there was nothing to remove.Drop-in. The new messages.wipe-done-empty key is merged into your language file on boot; your existing values and comments are preserved.
A complete rewrite. SnCrates 2.0.0 was written from zero on SnLib and does everything 1.16.2 did, with the same crates, the same commands and the same key items already in your players' inventories.
Read the "Before you update" section. This is a major version and it is a clean break, not an in-place upgrade: some stored data is deliberately not carried over.
Every table now carries the sncrate_ prefix, so 2.0.0 does not read the rows 1.16.2 wrote. On the
first boot you get empty key balances, statistics, opening history, physical block bindings and
reward filters.
The prefix is not cosmetic. CREATE TABLE IF NOT EXISTS never compares columns, so a table name
this plugin does not exclusively own can bind the schema to another plugin's table and fail every
query for the life of the install.
The one you will notice on the server: every placed crate block loses its binding and becomes an ordinary breakable block. Re-bind them from the editor.
If you ran a pre-release 2.0.0 build on a staging server, this empties those tables too, not only 1.16.2's.
Back up your database and your plugins/SnCrates/ folder before updating. Virtual key balances are
the thing worth restoring by hand if you have a busy server.
config-version is gone and nothing is ever regenerated. New keys merge into your files on boot and
your values, your comments and any extra keys you added survive. You can freeze a section you do
not want touched with a # sn:extensible marker.
Old files are simply not read. There is no importer and no migration path - that is deliberate.
SnCrates 2.0.0 needs SnLib 1.24.0 or later installed. It will not enable without it.
The screen that sets a reward's item, adds a reward or sets a crate's key item no longer has a slot you drag into. Hold the item in your main hand and click the capture cell.
This replaced a hand-built inventory that could not use the library's click protection and leaked real reward items in five separate ways.
mass-open-max now ships as 64 instead of unlimited. -1 and 0 still mean genuinely
unlimited, and the config comment says what that costs you: the open loop runs on the main thread,
so an unbounded mass open is a server freeze proportional to the number of keys.
Rather than running with per-player limits unenforced, an open is refused with a new
messages.open.data-loading line and a reload of the slice is attempted.
/crates key take and /crates key set now refuse on a crate that accepts no virtual keys,
instead of reporting success and doing nothing. New line: messages.keys.no-virtual-balance.give.[amount] stays optional for give, giveall and take, and required for set.animations.csgo.strip-size and animations.wheel.strip-size are capped at 256.cancel already did.blocks.bind-armed / blocks.unbind-armed now say "click ... either button", because either
button really does consume the armed gesture.CrateOpenEvent fires immediately after the session is registered rather than just
before. Still after the key is consumed and before the reward is delivered, so the documented
contract is unchanged.api-events.enabled, defaults.reward-weight.messages.general.reload-summary and reload-failed.These are the things a rewrite is most likely to break, so they were held constant on purpose.
sncrates:crate_id tag, so keys already in
players' inventories are recognised.%sncrates_total_opens%, %sncrates_keys_<crateId>%,
%sncrates_opens_<crateId>%, %sncrates_chance_<crateId>_<rewardId>%./crates, aliases crate and snc. A bare /crates still opens your own key
balance.CrateOpenEvent and CrateRewardEvent keep their signatures.Rebuilt as a SnLib consumer: configuration, language, colour, menus, item building, database access, the command tree and PlaceholderAPI all run on the library instead of on hand-rolled code. The plugin no longer bundles a menu library or a YAML library, which removes an entire class of "another plugin shipped a different version" crashes.
Supports Paper 1.20.x and 1.21.x on Java 21.
/clan givepoint <point> <amount> [clan|player] now accepts a player nick as its optional target argument. /c givepoint kills 1 Snopeyy credits the clan Snopeyy belongs to instead of failing with "No clan named Snopeyy was found".messages.player-no-clan ("<player> is not in a clan"), so the message says what is actually wrong./clan info uses. Omitting the argument still credits your own clan.givepoint now suggests clan names plus online player nicks.guis/permissions.yml layout notice added in 1.8.0, which its own text scheduled for removal in this version.Requires SnLib 1.20.0 or newer. SnClans refuses to enable against an older engine.
/tempblocks bypass switches your own tracking off while you build inside a zone, buckets included. Run it again to switch it back on.
The permission now gates the toggle instead of bypassing on its own. sntempblocks.bypass is replaced by sntempblocks.admin.bypass: holding it means you MAY flip the switch, not that your blocks stop being tracked. Previously any operator was silently unable to place a temporary block at all, which made a zone look broken while testing it.
The toggle lasts for your session and is dropped on logout, so a bypass left on by accident cannot keep filling an arena with permanent blocks. Revoking the permission also ends an active bypass at the next block placed, without waiting for a logout.
%sntempblocks_bypass% renders the viewer's own state, restyleable under status.enabled / status.disabled in the language file.sntempblocks.admin.bypass (default op, child of sntempblocks.admin).sntempblocks.bypass. Grant the new node to anyone who had the old one.BlockPlaceEvent now exits on an empty toggle set instead of calling hasPermission for every block placed anywhere on the server.
First stable release.
Player-placed blocks that expire inside WorldGuard zones, bucket liquids and their flow included.
Two engines, per zone
PER_BLOCK - every block expires on its own timer, with per-material lifetimes and a default.INTERVAL - one timer per zone; when it fires, everything tracked there disappears at once, with configurable warnings before it.What it guarantees
Surface
/tempblocks list | info <zone> | here | wipe <zone|all> | purge <zone>, plus SnLib's injected reload, help and debug. Aliases /tb, /stb.sntempblocks (the viewer's zone, or any named zone).Requirements: Java 21+, Paper 1.20.x or 1.21.x, SnLib 1.24.0+, WorldGuard 7.0+.
Audited before release (/sn-audit FULL, gate PASSED): two criticals found, fixed and verified, both of which could turn a temporary block permanent - liquid flow overwriting the record of a block it only waterlogs, and the zone-removal policy not running at boot.
Timed crafting workstations for Paper servers. Register a block in the world, and players right-click it to pick a recipe, spend the ingredients and wait out a timer before claiming the rewards.
A workstation is any block you point at with /customcrafting set <id> [recipe-set]. The id is how you address it everywhere else - in /customcrafting remove, and in every placeholder.
Each workstation is bound to a recipe set, which is simply the list of recipes its menu offers. One block can be a mining forge, another an alchemy table, and a third can offer both sets' worth of recipes - it is entirely up to how you group them in recipes.yml. A workstation that names no set uses settings.default-recipe-set.
Registered blocks are protected: they cannot be broken, blown up, burnt or pushed by a piston, so /customcrafting remove is the only way to unregister one. Turn that off with settings.protect-workstation-blocks if you would rather manage it yourself.
Right-clicking a workstation opens its menu. Every recipe of the set is placed at its own configured slot and looks exactly like the display block that declares it, so adding a recipe never means editing the menu file.
Clicking a recipe spends the ingredients immediately and starts the timer. Each craft rolls its own duration between the recipe's min and max minutes, so two players crafting the same thing do not finish in lockstep. The clock only runs while the player is online, and crafts survive a restart - a 90-minute craft started before a reboot picks up where it left off.
One workstation runs one craft per player at a time. A player who right-clicks a workstation they already have a craft on is told how long is left rather than being charged again.
When it is done, the same right-click claims it: the recipe's reward commands run from the console, and the craft is consumed first, so a reward line that fails cannot be replayed for a second payout.
Ingredients are items this plugin creates, declared in items.yml. They carry a signature only the plugin can write, so a renamed vanilla lookalike never counts as an ingredient - an anvil cannot forge one, and neither can a shop that sells a similarly-named stack.
The same file defines the results a recipe hands back. customcrafting give <player> <item> [amount] works from the console, which is how crates, quest plugins and the recipes' own reward lines hand out plugin items - a vanilla /give cannot produce one.
| Command | Description |
|---|---|
/customcrafting | Show the command list |
/customcrafting set <id> [recipe-set] | Register the block you are looking at as a workstation |
/customcrafting remove <id> | Remove a workstation |
/customcrafting bypass | Finish your own active crafts instantly |
/customcrafting give <player> <item> [amount] | Give one of this plugin's items |
/customcrafting reload | Reload configuration, items, recipes and language files |
/customcrafting debug | Toggle runtime debug output |
Alias: /cc, editable in config.yml under command.aliases. Write the full command name in reward lines, crate contents and anything else outside the plugin: the alias is yours to rename, and other plugins ship a /cc of their own.
| Permission | Default | Description |
|---|---|---|
customcrafting.* | OP | Everything below |
customcrafting.use | true | Basic usage |
customcrafting.admin | OP | All admin commands |
customcrafting.admin.set | OP | Register a workstation |
customcrafting.admin.remove | OP | Remove a workstation |
customcrafting.admin.bypass | OP | Skip craft time |
customcrafting.admin.give | OP | Give plugin items |
customcrafting.admin.reload | OP | Reload configuration |
customcrafting.admin.debug | OP | Toggle debug output |
customcrafting.admin.update | OP | Receive update notifications |
Crafting itself needs no permission: any player can use a workstation you registered.
reload, debug and the update notice are built by SnLib, which derives their permission from the plugin's own name, so what it actually tests is sncustomcrafting.admin.reload, .debug and .update. The nodes in the table are declared as parents of those, so granting the documented ones - or the customcrafting.* wildcard - is enough on LuckPerms and on any permission plugin that applies Bukkit child permissions. If yours does not, grant those three directly; there is no sncustomcrafting.* wildcard.
Requires PlaceholderAPI. <id> is a workstation id.
| Placeholder | Description |
|---|---|
%customcrafting_status_<id>% | Display name of the recipe being crafted |
%customcrafting_time_<id>_short% | Remaining time, short format |
%customcrafting_time_<id>_long% | Remaining time, full format |
%customcrafting_progress_<id>% | Crafting progress percent |
Short-format aliases: %customcrafting_time_<id>% and %customcrafting_<id>_time%.
What they show when there is nothing to report is under status: in the language file, and both duration formats are templates you control in config.yml.
config.yml - language, database, the default recipe set, workstation protection, the progress template and both duration formats.items.yml - every item the plugin can create, ingredients and results alike.recipes.yml - recipe sets. Each recipe declares its menu icon and slot, a min/max duration in minutes, the items it consumes and the console commands it runs on claim.guis/<recipe-set>.yml - the menu of that set. The file name is the set id.lang/messages_en.yml - all player-facing text.New keys merge into your files automatically on boot, with your values and comments preserved.
Crafts are stored in SQLite by default, which needs no setup. Set database.type: mysql and fill in the connection block to share them across a network.
Two configurable custom pickaxes for Paper servers: one that mines a cube, one that multiplies the drops. Both respect region protection per block rather than all-or-nothing.
Area Pickaxe breaks an N x N x N cube centred on the block you mine. The size is any odd number (3, 5, 7, ...), bounded by a max-size safety cap. An even value is rounded up and anything above the cap is clamped, with a warning on boot.
Duplicator Pickaxe drops extra copies of every block it mines. extra-drops: 1 doubles, 2 triples, and there is no upper cap. Copies can go straight into the miner's inventory, so auto-pickup cores do not swallow them.
Every block in a cube is removed with a real vanilla break, so drops, experience, tool durability and the MINE_BLOCK statistic behave exactly as they do for the block you clicked. Fortune and Silk Touch apply throughout.
A block that carries contents is never duplicated: a shulker box with items in it, a bee-filled hive. Empty containers duplicate normally. This keeps the Duplicator from turning one filled shulker into an unlimited source of whatever is inside it.
omnitool: true is a global switch that turns both pickaxes into an omni-tool. The instant a player starts mining, the pickaxe morphs in hand into the tool matching the block, at the same tier as its configured material: a Diamond Pickaxe becomes a Diamond Axe on wood, a Diamond Shovel on sand, a Diamond Hoe on leaves, and a Pickaxe again on stone. Because the held item genuinely becomes that tool, mining speed and drops are correct natively, and both abilities work on those block types.
A morph rebuilds the item from items.yml, so it keeps the configured name, lore and enchantments. A morph is one-way: an item that has become an axe stays an axe until it is mined with again.
Each pickaxe has its own regions block, so you can limit where its special ability works per WorldGuard region, in blacklist or whitelist mode, with an independent list each. When the ability is blocked the item still mines as a normal vanilla pickaxe; the swing itself is never cancelled.
WorldGuard is optional. Without it the pickaxes simply work everywhere. A restriction that is configured but cannot be enforced is reported as a warning rather than ignored silently.
| Command | Description |
|---|---|
/snpickaxes give <player> <area|duplicator> [amount] | Give a pickaxe to a player |
/snpickaxes reload | Reload configuration, items and language files |
/snpickaxes debug | Toggle runtime debug output |
/snpickaxes help | Show the command list |
Every subcommand is admin-only; there are no player-facing commands. The root command is gated by snpickaxes.admin, so a sender without that node is not offered /snpickaxes in tab completion. The children snpickaxes.admin.give, .reload, .debug and .update all default to OP.
config.yml holds what the pickaxes do: sizes, drop counts, region rules, the omnitool switch and the debug settings.items.yml holds what they look like: material, display name, lore, custom model data, glow, enchantments, unbreakable, keep-on-death and no-drop, one section per pickaxe. %size%, %extra% and %multiplier% are available in the name and lore.lang/messages_en.yml holds all player-facing text.New keys merge into your files automatically on boot, with your values and comments preserved.
advanced.block-break-event-priority defaults to MONITOR and should stay there unless a core cancels the break to auto-collect. Below MONITOR the handler also runs before WorldGuard, Lands and GriefPrevention, which would hand out copies for breaks those plugins then refuse. The plugin warns about this on every boot.
A teardown flush that can be bounded.
Bukkit clears the plugin's enabled flag BEFORE onDisable runs, so thenSync's is-enabled guard drops every success continuation from the first line of onInnerDisable onward. Blocking was the only way left to observe a teardown write, and join() had no timeout - a consumer flushing player state had to choose between freezing the server stop on one unreachable database and dropping the write ordering the flush exists for.
SnFuture.joinWithin(Duration) - join() under a budget. true = settled in time (the value is then one non-blocking join() away), false = the budget ran out with the work still running (not cancelled), and a throw = it FAILED, with the same CompletionException / CancellationException join() already throws. Same main-thread rules as join(): silent in teardown and bootstrap, warned elsewhere, and a main-completed future still refuses to be waited on from the main thread.DbConfig.connectTimeoutSeconds() / socketTimeoutSeconds() - a budget above the future is only half a bound. SnDb set no Hikari connectionTimeout and left socketTimeout at the driver's unlimited default, so one getConnection() against a black-holed host outlasted any consumer-side timeout. Two optional keys in the same database section, safe defaults, existing configs unchanged:database:
connect-timeout-seconds: 10 # default 10, clamp 1..3600 - Hikari connectionTimeout (both backends) + MySQL connectTimeout
socket-timeout-seconds: 30 # default 30, clamp 0..3600, 0 = unlimited - MySQL socketTimeoutStrictly additive: three new public methods, zero signatures changed, zero behaviour changed on any existing path. japicmp gate passes. SnApi.LEVEL 15 -> 16.
Placeholders on an event thread.
SnPapi.applyHere(viewer, text) resolves PAPI on the calling thread, the documented escape hatch for text assembled inside an event (above all AsyncChatEvent) that has no later main-thread point to resolve at. Fail-open; apply and applyOnMain remain the answer everywhere else.SnLang.get(key, viewer, Ph...) / getList(key, viewer, Ph...) render a fragment FOR a viewer, for text spliced into something else rather than sent.Strictly additive, japicmp gate passes. SnApi.LEVEL 14 -> 15.
Refinements to the 1.21.0 plugin-supplied stack surface. No API change - SnApi.LEVEL stays 13 and nothing public was added, removed or altered.
A template that declares neither display-name nor lore adds nothing to a supplied stack: no appended empty line, no cleared lore, no name written, no normalising pass. This already held in 1.21.0, but it held by convention - the item meta was still fetched. The emptiness test now runs before the meta is fetched at all, so a bare template cannot write to the stack even by accident. The result is the plain clone, equal to the input field for field.
Every shape of "undeclared" reaches that same guarantee: an absent key, display-name: "", and lore: []. The distinction it rests on is unchanged and now tested head-on:
raw-item: # adds nothing at all
click-actions:
- "[player] kit claim {kit}"
spaced-item:
lore:
- "" # a list holding an empty string DOES append a blank line
- "&7Click to claim"Scope, stated honestly: this is the contract of the overlay. The stack that reaches the inventory still carries the snlib_gui_item anti-theft marker - one PDC key, written after the overlay runs, as on every rendered GUI stack since 1.0.0.
docs/SNLIB-DOCS.md section 12 now itemizes it, so a consumer can size the surface from facts:
Ph[] clone, one Binding record, one ItemStack clone.ItemStack clone always, plus one Component per name and lore line the template actually declares - and nothing else for a bare template.SnItem.fromConfig render it stands in for (~20 YAML reads plus a full meta write).update-interval: ticks. A per-frame full-strip repaint at animation rates belongs on a raw Bukkit inventory.484 tests across 22 suites green; additive-only japicmp gate clean.
Plugin-supplied stacks in menus. All three GUI bind surfaces now accept a ready-made ItemStack, so a menu can display contents the plugin did not author - crate rewards, kit contents, shop stock, lootbox previews - whose enchantments, custom model data, head texture and custom name no YAML item definition can re-express.
The split is: the stack supplies the appearance, the template keeps supplying the behaviour.
s.bind(22, gui.template("reward"), reward.icon(), Ph.of("chance", 25)); // manual bind
s.bindPaged("kit-item", kit.contents(), (i, ph) -> ph.stack(i)); // paged entry
s.bindEach("stock", offers, (o, e) -> e.template("offer").stack(o.item())); // region cellNew public methods:
GuiSession.bind(int slot, GuiTemplate template, ItemStack stack, Ph... phs)PhCollector.stack(ItemStack stack)GuiEntry.stack(ItemStack stack)Overlay rules, identical on all three: a non-empty display-name replaces the stack's name, declared lore lines are appended after the stack's own lore, both resolved through the normal pipeline (viewer PAPI, local placeholders, colour, rgb). A template that declares neither leaves the stack visually untouched; nothing else of the template is applied.
Unchanged: view-requirements, the per-click matrix, click and deny actions, render precedence, itemAt, handleClick, the strict-clicks gate, bind lifetime across page changes, refreshes and inventory recreation, and the snlib_gui_item anti-theft stamp. A null stack is exactly the previous behaviour, so the surface is strictly additive: every consumer compiled against 1.20.3 keeps compiling and behaving identically.
SnApi.LEVEL 12 -> 13. The additive-only japicmp gate records three new methods and no removal.
/voucher giveall <voucher> [amount] [-s]Hands a voucher to every online player in one command. Same grammar as /voucher give minus the target: the trailing [amount] [-s] pair is read in either order, and amount is the amount per player, never a pool split between them. An auto-claim voucher dispatches its rewards to everyone instead of minting items.
The pass never aborts part way. A player who cannot be served is counted and skipped, and the sender gets a summary at the end naming how many received and how many were skipped. -s suppresses every line, sender and receivers alike.
New permission snsimplevouchers.admin.giveall (default op, child of snsimplevouchers.admin).
give.drop-overflowControls what happens when the voucher items do not fit in the receiver's inventory.
give:
drop-overflow: truetrue (default, and what every give did before this version): the receiver keeps what fits and the rest drops at their feet, so nothing is ever lost.false: all-or-nothing. A receiver without room for the whole stack gets nothing, and the giver is told they were skipped. A partial delivery never happens, so a giver is never told they handed over 10 vouchers that turned into 4.The key applies to all three ways a voucher item reaches a player: /voucher give, the new /voucher giveall, and the give-on-click of /voucher open. Auto-claim vouchers are unaffected, since they dispatch rewards and never produce an item that could overflow.
messages.inventory-full, messages.give.inventory-full and the four messages.giveall.* lines merge into your language file on boot; your existing values and comments are untouched.
SnTags 2.0.0 is a full rewrite on SnLib. Same plugin, same commands, same placeholders - rebuilt from zero on the shared engine.
This is a clean break, not a drop-in update. Read the upgrade section before installing it on a live server.
Back up plugins/SnTags/ and your database. Two of the changes below are irreversible.
Then, in plugins/SnTags/, delete config.yml, messages.yml and guis.yml.
This is not optional. 2.0.0 does not read the 1.x config layout, and leaving the old config.yml in place produces a file the server cannot parse. When that happens the plugin starts on built-in defaults - a MySQL server silently falls back to a new, empty SQLite file and serves every player no tags, with nothing in the log that names the cause. tags.yml is the exception: keep it, it is read as-is.
| Tag ownership (who owns which tag) | Kept. Same table, same schema. |
Personal tags from /tagadmin custom | Kept, including their numeric ids. |
tags.yml | Kept and read as-is. |
| Whichever tag each player had equipped | Reset. 1.x stored this in a table 2.0.0 does not read. Everyone keeps their tags and re-picks one from /tags. |
| Mixed-case tag identifiers | Merged, irreversibly. 1.x stored VIP and vip as two tags; on the first start they become one and the duplicate ownership row is deleted. Players keep the tag through the surviving row. |
config.yml, messages.yml, guis.yml | Not read, nothing converted. Re-enter your database credentials, your table-prefix and any restyling by hand. |
Placeholders now need a viewer. %sntags_tag% and %sntags_has_tag% parsed with no player - a hologram line, a console-context parse - are left as the literal text instead of resolving. 1.x substituted an empty string for %sntags_tag%. This is decided in the PlaceholderAPI bridge, above the plugin. If you branch on %sntags_has_tag% anywhere, make sure that parse carries a player, or the condition takes the else branch for everyone.
%sntags_tag% still returns raw, uncolorized text with its & and &#RRGGBB codes intact, and the expansion identifier is still sntags. That contract is unchanged since 1.0.0 and is safe for cross-network chat formats, TAB and scoreboards.
Tag display text is now validated on input. /tagadmin create and /tagadmin custom refuse non-cosmetic MiniMessage. Colours, decorations, <gradient> and <rainbow> keep working; <click>, <hover> and similar are rejected, because tag text reaches other players' chat and a clickable element there runs as whoever clicks it. Text already in tags.yml or in the database is not re-checked.
MySQL pool tuning is one knob. database.pool-size replaces the four pool.* keys of 1.x.
Hand-editing tags.yml while the server is running. /tagadmin create and /tagadmin delete rewrite the file from the copy the plugin loaded at startup, so edits you make by hand are lost if either command runs before a reload.
Run /tags reload after editing the file by hand and before using those commands. Editing it while the server is stopped needs no reload, and /tags reload on its own is always safe.
A fix is planned for 2.0.1. It is not in this release because the first attempt at it turned out to be worse than the bug - it could rebuild tags.yml from an incomplete read and drop the other tags - so it was reverted rather than shipped.
Everything below the commands is different: SnLib owns config, language, the menu, the database pool and the scheduler, and there is no hand-rolled infrastructure left in the plugin.
guis/tag-selector.yml. Slots, materials, lore, ordering and pagination are all yours to edit - no positions are hardcoded.lang/messages_en.yml.tags.yml. Move a block to reorder it; it is never sorted alphabetically./tags reload, /tags help and /tags debug come from SnLib.Java 21+, Paper 1.20.x or 1.21.x, SnLib. PlaceholderAPI is optional; without it the expansion is simply not registered.
Showing the 40 most recent releases.