Every plugin and every SnLib version, newest first.
Hardening from the fifth full audit (0 critical findings; these are warning-level fixes).
INCIDENT files for the next boot. Cursor-rescue and take-back records are cleared when their claim commits after the player quit./trade reload after editing server-name keeps the running name (console warning: rename with a restart) and still applies every other setting. Before, the whole reload was dropped.scope.id: written through a forced temp file and a no-replace move, so a crash on the first boot cannot leave an empty identity file.messages.read-failed). A cancelled unrelated inventory open no longer drops the pending menu.cancel-words (new {cancel} placeholder); the request cooldown no longer shows 0s; lock/unlock confirmations reach the admin and staff even after a slow database write.InventoryOpenEvent#titleOverride on Paper builds older than 1.20.2.messages.read-failed merges automatically. Existing lang/messages_*.yml files keep their current input-chat-prompt text; add {cancel} there by hand if you want the configured words shown.save-player-data-on-complete: false) still keep their value-incident files until removed by hand.com.sn.auction.api (API_VERSION 1.0.0): other plugins can observe and veto market activity.SnAuctionProvider.get(): listings by id, seller or winning bidder, active count, pending payouts, amount formatting.api-events.enabled switch in config.yml (default true); the key is added to existing configs automatically.First stable release of SnTrade: secure player-to-player trading of items and Vault / EdTools currencies, built on SnLib.
Requirements: Paper 1.20.4 to 1.21.x, SnLib, the Sn bundle license key. Vault and EdTools are optional.
Highlights
Audit: final Sn audit gate PASSED (all criticals blind verified, final independent close pass clean). Not yet boot-tested on a live server; run a dev-server boot first.
First stable release of SnAuction: a secure auction house for Paper 1.20.4+ / 1.21.x with fixed-price listings and bid auctions, paid in Vault or EdTools currencies. Requires SnLib 1.36.0+.
/ah adminLicensed through the Sn bundle key (plugins/.Sn-License/license.yml).
First stable release of SnPlayerWarps: player warps for skyblock servers, with a warp browser, sponsored slots, visits, a chat link, owner tools, island rules (SnSuperiorSkyblock / SuperiorSkyblock2), costs in any configured currency, staff tools and maintenance.
Requirements: Java 21+, Paper 1.20.4+ (1.20.x and 1.21.x), SnLib 1.37.0 or newer, an Sn license key (bundle).
Hardened by a full audit before release: every known critical defect was fixed and blind-verified, including sponsor price drift, styled title and island name injection, island placement gates, command-currency payment and refund verification, double-click menu actions, malformed sponsor prices, refunds around startup and reloads, and ordered database writes.
Note: island names now drop < > % [ ] \ in every view, so a name can never carry a placeholder or a tag; color codes are kept.
transferSlots(from, to, amount) and transferSellMultiplier(from, to, amount) move stored generator slots or stored sell multiplier from one player to another, online or offline. Both return a CompletableFuture<TransferResult> with a TransferStatus: SUCCESS, INVALID_REQUEST, SAME_PLAYER, UNKNOWN_PLAYER, BELOW_MINIMUM, NOT_ENOUGH_FREE_SLOTS, TARGET_LIMIT, BUSY, CANCELLED.sngens.max.<n> permission slots and every other multiplier source stay put, so rank slots can never reach another account.getSlotBreakdown(uuid) and getSellMultiplierBreakdown(uuid) split a player's slots by source and report how much can be transferred right now.SlotTransferEvent and SellMultiplierTransferEvent, for cooldowns, logs or taxes in other plugins.player-defaults.multiplier, and the receiver respects player-generator-limit / player-multiplier-limit when enabled./gens wipeuser) now goes through one ordered queue. An older write can no longer land after a newer one, and a wiped player's row can no longer come back.No config, message, permission or database schema change. See API.md or the Developer API page for details.
Items with no placeholders and no colour codes were already handed over untouched and are unaffected.
No config, lang or menu changes.
SnSpec.Builder.commandPriority(), API level 26)A plugin that is the core of its server mode can now declare that its commands win:
@Override
protected SnSpec buildSpec() {
return SnSpec.builder()
.config("config.yml")
.lang()
.commandPriority()
.build();
}sn.commands(), dynamic or declared in the plugin.yml, takes its bare name and each alias from another plugin's command, registered before or after it. /money runs the priority plugin's command while Essentials keeps answering as /essentials:money; namespaced keys are never taken.Command '/money' of SnDungeons took the name from Essentials (command priority); Essentials still answers as /essentials:money (aliases: Alias '/bal' of '/money' in SnDungeons took the name from Essentials (command priority); Essentials still answers as /essentials:bal).Command/BukkitCommand it registers itself.plugin:key form, vanilla/Bukkit/Paper commands, commands registered through Paper's Brigadier API, commands.yml aliases, and a root of another plugin that declared priority too (whichever holds the key keeps it)./<plugin>:<name> and nothing else took the name meanwhile. Nothing is given back while the server is stopping.commandPriority() behave exactly as in 1.38.0.The old-<file>-<yyyyMMdd-HHmmss>.yml backups the config merge leaves in a menu folder (guis/ or a loadFolder folder) were loaded as ghost menus (old-... / <ns>:old-...). They are now skipped (one debug line each); a menu merely named old-town.yml or oldies.yml still loads. The pruning of those backups (its match unchanged) and the menu listers share one stamp regex.
15 new tests (667 total, all green). japicmp against 1.38.0: 2 NEW METHOD (SnSpec.commandPriority(), SnSpec.Builder.commandPriority()) and 1 NEW CONSTRUCTOR (SnCommands(Sn, SnLang, boolean, boolean)), nothing removed.
Runtime roots without the "not declared" warning. A root registered at runtime on purpose (its name comes from the owner's config or content files) can now say so:
sn.commands().root(currency.command()) // "money", "souls", ... from the owner's files
.dynamic()
.sub("pay").arg("player", Args.onlinePlayer()).executes(this::pay).and()
.register();RootBuilder.dynamic(): neither the root nor its aliases log Command '/money' not declared in the plugin.yml of ...; dynamic registration via CommandMap, at startup or on any reload.Command '/money' of SnDungeons is taken by Essentials; kept it. This command answers as /sndungeons:money. An alias gets the same line (Alias '/bal' of '/money' in SnDungeons is taken by Essentials; kept it. This alias answers as /sndungeons:bal).A pre-enable gate. SnPlugin.onPreEnable() runs after the API-level handshake and BEFORE SnLib creates config.yml, lang/ or guis/, so a license gate there leaves only its own license file on an unlicensed install:
@Override
protected boolean onPreEnable() {
return LicenseManager.init(this, "myplugin"); // seeds and reads license.yml only
}true. false (or disabling the plugin inside the hook) ends the enable with the plugin disabled, no context and no SnLib file; neither onInnerEnable() nor onInnerDisable() runs, and the library adds no log line.sn() is still null inside the hook: log through getLogger().onInnerEnable() rules: after disabling, one Enable aborted: <reason> line; without disabling, SEVERE with the stack trace and the plugin is disabled.Behavior change: onInnerDisable() needs a context. It now runs only for a plugin that got a context. A failed API-level handshake or a context init that threw used to call it anyway; with nothing enabled there is nothing to undo, so they no longer do.
Fix: namespaced alias keys no longer linger. An alias whose bare name another plugin holds still claims its plugin:alias form, but that key was never released: it outlived the root as a ghost command after an unregister or after the alias was dropped from the config. It is now released in both cases.
SnApi.LEVEL 24 -> 25 (2 new public methods, additive gate passes).
Root fallback subcommand. A root can now name one root-level leaf that takes over an unmatched first token, so /trade Steve runs /trade request Steve:
sn.commands().root("trade")
.sub("request").arg("player", Args.onlinePlayer()).executes(this::request).and()
.fallbackSub("request")
.register();RootBuilder.fallbackSub(String): a first token matching no declared subcommand or alias becomes the first argument of that leaf, with every token passed as its arguments. Declared subcommands always win (a player literally named accept is reached through /trade request accept), and the bare root keeps its onEmpty hook or generated help./trade <player>).register() throws IllegalStateException when the name matches no declared root-level leaf or names a group./trade acept answers "Player not found: acept").SnApi.LEVEL 23 -> 24 (1 new public method, additive gate passes).
/gens upgrade by tier listed the highest tier first. The sort used the upgrade cost, and the last tier's upgrade cost is 0. It now follows the upgrade chain, lowest tier first, the same order /gens inspect uses.No config, message, permission or API change. API_VERSION stays 1.5.0.
/gens inspect <player|uuid>: a read-only staff menu that lists a player's generators, or their whole island's, one item per tier with the amount. It looks like /gens upgrade, but nothing in it upgrades, removes or changes anything.
sngens.admin.inspect (default op, also granted by sngens.admin). Layout in the new gui/inspect_gens_gui.yml./gens upgrade always showed page 1 again. It now changes page.New keys are merged automatically (update-configs): gui/inspect_gens_gui.yml, and inspect-loading, inspect-busy, inspect-failed, help-entries.inspect in lang/messages_en.yml.
No surface change. API_VERSION stays 1.5.0.
events.yml can now show a boss bar while it runs. Add a bossbar section to the event:
bossbar:
enabled: true
text: '&#ff00ff&lChaos Drops &8| &7Time left: &f{time_left}'
color: PURPLE # PINK, BLUE, RED, GREEN, YELLOW, PURPLE, WHITE
style: SOLID # SOLID, SEGMENTED_6, SEGMENTED_10, SEGMENTED_12, SEGMENTED_20{name}, {time_left}, {duration}. The bar starts full and drains as the event runs out. Every online player sees it, including players who join mid-event, and /gens reload applies a new look right away.bossbar section, or with enabled: false, shows no bar. events.yml is never rewritten on update, so on an existing server add the section to each event you want it on./gens reload, no longer ends instantly the next time the rotation picks it./crates editor, pick up any item from your own inventory and click a crate with it. The crate list draws that crate with the item from then on, instead of the chest (or whatever templates.crate.material is). The crate stores a copy of one item: the item stays on your cursor, so you can click the next crate with it or put it back.{lore} placeholder on the crate template places it wherever you want it.guis/editor-main.yml now declares player-inventory: open, which is what lets you pick an item up while the editor is open. Nothing can be put INTO the menu: clicks on its cells stay cancelled. Existing installs get the new keys on the next boot; the two new lore hint lines only appear in a fresh file.editor-icon:. A value that cannot be read is reported once in the console and the crate is drawn with the default icon; the value is kept until you reset the icon.Crate#getEditorIconClone(), Crate#withEditorIcon(...), Crate.Builder#editorIcon(...).Now requires SnLib 1.28.0 or later (was 1.24.0).
Per-module resource folders and group help. A large modular plugin can now keep every module's files together under its own folder, and give each module's admin branch its own help:
modules/party/config.yml
modules/party/guis/*.yml
modules/party/lang/messages_en.ymlsn.guis().loadFolder("modules/party/guis", "party") / unloadFolder(folder): seeds and loads an extra folder's menus (managed, gated by update-configs, exactly like guis/) under party:<file> ids, so [open] party:shop works. Folders are remembered and reloaded with guis/, each folder owns its namespace, and registering a folder again re-reads its files.sn.lang().addSource("modules/party/lang", "party") / removeSource(folder): seeds and merges the folder's messages_en.yml (and the active translation against it) and serves its keys as party.<key>, with the global prefix. A global key always wins, and a folder key can never turn a global message into a section.SubCommandBuilder.groupHelp(): /dg admin party and /dg admin party help [page] list only that group's leaves, paginated, while /dg help shows the whole group as ONE line. It replaces the hand-written per-game help of SnMiniGames. Two new lang keys, snlib.help.group-header and snlib.help.group-footer, merge into every plugin's lang file on boot.onInnerEnable count.sn.yml().managed("modules/party/config.yml") already worked and is unchanged.SnApi.LEVEL 22 -> 23 (5 new public methods, additive gate passes).
config.yml. holograms.provider is snlib by default, which is built in and needs nothing else. decentholograms and fancyholograms draw them through those plugins instead, and there every viewer sees their own value of a player placeholder such as %sncrates_keys_<crate>%. A provider that is missing, or too old or new to work, falls back to snlib with one console warning. Switching provider takes effect on /crates reload.holograms.enabled is the master switch. height is the distance from the block's top face to the lowest line, and means the same under every provider. update-interval-ticks sets how often placeholders refresh, and lines is the template ({crate} is the display name, {crate-id} the id).hologram: section can replace lines or height for that crate only, or hide its holograms with enabled: false. Keys you leave out follow config.yml, and an editor save keeps the section exactly as you wrote it, placeholders included.Crate#getHologram(), Crate#withHologram(...) and the new com.sn.crates.model.CrateHologram.Existing installs get the new holograms: block in config.yml on the next boot, with holograms switched on. Set holograms.enabled: false to keep crate blocks bare.
farm challenge can now come from RivalHarvesterHoes (RivalBlockBreakEvent) on servers without EdTools.xp.passive.source (EDTOOLS default, or RIVAL_HARVESTER_HOES) decides only when BOTH are installed; with just one, that one is used. Needs a restart.SUGAR_CANE, WHEAT...) as its id in xp.passive.tool-overrides and in farm-challenge filters.EDTOOLS on boot./ban <player> content18 stored an 18 second ban with the no-reason text instead of applying the content18 ladder, because the old parser skipped letters and read the digits. A reason that is exactly a template id of the command's type is now always that template, and a token only counts as a duration when all of it is one.w (weeks), mo (months of 30 days) and y (years of 365 days), chainable as 1d12h. m stays minutes. 1mo used to read as one minute.{duration} is written with the new messages.format.unit-* keys (unit-day, unit-days ... unit-seconds), merged into your lang file on boot. Translate them to change "3 days" in every broadcast, screen, history line and Discord embed./snbans rollback <time> reads the same grammar on Paper and on the proxy.5x, 2dias, -5d) is refused on commands; in templates.yml such a step is applied as permanent with a console warning, as before.API: no surface change (API_VERSION 1.0.0).
winners, so a timeout always left more of them, and every one got the winner title, the winner broadcast and a podium reward handed out in UUID order.winners winners) by a per-game tiebreak:
messages.<game>.tiebreak, broadcast on a timeout to explain who got the tie. Added automatically to existing lang files.holograms.provider option in config.yml: auto (default, DecentHolograms first), decentholograms or fancyholograms. If the chosen plugin is missing, SnGens falls back to the other one. The provider is read at startup only; restart the server after changing it.& colour codes, &#RRGGBB hex and MiniMessage.Drop in the new jar. update-configs adds holograms.provider: auto to your config.yml, so servers running DecentHolograms keep working without changes.
/gens pickup vault writes are now ordered with every other vault write. A pickup used to write the player's vault directly while other vault writes went through an ordered queue. With a slow database, a pickup write could land after a /gens recover had emptied the vault and bring it back, so the same generators could be recovered twice after a restart. The pickup now goes through the same queue and removes the generators only once its vault write is confirmed.RefundIssuedEvent for a pickup fires only once the vault write is confirmed; a pickup aborted by a failed write fires nothing. API version stays 1.5.0 (no surface change)./battlepass now opens the pass menu on the page holding the player's highest reached tier instead of always on page 1 (a player on tier 23 lands on the page with tiers 19-27), so nobody pages through tiers they already passed./battlepass challenges directly) lands where a fresh open would.presentation.open-at-current-tier (default true, applied on /battlepass reload). Set it to false to keep the old always-first-page behavior. Added automatically to existing configs on boot./unban, /unmute and /unblacklist are now announced on every backend, honouring the -s / -p they were typed with (a silent unban reaches snbans.notify holders everywhere, a public one every player), under each server's own broadcasts toggles. Each server announces it exactly once. Delivery uses a new snbans_reverts table created on boot with no migration and no new database privilege.{reason} is stripped of colour codes, hex colours, MiniMessage tags and SnLib markers, so a console name like &#FF0000WatchDog posts as WatchDog and status words post without &a.snbans.overwrite node (default op). /ban, /mute and /blacklist (and their IP forms) on an account that already has an active punishment of that type replace it instead of answering "already has an active ban". The old one stays in /history as lifted by whoever overwrote it, only the new one is enforced and announced, and nobody is told the player was freed. The console never overwrites, so anticheats and chat filters punishing through it cannot shorten a sanction in force; a blacklist is only replaced by one that still covers the old address.Developer API: no surface change, API_VERSION stays 1.0.0. An overwrite fires PunishmentIssueEvent (cancel it to keep the old punishment), then PunishmentRevokedEvent for the replaced one (no PunishmentRevokeEvent), then PunishmentIssuedEvent.
Every backend should run this version for unbans to be announced across servers: an older build does not publish the notices.
storages.yml > collector.upgrade: enabled, max-level (1 to 32), island-bound and a price per level.
island-bound: true, a Collector never reaches into another island.{level} and {area}) through break, GUI Remove, admin pickup, island kick/leave/ban/disband refunds and /gens recover.economies section in config.yml. vault is built in; add any currency with type: PLACEHOLDER, a live balance placeholder and console take-command / give-command. A price such as vault: 250000 plus gems: 50 charges every economy or none./gens collector adjust <percent> and /gens hopper adjust <percent> scale every item type stored in the block you look at (+10, -10, 12.5%, minimum -100). Amounts round to the nearest item; holograms, open menus and the database update immediately. Range: storages.yml > admin-adjust.target-range./gens collector give <player> [amount] [level] gives upgraded Collectors./gens collector pickup <player> with the owner offline stores the Collectors in the owner's vault at their levels and drops their contents at each block, whoever runs it.storages.yml > collector.break.drop-cap (default 256) caps the stacks a Collector drops. A player break that would drop more is refused with a message instead of voiding items.collector.upgrade.levels or economies exists in your file, update-configs no longer re-inserts entries you removed.{level} / {area} to collector.item.lore and collector.hologram.lines yourself if you want them shown./gens recover and give commands never build stacks above the item's max stack size.CollectorUpgradeEvent (cancellable, before charging), CollectorView#level(), #effectiveLevel(), #areaSide(), StorageRefundIssuedEvent#getCollectorLevels().sngens_collector.level and sngens_refund.collector_levels automatically on first start (SQLite and MySQL).EdToolsBreakBlockEvent asynchronously, from its own worker threads, one event per block. When the killing blow came from an area tool, every block of that swing ended the fight again on its own thread and ran rewards.commands again: up to one payout per block (25 for a 5x5 tool). Players could trigger this on purpose.lootbox give) no longer run off the main thread./packetbosses reload, every running fight against that boss was ended and its stored row deleted, as if the boss had been removed on purpose. Running fights now keep going on the boss they started with, the same way paused fights were already kept. A boss that really was deleted or disabled, on a load that read every file, still ends its fights.damage-per-break says they should.No config, lang, command or permission changes.
An item reward stores a byte-exact snapshot of the item it was captured from, which is what lets a lootbox hand out another plugin's item at all. That snapshot is frozen: when the plugin that mints the item later changes how it renders the name or the lore, the stored copy keeps looking identical but its component tree differs, and vanilla refuses to merge the two. The symptom is "the item from the lootbox does not stack with the real one".
/lootbox audit (lootboxes.admin.audit) lists every reward item stored in more than one
non-stacking variant, with how many rewards carry each variant and where they live. Stacking is
decided with the server's own merge test, so a difference the game ignores is never reported./lootbox sync (lootboxes.admin.sync) takes the item in your main hand and refreshes
every reward meant to be that item but no longer stacking with it, across every lootbox at
once. Each reward keeps its weight, delivered amount, delivery method, command template and
enabled state. Rewards that already stack are untouched, so running it twice does nothing.display-item: reward rebuilds the item
from yml fields and drops the whole PDC) is matched by material plus name and regains the tag
on sync. The reverse never happens: a correctly tagged reward is never replaced by an untagged
lookalike.audit.max-items and audit.max-variants-per-item in config.yml, and the
messages.audit.* / messages.sync.* language keys.example.yml and the README now spell out display-item: vs nbt: for custom items, and that
an item unique by construction (a player head with a random profile id) can never stack from a
snapshot: use method: COMMAND so its owning plugin mints it on win.roll-confirm band of config.yml, a roll button opens a confirmation menu instead of rolling, and its Cancel button sits on the very cell the player was clicking. Spam lands on Cancel and nothing is lost. Confirm rolls exactly as before, and closing the question rolls and spends nothing.roll-confirm.traits is checked against the pet's current trait. roll-confirm.boosts is checked against every stat the button would reroll: that stat for a single-stat button, any of the three for Roll Every Boost. Ids ignore case, an empty list never asks, and the defaults are prodigy and mythic, the rarest shipped entries. The band merges into existing installs on its own.guis/traits_confirm.yml and guis/boosts_confirm.yml, seed on their own. Each has the SAME row count as the menu it asks for and Cancel on the same cells as that menu's roll buttons, because a menu with a different row count sits at a different height on screen. If you move a roll button or change the rows of guis/traits.yml / guis/boosts.yml, do the same in the matching confirmation.confirm-ticks, and the bet starts as soon as the charge shows up. An economy that applies commands at once is still confirmed immediately.messages.errors.charge-pending).currencies.<id>.confirm-ticks (default 60, at most 200; 0 demands the move in the same tick). Existing currency entries use the default without any edit.pet-items.item were rendered with the pet placeholders and colours only, so a %...% token (for example %math_2:half-up_1+{buff-value}/100%) printed raw on the head while the same line rendered fine in the menu. They now get the same per-player PAPI pass as a menu cell, after the pet placeholders, so a pet placeholder can sit inside a PAPI token.rewards.yml entry can now be a map with a command and an optional name. Plain command lines keep working exactly as before, and both forms can be mixed:
rewards:
- command: "pets admin givebox {player} Box_Basica 1 -sf"
name: "&6Basic Box"
- "give {player} diamond 1"{name} in messages.gift-claimed lists the names of the rewards the claim paid: A, A and B, A, B and C. Unnamed rewards are left out; a claim with no named reward shows reward-names.none.reward-names: (separator, last-separator, none), merged into existing language files on boot.sngifts_reward_names, created automatically), so a restart mid-day keeps them.gift-claimed value is kept: add {name} to it where you want it./gifts resetgifts.rewards.yml is never rewritten by the plugin; add command/name entries by hand.Requires SnLib.jar 1.27.0 or newer.
messages.scroll-level-used-many (English and Spanish), sent when more than one scroll was used and naming how many. It merges into existing installs on the next boot.[levels] value can no longer wrap a pet's level negative.scrolls: header comment in config.yml was reworded (fresh installs only). No config key, menu, command, permission or database column changed.Drop-in. Public API unchanged (API_VERSION 1.0.0).
/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.Showing the 40 most recent releases.