Every plugin and every SnLib version, newest first.
/mg join no longer requires a game. Without one, you join a random round that is still open (waiting for players and not full). /mg join <game> works exactly as before.messages.commands.join.no-open-round, sent when no round is open to join. It merges into existing lang files automatically.No config key, permission or SnLib change (still SnLib 1.15.0).
Payload may not be larger than 32767 bytes once its shop grew past about 32 KB, both on reload and when joining that backend right after it restarted. The proxy now sends messages that large in parts and the backend puts them back together; smaller messages are unchanged.Replace the jar on the proxy and on every backend, and restart the proxy: the fix lives there, so large shops keep disconnecting players until the proxy runs 1.14.2. A backend still on 1.14.1 ignores the split messages and keeps its previous shop until it is updated.
Full documentation lives in the Sn-Releases repo under docs/gitbook/sncredits/.
ConcurrentModificationException traces from Paper's async command builder after /snchat reload when blockcommands.yml had been edited. 2.1.1 only covered a reload that changed nothing. The command whitelist now rebuilds its placeholder commands before the command tree is sent, so no player is left with stale tab completion.command.aliases no longer gets a placeholder command.com.mysql.cj.jdbc.Driver). The legacy com.mysql.jdbc.Driver shim shipped by Paper printed a [STDERR] deprecation warning every time the plugin enabled with mysql.enabled: true. Behaviour is otherwise unchanged; SQLite installs are unaffected.Each companion can dress its Bedrock substitute to match its own head.
1.12.0 gave Bedrock players a baby zombie wearing the companion's head, dressed in one armour set configured for the whole server. With a varied collection a single colour clashes with most heads, so a companion file can now choose its own leather colour:
# companions/<id>.yml
bedrock:
armor-color: "DD7430"It overrides bedrock.armor.color from config.yml for that companion alone. A companion that says
nothing keeps the global colour, so nothing changes on upgrade.
# optional. Quote it: an unquoted 123456 is a number to YAML and is
rejected with a warning.bedrock.armor.chestplate / leggings / boots on LEATHER_*
(the shipped default). Any other material is worn exactly as configured.companions/ is seed-only, so existing files do not get the key by themselves - add it by hand where you
want it. Fresh installs find a commented example in companions/ember_fox.yml.Whether a per-item leather colour survives Geyser's translation to Bedrock has not been confirmed. If it does not, Bedrock players see plain undyed leather instead; nothing else breaks.
No new command, no new permission, no new language key, no database change, and no public API change -
API_VERSION stays 1.0.0.
bm: modelsA head with a BetterModel model (bm:<id>) was invisible to Bedrock players. The cause was the
render primitive, not the model: Nexo's Scaffolding addon bridges BetterModel content to Bedrock
through EntityTracker only, and this plugin drew its engine heads with a DummyTracker, which
that bridge never sees. bm: heads are now bound to a real carrier entity, which puts them inside
the bridge. Verified end to end on a real Bedrock client.
An EntityTracker costs a real server entity (ticking, persistence, entity count), and that cost is
per entity. It is worth it here because these are a handful of static decorations: heads run
with rotation-speed, bounce-speed and bounce-height at 0, so the carrier never moves and never
sends a movement packet.
Do not carry this change over to SnPets, SnCompanions or EdToolsMinions. Those are hundreds of
moving entities, several per player, where the arithmetic does not work; they keep the
substitute-render approach. This note exists for whoever finds EntityTracker here later and assumes
it is the fleet-wide pattern.
bm: head is made of nowOne invisible marker armor stand carrier per head: no gravity, silent, invulnerable,
non-collidable, never saved to the world, tagged in persistent data and with the scoreboard tag
snrotatinheads. It is reaped by the existing orphan purge, and a carrier removed by an entity
cleaner is rebuilt within a second.
view-range on a bm: head is now capped by vanilla entity tracking. A value above
spigot.yml entity-tracking-range.other (64 by default) has no effect. The shipped default of 48
is unaffected. It is also now the client-side Display cutoff, so it scales with each player's
Entity Distance video setting - the same way plain heads have always behaved.bm: head only exists while its chunk
is loaded (it already worked this way - heads spawn and despawn on chunk load/unload), and each one
adds one invisible entity to entity counts.Heads without a model, meg: (ModelEngine) heads, holograms, click actions, sizes and animations are
unchanged.
Bedrock players can finally see their companions.
Everything this plugin draws a companion's body with is a Display entity, and Geyser has no Bedrock
definition for one: it drops the spawn without a word. The name plate is a TEXT_DISPLAY, which Geyser
does translate, so the symptom was not "nothing" but the companion's name frozen in mid-air over an
empty spot - the label lines were arriving all along, riding a vehicle that never came.
When the viewer is on Bedrock they are now sent a different entity instead: a baby zombie wearing the companion's own head plus leather armour, moving by exactly the same arithmetic as the real companion and carrying the name plate. Java players receive byte-for-byte what they received before and see no change whatsoever.
Nothing, to upgrade - the new config band is merged into config.yml on boot, preserving your values.
For the feature to do anything you need Floodgate on the server (it is an optional softdepend).
Without it every player is treated as a Java client and the whole band does nothing. Installing or
removing Floodgate while the server is running is handled live: every online player is re-checked and
every formation rebuilt a tick later, so no restart is needed.
config.yml band: bedrock| Key | Default | What it does |
|---|---|---|
enabled | true | Send Bedrock viewers a substitute |
scale | 1.0 | Multiplied by the companion's own model.scale. 1.20.5+ clients only |
height-offset | 0.0 | The zombie stands on the ground where the companion is. The one value to nudge in game |
baby | true | Half height, oversized head, which is what makes the worn head legible |
invisible | false | Off by default: the dressed figure is the point |
mount-label | true | Leave this on - see below |
armor.chestplate / .leggings / .boots | LEATHER_* | Any armour material; NONE empties a slot |
armor.color | "" | Leather dye, RRGGBB hex. Quote it. Unverified on Bedrock |
mount-label: false does not hide the name plate. The label lines are sent to every viewer before the
companion is, so turning it off only stops them being attached to anything - they stay frozen in the air,
which is the exact bug this release fixes. It is a diagnostic for a broken mount, not a way to hide a label.
formation.facing is OWNER_YAW/OUTWARD/CENTER and has no
"off" value, so there is nothing to switch.animation.bounce.height: 0.0.invisible: true does
not help, so Bedrock's client-side entity push may nudge the player. Keeping it a baby halves the box.
This is the first thing to check in game.head-texture. So give every companion a head-texture
even if it renders as a model, or its Bedrock substitute wears the default head. All three shipped
companions already do.ITEM_DISPLAY and vanish, while the
reveal's name line is a TEXT_DISPLAY and arrives - leaving the name hanging alone, the same orphan-label
symptom this release removes everywhere else. Known remaining gap, deliberately out of scope here.Nothing. Without Floodgate, or with enabled: false, the decorator never enters the render chain at all.
Wrapped but with no Bedrock viewer watching that companion, the per-pass cost is three stores, one field
read, one branch and one virtual call - no allocation, no map lookup, nothing asked about the viewers.
Floodgate is asked once per player, on join, and never from the animation tick.
No new command, no new permission, no new language key, no database change, and no public API change -
API_VERSION stays 1.0.0.
If your network runs Geyser and Floodgate, your Bedrock players saw nothing where a pet should be, while Java players saw the model perfectly. This release fixes that.
Two independent causes, one answer.
Every pet BODY this plugin draws is a Display entity: the head backend is an ITEM_DISPLAY, and a
model pet's bones ride an invisible ITEM_DISPLAY carrier. Geyser has no Bedrock definition for
ITEM_DISPLAY, so it discards the spawn in silence. The pet's name plate is a TEXT_DISPLAY,
which Geyser does translate, so what a Bedrock player actually received was the pet's name frozen
in mid-air at the spot it first appeared, with no pet under it and nothing moving it.
Separately, models are drawn through BetterModel's DummyTracker, and the Bedrock content bridge
only knows EntityTracker, so it never learns the pet exists at all.
A Bedrock viewer is sent a substitute their client can actually render: an armor stand wearing the pet's own head texture, following the pet by exactly the same movement arithmetic, with the pet's name plate riding it.
Java players are completely unaffected. They receive the same packets they received before, byte for byte. And on a server without Floodgate, nothing changes at all.
bedrock: config bandIt is added to your config.yml automatically on the first boot; your existing values are kept.
| Key | Default | What it does |
|---|---|---|
enabled | true | Off restores the previous behaviour exactly |
scale | 1.0 | Multiplied by the pet's own model.scale, so pets/*.yml needs no changes |
height-offset | -0.7 | Normally negative: an armor stand's head sits above its feet |
small | true | A small stand; off roughly doubles the height, so double the offset too |
invisible | true | Hides the stand's body, leaving only the head it wears |
mount-label | true | Lets the pet's name plate ride the substitute |
If a Bedrock player reports seeing nothing, flip invisible to false first: some Bedrock versions
hide an invisible entity's equipment along with its body, which would hide the very head the
substitute exists to show. Every key takes effect with /pets reload, no restart needed.
minecraft:scale attribute, which only exists on clients 1.20.5
and newer. Below that the substitute renders at its natural size.pets/, lang/ or guis/. The developer API
is untouched and API_VERSION stays 1.0.0.{prefix} and {suffix} now accept hex written without the &. A LuckPerms prefix like
#E70000&lO#F30000&lW#FF0000&lN#E60000&lE#CD0000&lR #CD0000 - the shape every hex-prefix
generator produces - used to reach chat as literal text, because the render pipeline only knew
&#RRGGBB and <#RRGGBB>. It now renders as colour.&#RRGGBB is left alone, so is the
#RRGGBB inside a MiniMessage tag such as <gradient:#FF0000:#00FF00>, and so is a # that
is not followed by six hex digits.config.yml, and hex
a player types into their own message, still needs the & - applying this to the message body
would let anyone colour chat without holding snchat.color.ShopMenuOpenEvent (cancellable). Fired when a player right-clicks a shop and
SnDisplayShops is about to open one of its menus. It carries the player, the shop and a
ShopMenuType of OWNER or BUYER - decided from who clicked, not from what the shop is.api-events.enabled (default true). With it off nothing is dispatched and
every cancellable event answers "not cancelled", so no other plugin can stop a menu from opening
SnDisplayShopsAPI.getApiVersion(). The four existing
facade methods are untouched: this release only adds.Nothing to do. The new key is merged into your config.yml on boot with your values and comments
preserved, and a server with no plugin listening for the event behaves exactly as 2.8.0 did.
An enum key survives the YAML boolean trap. A server that set debug.level: OFF - spelled exactly as the comment above the key spells it - got two WARN lines naming a value it never wrote, and ended up at DEBUG, the loudest level, from an owner asking for silence:
Invalid value in config.yml -> 'debug.level': received 'false', using default 'DEBUG'
Invalid value in debug.level: 'false', using DEBUGYAML 1.1 resolves six unquoted spellings - on, off, yes, no, true, false, in ANY case - to a boolean before any Sn code sees the value, so OFF arrived as the boolean false.
SnYml.getEnum(key, type, def): reads an enum constant case-insensitively and maps a boolean raw back onto the constant that produced it (false to the enum's OFF/NO/FALSE, true to its ON/YES/TRUE). Unquoted stays valid, quoting keeps working. Use it for every enum-valued key.SnDebug reads debug.level through it, so both warnings are gone and OFF means OFF.SnApi.LEVEL 21 -> 22 (new public method, additive gate passes).
lang/messages_es.yml on the first boot, because SnLib only
auto-seeds the English file and the Spanish one has to be on disk before the
lang module loads. That call was made unconditionally, and Bukkit's
saveResource answers a destination that already exists with its own warning
rather than an exception, so from the second startup on the console showed
Could not save messages_es.yml to plugins/SnPets/lang/messages_es.yml because messages_es.yml already exists.replace = false).No configuration, command, permission or API change. Upgrading is a jar swap.
Could not save messages_es.yml to plugins/SnCompanions/lang/messages_es.yml because messages_es.yml already exists on every startup after the first.lang/messages_es.yml itself, because SnLib only ever seeds the English file and the Spanish one has to exist before the lang module loads. Bukkit's saveResource does not fail when the file is already there, it warns, so a healthy install kept warning about a file the plugin had written on purpose.lang/messages_es.yml is still written on boot, and no file on disk is ever overwritten.notifications.challenge-reminder.interval-minutes (default 20), counted per player. A fresh joiner is left alone for join-delay-seconds (default 60); enabled: false stops it, live on /battlepass reload.notifications.challenge-reminder in lang/messages_en.yml, with {challenges} (ready) and {slots} (total). Emptying BOTH title and subtitle now sends no title at all instead of a blank one - that is how you make the reminder chat-only. MiniMessage works there, so the line can be made clickable with <click:run_command:'/battlepass challenges'>.%battlepass_challenges_ready%: the same count the reminder nudges with, for scoreboards.No command, permission or schema change. New config and language keys merge in on boot.
Three fixes to the schedule that shipped in v1.11.0 earlier today. If you are on 1.11.0, take this one.
An entry whose map: names a map that does not exist still opens its round on the map rotation, which is the intended fallback. But the warning saying so was cleared by that very success, so it fired again on the next run: a */5 * * * * entry with a typo'd map id logged 288 console warnings a day.
The two warning conditions are independent now. "Could not open" still re-arms after a successful open, because a missing waiting spawn is transient and an admin can fix it without touching the schedule. "No such map" warns exactly once, because only a reload can fix an id.
%snminigames_next_game_id% returned a translatable wordWith nothing scheduled it returned the status.none word from the language file. That placeholder exists to be compared against, so a translated status.none would silently change what a scoreboard condition matched. It now returns nothing, matching %snminigames_game%.
%snminigames_next_game% is unchanged and still returns the status.none word - that one is only ever displayed.
ScheduleService recorded a cron job's id after arming it rather than before, so a throwing arm could have left a job no cancel path knew about. Not reachable in practice, since the expression is validated first.
The %snminigames_next_in% examples read as a two-component value (1d 3h). The renderer prints every non-zero component above the minute, so 1d 3h 27m is the realistic shape. Corrected in the README and the online documentation.
Drop in the jar. No config, permission or command change from 1.11.0.
Until now every game opened its own rounds on a treadmill: queue.auto-start-interval seconds after the last one ended, set per game in games/<game>.yml. Two things change that.
A master switch. queue.auto-start-timers: false in config.yml stops every game opening rounds on its own. Rounds then come only from the schedule and from /mg admin start.
A schedule, in one file. The new schedule.yml pairs each minigame with a cron expression, one entry per time slot:
enabled: true
entries:
tntrun-evening:
game: tntrun
cron: "daily 20:00"
spleef-friday-night:
game: spleef
cron: "0 22 * * 5"cron takes daily HH:mm, hourly :mm, or a five-field expression with *, lists, ranges and steps. A fired entry opens the round exactly like /mg admin start, so the game's own queue.countdown still runs and its queue.min-players still decides whether it actually begins. An entry whose game already has a round running is skipped, and a round missed while the server was offline is never fired retroactively.
The two switches are independent: run the schedule alongside the per-game timers, instead of them, or neither. entries is yours - an entry you delete stays deleted across updates.
| Placeholder | Returns |
|---|---|
%snminigames_next_game% | Display name of the minigame that opens next |
%snminigames_next_game_id% | The same game's plain id, for comparisons |
%snminigames_next_time% | Clock time of that opening |
%snminigames_next_in% | How long until it: 1d 3h, 2h 15m, 23m, 45s |
%snminigames_next_time_<game>% | When that ONE game next opens |
%snminigames_next_in_<game>% | How long until that one game opens |
Seconds only appear inside the last minute. With nothing scheduled they return the new status.none word from the language file, so you can restyle it once and every view follows.
/mg admin schedule (snminigames.admin.schedule) lists every entry with its next opening and flags any whose cron expression could not be read. An unusable expression is otherwise only a single line in the startup log, so this is the place to check your work.
A game-scoped placeholder typed in mixed case (%snminigames_state_TNTRUN%) reported the idle value through a live round. Ids now match case-insensitively everywhere, as the game registry always did.
Nothing to do, and nothing changes on its own. config.yml gains auto-start-timers: true, which is what every game already did, and schedule.yml is seeded with enabled: false and two parked examples. Turn both around only when you want the schedule to take over. The new permission is already a child of snminigames.admin, so an existing admin group needs no edit.
Requires SnLib 1.15.0 or newer.
item-model: now shows on the /voucher open icon. v2.3.0 added the key to the voucher-file schema and applied it to the physical voucher, but the two admin menus rebuild their icons from the voucher template of guis/*.yml rather than from the voucher item, so a voucher with an item-model: was listed as its bare material while the item in hand wore the resource-pack model.guis/categories.yml and guis/vouchers.yml) now forward the voucher's own key through item-model: "{item-model}". The line merges into an existing install on the next boot, keeping your edits - no file to delete.item-model: is unaffected: the placeholder resolves to nothing and an empty key is skipped silently.custom-model-data has the same behaviour in the menus and is NOT fixed here: menu files read that key as an integer with no placeholder pass, so it cannot be forwarded as text. Closing it needs a SnLib dependency bump (the per-entry stack override added in SnLib 1.21.0); this plugin is pinned at 1.20.3.
Vouchers can now speak for themselves. Three new optional keys in a voucher file, none of them mandatory - every existing voucher behaves exactly as before.
message: at the top level of the file - sent on any successful claim of that voucher.message: on an individual reward entry - sent only when that command actually runs, so a WEIGHTED/RANDOM crate can name the prize the player actually drew.deny-message: on a reward entry - sent when that entry's condition: did not pass.All three support & and &#RRGGBB colours, the five placeholders {voucher} {category} {amount} {count} {player}, and PlaceholderAPI. A blank value turns one off, exactly like a lang key.
deny-message firesOnly when the claim hands out nothing - when every reward on the voucher failed its condition. A claim that ran at least one command is a success and reports itself as one, so a player who got their reward is never also told about the tiers they did not qualify for.
When at least one failing entry has a deny-message, those messages replace the generic claim.no-eligible-reward line rather than printing under it. Entries without one stay silent, so you choose exactly which requirement is worth explaining. The voucher is not consumed either way, as before.
A multi-claim crate draws many times in one click, so identical reward messages collapse into a single line with a count instead of flooding chat:
You got a diamond! x87
A netherite ingot! Lucky. x3The x87 wrapper is the new claim.reward-grouped key in lang/messages_en.yml, so it can be restyled or translated. A prize drawn exactly once is sent with no suffix.
Giving a reward its own message: also stops that voucher from being swept up by a bulk.aggregate-by-category click on a sibling. Two vouchers that announce themselves differently are two different rewards from the player's seat, and consolidating them would report the wrong text. This only ever makes a click consume less, never more.
lang/messages_en.yml gains claim.reward-grouped automatically on boot, with your own values and comments preserved. Your voucher files are untouched.
vouchers/example.yml documents all three keys, but the vouchers/ folder is seed-only: an existing install keeps the example it already has and will not receive the new documentation. It is on the GitBook and in this release instead.
item-model: key in the item: block: the 1.21.2+ minecraft:item_model component, for resource packs that address items by model key instead of through custom-model-data.custom-model-data - a voucher may carry both, and neither overwrites the other.namespace:path. A key with no namespace means minecraft:, which is almost never what a pack wants.vouchers/example.ymlThe example file documents the new key, but the vouchers/ folder is seed-only: an existing install keeps the example.yml it already has and will NOT receive the new documentation. Only fresh installs get it. Your own voucher files are untouched either way, and the new key works regardless.
It ships commented out on purpose - an item_model key that no installed resource pack defines renders as the missing-model block, so a copied example would look broken.
A long teleport inside ONE world no longer makes companions disappear.
1.11.1 fixed this for a world change, and that was the wrong trigger: the cause is a DISTANCE, not a world. Teleporting far enough inside a single world - /tp 100 0 100 to 1000000 0 1000000, a /home, an island warp - broke in exactly the same way.
A client throws an entity away when it unloads the chunk that entity lives in. A player who travels further than the server is still sending them chunks for leaves every companion behind as something their client has already forgotten, and from that moment the plugin is moving companions that no longer exist: the packets are dropped in silence, so the server believes the companions are placed and visible while the player sees nothing. The packet that mounts a model companion's parts onto the invisible carrier they ride is only ever sent when a companion is CREATED, so they could never come back on their own.
The animation pass now measures how far each owner moved, in chunks, against the distance their own client is still being sent chunks for, and rebuilds the formation where they landed whenever they outran it. It watches the movement itself rather than a teleport event, so it does not care how the player got there - a command, a plugin, a portal, a warp, a vehicle or a respawn all look the same.
It also fixes the other half: a player already standing where you land now sees your companions in the right place, where before they were sent your companions at the coordinates you had just left.
Drop-in. No new setting, no file changes, nothing to migrate. The distance is derived from your server's own view-distance and each player's render distance, held between 2 and 12 chunks, so it can never drift out of step with your setup. Ordinary walking, sprinting and elytra flight never rebuild anything.
Requires SnLib.
Pets no longer go invisible when you teleport a long way inside ONE world.
1.24.1 fixed this for a world change. It turned out to be the wrong trigger: the cause is a
DISTANCE, not a world. Teleporting inside a single world - /tp 100 0 100 to
1000000 0 1000000, a /home, an island warp - broke exactly the same way, and nothing was
watching for it.
A client throws an entity away when it unloads the chunk that entity lives in. An owner who teleports further than the server sends them chunks leaves every pet behind as something their client has already forgotten, and from then on the plugin is moving pets that no longer exist: the move packets are dropped in silence, so the server believes the pets are placed and visible while the player sees nothing. Since the packet that attaches a model's parts to the invisible carrier they ride is only ever sent when a pet is CREATED, they could never come back on their own.
The animation pass now measures how far each owner moved, in chunks, against the distance their own client is still being sent chunks for, and rebuilds the whole formation where they landed whenever they outran it. It reacts to the movement itself rather than to a teleport event, so it does not care how the player got there: a command, a plugin, a portal, an island warp, a vehicle or a respawn all look the same.
Also fixed: someone already standing where you land now sees your pets in the right place. Before, they were sent your pets at the coordinates you had just left.
Nothing to configure and nothing to migrate: the distance is read from your own view-distance
and from each player's render distance, so it can never drift out of step with your server.
Ordinary walking, sprinting and elytra flight never trigger a rebuild.
logs/<date>.log only ever recorded completed purchases and sales, so every other way stock moves left no trace. All seven are recorded now:
| Action | What it is |
|---|---|
BUY | a player bought from a shop (stock went down) |
SELL | a player sold to a shop (stock went up) |
DEPOSIT | the owner put stock in (shift-click or the + button) |
WITHDRAW | the owner took stock out |
PICKUP | the shop was picked up and its stock returned |
DESTROY | a shop was removed with stock still in it and the stock was destroyed - the SuperiorSkyblock teardowns are the only paths that do this |
EXTERNAL | another plugin took stock through the API (a sellwand) |
BUY and SELL name the player's side, so a shop set to BUY produces SELL lines - the same wording every buyer-facing message already uses.
/dshop logs - read the log in gameNew command under the new permission sndisplayshops.admin.logs (default op, added to the sndisplayshops.admin children map). Filters are key:value tokens in any order; press TAB for the list.
/dshop logs days:7 action:withdraw,pickup,destroy player:Steve
/dshop logs shop:aaaaaaaa near:100 sort:oldest
/dshop logs min-unit:1 max-unit:1 days:30 # find a mispriced shop
/dshop logs material:hand days:7 stats # summary for what you are holdingaction: player: owner: shop: world: item: material: currency: days: from: to: min-qty: max-qty: min-unit: max-unit: min-total: max-total: near: sort: page: per-page:, plus a bare stats for a summary (movements per action, money per currency, top items, top players).
Notes: player:/owner: take a name or a uuid, shop: takes a uuid prefix, material:hand and near: read the sender, an unknown filter or an unreadable value stops the query and says which token was wrong, and days: together with from:/to: is refused rather than resolved by a hidden precedence. The scan runs off the main thread and is cached per sender, so paging does not re-read the files.
The actor column is now player= / player-uuid=; before 2.8.0 it was buyer= / buyer-uuid=, which stopped being true once the column carried depositors too. material= is new. /dshop logs reads both spellings, so your existing files stay fully searchable - but a script of your own that greps for buyer= needs updating. Every new file opens with a # SnDisplayShops log format 2 line so the two generations are told apart at a glance.
trade-log:
max-rows-per-event: 32 # per-variant lines one PICKUP or DESTROY may write
query:
default-days: 1
max-days: 30
max-results: 1000
max-scanned-lines: 500000
page-size: 8
cache-seconds: 60All auto-merged on boot; your existing values and comments are kept. The action words are restylable under status.action-* in the language file - they style the listing only and cannot change the file format.
SnDisplayShopsAPI is untouched: same constructor, same four methods, same return meanings. A successful removeFromStorage is simply recorded as an EXTERNAL line now.
A "deposit all" button in the owner menu, for clients that cannot shift-click.
A shift-click was the only way to put stock into a shop. Bedrock players, and anyone proxied through Geyser, cannot send one: their cursor click on the display cell assigns what the shop trades instead of depositing it. So stocking a shop was not awkward for them, it was impossible.
The new + control moves every matching stack from your inventory into stock in one click.
Your guis/owner.yml gets the new deposit: section merged in automatically, but your
layout: is never rewritten - the updater adds keys you are missing and never touches a value
you already have. So on an existing server the button exists but is not placed.
To turn it on, put a + on any free cell of your layout and run /dshop reload. The shipped
default puts it at slot 47, immediately left of the withdraw button:
layout:
- "fffffffff"
- "ffffdffff"
- "ffcfmfpff"
- "ffffeffff"
- "sssssssss"
- "<f+wkfif>"Leaving the + out is a legal choice and nothing warns about it. Check the letter against your
own menu size: a 45-slot menu has no slot 47.
Two lore lines and messages.shop-deposit-hint were reworded to name both gestures. Those are
values you already have on disk, so they are not rewritten either - edit them if you want the
new wording.
First stable release of SnDiscordLink.
Links Minecraft accounts to Discord accounts with one-time codes. Several servers share one MySQL database and elect a single embedded Discord bot through a database lease, so a player links once and every server, placeholder and reward follows.
/discord link in game redeemed with /link <code>
in Discord, and /link in Discord redeemed with /discord verify <code> in game.config.yml everywhere.linked-role given on link and removed on unlink, with optional nickname sync.claims: in-game rewards gated by a boost or a Discord role, each with its own cooldown.rank-roles: permanent LuckPerms groups mirrored to Discord roles. Only mapped roles are
ever added or removed, one PATCH per member.role-rewards: Discord roles that run on-gain / on-lose actions in game.rewards: action lists run once on link, on unlink and on a Discord leave./link, /unlink and the staff group /dlink info | unlink | accounts | stats.Java 21+, Paper 1.20.4+, SnLib 1.34.3+. JDA 6.6.0 is downloaded by Paper into libraries/ on
first boot, never shaded. Optional: LuckPerms, PlaceholderAPI.
Obfuscated with SnObfuscator (59 classes renamed, verify 60 structural / 60 data-flow / 0 failed).
SetPassengers that
mounts a model companion's bones onto its carrier. The bones stayed behind, unmounted and
invisible, for the rest of the session./companions reload already ran (discard -> buildRenderers -> reconcileViewers); buildRenderers is the
one that resolves every companion's target from the owner's CURRENT location. Covers the respawn
path too, and applies to companions drawn as player heads as well as BetterModel ones.Drop-in. No config, lang, GUI, companion, egg, command, permission or schema change; zero api/
contact, so API_VERSION stays 1.0.0. Port of the same fix shipped in SnPets 1.24.1.
enchantments key now works. wands.yml has shipped sellwand.item.enchantments: [DURABILITY;1] since the sellwand existed, but nothing read it, so the wand never got its glint. The key is now applied, and every item section in the plugin accepts it: wands, generator drops, shop icons and menu buttons.sellwand:
item:
material: BLAZE_ROD
enchantments:
- DURABILITY;1
flags:
- HIDE_ENCHANTSEntries are NAME;LEVEL. The level is optional and defaults to 1, so a bare MENDING works. Legacy names still resolve, so DURABILITY gives you today's Unbreaking and LOOT_BONUS_BLOCKS gives Fortune. Levels above the vanilla maximum are allowed. Pair the key with the HIDE_ENCHANTS flag for a glint with no extra lore line.
An unknown name or a bad level skips that one entry and leaves the rest of the list alone. Set debug: true in config.yml to see which entry was rejected.
Wands already in a player's inventory are untouched, since a use only rewrites the name and lore. Newly issued wands pick the enchantment up.
item_model. The five wand items in wands.yml (sellwand, adminwand, the free and radius upgrade wands, and buildwand) gained an item_model key that sets the vanilla minecraft:item_model component. Point it at a model your resource pack ships, as namespace:path:sellwand:
item:
material: BLAZE_ROD
item_model: "mypack:sell_wand"A bare sell_wand resolves to the minecraft namespace. Leave it empty to keep the material's default model. Unlike custom-model-data, this needs no numeric predicate in the pack, which is what downloaded model packs expect.
item-model works as well, if you prefer the hyphenated spelling used by the other keys.debug: true in config.yml to see which value was rejected.Requires Minecraft 1.21.4 or newer for the component to render. On older clients the key is simply inert.
Your existing wands.yml gains the new key automatically on the next start, with your values and comments untouched.
/pets reload path already did for every formation on the server.Drop-in. No config, lang, command, permission or database change, and nothing to migrate.
no release tag matching prefix '<id>-'. On Sn-Releases (203 releases) that hid 16 plugins, and the set drifted with every release anyone published - which is why the failures looked random. The feed is now walked page by page to the end of the repo (cap: the newest 1000 releases), so a plugin is found wherever its release sits.No public API surface was added (the new reader is internal), so SnApi.LEVEL stays 21: consumers pick this up by replacing the jar, with no recompile.
Year-based server versions are recognized. Every Paper server on Minecraft 26.1 or newer started with a false warning:
[SnLib] '26.2.build.2632-stable': unrecognized version, assuming compat targetMinecraft moved from the 1.21.x line to year-based numbering in 2026 (26.1, 26.2, 26.1.2), and Paper reports it as YEAR.DROP[.PATCH].build.N-status. SnLib's version parser read the numbers correctly but still gated on "major must be 1 and minor at most 21", so every 26.x server was flagged as unknown. Nothing was broken behind the warning: the unknown-version path already assumed full support, which is the right answer on 26.x.
SnVersion now recognizes both schemes. A year-based version is newer than the whole 1.x line by construction, so every supports(minor) and supports(minor, patch) gate passes there. No warning.Detected server: line on boot already says what SnLib saw. Only a version string that carries no MAJOR.MINOR pair at all still logs the single forward-compatibility warning.ParsedVersion record with a JUnit test over the classic 1.21.1-R0.1-SNAPSHOT, the floor 1.20.4, 26.2.build.2632-stable, 26.1.2.build.63-stable, the -alpha channel and an unparseable string.No public signature changed, so SnApi.LEVEL stays 21 and nothing recompiles: drop the new jar in and the warning disappears on 26.x. Consumers never read the version numbers themselves (they gate through SnVersion.supports and SnCompat.probe), so no consumer needs a change. If your own code compares SnVersion.MINOR directly, stop: on 26.2 the constants are 26 / 2 / 0, and only supports(...) knows that this is newer than 1.21.
/fourinline preview board now cancels the game. The close guard reopened the board of every live seat - the rule that keeps a real game alive when one of its two players closes their window - so a preview session outlived the window it was played in. The next /fourinline preview was then refused with "you are already in a game" until the turn countdown surrendered the admin against themselves./fourinline top over a preview and closing it again returns to the game.Two new keys are merged into lang/messages_en.yml on boot, values preserved:
messages.preview.cancelledstatus.result-cancelledmessages.preview.started now says the board can be closed to end a preview; an existing server keeps its current text (the merge never overwrites a value you have).
display now closes its own format codes. With display: '&6[&e&l50&6]' the
closing bracket could render bold once the badge was shown by a plugin other than SnRankUp.
The reset a colour code performs is only IMPLICIT in vanilla, and a consumer that maps
legacy codes to MiniMessage tags one for one emits <gold> without closing <bold>, so the
bold escaped the badge and ran to the end of the line.rankup.yml is read, and in what %snrankup_prefix%,
%snrankup_next_prefix% and %snrankup_top_value_<N>% publish.An opens: button's price: now accepts either the plain number it always did, charged in the
egg's price.currency, or a map naming its own currencies:
opens:
- amount: 1
price: 1000 # as before: 1000 of price.currency
- amount: 3
price: # new: charged in BOTH, together
vault: 2500
"edtools:essence": 300A map is an AND, never a choice. The button is refused unless every wallet covers its part, all of them are taken together, and if the open then fails they all come back. Quote a currency inside the map: the colon is the yml key separator.
eggs.yml. price: gained a second accepted shape, not a new
key, so a file that only writes numbers is priced exactly as it was in 1.10.0. That was verified
off-server with 1,952,456 comparisons of the old formula against the new one across button shapes,
amounts and both currency kinds: 0 differences, down to the last bit.vault leg last, because vault is the only one that settles a tick later and the only
one that cannot be handed back to a player who has already logged off. If any leg refuses, every
leg already taken is given straight back and the console names each one.vault nor edtools:<id>, or
the same currency twice costs that button: it is dropped from the menu with one console
warning rather than quietly charged for less.New {prices} placeholder on the egg buttons and on egg-charged, egg-no-money and
egg-refunded, built from two new language keys:
menus:
eggs:
currency-vault: "Monedas"
currency-essence: "Esencia" # add one per EdTools currency id
price-entry: "{price} {currency}"
price-separator: "&7 + &f"which renders 500 Monedas + 500 Esencia. A language that says it differently writes
price-entry: "{price} de {currency}" once and gets it everywhere.
{price} and {currency} are still bound, now from each button's own first currency, so a
guis/eggs.yml or a language file written before 1.11.0 keeps working untouched. On egg-no-money
they name the currency the player was short of.
companion-items.enabled now ships as false, and guis/main.yml no longer declares the
shift + right click or drop bindings that took a companion out, nor the lore that advertised them.
On a fresh install a companion cannot become an object that changes hands.
An existing server keeps its own settings. Values already on disk are preserved and never
pruned, so the feature stays on there until you set companion-items.enabled: false yourself. To
turn it back on later, set the switch and restore shift-right-click-actions and
drop-click-actions running [companions-extract] {instance} on the companion-entry template -
the commented block left in that file spells it out, and neither list is merged back in on boot.
0 now opens on a server with no economy plugin, and the menu draws
it as buyable. 1.10.0 refused it. A free price asks nothing of any provider.API_VERSION stays 1.0.0 and the api/ package is byte-identical to
v1.10.0./crates and the preview's info and withdraw buttons are all drawn from the crate's physical key item, and SnLib paints a template over a supplied item by appending the template's lore under the lore the item already carries. The result was every player reading the key's redeem instructions above the balance the icon exists to show, with no way for a menu file to remove or move them.{lore} placeholder. Those lines are not gone: write {lore} in a template's lore: and they come back, one lore line per line the key has, exactly where you put it. Nothing shows it by default.
guis/key-balance.yml - the default template and any per-crate templateguis/preview.yml - the info and withdraw templatesguis/preview-compact.yml - the info templateExisting installs get the new behaviour with no file edit; nothing in your menu files is rewritten.
A companion is created at level 0, not at level 1. Every path that mints one now starts it
unlevelled: opening an egg, a fusion result that inherits no level, and /companions admin give
when you give it no [level] token (its floor and its default are both 0 now).
Nothing you already have moves. Every companion keeps the level it has, and a level is worth
exactly what it was worth on 1.9.0: the buff, EdTools and buff-display ramps are untouched.
Because a ramp simply extends one step down, a companion sitting at level 0 is worth
initial - per-level - and a companion written with initial equal to per-level is worth exactly
level x per-level at every level including 0, which is what keeps a buff-display block agreeing
with the plugin that actually pays it.
The experience curve gains one rung, on purpose. experience.base is now the cost of leaving
the FIRST level, and level 0 and level 1 share that one requirement; every level above still adds
per-level. Extending the curve down instead would ask for base - per-level, and a requirement of
zero is the value that means "this companion does not progress at all" - the shipped ember_fox.yml
declares base: 500 with per-level: 500, so that extension would freeze every fresh copy of it at
level 0 forever. The curve position is clamped at 1 instead.
Smaller things that follow:
/companions admin setlevel <player> <instance> 0 is accepted, so a companion can be put back to
fresh.companions table declares level ... DEFAULT 0; a table that already exists keeps
DEFAULT 1 and neither notices, because every INSERT names the column. No migration runs.For API consumers: CompanionView.level() can now be 0, and
CompanionLevelUpEvent.getPreviousLevel() returns 0 on a companion's first level-up. The surface
is unchanged and API_VERSION stays 1.0.0; only a consumer that assumed a floor of 1 has anything
to adjust.
Documented, not code: tab completion of companion ids past the hundredth needs SnLib 1.34.1 or later at runtime - older versions cut the option list at 100 before matching what you typed. Typing an id in full always worked.
No configuration key was added, removed or renamed. Only comment text changed, so an existing server
sees no edit to config.yml or to its companion files.
Completes the v2.57.1 hotfix of the upgrade API. Implementation and documentation only: API_VERSION stays 1.4.0, no public method, event or constant changed.
v2.57.1 started honouring the withdrawal result, which is correct, but it also made a tier that costs nothing depend on the economy answering a zero withdrawal with SUCCESS. Several Vault providers answer a non-positive amount with FAILURE or NOT_IMPLEMENTED. A free tier, or one whose upgrade-cost key is simply absent and loads as 0.0, was therefore refused with NOT_ENOUGH_MONEY, and only after every per-hop event had already fired. A quoted total of 0 now skips the economy entirely and returns SUCCESS with charged 0.
The charge itself is third-party code: a provider that fires a synchronous balance event hands a listener one more window between the revalidation and the mutation. That reproduced the paid row on an air block that v2.57.1 closed on the other side of the withdrawal. The block is now read once more after the charge and before the generator is registered. If it is no longer the same generator on the same tier, the money is given back and the call refuses with NOT_A_GENERATOR or BUSY, so charged stays 0. A refund the economy rejects leaves a warning in the server log naming the amount and the player.
Location#getWorld() throws IllegalArgumentException("World unloaded") on Paper once the world reference has been cleared, so the previous getWorld() == null guard could never protect anything. upgradeGenerator, upgradeGeneratorTo, simulateUpgrade, simulateUpgradeTo, getGeneratorAt and isGenerator now use Location#isWorldLoaded() and answer normally instead of throwing.
GeneratorUpgradeEvent now says what a listener may assume: a multi-level upgrade fires one event per hop before anything is charged, and an event that was not cancelled is a proposal, not a commit. A later hop may still be cancelled, or the upgrade may still be refused after the events, and no compensating event is fired. simulateUpgrade and simulateUpgradeTo document that a dry run stops before the events, so a simulation never returns CANCELLED. API.md carries the same two points.
No config, message, permission, PDC, database or dependency change. Drop-in over 2.57.1.
buff-display: a companion whose effect lives in another pluginSome companions exist for a plugin that is not this one. A plugin that reads a player's equipped
companions through the SnCompanions API and grants a boost of its own for some of them is looking at
a companion that declares no buff: and no edtools-boosts:, because SnCompanions has nothing to
apply for it - and such a companion used to render as Damage 0.0% on every menu, item and
hologram.
The new optional block in companions/<id>.yml:
buff-display:
name: "&dBattle Pass XP"
initial: 0.4
per-level: 0.4
max: 20.0{buff} becomes name and {buff-value} becomes initial + (level - 1) x per-level, capped by
max (absent or 0 = no ceiling). SnCompanions never applies it - it only shows it, and it is
the LAST fallback: a companion that declares a real buff: keeps showing that, a companion whose
effect is its edtools-boosts: block keeps showing that, and only a companion with neither reads
this block. No companion that showed something before 1.9.0 shows anything different now.
The name is the switch: a name with no numbers is a legitimate declaration with no figure to it,
while numbers with no name are ignored with a console warning. Negative values are warned about and
read as 0 (a negative max means no ceiling at all). Nothing in this plugin widens the ramp and
nothing validates it, so write the same numbers the other plugin uses.
companions/ is seed only: the commented example ships in ember_fox.yml for a fresh install,
and you add the block to the companion files you already have by hand.
/companions admin setboost an EdToolsBoostManager javadoc still promised (the
leaf that actually fires that seam is setexp). Documentation only, no behaviour change.config.yml or lang/; both locales still carry the same 133 keys, verified by
parsing every one of the jar's 12 yml documents before release.API_VERSION stays 1.0.0 and nothing under api/ changed.Fixes two defects in the upgrade API shipped in v2.57.0. API_VERSION stays
1.4.0: no method, constant or signature changed, so this is a drop-in patch.
The withdrawal result is now honoured. Vault providers refuse a withdrawal by
returning an unsuccessful response rather than by throwing, and that response was
being discarded. A provider that refused the charge left the generator upgraded
anyway, with UpgradeResult.charged() reporting money that never moved. The
result is now checked: a refused withdrawal returns NOT_ENOUGH_MONEY with
nothing charged and nothing changed.
The generator is revalidated after the per-hop events. Those events run
third-party code, and nothing was re-read afterwards. A listener that removed the
generator sent the write down the "new generator" path, which created a paid
database row pointing at an empty block and quietly bypassed the placed-generator
limit; a listener that upgraded the same generator re-entrantly could charge the
actor twice. SnGens now looks the generator up again before any money moves and
refuses with NOT_A_GENERATOR or BUSY if it is gone, changed tier, became
corrupted, or had its scope locked meanwhile.
Also in this release:
player.getLocation() works as well as GeneratorView#location(), for
upgradeGenerator, simulateUpgrade, getGeneratorAt and isGenerator.
Previously only an exact block-corner location matched, because the internal
key carries decimals, yaw and pitch.CORRUPTED, NO_ACCESS and BUSY now report the quoted totalCost(), as the
documentation already promised.failedRequirements() no longer repeats the same message once per tier when
several tiers share one requirement.The NOT_ENOUGH_MONEY and BUSY documentation was widened to cover their new
cases. No configuration, language, permission, PDC or database change.
API_VERSION 1.4.0 (additions only, nothing removed or re-signed).
A consumer building its own upgrade menu can now list the whole upgrade chain, price it, dry-run it and execute it, without reimplementing any of SnGens' upgrade rules.
Reads (synchronous, in memory)
getUpgradePath(String fromGeneratorId) -> List<UpgradeStepView>: every tier
reachable after that type, in chain order, with per-step and cumulative cost.quoteUpgrade(String fromGeneratorId, int levels) -> Optional<UpgradeQuote>:
the price of climbing N tiers, capped at the end of the chain.quoteUpgradeTo(String fromGeneratorId, String targetGeneratorId) -> Optional<UpgradeQuote>:
the price of reaching an exact tier. Empty when the target is not ahead of the origin.Execution (synchronous, world-mutating)
simulateUpgrade(Player, Location, int levels) and
simulateUpgradeTo(Player, Location, String targetGeneratorId): dry runs. Same
checks, same result, nothing charged and nothing changed.upgradeGenerator(Player, Location, int levels) and
upgradeGeneratorTo(Player, Location, String targetGeneratorId): perform the climb.All four return an UpgradeResult carrying a typed UpgradeStatus, the final
tier, the levels climbed, the quoted total, the amount actually charged and the
failed requirement messages.
Semantics
GeneratorUpgradeEvent fires per hop, all of them before any money moves.
Cancelling any hop refuses the whole request with CANCELLED and charges nothing.upgrade-requirements is enforced, not just the
current tier's./gens upgrade does not apply. Access is owner or island mate,
exactly like a shift + right click upgrade.New public types: UpgradeStatus (12 constants), UpgradeStepView,
UpgradeQuote, UpgradeResult.
The existing upgrade paths (hand upgrade, /gens upgrade menu, upgrade wands) are
untouched. No config, language, permission, PDC or database change. Documented in
API.md and on the docs site.
{stock}: everything the shop holds, every variant summed.
Available in the display-name and lore of every state-bound element, and shown on the
withdraw button - the button that empties exactly that.1.500.000), the same formatting the buyer menu's {stock} uses.
It is not {amount}, which stays one variant's stock, shortened, in the stock grid.Showing the 40 most recent releases.