0.2.19alatestAllium v0.2.19ayesterday Allium v0.2.19a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Trading cards get a full workshop redesign. Right-click a card to open its card menu, then manage bonus boosts, merge eligible cards, or trade a card in through dedicated menus. The menus use the Nexo glyph titles and matching button artwork, with small-caps card text and a level bar that shifts color as the card approaches its next level. - Card progression is more predictable. Signature boosts are fixed by tier rather than rolled: Simple starts with three, Ultimate with four, and Fabled with five. Bonus boosts remain tier-unlocked and are rolled separately. Crafted cards now receive their quality and signatures from the configured card tables. Removing a card also removes its stat boosts and restores the player's previous scale. - Merging and trading are safer. A merge preserves the better quality, creates the next-tier card with that tier's fixed signatures, and consumes its inputs only after a valid result is available. Trade-in now pays the configured reward, including recovery for payouts that could not be delivered immediately. - Crop stages can render as CraftEngine blocks. Crop definitions support block references and staged custom visuals, with a fallback renderer when CraftEngine is unavailable. CraftEngine items can also be resolved through the item integration layer. - Command visibility and compatibility are tightened. Command suggestion filtering now follows the command tree and command paths, while API compatibility checks cover the supported Paper versions in CI. CraftEngine replaces the removed Oraxen compile-time item and smelting integrations. - Additional fixes and content. This release adds the Bong item and listener, improves crop interactions and rendering, and updates nickname, morph, and item resolution behavior.Upgrade notes
- Back up existing card and trading-card configuration before upgrading; review the updatedtradingcards/config.ymlandtradingcards/boosts.ymlfor the revised tier, signature, and bonus-slot settings. - CraftEngine is optional. Crop stages retain the configured fallback behavior when it is not installed.0.2.18aAllium v0.2.18a3 weeks ago Allium v0.2.18a
Wiki: https://github.com/castledking/Allium/wikiHighlights
Two large features, both developed since the last release and both landing here for the first time. - The kitchen — A newharvest/kitchen.ymladds a crafting chain built on physical placed items rather than a recipe book: flour bags that hold 256 flour, a kneading station that turns water plus flour into dough, a placed pie you assemble one ingredient at a time with the instructions floating above it, baking in a Nexo furniture stove, cooling back down if you leave it, and eating it a slice at a time. Nine pie types ship by default, including three-quality berry pies. - Trading cards — 36 mobs across five tiers, 180 cards in all. Cards drop from mob kills at a configurable chance, carry an integer quality from 1 to 100 that maps to one of eight named bands, level up on the XP your equipped card earns, and grant stat boosts through AuraSkills. A card is a self-contained item: its whole state lives in the item's own data, so an offline card grants exactly what its lore says it will. - Cards equip into a Relique slot — A newcardslot definition and validator let players equip one card at a time. Boosts apply on equip and come off on unequip, and XP earned while equipped is written back to the card so it survives relogging. - Boost slots roll one at a time — Each card has bonus slots that open with its tier (one on Simple, five on Fabled). They roll, lock and clear individually, each at its own escalating price, so a single bad reroll no longer costs the whole card. - `/morph` on a Fabled card — The reward for merging all the way to the top tier. Wearing the mob's disguise is cosmetic (LibsDisguises is packet-only), and Allium supplies the behaviour: stealth until you strike first, mob AI that targets you like the thing you're wearing, and the mobs that witnessed the attack converging on you. - A card frame drawn entirely in the lore — The tier's panel is a bitmap font glyph, not a sprite pair and not a shader. One style id and one name key replace the previous per-tier, per-lore-length scheme. - `[frame:<tier>]` frames any item, from any plugin — Other plugins cannot draw the card frame themselves: they parse their own config text and none can set a tooltip style or blank an item's name. A marker lore line is something any of them can carry, so the frame is applied to the copy of the item sent to the player over a PacketEvents listener. The item on the server is untouched. - Chat hovers are framed too — A hover tooltip has no tooltip style, so vanilla's panel is drawn behind it whatever the text asks for. Hover lines step one pixel further left, to the panel sprite's outer pixel, and no panel pixel shows.Technical Details
The kitchen
#### Flour bags A bag is a keyed item with a count in its own data. Right-click with an empty cursor takes one out; right-click while holding the stored item adds one to your cursor stack. Left-click with an item puts it in; shift-left-click takes a whole stack out.
The click table is deliberate, and one part of it is a bug fix worth knowing about. Plain left-click picks the bag up and moves it around your inventory — it does not withdraw. Shift-left-click is the withdraw gesture. An earlier revision made plain left-click withdraw, which left no way at all to pick a non-empty bag up and move it around an inventory. Two behaviours are worth calling out because they look like bugs and are not. Creative mode is skipped entirely: creative inventory clicks are client-authoritative and fight any cursor change made on the server. And a bag stack must be exactly one item, because a stack of several bags would multiply whatever went in. #### Kneading Right-click a station holding a water bucket or water bottle, then flour, then more flour. A cauldron keeps its water the vanilla way and its level is the water count, so there is nothing to get out of sync; other stations (Nexo furniture or custom blocks) have no water of their own, so what you pour in is counted instead.bags: flour_bag: item: nexo:flour_bag stores: nexo:flour capacity: 256 lore: "<gray>Flour: <white><amount></white>/<capacity>"
You can knead straight out of the bag — each click takes one flour out of it. When both counts are met the station is emptied and dough pops out. Listingkneading: stations: blocks: [CAULDRON] nexo: [] # Nexo furniture or custom block ids water: { required: 3, bucket: 3, bottle: 1 } flour: { item: nexo:flour, required: 3 } result: { item: nexo:dough, amount: 1 }CAULDRONwithwater.requiredabove 3 is a hard configuration error that disables kneading entirely, because a cauldron can never hold that much. #### Pies Right-click the top of a block with a crust to put a pie down, then right-click the pie with each ingredient in turn. The floating text above it shows what is needed next. The default step order is egg → sugar → filling → dough. The filling step is what fixes which pie this is — once some filling is in, the rest has to match it — and the hologram cycles through every available flavour until you choose. Chocolate and pumpkin skip the dough top entirely viaskip-steps: [4], since neither has a lattice:
A pie is anpies: types: blackberry: filling: [nexo:blackberries, nexo:blackberries_silver_star, nexo:blackberries_golden_star] chocolate: filling: minecraft:cocoa_beans skip-steps: [4]ItemDisplayshowing the current model plus anInteractionhitbox so it can be clicked. Both are persistent entities and all of the pie's state lives in the display's data — there is no database row to fall out of sync with, and a pie is saved and unloaded with its chunk like any other entity. The two halves are cross-linked by UUID and tagged so neither is mistaken for something else on a chunk reload. Picking a pie up refunds what went into it: an unfinished pie gives back the crust and every ingredient recorded along the way. Breaking the block underneath gives back only the crust — the refunds are not repeated there. A pie that has been partly eaten refuses pickup with "Someone's already eaten some of this." #### Baking, cooling, eating Baking is a normal server cooking recipe, typically a Nexo recipe turning a cold pie into a baked one. The cold pie and the unbaked pie are deliberately the same item, so a pie that has gone cold can be put back in the furnace to warm up again. Nothing in Allium needs configuring for it. Cooking happens in a Nexo furniture stove. It is a full furnace — real burn time, real fuel, real XP — with the fidelity details that make it read as one:
Recipe lookup walks the server's recipes for one whose input choice matches, so custom recipes work. A lava bucket becomes a bucket, a wet sponge converts a lone bucket in the fuel slot into a water bucket, progress drains twice as fast when the fire goes out, and fractional XP rounds up at random exactly as vanilla does. Progress is pushed to viewers through the inventory properties, so the flame arrow and progress bar animate. Hoppers cannot reach it. Baked pies carry a cooling deadline. A placed pie that passes it flips to its cold model; a dropped one is swapped for the cold item, preserving the stack size. Pies in inventories keep their clock running but only change when they're put down or dropped, and a pie that went cold in someone's pocket comes out cold. Then four slices, with hunger and saturation configurable, each bite taking the model down one step. #### Where kitchen state lives, and what breaks without what | State | Where | |---|---| | Kneading progress | Chunk data, asfurnaces: stations: kitchen_stove: title: "Stove" type: FURNACE # FURNACE | SMOKER | BLAST_FURNACE speed: 1.0"water,flour"| | Bag fill level | Item d0.2.17aAllium v0.2.17aSep 21, 2026 Allium v0.2.17a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Allium loads and runs with no other plugins installed — The plugin previously died at load withNoClassDefFoundError: com/github/retrooper/packetevents/event/PacketListenerCommonbecause the main class named a PacketEvents type directly, and the JVM verifies that class before any "is PacketEvents installed?" check can run. Registration moved behindPacketEventsLoader, and three chat listeners that Bukkit was silently skipping on DiscordSRV-less servers now register normally. Verified on a clean Paper 26.3 server whoseplugins/folder contains nothing butAllium.jar. - Head drops are configurable in a new `spawner_heads.yml` — Every mob that drops a spawner-core head is listed with its own drop chance, Looting chance and per-level bonus. Delete an entry (or setenabled: false) to stop that head dropping, or add a mob by giving it asoundandtexture. Reloads with/core reload. - `override-moremobs-head-drop` ends double head rates — With a "More Mob Heads" style datapack installed, both it and Allium dropped a head for the same mob. The new setting (defaulttrue) removes the datapack's head for any mob inspawner_heads.yml, leaving exactly one source. Set it tofalseand both drop, so a single kill can produce two heads. - Mob plushie crafting — A mob head in the center of a crafting grid with any eight wool around it makes that mob's Nexo plushie (nexo:axolotl_plushieand friends). Datapack heads, Allium's own heads and vanilla skulls all work; all 78 plushies in the pack are reachable. - Stacked mobs drop heads — SpawnerMeta's stacked kills never fire a death event, so only the final mob in a stack could ever drop an Allium head. Stack kills are now detected directly and rolled like any other kill, multiplied by the spawner's looting upgrade. - "Ban Player" in Discord's right-click Apps menu — Staff right-click any relayed message (chat, join, leave, death, advancement) and get a dialog for player, duration and reason, which runs a normal console ban. Gated bydiscord-moderation.staff-role-idand aban-exempt-groupslist that refuses to ban owners/admins. - Custom unknown-command helper text —hide.ymlgainedunknown-command-completion-help-message, which replaces the vanilla "Unknown or incomplete command" line the client shows while typing. The client renders that text itself, so Allium ships it as a resource-pack language override. - ModGuard stops kicking innocent players — A/core modguard reloadleft the old probe's join listener alive, so several probes raced on the same player and each scored the others' sign echoes as a mod detection. A vanilla player was kicked for "Meteor Client" two seconds after joining.Technical Details
Loading without any other plugin
Three separate problems, all of which stop code running before its own availability guard: | Problem | Why it fires | Fix | |---|---|---| | Main class rejected |PluginStartcalledPacketEvents.getAPI()...registerListener(...). The JVM verifies the whole main class when Paper loads it, and the verifier must resolvePacketListenerCommonto check the argument. | Registration moved intoPacketEventsLoader.registerListener(Object).PluginStartno longer names a PacketEvents type. | | Chat listeners skipped | Bukkit reflects over a listener's methods when registering it, and a missing parameter type makes it drop the entire class.AlliumChannelManager,SpamBlockerManagerandChatFilterManagerall named DiscordSRV event types, so channels, spam blocking and chat filtering were dead without DiscordSRV. | DiscordSRV handlers moved into nested classes, which Bukkit never scans. Bukkit reflects over declared methods too, so two private helpers and a lambda capture were retyped toObjectwith an internal cast. | | SpectatorTeleport always disabled | It reflectively loadednet.survivalfun.core.commands.NV, a package name from before the rename, and failed on every server. | ConstructsNVdirectly. | A missing item-provider plugin (e.g. Nexo) also tripped Harvest's validator on every single config path; those are now reported once per file. On a bare Paper 26.3 server the startup log drops from 64 error lines to 11, four of which are the legitimate "nexo has no resolver" notices.
The 15 head drops moved out of hardcoded Java intospawner_heads.ymlplugins/Allium/spawner_heads.yml. The file is authoritative: a mob that is absent or disabled drops nothing.settings: override-moremobs-head-drop: true heads: ENDERMAN: enabled: true chance: 0.001 looting-chance: 0.0011 looting-chance-per-level: 0.0001 sound: "entity.enderman.scream" texture: "eyJ0ZXh0dXJlcyI6..."sounddoubles as the head's identity: Allium reads thenote_block_soundcomponent back to decide which mob a head belongs to, and every configured sound is registered withMobHeadRegistryat load, so a mob added here works for spawner crafting and plushies without a code change. Withoverride-moremobs-head-drop: true, anEntityDeathEventfor a configured mob has any datapack head for that same mob removed from the drops before Allium rolls its own. Mobs that are not configured are never touched, so the rest of the datapack behaves normally. The one gap is SpawnerMeta's stacked "virtual kills", which drop their loot outside the death event and are not filtered.Telling heads apart
Head identification used the mob name inside the head'snote_block_sound, but the datapack borrows sounds: mooshroom usesentity.cow.ambient, cave spider usesentity.spider.step, guardian uses the elder guardian's flop, red sheep uses the evoker's wololo. Mooshroom heads therefore counted as cow heads and cave spider heads as spider heads for spawner crafting. Heads are now matched by theitem_namethe datapack stamps on them ("Red Mooshroom Head", "Cave Spider Head"), generated intospawnercraft/more-mob-heads-names.txt(210 names across 84 mobs). Allium's own heads set noitem_nameand still resolve through their sound.Plushie crafting
PlushieCraftListenerwatches the crafting grid for a head in the middle and eight wool of any color around it, then offers the Nexo item<mob>_plushie. Three ids do not follow the pattern and are aliased:ender_dragon→enderdragon_plushie,copper_golem→copper_golem_unoxidized_plushie,zombie_nautilus→zombie_nautilus_temperate_plushie. Variants collapse to the base plushie, so all three cow climates and both mooshroom colors give a cow and a mooshroom plushie respectively. Nothing is registered as a real recipe, so the listener consumes the grid and hands over the item itself, one per click. The feature only loads when Nexo is installed andspawnercraft.plushiesis true.Heads from stacked mobs
SpawnerMeta cancels the killing blow on a stack larger than one, drops that mob's loot by hand, decrements the stack and heals the entity, so noEntityDeathEventis ever fired. Allium now snapshots the stack size on a lethal hit (priorityHIGH) and confirms atMONITORthat the hit was cancelled and the stack shrank by exactly one before rolling a head — a blow cancelled by a protection plugin leaves the stack alone and produces nothing. The killer is resolved the way SpawnerMeta does it (attacker, projectile shooter, or a tamed pet's owner) and the head is multiplied by the spawner's looting upgrade, matching how SpawnerMeta treats the final kill.Discord "Ban Player"
DiscordSRV bundles JDA 4, which predates both message context menus and modals, so the command is registered as a global application command over raw REST with the bot's own token (DiscordSRV bulk-overwrites guild commands, which would delete it). Interactions arrive by flippingRAW_EVENTSon DiscordSRV's live JDA session; a context-menu click reaches Disco0.2.16aAllium v0.2.16aSep 10, 2026 Allium v0.2.16a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- `/time pause` freezes a world's day-night cycle — A world can be pinned to a single tick and persisted there across restarts. Two layers cooperate:doDaylightCycleis disabled while paused, and a per-tick task re-asserts the saved tick in case any plugin (including/time set) tries to move the clock. - Essentials-style player matching across the command suite — A sharedPlayerMatchernow resolves partial names, nicknames, and prefixes the same way Essentials does. It is rolled out to/tp,/msg, mail,/seen,/fly,/god,/balance,/cheque,/home,/locator, and/core(restore/dialog), and it is vanish-aware: a hidden player is never matched by someone who cannot see them. - First-time players who open with a bot probe are punished — A brand-new player whose very first command is/plugins,/pl, or/version(all bot tells) is banned by default via a configurable punishment command. Legitimate players never run these as their opening command; bot clients do it immediately after spawning. - The ModGuard translation probe is now a three-mode, multi-check system — Up to three checks are packed onto a single ghost sign, akey.forwardcontrol line detects sign-spoofing/exploit-preventer mods, and each check runs in one of three modes (translate,keybind,meteor). The rewrite also kills the false positives from clients that echo the raw key padded with a letter. - Allium's vanish and party isolation now drive EssentialsX's `canSee` — A new visibility bridge (AlliumVisibilityApi) is exposed to EssentialsX'sAlliumVisibilityBridgeover reflection, so vanishing and party-only isolation survive Essentials' own visibility decisions instead of being undecided. - Offline home limits resolve through LuckPerms —HomeLimitscan now answer how many homes an offline player's permissions grant, so/core sethomesno longer reports "unknown (offline)" when LuckPerms is installed. Covered by new unit tests. - Auto-restart speaks through Discord — Restart schedules, countdowns, and the final shutdown are relayed to Discord via DiscordSRV as colored embeds, and three placeholder names (%allium_restart_time%,%allium_restart_seconds%,%allium_restart_scheduled%) expose the restart state to chat formats. A placeholder-delegation bug that silently emptied these placeholders is fixed.Technical Details
/time pause: the day-night cycle, frozenTimePauseManagerowns the feature. Pausing a world captures the current tick and stores it; from then on two layers keep the clock pinned: | Layer | Mechanism | |---|---| | Gamerule |doDaylightCycleis set tofalsefor the duration of the pause, so the vanilla tick loop cannot advance the clock. Unpausing restores it totrue. | | Re-assertion | A 1-tick repeating task callssetTime(savedTick)on every server tick for every paused world. This catches/time set, plugin resets, or any drift before the gamerule write takes effect, and it stops itself when the last world unpauses. | The pause is durable. State lives in a newpaused_worldsdatabase table (worldprimary key,paused_tick,paused_by,paused_at); storage migrates the database from version 8 to 9. On startuprestorePersisted()replays every recorded pause, and aWorldLoadEventhook applies the same pin to worlds that load later in the session (e.g. a lazily-loaded Nether), so a paused dimension cannot silently drift between loads. Feedback is wired through lang (time.pause.enabled/time.pause.disabled), the commands are: | Command | Effect | |---|---| |/time pause [world]| Toggles the pause for the sender's world, or the named world (console uses it too) | |/day pause//night pause| Same toggle from the two sub-commands | | tab |pause [world]suggestions for holders of the permission | The permission isallium.time.pause(default: op)./timeusage strings now advertise the pause subcommand.Essentials-style player matching everywhere
PlayerMatchercentralizes the resolution that commands previously did with bareBukkit.getPlayer. It mirrors Essentials'matchUserorder: 1. Exact UUID (when the term parses as one) 2. Exact real name, case-insensitive 3. Real-name prefix — closest match wins (shortest name, then alphabetical, sojprefersjoeoverjoey) 4. Nickname prefix 5. Nickname contains (Essentials' display-name fallback) Two vanish rules apply: a player hidden from the sender is never a candidate, and console matches everyone.PlayerMatcher.tabCompletereturns real in-game names sorted best-first, with nickname-only matches ranked after direct name matches, and also respect visibility. The roll-out replacedBukkit.getPlayer/getServer().getPlayer/tab-complete filtering in/tp(every path),/msgand mail recipients,/seen,/fly,/god,/balance,/cheque,/home,/locator bar show|hide|reset, and/core(restore,dialog show,sethomes,homebyplayer)./seenalso dropped its player-only restriction: it now runs from console and reports toCommandSenderinstead of aPlayer, with sounds/particles skipped for console users.Anti-bot
AntiBotListenertracks players currently in their first-ever session (fromPlayerJoinEventwhenhasPlayedBefore()is false). If their very first chat command is one ofanti-bot.commands-to-punish(defaultplugins,pl,version), the command is cancelled and every line ofanti-bot.punishment-commandis dispatched from console with%player%substituted:
Root-command extraction strips the leadinganti-bot: commands-to-punish: - plugins - pl - version punishment-command: - "ban %player% -s [SC] Bots aren't allowed on this server"/and anynamespace:prefix, sominecraft:pluginstriggers too. A player holdingallium.antibot.bypassis skipped. The guard fires only once per player (the set entry is consumed by the first command), and leaving the server clears it.ModGuard translation probe: three modes and a control line
The probe still works through the MC-265322 ghost-sign trick, but the plumbing is different: - Multi-probe signs. Up toCHECKS_PER_SIGN(3) checks are packed onto one sign, one per line, and the echoed lines are evaluated as a batch before the next sign is queued. Fewer sign round-trips means faster full sweeps and less disruption to the victim's screen. - Control line. The fourth line always carries the vanillakey.forwardkeybind. If it comes back as the rawkey.forward, a sign-spoofing/exploit-preventer mod is intercepting the probe and the batch is marked as such — coincident hits are treated as lower-confidence rather than certain. - Three evaluation modes (per-checkmode:inmodguard/config.yml): | Mode | Hit rule | |---|---| |translate| Resolved ≠ fallback prefix → hit. Exact echo of the raw key → protected (the client is not confirming). Anexpectedlist narrows hits to exact case-insensitive matches. | |keybind| The keybind echoes a bound-key name; an echo equal to or containing the raw key is treated as the un-resolving vanilla client (clean), anything else is a hit. | |meteor| Exact echo of the key is a hit by itself (Meteor registers these), fallback echo is clean, anything else is a hit. | - Key-plus-letter immunity. Vanilla and many clients resolve an unknown key by echoing the raw key as-is; some report-playing clients instead append a single character (the dead key can land askey.forwardX-style padding).isKeyPlusLettertreatskey+ exactly one letter as a clean echo, which removes the WaylandCraft-style false positive while still catching real keybind resolutions. - Layer 1 expanded.banned-modsnow covers the hackops client ids (Meteor, Wurst, LiquidBounce, Autism, Baritone, ...), the CheckHacks list (Freecam0.2.15aAllium v0.2.15aAug 21, 2026 Allium v0.2.15a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- LiquidBounce's plugin-enumeration probe now times out — Running.serverinfo Pluginsagainst an Allium server producesPlugin detection timed out. It may be due to the server blocking the request.instead of a plugin list. This was verified black-box against a live LiquidBounce client. - The command-suggestion protocol is now filtered at the packet level — A new/rewrittenCommandSuggestionsListenerintercepts the rawTAB_COMPLETErequest/response pair via PacketEvents and enforces Allium's visibility rules on the wire, where Bukkit events cannot reach. - Every namespaced suggestion is stripped unless explicitly allowlisted — Unknown namespaces (jbt:*,someplugin:*) can never leak regardless of any blocklist configuration, becausepluginname:commandentries are removed wholesale. - The packet-level protection is independent of the existing command lists — With bothremove-unsafe-commandsdisabled andcommand-suggestion-filtersemptied, the black-box test still passes. The active protection is the packet-level bare-/response cancellation plus the namespaced-suggestion stripping rule.Background: how the leak worked
LiquidBounce's primary stealth plugin-detection mechanism (verified in its source atServerObserver.captureCommandSuggestions()) does not rely on/plugins. It sends:
This bypassesServerboundCommandSuggestionPacket(completionId, "/") → server-side Brigadier completion for the root node → ClientboundCommandSuggestionsPacket(match[]) → extract every suggestion containing ":" → "pluginname:command" ⇒ pluginnameDECLARE_COMMANDSfiltering entirely: even with a curated command tree, a separate root completion request can expose registered namespaced commands that reveal plugin identities.Why Bukkit events could not cover this
Paper's own maintainers document the limitations of event-level tab-completion handling: - [PaperMC/Paper#12050 — "TabCompleteEvent doesnt Work"](https://github.com/PaperMC/Paper/issues/12050): electronicboy explains that "tab completion is mostly handled on the client these days, with the client only asking the server to complete stuff it's told to ask the server about (i.e. the legacy bukkit command system); stuff like the root command is sent to the client entirely." Malfrador adds: "That event is only called for bukkit commands. It is not called for commands using the new brigadier command API." - [PaperMC/Paper#10833 — "TabCompleteEvent not being called"](https://github.com/PaperMC/Paper/issues/10833): the same gap is reported following Paper's command API changes. - [PaperMC/Velocity#1168](https://github.com/PaperMC/Velocity/issues/1168): documents the distinction between Brigadier command data and the older Bukkit completion mechanism. The consequence is thatTabCompleteEvent/AsyncTabCompleteEventhandlers such as Allium'sCommandManager.onTabCompletecan rewrite legacy Bukkit-command completions, but they do not provide reliable control over the modern Brigadier suggestion round-trip or the final suggestion packet. Packet-level enforcement via PacketEvents closes that gap.What changed
The listener enforces the same visibility policy at the protocol boundary: | Behavior | Detail | |---|---| | Request tracking | IncomingCommandSuggestionsListener(packet level, rewritten)Play.Client.TAB_COMPLETEtexts are cached by transaction id (bounded map, TTL pruned). | | Untracked responses cancelled | APlay.Server.TAB_COMPLETEwhose transaction id has no cached request is dropped (cancel-unknown-suggestion-responses). | | Bare `/` probe cancelled | Any tracked request shorter than two characters — i.e. exactly LiquidBounce's/probe — gets its response cancelled outright. The client never receivesClientboundCommandSuggestionsPacketand the enumeration probe times out. | | Namespaced stripping | Every match containing:is removed unless listed inallow-namespaced-suggestions. This preventsjbt:mob_bagand similar identifiers from leaking through suggestions. | | Prefix blocklist kept |command-suggestion-filtersstill applies as an additional layer on plain suggestions. | | Group rules enforced | Plain suggestions additionally pass throughCommandManager.shouldAllowTabComplete(player, cmd)— the samehide.ymlgroup policy that governs execution and the command tree. Policy stays in one place; the listener is purely an enforcement point. | | Empty responses cancelled | If filtering removes every suggestion, the response is cancelled instead of sending an empty list. | | Legit completion preserved | Requests that do not start with/(chat/player-name contexts) pass untouched;/he,/msg <partial>, etc. continue working because their input length is ≥ 2. |
New settings underhide.ymlhide.settings: -cancel-bare-slash-request-cancel-unknown-suggestion-responses-allow-namespaced-suggestionsComments were updated to document the new packet-level protection. Existing keys remain unchanged and supported.Architecture note
The command visibility policy remains inCommandManager; the PacketEvents listeners remain thin enforcement points.DeclareCommandsListenerstill readsunsafe-commands-listrather than deriving its behavior from group rules. Unifying those policies is deferred future work — the existing static lists are independent of the packet-level suggestion-probe defense.Verification
- Maven suite: 170 tests, 0 failures, 0 errors —mvn installgreen. - Black-box: LiquidBounce.serverinfo Pluginsagainst the test server returns the timeout message instead of a plugin list. Confirmed withremove-unsafe-commands: falseand an emptiedcommand-suggestion-filterslist, isolating the packet-level listener as the effective protection. - Legitimate completion:/he,/help, and/msg <partial>continue to provide normal completion for unprivileged players. - Namespaced suggestions: Hidden namespaced entries such asjbt:*are removed from suggestion responses unless explicitly allowlisted.0.2.14aAllium v0.2.14aAug 12, 2026 Allium v0.2.14a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Slime Jump bounces for real — Landing on a slime mob now cancels the fall damage and launches you back up. In v0.2.13a the bounce velocity was set inside theEntityDamageEventhandler, and the server wiped it the moment the event finished, so the bounce never happened (and you still took damage). The velocity is now applied on the next tick, and the damage is cancelled outright instead of halved. - The Mob Disarmer covers more of the mob book — Endermen's carried block is stripped and dropped as a guaranteed item, and sulfur cubes are sheared through Paper's vanilla path so the absorbed block always comes out. A disarmed mob is also blocked from picking its own equipment back up for two minutes, ending the loop where a zombie re-equipped its sword before the pieces had finished rolling. - Both charged tools are far less suicidal — The break chance on the first two uses dropped from 5% to 3%, and the "risky" final use from 25% to 5% on the Mob Disarmer and Phantom Obliterator alike. - Dropping idle handcuffs no longer eaten — Dropping a cuff family with nobody restrained used to spam "No one is currently restrained by you." and consume the drop. Now only the family that is actively restraining someone is intercepted as the release/cancel control; an idle family drops like any other item. - Command visibility rules pinned by tests — The tab-complete filtering was extracted into pure functions and is now covered by unit tests that fix the relationship between the execution list and the tab-completion list.Technical Details
Slime Jump bounces for real
SlimeJumppreviously listened for fall damage, multiplied it bydamageReductionFactor, and set the player's velocity synchronously inside the handler. Two things were wrong: 1. The server applies its own landing velocity after the damage event completes, so any velocity set in the handler was overwritten before the next tick. The bounce never actually happened — all the code did was halve the fall damage. 2. The damage reduction silently depended on adamageReductionFactorthat was not exposed in config, and the "cushioned" message fired only once per player for the lifetime of the server process. The listener now works the way the feature implies: - When a player takes fall damage while a slime mob sits within the 2-block check radius below them, theEntityDamageEventis cancelled entirely — no damage is taken. - The bounce velocity is computed from the damage that would have been taken (damage × bounceMultiplier, capped atmaxBounceVelocity, currently 0.2 and 2.0), preserving 80% of horizontal momentum. - The velocity is applied viaSchedulerAdapter-freeplugin.getServer().getScheduler().runTask(...)on the next tick, after the server's landing logic has finished, so the launch survives. ThedamageReductionFactorfield and constructor parameter were removed; the registration inPluginStartand the default constructor were updated to match.Mob Disarmer: new targets
The disarm gate used to requireEntityEquipmentwith something on it, which excluded two mobs whose loot lives outside that system: | Target | What changed | |---|---| | Enderman |getCarriedBlock()is read, the block is cleared withsetCarriedBlock(null), and it is dropped as an item with a guaranteed 100% chance — it was never inEntityEquipment, so an enderman carrying grass was previously "empty". | | Sulfur cube (Chaos Cubed, MC 26.2) |SulfurCube#shear()is called through Paper's vanilla shearing path, but only whenreadyToBeSheared()says natural shears would work. The cube's absorbed block drops at 100% with the usual sounds and effects; an empty cube is left untouched and no charge is spent. | Both are counted in the "piece(s) removed" report the tool prints after a disarm.Two minutes of no pickup
MobPickupSuppression(new,items/impl/) stops a freshly disarmed mob from re-equipping the same drops. It writes amob_disarmer_pickup_blocked_untilepoch-millis deadline into the mob's persistent data container when the Disarmer fires, andMobDisarmerListenercancelsEntityPickupItemEventfor non-player entities atHIGHESTpriority while that deadline is in the future. Once it passes, the key is removed and the mob behaves normally. The deadline is in the PDC rather than a map, so it survives plugin restarts, and the code deliberately leaves the mob's vanillaCanPickUpLootflag untouched — that flag varies by mob type and spawn path, and rewriting it would be a permanent behavioural change for the entity.Gentler tools
Both charged tools from v0.2.13a had their break chances softened: | Use | Before | After | |---|---|---| | First and second uses | 5% | 3% | | Final (last charge) use | 25% | 5% | The design rationale is unchanged — the last use is still riskier so players cannot safely reset the recharge timer — but a 25% chance of destroying a fully-charged tool on the final swing was too punishing.Handcuff drop behaviour
onPlayerDropItemfilters the restrained players by the family of the dropped cuffs (staff vs claim). Previously, dropping a family with nobody restrained sent "No one is currently restrained by you." and cancelled the drop — which meant a player restraining someone with the staff family could not drop an idle claim-cuff stack, and vice versa. That early-return is now a plainreturn, so only the family that is actively restraining someone is treated as the release/cancel control, and idle cuffs drop like any other item.Command visibility rules, now pure and tested
ThePlayerCommandSendEventfiltering was split out ofonCommandSendinto two static, side-effect-free functions —shouldAllowTabComplete(List<CommandGroup>, String)andfilterRootCommands(...)— withCommandGrouploosened to package-private so tests can construct real groups. The extracted code is behaviour-identical; the newCommandVisibilityRulesTestpins down the intended semantics: - A whitelist group's executable commands stay hidden from tab completion unless they are also listed in the group'stabCompletes— being allowed to run a command no longer implies it is visible. -tabCompletescan expose a command that is not in the execution list at all. - Blacklist tab rules hide roots independently of the command rules.Verification
The Maven suite passes with 170 tests, up from 161. Four new test classes: | Test | Coverage | |---|---| |EndermanCarriedBlockTest(2) |stripCarriedBlockclears and returns the carried block, and ignores empty-handed endermen | |MobPickupSuppressionTest(2) | Pickups are cancelled up to the two-minute deadline and allowed once it expires; a persisted deadline survives a plugin restart | |SulfurCubeShearingTest(2) | A cube with ejectable content is sheared exactly once; an empty cube is left untouched | |CommandVisibilityRulesTest(3) | Whitelist executable commands stay hidden unless tab-listed; tab-listing exposes independently; blacklist tab rules hide independently | The Slime Jump bounce, the enderman strip, the sulfur-cube shear and the handcuff drop behaviour need a live Paper/Canvas server (the bounce in particular is a server-timing fix) and were verified in game rather than by unit test.0.2.13aAllium v0.2.13aAug 11, 2026 Allium v0.2.13a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Permission migration no longer fires on wildcard grants — The SFCore-eracore.*→allium.*migration asks Vault whether a player holds each old node. Vault answers resolved checks, not "is this node set", so a player carrying*— or an op — answered yes to everycore.<node>question and had the entire permission list written onto their account as realallium.feed,allium.trash, … nodes. Handing*to someone temporarily was enough to trigger it. Migration now probes with a node nobody can hold; when that comes back true, the account resolves everything and is left alone until the blanket grant is gone. Groups had the identical defect and got the same guard. - Mob Disarmer and Phantom Obliterator — Two charged Nexo-backed tools. The Disarmer strips a mob's held items and armour and rolls each piece against the mob's own vanilla drop chance; the Obliterator clears unnamed phantoms within 64 blocks. Both carry three charges in the item's PDC rather than in a per-player map, so the cooldown belongs to the tool and survives trading, storage and restarts. - Claim handcuffs — A second handcuff family for landowners. Restraining an untrusted player inside your own claim starts a five-second countdown that ends in a GriefPrevention claim ban; dropping the cuffs cancels it, and so does the target leaving the claim. All GPExpansion access is reflective, so Allium still starts without it and works against either upstream GriefPrevention or GriefPrevention3D. - Chat colours are parsed once, against permissions — Legacy codes and MiniMessage tags used to fight over the same message:C8F9B&lVIPwas pushed through the MiniMessage parser, which printed the codes instead of applying them. Both systems now filter against their own permission sets and fold into a single MiniMessage string that is deserialized once, so one message can mix&a,&#RRGGBBand<gradient>freely. - One answer for how many homes a player gets — Numbered permissions and the/core sethomesoverride disagreed depending on which code path asked. The effective limit is now the higher of the two everywhere,/core sethomes <player>shows the breakdown, and%allium_homes_max%reportsunlimitedinstead of a number nobody set. - `/seen` shared IPs are symmetric — Shared accounts were matched against the target's current address only, so the moment either account changed IP they stopped listing each other. The lookup now walks the full IP history. - Reliability —%allium_nickname%parses without PlaceholderAPI or Essentials, Essentials-only nicknames are imported on join,/spynow clears targeted spying along with global, spawner cores respond to right-clicking a block, and the ModGuard translation probe stops reporting a hit when the client echoes the raw key.Technical Details
Permission migration and blanket grants
Two migrations carry SFCore'score.*nodes forward:performPermissionMigration()walks every Vault group once at startup, and an innerPlayerPermissionMigrationListenerwalks each player on join untilplayer_migration_completedis set. Both decide what to migrate withplayerHas/groupHas. Those are permission checks. LuckPerms resolves them through wildcards, and Bukkit resolves an unregistered node — which everycore.<node>is, since onlyallium.*is declared inplugin.yml— to itsOPdefault. A player holding*, and any op, therefore answered true to all ~120 entries inpermissionsToMigrate. The migration dutifully "moved" each one and calledplayerAddfor itsallium.equivalent, permanently writing a full permission list onto an account that had none of it. Staff handing out*to help a player with something produced exactly this. The probe. Before any writes, the account is asked aboutallium.migrationprobe.<random UUID>— a node that cannot legitimately be granted. A true answer means everything resolves true and there is nothing meaningful to read, so migration is skipped:
Skipped players are deliberately not markedSkipping permission migration for Steve (UUID: …): holds a wildcard/op grant that resolves every permission.migrated_perms = TRUE. Once the wildcard is taken away, their real nodes migrate on a later join. Groups are checked the same way withgroupHasand skipped individually, so one admin group carrying*no longer poisons the whole pass. Wildcard fan-out. A genuinecore.*node was migrated toallium.*and then fell through into the per-node loop, where the not-yet-removed wildcard still answered true forcore.feed,core.trashand everything else — producing the same spray of individual nodes. The per-node loop is now theelsebranch on both the player and group paths: a realcore.*migrates on its own and nothing else is written. | Situation | Before | Now | |---|---|---| | Player holds*| Everyallium.<node>written explicitly | Skipped, retried after the grant is removed | | Player is op | Everyallium.<node>written explicitly | Skipped | | Player holdscore.*|allium.*plus every individual node |allium.*only | | Player holdscore.feed|allium.feed|allium.feed(unchanged) | | Group holds*| Entire list stamped onto the group | Skipped | Accounts polluted by earlier builds keep their spurious nodes; this release stops the write, it does not retract past ones.Mob Disarmer and Phantom Obliterator
Both tools live initems/impl/, register throughCustomItemRegistry, and keep every piece of state on the stack itself. | | Mob Disarmer | Phantom Obliterator | |---|---|---| | Base item | Diamond sword | Netherite sword | | Use | Right-click a mob | Right-click air | | Effect | Strips hands, then head→feet | Kills unnamed phantoms within 64 blocks | | Charges | 3 | 3 | | Recharge | 120s after the last charge is spent | 120s | | Break chance | 5% on uses 1–2, 25% on the last | Same | | Bypass |allium.admin|allium.admin| | Nexo ids |mob_disarmer,_2-3,_1-3,_0-3(CMD 1004–1007) |phantom_obliterator,_2-3,_1-3,_0-3(CMD 1008–1011) | Charges belong to the tool. Counts, the recharge deadline, the broken flag and a lifetime use counter live in the stack's PDC, so the cooldown travels with the item rather than with whoever last held it. Refills are all-or-nothing — the item is either charged or recharging — and a recharge that completes while the owner is offline is settled on their next join. The item transforms in hand. Each charge count is its own Nexo item, so spending a charge rewrites the stack's item model, custom model data and both id tags (allium:custom_item_idandnexo:id) to the matching variant, and rewrites them back when the recharge lands. Because the tags match what Nexo writes, a stack handed out by/nexo give mob_disarmer_1-3is recognised here too. Drops follow the mob, not the tool. Each stripped piece is paired with the drop chance read off the mob before the slot is cleared — 8.5% for naturally spawned gear, 2.0 for anything the mob picked up, whatever a summon command asked for otherwise — and rolled individually. Villagers are the exception and drop everything. If a mob turns out to have nothing equipped, no charge is spent. Neither sword can be destroyed by durability.SwordToolDurabilityListenerintercepts the final durability point: the stack is left one point from breaking, flagged worn, and given stick-level attack damage viaApiCompat.ATTACK_DAMAGE(added this release for the 1.21.3GENERIC_ATTACK_DAMAGErename). An anvil repair clears only that combat penalty — it does not restore charges, and a broken tool, the 25% roll, stays broken.Claim handcuffs
HandcuffsItemwas rewritten from a single PDC flag into four persistent states across two families: | Type | Id | Family | CMD | |---|--0.2.12aAllium v0.2.12aAug 4, 2026 Allium v0.2.12a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- `/core hide fix` repairs commands Allium cannot classify — Allium's resolver pipeline asks a chain of adapters which permission a command requires. When every adapter declines, the result isUNKNOWNand Allium allows the command by default, which means its own no-permission message never appears. ExcellentQuests'/questshit exactly this. The new/core hide fix <command> <permission>records a permission for that command, and any unresolved command is now announced on console once per session so it can be found in the first place. - `%allium_gradientdisplayname%` is colorable again — The placeholder used to always come back pre-colored, so writing&6%allium_gradientdisplayname%in a chat format or join message had no visible effect. When GradientPlus has no gradient or static color selected, the placeholder now returns a plain, uncolored name and the surrounding format decides the color. A real GradientPlus selection still renders its animated gradient exactly as before. - `#<message>` respects `allium.staffchat` again — The permission check existed on the tracking pass but not on the pass that actually routes the message, so any player could type#hellointo staff chat and be subscribed to staff chat afterwards. - `/delmsg` no longer refuses the next identical message — Deleting a message also marked any message with the same text sent within three seconds as deleted, so a repeated line reportedMessage has already been deleted.- Reliability — Alt-account group reads use LuckPerms' live in-memory user instead of a fresh storage copy, and a leading color code in a language string emits§r§xrather than the literal&r.Technical Details
Manual command permission overrides
Allium resolves a command's required permission through an adapter pipeline — Paper basic commands, Brigadier, NightCore, vanilla, LuckPerms, and finally registered permission nodes. When no adapter can classify a command,PermissionResult.unknown(true, command)is returned: Allium allows the command and leaves enforcement to the command's own framework./questsfrom ExcellentQuests resolved this way. The NightCore adapter correctly determined that the command needs no permission, but did not treat itself as authoritative enough to claim the result, so the pipeline fell through toUNKNOWN. Nothing was insecure — the framework still enforced its own rule — but Allium could not substitute its own formatted no-permission message, and there was no way to tell this was happening withoutdebug-modeenabled. Two changes address that. Unresolved commands are announced. The first time a command resolves asUNKNOWNin a server session,CommandManagerforce-logs anERRORto console regardless ofdebug-mode:
The warning is emitted once per command label per session, and the set is cleared on config reload. Namespaced invocations are reduced to their bare label, soCould not resolve a permission for /quests - allowing by default. Use /core hide fix quests <permission> to set one./plugin:questsand/questswarn once between them. Overrides can be set manually. | Command | Purpose | |---|---| |/core hide fix <command> <permission> [-denyalts]| Set or replace the permission for a command | |/core hide fix remove <command>| Delete an override | |/core hide fix check <command>| Show an override's permission, alt rule, source and author | |/core hide fix list| List every stored override | An override is consulted only after every adapter has declined. It cannot override a real adapter decision, so setting one on a command Allium already understands has no effect on that command's permission. When an override does apply, the result carries the newResolutionType.MANUAL_OVERRIDEand the player is checked against the recorded node withPlayer#hasPermission, meaning a denied command now produces Allium's standardno-permissionmessage like any other.fixlives under/core hidefor grouping only. It does not touchhide.yml; the whitelist/blacklist group system there is unrelated and unchanged. Alt accounts.-denyaltsrecords a flag alongside the permission and reuses the alt detection already present inConnectionManagerandCommandManager— the Vault/LuckPermsaltgroup, minus players exempted in the database. No new alt-detection logic was added. Unlike the permission itself, the alt rule is evaluated for any command carrying an override, not only unresolved ones: a player who is alt-restricted, not exempt, and would otherwise be allowed is cancelled with the existingalt-account-restrictedmessage. Storage. Overrides live in a newcommand_permission_overridestable created at startup: | Column | Notes | |---|---| |command_label| Primary key, lowercased | |permission| The node checked against the player | |deny_alts| Boolean, default false | |source| Free-form origin tag, defaultnightcore| |set_by| UUID of the staff member who set it, null from console | |updated_at| Timestamp, refreshed on every write | Writes useMERGE ... KEY(command_label)so setting an existing override replaces it in place.CommandPermissionOverrideStorekeeps an in-memory copy that the resolver reads on the command path; it is refreshed after everyfix/removeand on config reload, so no restart is needed. Every database method degrades to empty/false if the table is missing rather than throwing.Gradient display names accept an external color
GradientPlus emits one color code per character. When a player has not selected a gradient or static color with/gradient, it emits its default white for every character, which is indistinguishable from deliberately choosing white. Allium previously handled that badly in two ways. If no colors were extracted at all, it fell back to the trailing color of the player's Vault prefix and applied that to the name. If white was extracted, it built a white-to-white animated gradient. Either way%allium_gradientdisplayname%returned a string that already carried a color, so a format like&6%allium_gradientdisplayname%was overridden by the placeholder's own output. The placeholder now treats "no extracted colors" and "every extracted color is#FFFFFF" as unset and returns the escaped visible name with no color codes and no MiniMessage tags. The surrounding format applies:
A real GradientPlus selection is untouched. Multi-color presets still use their first and last character colors, solid presets still pair with their nearest named Minecraft color, and both keep the two-stop phase animation introduced in v0.2.11a. Three hard-coded color paths were removed to get there, in bothformat: "&8[&6Member&8] &6%allium_gradientdisplayname%&7: &f<message>"GradientNameManagerand the mirrored implementation inFormatChatListener: 1. Thecolors.isEmpty()fallback that read the player's Vault prefix and adopted its trailing color, along with thegetPrefixVault lookup it needed. 2.extractTrailingColor, which existed only to feed that fallback. 3.normalizeColor, whose#FFFFFFdefault silently turned an unparseable color into white. Chat's fallback formatter continues to delegate to the same implementation as the tab-name animator, so the two cannot drift apart.#<message>staff-chat permissionAlliumChannelManagersees each chat message twice:onPlayerChatEarlyatLOWESTfor tracking, andonPlayerChatatLOW, which chooses the destination channel. The#one-shot shortcut was gated onallium.staffchatin the first handler but had no permission check at all in the second. Because routing happens in the second handler, any player typing#hellohad their message delivered to the staff channel, and the shortcut'saddReadChannelcall then subscribed them to staff chat so t0.2.11aAllium v0.2.11aAug 1, 2026 Allium v0.2.11a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Animated GradientPlus names no longer rewind mid-cycle — The visible jolt was a phase-zero discontinuity created by Allium's generated three-stop MiniMessage gradient, compounded by TAB and Bukkit both trying to own the same tab-list name. Animated names now use a continuous two-stop path and a single writer. Multi-color presets use their real endpoints, while solid presets retain the subtle second-color animation by moving toward the nearest Minecraft color. - Store complete custom items from your hand —/core item add <id>captures an item intoplugins/Allium/items/<id>.yml; it can then be listed, updated, removed, reloaded or given without writing Java. The lossless item snapshot retains vanilla components and third-party metadata, while a readablevanilla:block makes common properties editable. Stored items also resolve through/give <player> ci:<id>and/i ci:<id>. - Harvest fields behave more like fields — Crop stage durations can be staggered so a field planted together does not ripen in lockstep. Mature crops can be picked or uprooted with different regrowth rules, growing-crop feedback can be exact, vague or silent, water and lava receive configurable vanilla-like behavior, and pistons now break crops instead of being stopped by them. - Minecraft-style selectors and destination teleports — The shared selector engine supports@p,@r,@a,@eand@splus common filters./tphereaccepts multiple names and selectors, while the new/therecommand sends players, entities, selected pets or the sender to the block being looked at. - Chat respects the rest of the server — A cancelled chat event stays cancelled, recipients removed by mute, ignore or range plugins remain removed,/delmsgreliably removes every per-viewer copy of a message, and Discord-to-game messages resolve Nexo glyph placeholders before delivery. - Reliability pass — Folia repeating tasks can now be cancelled reliably, explicit Bukkit permission nodes win over coarse Brigadier op-level checks, WorldEdit-style//commands resolve correctly, Floodgate players are excluded from the Java translation probe, and Allium no longer prints Bukkit's raw usage line over its own formatted command errors.Technical Details
Smooth animated names without losing solid-color movement
The animation counter itself was already correct. It matches the UnlimitedNametags phase cycle exactly:-1.0through1.0in0.1steps, formatted with a US decimal separator and wrapped after1.0. The rewind came from how Allium transformed GradientPlus output. GradientPlus emits a color code before each character, including presets that are visually solid. Allium took the first and last extracted colors, invented a third stop by choosing the nearest named Minecraft color, and produced a tag shaped like this:
MiniMessage reverses the color-stop array for negative phases. That is continuous for two endpoints, but with three or more stops the path itself changes as the phase crosses zero. For the live purple preset, the rendered jump from<gradient:#965CEA:#965CEA:blue:phase>Player</gradient>-0.1to0.0was roughly three and a half times the movement of an ordinary frame, which looked like the name went backward once and then continued. Allium now emits exactly two stops: - A real multi-color GradientPlus name uses its first and last character colors. - A solid GradientPlus preset pairs its color with the nearest Minecraft named color, preserving the subtle color movement without reintroducing a third-stop seam. The purple example becomes#965CEAtoblue. - Chat's fallback formatter delegates to the same implementation as the tab-name animator, so the two cannot drift apart again. There was a second source of instability in the tab list: multiple systems could write the same frame.GradientNameManager, the PacketEvents tab manager, Bukkit'splayerListNameAPI and TAB's own format cache could all update or restore an animated name at slightly different times. The ownership rules are now explicit: - When TAB's API accepts an animated name, TAB is the only output path for that player. Bukkit is used only as a fallback when TAB is unavailable. - The PacketEvents tab manager no longer runs a second every-tick animation loop. - Relisting an existing TAB entry updates only itslistedstate and preserves the current animated display name. - Recreating a missing animated entry does not install TAB's cached placeholder result as a forced display name; the current animation owner immediately reasserts the live frame. - Party visibility changes schedule one next-tick reconciliation instead of the old chain of repeated rewrites. Each viewer/target transition carries a revision, so delayed work from an older visibility state cannot overwrite a newer one. Regression tests render frames on both sides of the phase midpoint (-0.1,0.0,0.1) for solid and multi-color inputs and assert equal adjacent color movement. Separate tests enforce TAB/Bukkit writer exclusivity and the tab-relist policy.Stored custom items
Stored items live one-per-file underplugins/Allium/items/. Keeping definitions separate means one unreadable file is skipped without preventing every other item from loading, and makes individual items easy to copy, edit or version-control. | Command | Purpose | |---|---| |/core item add <id>| Capture the stack in the player's main hand | |/core item update <id>| Re-capture an existing item while preserving its Allium options | |/core item remove <id>| Delete the definition and unregister it | |/core item reload| Reload all stored item files without restarting | |/core item list| List built-in and stored item IDs | |/core item give <player> <id> [amount]| Give an item, respecting its permission and maximum amount | |/give <player> ci:<id>or/i ci:<id>| Use the existing custom-item give pipeline | IDs are normalized to lowercase and may contain onlya-z,0-9,_and-. A stored definition cannot take the ID of a built-in behavior item such astree_axe,spawner_changeroritem_renamer. Each generated file contains three layers: 1.snapshot— Bukkit's serializedItemStack, used as the lossless base. This retains components Allium does not model directly, including custom model data and metadata added by item plugins. 2.vanilla— editable fields applied over the snapshot, including name, lore, damage, durability, stack size, model, rarity, repair cost, tooltip/glint state, item flags, enchantments and attribute modifiers. 3.allium— delivery options:notify-receivers, an optional receiverpermission, andmax-give. Deleting one property from thevanilla:block removes that modeled property from the rebuilt item. Deleting the entire block hands control back to the untouched snapshot. A file may also be authored with onlymaterial:and no snapshot for a simple vanilla item. ItemEdit and ItemTag are optional soft dependencies. Allium still starts without them; when present, their item data remains part of the captured snapshot and their load order is respected. Nexo and Oraxen metadata is preserved the same way. New permission nodes split routine use from item administration while preservingallium.adminas a compatibility override: | Permission | Grants | |---|---| |allium.item.give|/core item give| |allium.item.list|/core item list| |allium.item.admin| Add, update, remove and reload stored items | The custom-item registry is now case-insensitive at registration time and safely replaces its own definitions during reload. Give's armor probe also resolvesci:items without constructing or reporting an invalid item twice.Harvest interaction, growth and regrowth
harvest/config.ymlnow hascrop-defaults:. Every setting can be over0.2.10aAllium v0.2.10aJul 20, 2026 Allium v0.2.10a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- v0.2.9a shipped an unshaded jar and could not enable — The published v0.2.9a download was the thin jar with no bundled libraries, so every server failed on startup withNoClassDefFoundError: com/zaxxer/hikari/HikariConfig. The plugin code was fine; the release pipeline picked the wrong file. If you downloaded v0.2.9a, replace it with this build. - No longer crashes on servers older than the API it was built against — Allium compiles against a recent Paper API but supports servers back to 1.21.1. Several references to blocks, attributes, game rules and entity types that only exist in newer versions were throwingNoSuchFieldErrorat runtime — disabling the tree axe, the locator-bar tab list,/heal, handcuffs and creative-mode potion restrictions, and in some cases the whole plugin. Every such reference now resolves safely by name. - A full audit, not a spot fix — Rather than fixing only the crash that was reported, everyMaterial,Attribute,EntityType,GameRule(and every other Bukkit registry) constant referenced anywhere in the plugin was checked against the actual 1.21.1 API. All offenders are listed below and the jar is verified to load clean on 1.21.1.Technical Details
The v0.2.9a release jar was the wrong artifact
mvn installleaves two jars intarget/: | file | size | contents | |---|---|---| |Allium.jar| ~4.99 MB | shaded, HikariCP and H2 relocated undercodes/castled/allium/libs/| |Allium-0.2.9a.jar| ~2.04 MB | thin jar, 571 entries, no bundled libraries | The release workflow selected the upload withfind target -name "Allium*.jar" ... | head -n1. That pattern matches both, andfindreturns them in directory order rather than any meaningful one, so the build published the thin jar. Its class files still reference the un-relocatedcom.zaxxer.hikaripackage, hence the failure insideDatabase.<init>the momentonEnablereached the database layer. The workflow now targetstarget/Allium.jar— the shade plugin's<finalName>— explicitly, and fails the build outright if that file is missing or if it does not containcodes/castled/allium/libs/hikari/HikariConfig.class. A silently thin jar cannot reach a release page again. Worth knowing for anyone building locally:pom.xmldeclares<project.build.finalName>inside<properties>, which does nothing — the real setting is<build><finalName>. That mismatch is why two differently named jars exist side by side. Always taketarget/Allium.jar.
Allium compiles against a much newer Paper API than the oldest server it supports at runtime (1.21.1). Every direct reference to aNoSuchFieldErroron newer-than-runtime registry constantsMaterial,Attribute,GameRule,EntityTypeor similar constant that only exists in a later version compiles cleanly and then throwsNoSuchFieldErrorthe moment the JVM resolves it. When the reference sits in a static field initialiser, the failure lands in<clinit>and the class can never be constructed — so a single missing constant can disable a whole feature or, if it is on the enable path, the entire plugin. (try/catcharound the call does not help:NoSuchFieldErroris anError, not anException, and the existing handlers only caughtException.) Each offender was fixed at the reference, not by lowering the compile target: - Tree axe — `PALE_OAK_LOG` (1.21.4+), `BUSH` / `FIREFLY_BUSH` / `LEAF_LITTER` / `WILDFLOWERS` / `SHORT_DRY_GRASS` (1.21.5+).TreeAxeManagerbuilt its material sets from hard enum constants in static initialisers, so constructingTreeAxeListenercrashed and tookPluginStart.registerCommands— and the plugin — down. Now built through amaterials(String...)helper that resolves each name withMaterial.matchMaterialand skips what the server does not know. This also matches how the rest of the class already worked (getValidGroundTypeshas always switched on material names). - Locator bar — `GameRule.LOCATOR_BAR` (1.21.6+). Read in four places, including a repeating global task (retryTabListManagerInit) that loggedGlobal task ... generated an exceptionevery cycle, and inPartyManagerplayer-visibility logic. All four now go throughApiCompat.isLocatorBarEnabled(world), which resolves the rule withGameRule.getByNameonce and reportstrue(the previous default) when the rule is absent. - *`/heal` and handcuffs — `Attribute.MAX_HEALTH` / `MOVEMENT_SPEED` (renamed from `GENERIC_in 1.21.3).**ApiCompatresolves whichever spelling the server has via reflection;/heal` falls back to 20 hearts and the handcuffs item skips the slow-down modifier when neither exists, rather than crashing the command or the item build. - Creative-mode potion restriction — `EntityType.SPLASH_POTION` / `LINGERING_POTION` (split from `POTION` in 1.21.5).CreativeManagernow comparesevent.getEntityType().name()as a string, so it loads on any version; older servers deliver both kinds asPOTIONand it treats them as splash potions. - Waypoint attribute — `Attribute.WAYPOINT_TRANSMIT_RANGE` (1.21.6+). The Citizens NPC re-enforcement loop resolves it throughApiCompatand simply does nothing on servers that lack it. The sharedApiCompathelper (codes.castled.allium.util.ApiCompat) centralises these lookups so future version-gated constants have one obvious home.Verifying nothing else in the plugin has the same problem
Rather than guessing which constants were too new, this was done mechanically for every registry, not justMaterial: everygetstatic org/bukkit/**reference was extracted from the compiled classes and diffed against the actual 1.21.1paper-api. That coveredAttribute,EntityType,Enchantment,Particle,Sound,Statistic,PotionEffectType,GameRuleand ~25 others. Two categories of false positive were ruled out: - javac enum `switch` tables. Aswitchover an enum compiles to a synthetic$SwitchMapwhose every entry is individually wrapped in aNoSuchFieldErrorhandler (e.g. 2,154 such handlers inFireballExplosion), so unknown constants are skipped as the table is built. Thesegetstatics are always immediately followed byordinal()and were filtered out. - Mixed-case constants likeServicePriority.Highest— present in 1.21.1, only flagged by an uppercase-only extraction pass. After the fixes, the sweep reports zero genuine missing references. As a final check the shaded jar was loaded against a real 1.21.1paper-apiand every previously-crashing class (TreeAxeManager,HandcuffsItem,Heal,CreativeManager) was force-initialised successfully, withApiCompatresolvingMAX_HEALTH → GENERIC_MAX_HEALTHandWAYPOINT_TRANSMIT_RANGE → nullas expected. Verified across the 1.21.1 API; the same mechanism degrades cleanly on every version up to the 1.21.11 API also present in the build environment.0.2.9aAllium v0.2.9aJul 20, 2026 Allium v0.2.9a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Allium Harvest — A new module for custom crops and spawner model overlays. Crops render as display entities using any Nexo or Oraxen model, so they never occupy a block state. Includes growth paths, fertilizers, sprinklers, an optional soil lifecycle, a developer API and 11 Bukkit events. Full documentation on the wiki. - Spawner models never touch the spawner block — Per-entity-type visual overlays for vanilla spawners. The real block is never replaced, hidden or altered, so mob spawning, silk touch, redstone and every other plugin's view of it stay exactly vanilla. - `/minecraft:` commands work again with a granted permission node — Fixed the v0.2.7a permission resolver denying vanilla commands to non-op players who held the matchingminecraft.command.*node. - Class preloading — Allium now loads its own classes at startup, removing theNoClassDefFoundErrorfailure mode that hit when the jar was replaced under a running server. - H2 is the only bundled JDBC driver — SQLite and MySQL are still supported but their drivers are no longer shipped, cutting roughly 14 MB from the jar.Technical Details
Allium Harvest
81 classes, ~8,300 lines, 66 unit tests. Everything is configured underplugins/Allium/harvest/:
Mutual exclusivity is the idea the whole system is built around. Two places choose between outcomes — the growth path at planting, and the quality tier at harvest — and both must pick exactly one.harvest/ config.yml storage, engine pacing, soil, sprinklers, spawners crops/tomato.yml one file per crop fertilizers.yml quality / yield / speed / variation / retainers sprinklers.yml sprinkler tiers spawner-models.yml per-entity-type spawner overlaysmode: WEIGHTED_ONEdraws a single number across the summed weights and walks entries until it lands, so one roll produces one winner.mode: INDEPENDENTrolls each entry separately, which is correct for bonus drops like seed returns but wrong for quality tiers, where it hands out a regular tomato and a silver-star tomato from the same harvest. SettingINDEPENDENTon aprimary:table produces a reload warning naming the exact YAML path. Five loot modes ship:WEIGHTED_ONE,WEIGHTED_MULTIPLE_WITHOUT_REPLACEMENT,INDEPENDENT,GUARANTEED_ALLandSEQUENCE. Growth paths are rolled once at planting and written to the database immediately, so a path survives restarts, chunk cycling, reloads and reconnects and is never re-rolled. Because a path controls both models and drops, a rare path can reuse the common path's early models and only reveal itself at maturity — players cannot tell a golden tomato from a normal one while it grows. Multi-block crops occupy more than one cell viafootprint:, given as offsets from the anchor. Cells are reserved transactionally: every target is checked before any is claimed. If the footprint is obstructed when the final stage comes due, the crop matures at its previous single-block stage rather than overwriting a player's blocks. Growth clocks areREAL_TIME(wall-clock, including while unloaded) orLOADED_TIME(frozen on unload). Real-time crops catch up when their chunk returns, bounded bymaximum-catch-up-stagesso a year-old farm does not resolve a thousand stages at once; time still owed after the cap resumes rather than being discarded. Fertilizers carry any combination of five effects —quality,yield,growth.speed-multiplier,variationandsoil.retain-for. There is no built-in concept of levels; each tier is a separate entry tuned on its own numbers.qualityandvariationmultiply weights before the draw, so they shift the odds toward rare outcomes while it stays one roll.variationandsoilonly work when applied to soil rather than to a growing crop, because the path is rolled the instant the seed goes in — applying one to an existing crop tells the player it must be worked into the soil first.yielddeliberately does not touchadditional:tables, since multiplying seed returns would let a yield fertilizer pay for itself. Soil lifecycle is off by default. When enabled, a block starts its clock the first time something is planted on it — not when the farmland is placed — so enabling it does not write a row for every farm block on the server. Oncelifetimeelapses the block refuses new plantings until fed by a retainer; crops already growing are never destroyed. The record outlives the block forforget-exhausted-after, so breaking and replacing farmland resumes the same timer instead of resetting it. There is no scanning task at all: expiry is a stored timestamp compared against the current time on read, which makes wear exact across restarts and unloaded chunks at zero per-tick cost. Sprinklers are ordinary placed blocks that Allium remembers. Overlapping sprinklers stack multiplicatively with each other and with speed fertilizers — two0.8sprinklers give0.64, not0.6— so each source keeps its own plain meaning instead of needing a lookup table. A floor of 5% of the configured duration means no amount of stacking makes crops instant, andmaximum-per-chunk(default 64) bounds coverage lookups so a player cannot degrade performance by carpeting a chunk. Clickable crops. Display entities have no hitbox of their own, so a click aimed at one passes straight through.crop-visuals.clickablepairs each crop with an invisible Interaction entity sized byinteraction-width/interaction-height, letting the plant itself be right-clicked to fertilize or harvest and punched to break — with no real block standing in for it.Spawner models
Per-entity-type overlays configured inspawner-models.yml, withvariants:selecting by stack size (highest qualifyingminimum-stackwins). Stack size comes from aSpawnerProviderservice; without a stacking plugin registered, every spawner reads as size 1. The real spawner block is never replaced, never set to air, and its behaviour is never altered — the model is oneItemDisplaypositioned to cover the cage. A model smaller than a full block leaves the cage partly visible by design, so models should be authored to cover it rather than expecting the plugin to hide it. Visuals are derived state and the database row is authoritative. On chunk load, break, explosion, type change, restart or/harvest spawner refresh, the plugin converges to exactly one correct display per tracked spawner: orphaned entities with no record are removed and duplicates culled. Because reconciliation is idempotent, restarts and reloads never duplicate models. Discovery on chunk load reads the chunk's block-entity list rather than iterating blocks, bounded bymaximum-block-entities-per-tick.Harvest performance model
There is no task per crop, per spawner, per sprinkler or per soil block. One repeating async scan finds crops whose due time has passed and dispatches each stage advance to the region thread owning it, bounded per pass bygrowth-engine.checks-per-tick. Growth never forces a chunk load — only loaded chunks are scanned. All SQL runs on a dedicated single-thread executor with writes coalesced between flushes, so nothing touches the main or region threads. Folia is supported throughout: world and entity work goes to the correct region scheduler, and no Bukkit world state is touched asynchronously. Crop display entities are not persisted as entities. They are non-persistent and rebuilt from the database on chunk load, which is why a crashed server never leaves orphaned models behind.
v0.2.7a introduced a command permission resolver pipeline to stop/minecraft:commands denied despite a granted permission nodeCommandManageroverriding native Brigadier permissions. A player grantedminecraft.command.killthrough LuckPerms still got `You don't have permi0.2.8aAllium v0.2.8aJul 19, 2026 Allium v0.2.8a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Deleted chat messages no longer reappear on second deletion — Fixed a thread-safety bug wherePacketChatTrackerImplcaptured resend packets as new chat history entries, causing deleted messages to resurface on subsequent/delmsgruns. Required a server restart to clear. - Shared `ChatMessage` instances across collections —playerMessagesandplayerChatHistorynow reference the same object for packet-tracked messages, sosetDeleted(true)on one automatically affects the other. - Faster chat clear — The invisible-character clear phase now batches 20 characters per message instead of sending 120-180 individual packets, cutting the clear from ~180 packets to ~9.Technical Details
Thread-Safe Resend Suppression
The previous fix in v0.2.7a removed duplicate tracking fromFormatChatListener, but a second bug remained:PacketChatTrackerImplused a plainHashSetforplayersBeingResent, and removed the player UUID immediately whenresendChatHistoryToPlayer()returned. PacketEvents may process outgoing packets on a different thread or slightly later, so resend packets were captured as brand-new chat history entries with fresh IDs,deleted = false, andSYSTEM_UUIDas the sender. The next/delmsgmarked the original entries deleted, but the replay-generated copies survived and reappeared on the next resend. After 3+ seconds the duplicate-time-window check indeleteMessagecould no longer catch them. Fix (three layers): 1. `playersBeingResent`:HashSet→ConcurrentHashMap.newKeySet()(thread-safe). 2. `suppressPacketTracking`: NewAtomicBooleanchecked by all three packet handlers viashouldIgnoreTracking(). Settruefor the duration ofresendChatHistoryToAllPlayers()so no packets from the broadcast are tracked, regardless of per-player timing. 3. Delayed cleanup: Player UUID removal fromplayersBeingResentand guard release (resendAllInProgress,suppressPacketTracking) are delayed 1 tick viaSchedulerAdapter.runLater, giving PacketEvents time to process the outgoing resend packets before tracking resumes.DeleteMsg.resendInProgressalso releases after a 1-tick delay to prevent rapid successive/delmsgcommands from scheduling overlapping resends.Shared
ChatMessageInstancesPacketChatTrackerImplpreviously calledchatMessageManager.storeMessage()(which creates oneChatMessageinplayerMessages) and then constructed a separateChatMessageobject fortrackMessageForPlayer()(which stores inplayerChatHistory+globalChatHistory). Two different objects for the same logical message meantsetDeleted(true)on one did not affect the other. NewChatMessageManager.storeMessageObject()returns the actual stored instance.PacketChatTrackerImplnow calls this method and passes the same reference totrackMessageForPlayer(), so both collections share one object. Settingdeleted = trueon it propagates everywhere. Additionally,ChatMessage.deletedis nowvolatilefor cross-thread visibility (PacketEvents handlers may run off the main thread).Batched Chat Clear
The clear phase previously sent 120-180 individualplayer.sendMessage()packets, each containing a single invisible Unicode character. This was the dominant cost in the 2-3 second resend time due to per-packet network overhead. Now characters are batched 20-per-message with\nseparators into a singleComponent.text(). Minecraft renders the newlines as separate chat lines, so the visual effect is identical — old messages are pushed off-screen — but in ~9 packets instead of ~180. The same batching was applied to theDeleteMsg.clearChatFallback()path. Unused imports (java.util.Collections,java.util.concurrent.ThreadLocalRandom) were cleaned up from both files.0.2.7aAllium v0.2.7aJul 19, 2026 Allium v0.2.7a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- New pluggable command permission resolution pipeline — The ad-hoc permission heuristics inCommandManagerhave been replaced by a dedicatedcodes.castled.allium.permissions.commandpackage. A chain of framework-specific adapters (PaperBasicCommand, Brigadier, NightCore, Bukkit, LuckPerms, registered-permission conventions) resolves whether a player may run a command, and reports how the decision was made. Unit-tested with JUnit 5. - Vault Chat abstract-class support — When the Vault Chat class is an abstract class rather than an interface, the provider is now assigned directly instead of being skipped, so chat formatting works with permission plugins that expose Chat through an abstract base. - Deleted chat messages no longer reappear — Fixed duplicate per-player message tracking that broke content matching indeleteMessage, causing deleted messages to resurface on resend. - Nickname formatting fixes —%allium_nickname%is now recognized as a player placeholder in chat formats, and channel messages use the formatted nickname (falling back to the real name when no nickname is stored). - Oraxen → Nexo migration — Built-in custom items (Tree Axe, Spawner Changer) now referencenexo:item models instead oforaxen:.Technical Details
Command Permission Resolver (
permissions/command)CommandManagerpreviously resolved permissions through a singlehasPermissionForCommand()method that had accumulated special cases (VanillaCommandWrapperclass-name checks, Paper-internals safety nets, Allium-derived permissions). That logic is now factored into an adapter pipeline:
Key contracts: -DefaultCommandPermissionResolver ├── (extension point: CommandAPI, ACF, cloud, …) ├── PaperBasicCommandPermissionAdapter — reflects the wrapped Paper BasicCommand, calls canUse()/permission() ├── BrigadierCommandPermissionAdapter — finds the CommandNode field, walks requires() predicates; VanillaCommandWrapper without an inspectable node → allow (defer to native dispatch) ├── NightCoreCommandPermissionAdapter — reads NightCore's public command tree reflectively (no hard dependency), tests root and sub-command nodes ├── BukkitCommandPermissionAdapter — explicit command.getPermission() → testPermissionSilent() ├── LuckPermsCommandPermissionAdapter — queries CachedPermissionData against candidate permission strings (reflective, no hard dependency) └── RegisteredCommandPermissionAdapter — checks candidate permissions registered with Bukkit's PluginManager (wildcards only count as a grant, never as a denial)CommandPermissionAdapter.resolve()returnsOptional.empty()when the adapter does not understand the command (next adapter is tried), or aPermissionResultwithResolutionType.UNKNOWNwhen it owns the command but cannot safely inspect its permission rule. -PermissionResultis a record carryingallowed, theResolutionTypethat decided, thematchedPermission(evidence), and the resolvedCommand. Anullcommand means the command is unknown → Allium's unknown-command message. -PermissionCandidatesgenerates conventional permission strings (<plugin>.<module>.command.<action>etc.) from the owning plugin, command labels/aliases, and arguments. A GUI "open" action is only inferred when the owning module actually registers…command.open, so/ah-style commands work without every no-argument command looking like "open". -DefaultCommandPermissionResolvercaches the registered-permission snapshot for 2 s and accepts additional adapters via its constructor — the extension point for CommandAPI, ACF, cloud, or other frameworks.CommandManagerbehavior changes: - Command resolution (including namespacedplugin:commandfallback) now happens inside the resolver;resolveCommandForPermissionCheck,hasPermissionForCommand, andgetDerivedPluginCommandPermissionwere removed (~130 lines of heuristics). - Withdebug-mode: true, every resolution is logged:Permission resolution for /<cmd>: allowed=…, type=…, matched=…— making "why was this command blocked/allowed" answerable from the log. - The legacy aliases are retained as a final fallback:/flypasses withallium.flyorallium.tfly,/pingwithallium.ping. - The directnet.milkbowl.vault.permission.Permissionimport was dropped; Vault permission setup remains reflective. Tests:CommandPermissionAdaptersTest(445 lines) andPermissionCandidatesTest(126 lines) cover the adapter pipeline and candidate generation. JUnit Jupiter 5.12.2 and Surefire 3.5.2 were added to the build for this.Vault Chat Proxy Fix
createChatProxybailed out withnullwhen the VaultChatclass was an abstract class instead of an interface (chatInterface.isInterface()false), which silently disabled Allium's chat formatter. It now returns the provider directly in that case — a JDK dynamic proxy is only possible for interfaces, but the concrete provider is already usable as-is. Additionally, the "Vault Chat not available" message was upgraded fromINFOtoWARNand now explains the common cause: permission plugins like LuckPerms needchat: truein their config to provide chat services — Vault alone does not provide chat formatting.Chat Message Deletion Fix
FormatChatListenerused to build its ownChatMessageManager.ChatMessageand track it for every online player, in parallel withPacketChatTrackerImpl(packet capture). The listener-built entries were rendered from a different format template, so their plain text did not match the components stored byAlliumChannelManager. Content matching indeleteMessagethen failed for those entries, they stayed undeleted, and the "deleted" message reappeared on resend. The listener-side tracking was removed;PacketChatTrackerImplis now the single owner of per-player chat history. Also inFormatChatListener, hover components are now wrapped explicitly withHoverEvent.showText(...)instead of passing the component directly tohoverEvent().Nickname Handling
-%allium_nickname%in chat format templates is now substituted with the player component (same treatment as<player>/<display_name>), in bothFormatChatListenerandAlliumChannelManager. -AlliumChannelManagerno longer returns the raw stored nickname: an empty/null stored nickname falls back toplayer.getName(), and a present one is passed throughgetFormattedNickname(player, stored)so nickname formatting (colors, prefix rules) applies in channel messages.
Teleporting a player now removes their entry from/backCache Invalidation (TP)locationCache, so the next/backre-evaluates fromlastLocation/the database instead of a stale cached location.Adventure Component Builders
ComponentBuilder.build()calls were replaced withasComponent()inHelp,Notes, andWarp(deprecation cleanup; behavior unchanged).Oraxen → Nexo
TreeAxeItem,SpawnerChangerItem, andcustomitems.ymlnow referencenexo:tree_axe/nexo:spawner_changeritem models instead of theoraxen:namespace.Build
- Version bumped0.2.6a→0.2.7a. - Addedorg.junit.jupiter:junit-jupiter:5.12.2(test scope) andmaven-surefire-plugin:3.5.2.0.2.6aAllium v0.2.6aJul 7, 2026 Allium v0.2.6a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Paper Brigadier compatibility —CommandManagernow correctly handles commands registered via Paper'sLifecycleEvents.COMMANDS(e.g., AbyssalLib'sCommandBus, custom Brigadier dispatcher commands). These commands appear asVanillaCommandWrapperin the command map, whosetestPermissionSilent()can false-negative. Allium now allows these commands through and defers permission evaluation to the server's native Brigadier dispatch. - Derived permission scoped to Allium-owning plugins only — The legacy heuristic that produced synthetic permissions like<pluginName>.<commandName>is now only applied to commands owned by Allium itself. For all other plugins — whetherPluginCommand,VanillaCommandWrapper, or custom wrappers — Allium trusts the command's native permission system.Technical Details
Problem
Commands registered through Paper's modernLifecycleEvents.COMMANDSAPI (used by AbyssalLib'sCommandBus, among others) do not go through Bukkit'splugin.ymlcommand registration. Instead, they registerLiteralArgumentBuilder<CommandSourceStack>nodes directly into Mojang's BrigadierCommandDispatcher. Paper wraps these dispatcher-only nodes inVanillaCommandWrapper(org.bukkit.craftbukkit.command.VanillaCommandWrapper) so they appear inSimpleCommandMap.knownCommandsfor Bukkit API compatibility. However,VanillaCommandWrapper.testPermissionSilent()does not reliably evaluate the underlying Brigadier node'srequires()predicates, often returningfalseeven when the player holds the correct permission. This caused Allium'sCommandManagerto block all such commands and display the Allium "no-permission" message, even when the command's native permission system would have allowed execution.Attempted Fixes (Rejected)
1. Class name matching (className.contains("Brigadier") || className.contains("Wrapper")) — fragile, case-sensitive, and missesVanillaCommandWrapperin the general case since the wrapper extendsCommand/PluginCommandvia different paths depending on the Paper version. 2. `PluginIdentifiableCommand` instanceof check — Paper'sMinecraftCommandMap$CommandWrappermay implementPluginIdentifiableCommandwith anullplugin, making the check useless. 3. `PluginCommand` instanceof check — some Paper wrapper variants extendPluginCommanddirectly, so negatingPluginCommanddoesn't reliably capture dispatcher-only commands.Actual Fix
The fix uses a class-name-exact check forVanillaCommandWrapperas the first gate inhasPermissionForCommand():
This immediately allows all Brigadier-dispatcher-registered commands and defers permission evaluation to the server's native dispatch (which correctly evaluates theif (className.contains("VanillaCommandWrapper")) { return true; }CommandNode.canUse()predicates at execution time). ForPluginCommandinstances, the revised logic is: 1. Explicit permission set —command.getPermission()is non-null →testPermissionSilentalready checked it, allow. 2. Derived permission — only computed for commands whosePluginIdentifiableCommand.getPlugin()returns"Allium". The derived string isallium.<baseCommand>. 3. All other commands (PluginCommandfrom other plugins, vanilla commands with matching Bukkit wrappers) — trusttestPermissionSilent.
The fallback ingetPluginForCommandFallback FixgetPluginForCommandpreviously returnedcommandName.toLowerCase()when no plugin could be identified, causing the derived permission to be<commandName>.<commandName>(e.g.,relique.relique). This is nownull, which propagates throughgetDerivedPluginCommandPermissionasnull— treated as "no permission required".Support and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.2.5aAllium v0.2.5aJul 6, 2026 Allium v0.2.5a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Translation probe config corruption fixed —applyBundledDefaultsno longer callsconfig.save(), preventing the old JAR's defaults from being written to disk on every restart. A newseedMissingProbeConfig()method writes thetranslation-probesection to disk only when the file actually lacks it (usingcontains(path, true)to ignore in-memory defaults). - Hardcoded probe check fallback —loadChecks()now cascades: disk config → bundled resource →createDefaultChecks()with 16 hardcoded checks (meteor + 14 IPN + 2 libIPN). This ensures the probe always has checks even when both the disk config and old JAR resource lack the section. - Probe enabled fallback —isEnabled()now falls back to rootenabled, so the probe registers even when thetranslation-probesection is entirely absent from the config file. - Stealthier probe —CloseWindowis sent immediately (no 2-tick delay, sign GUI never renders), and the sign position isY - 30below the player (out of view). Zero visual flicker. - `/allium modguard reload` — New command unregisters the probe, reloads the config from disk, and re-registers. Requiresallium.admin.Bug Fixes
| Bug | Root Cause | Fix | |-----|-----------|------| | Config file overwritten on every restart |applyBundledDefaultscalledconfig.save()withcopyDefaults(true), writing old JAR defaults back to disk | Removedconfig.save()andcopyDefaults(true)fromapplyBundledDefaults| |translation-probesection never persisted to disk |seedMissingProbeConfigusedconfig.contains("translation-probe")which returnedtruefrom defaults set byapplyBundledDefaults— even though the file lacked the section | Changed toconfig.contains("translation-probe", true)to ignore defaults and check only the file | | Probe has no checks after config corruption |loadChecks()had no fallback when both disk config and bundled resource lacked the checks section | AddedcreateDefaultChecks()with 16 hardcoded checks as final fallback | | Probe doesn't fire whentranslation-probesection is missing |registerTranslationProbe()andisEnabled()returnedfalseforconfig.getBoolean("translation-probe.enabled", false)when the section was absent | Changed fallback toconfig.getBoolean("translation-probe.enabled", config.getBoolean("enabled", false))| | Probe delay too slow (3s) and timeout too long (4s) | Config defaults fordelay-ticks(60) andtimeout-ticks(80) were unnecessarily conservative | Changed to 10 ticks (0.5s) and 30 ticks (1.5s) | | Sign GUI visible to player on join |CloseWindowwas sent after a 2-tick delay, giving the client time to render the sign |CloseWindowsent immediately in the same packet batch; sign placed 30 blocks below player | | No reload command — required full restart for config changes | No implementation | Added/allium modguard reloadwhich callsModGuardManager.reload()|Files Changed (4)
| File | Change | |------|--------| |managers/security/ModGuardManager.java|applyBundledDefaultsno longer saves to disk; addedseedMissingProbeConfig()withcontains(path, true)guard; addedreload()static method; added/allium modguard reloaddispatcher | |managers/security/ModGuardTranslationProbe.java| AddedcreateDefaultChecks()(16 hardcoded checks);loadChecks()now cascades config → bundled → hardcoded;isEnabled()falls back to rootenabled;CloseWindowimmediate; sign Y -30; delay-ticks 10, timeout-ticks 30 | |modguard/config.yml| Cleaned up: removeddebug,regex-matching,case-sensitive,banned-mods,allowed-mods; addedtranslation-probesection with all 16 checks withrequire-corroboration-for-kick: false| |commands/Core.java| AddedhandleModGuardCommand()for/allium modguard reload|Technical Details
Config Persistence Chain
The old code had two bugs forming a corruption loop: 1.applyBundledDefaultscalledconfig.save()with the old JAR's bundled config (notranslation-probesection), overwriting user changes on every restart 2. Even after removingconfig.save(),seedMissingProbeConfigcheckedconfig.contains("translation-probe")— butapplyBundledDefaultshad already set the NEW bundled resource as defaults, which DO contain the section. Socontains()returnedtruefrom defaults andseedMissingProbeConfigreturned early without persisting Fix:seedMissingProbeConfignow usesconfig.contains("translation-probe", true)to skip default-only matches. It also writes the full 16-check section to disk when absent, so subsequent restarts load it directly from the file.Probe Check Fallback Chain
loadChecks()now follows this order: 1.config().getConfigurationSection("translation-probe.checks")— reads from the in-memory config (loaded from disk or defaults) 2.loadBundledChecks()— reads from the JAR resource (modguard/config.yml) and copies checks into the in-memory config 3.createDefaultChecks()— 16 hardcoded checks: meteor + 14 IPN (key.inventoryprofiles.\*) + 2 libIPN This ensures the probe always has checks regardless of which JAR version is deployed or whether the config file contains the section.Support and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.2.4aAllium v0.2.4aJun 30, 2026 Allium v0.2.4a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Vault softdepend crash fixed — Allium no longer crashes at startup when Vault is absent. All Vault-typed fields and method signatures were changed toObject, with casts at call sites.VaultEconomyProvideris only created when Vault'sEconomyclass is actually loadable. TheFormatChatListeneris skipped entirely when Vault Chat isn't available. - PacketEvents softdepend crash fixed — Allium no longer crashes at startup when PacketEvents is absent. TheGlowcommand andGlowListenerare guarded behindisPacketEventsAvailable(), and theglowCommandfield was relaxed fromGlowtoObjectto prevent class loading. - CrowBar 128-block radius limit removed — AddedshouldBeVisibleIgnoreDistancetoPartyManagerso players beyond 128 blocks remain visible on the locator bar, with only same-world and forced-visibility checks applied. - NPC locator bar flash fixed —PartyManager.updatePlayerVisibilitynow checksviewer.canSee(target)before callingshowPlayer, preventing NPC info packets from being resent every 5 ticks when the NPC is already visible.Bug Fixes
| Bug | Root Cause | Fix | |-----|-----------|------| | Plugin fails to enable without Vault | Vault types in field/method signatures causeNoClassDefFoundErrorduring class loading;new VaultEconomyProviderruns before Vault availability check | Changed Vault-typed fields/methods toObject; guardedVaultEconomyProvidercreation; skippedFormatChatListenerwhenvaultChat == null| | Plugin fails to enable without PacketEvents |Glowclass references PacketEvents types in method signatures;new Glow(this)at unconditional line 1524 forces class loading | GuardedGlow/GlowListenercreation behindisPacketEventsAvailable(); changedglowCommandfield toObject| | Players beyond 128 blocks hidden on CrowBar |shouldBeVisibleapplies distance filtering which limits range to 128 blocks | AddedshouldBeVisibleIgnoreDistancethat skips distance check while keeping same-world and visibility overrides | | NPCs flash on locator bar every 5 ticks |updatePlayerVisibilitycallsshowPlayerunconditionally every interval | GuardedshowPlayerbehindviewer.canSee(target)|Files Changed (8)
| File | Change | |------|--------| |PluginStart.java| Changed Vault fields/methods toObject; guardedVaultEconomyProviderandFormatChatListenercreation; guardedGlow/GlowListenerbehindisPacketEventsAvailable()| |commands/Glow.java| No structural change (guarded at call site) | |DB/PermissionCache.java|getVaultPermissionreturnsObject| |listeners/security/CommandManager.java|vaultPermissionfield /setupVaultPermissionnowObject| |listeners/security/MaintenanceManager.java|permsfield / constructor param nowObject| |managers/core/PartyManager.java| AddedshouldBeVisibleIgnoreDistance;canSeeguard inupdatePlayerVisibility| |packetevents/CrowBarDataSender.java| UsesshouldBeVisibleIgnoreDistanceinstead ofshouldBeVisible| |listeners/security/ConnectionManager.java|vaultChatnull guard inreloadChatFormatter|Technical Details
Vault Class Loading Fix
Paper 1.21+'sPluginClassLoadereagerly resolves type references during class definition. HavingVaulttypes in field declarations or method return types causesNoClassDefFoundErroreven when the code path never executes. Fix: all Vault-typed fields and return types are nowObject, with casts at every call site.PacketEvents Class Loading Fix
Same mechanism as Vault — PacketEvents types inGlow's method signatures crash class loading.Glowis now only instantiated whenisPacketEventsAvailable()returns true, so the class is never loaded on servers without PacketEvents.Support and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.2.3aAllium v0.2.3aJun 29, 2026 Wiki: https://github.com/castledking/Allium/wikiHighlights
- CanvasMC 26.2 support — Migrated from Paper API to Canvas API (io.canvasmc.canvas:canvas-api). Canvas is a Folia fork targeting Minecraft 26.2 (build 835). All scheduling now goes through the newSchedulerAdapterin thecodes.castled.allium.schedulerpackage, which properly handles Canvas's regionized threading model. - New SchedulerAdapter — Complete rewrite of the scheduling layer. The oldutil/SchedulerAdapter.java(587 lines of reflection-heavy Folia detection) is replaced by a clean 426-linescheduler/SchedulerAdapter.javawith a dedicatedTaskHandleabstraction. The old class is kept as a deprecated thin wrapper delegating to the new one, ensuring backward compatibility. - Tab completion API — NewAlliumTabCompletionsEvent+AlliumTabCompletionsListenerthat populate tab completions with all online non-vanished players when PartyManager is enabled. Other plugins can fire this event to get Allium-aware player name suggestions. - Vault initialization guard — Fixed a race condition whereregisterVaultDependentListeners()could be called multiple times if Vault initialized successfully and then the retry timer fired again. Added aregisteredboolean flag to ensure one-time registration. - FAWE removed — FastAsyncWorldEdit dependency and repositories dropped frompom.xml. No longer a soft dependency.Minecraft Version Support
| Version | Status | |---------|--------| | 26.2 (Canvas build 835) | Primary target | | 26.2 (Folia) | Supported | | 1.21.x (Paper/Spigot) | Supported via fallback |API / Dependency Changes
-pom.xml:io.papermc.paper:paper-apireplaced withio.canvasmc.canvas:canvas-api-pom.xml:<paper.version>bumped from26.2.build.37-alphato26.2.build.835-alpha-pom.xml: Addedcanvasmc-releasesandcanvasmc-snapshotsrepositories -pom.xml: RemovedFastAsyncWorldEdit-Coredependency -pom.xml: Removed IntellectualSites repositories -plugin.yml: RemovedFastAsyncWorldEditandWorldEditfromsoftdependNew Files
| File | Description | |------|-------------| |scheduler/SchedulerAdapter.java| Full Folia/Canvas scheduling adapter with global, async, entity, and location scheduling | |scheduler/TaskHandle.java| Lightweight abstraction over BukkitTask and Folia ScheduledTask | |events/AlliumTabCompletionsEvent.java| Custom event for populating player name tab completions | |listeners/AlliumTabCompletionsListener.java| Handles the event — returns all online non-vanished players when PartyManager is enabled |SchedulerAdapter API
The newcodes.castled.allium.scheduler.SchedulerAdapterprovides:
All methods return// Global (main thread equivalent) SchedulerAdapter.runGlobal(plugin, runnable) SchedulerAdapter.runLaterGlobal(plugin, runnable, delayTicks) SchedulerAdapter.runRepeatingGlobal(plugin, runnable, delayTicks, periodTicks) // Async SchedulerAdapter.runAsyncNow(plugin, runnable) SchedulerAdapter.runAsyncLater(plugin, runnable, delayTicks) SchedulerAdapter.runAsyncRepeating(plugin, runnable, delayTicks, periodTicks) // Entity (Folia entity scheduler, Bukkit fallback) SchedulerAdapter.runEntity(plugin, entity, runnable, retired) SchedulerAdapter.runLaterEntity(plugin, entity, runnable, delayTicks) SchedulerAdapter.runEntityRepeating(plugin, entity, runnable, retired, delayTicks, periodTicks) // Location (Folia region scheduler, Bukkit fallback) SchedulerAdapter.runAtLocation(plugin, location, runnable) SchedulerAdapter.runAtLocationLater(plugin, location, runnable, delayTicks) SchedulerAdapter.runAtLocationRepeating(plugin, location, runnable, delayTicks, periodTicks) // Utility SchedulerAdapter.cancelTasks(plugin) SchedulerAdapter.isFolia()codes.castled.allium.scheduler.TaskHandlewhich wraps bothBukkitTaskand Folia'sScheduledTask.Deprecated API
codes.castled.allium.util.SchedulerAdapteris now a deprecated wrapper. All methods delegate to the newcodes.castled.allium.scheduler.SchedulerAdapter. Existing code continues to work but should be migrated: | Old (util) | New (scheduler) | |------------|-----------------| |run(task)|runGlobal(plugin, task)| |runLater(task, delay)|runLaterGlobal(plugin, task, delay)| |runTimer(task, delay, period)|runRepeatingGlobal(plugin, task, delay, period)| |runAsync(task)|runAsyncNow(plugin, task)| |runLaterAsync(task, delay)|runAsyncLater(plugin, task, delay)| |runAtEntity(entity, task)|runEntity(plugin, entity, task, null)| |runAtEntityLater(entity, task, delay)|runLaterEntity(plugin, entity, task, delay)|Files Changed (31)
| File | Change | |------|--------| |pom.xml| Canvas API, build 835, removed FAWE, added Canvas repos | |plugin.yml| Removed FAWE/WorldEdit softdepend | |scheduler/SchedulerAdapter.java| New — full scheduling adapter | |scheduler/TaskHandle.java| New — task handle abstraction | |events/AlliumTabCompletionsEvent.java| New — tab completion event | |listeners/AlliumTabCompletionsListener.java| New — tab completion handler | |util/SchedulerAdapter.java| Rewritten as deprecated wrapper (~500 lines removed) | |PluginStart.java| Vault init guard, SchedulerAdapter migration for channel/Discord retry | |Core.java|Bukkit.getScheduler()calls replaced with SchedulerAdapter | |PregenCommand.java| SchedulerAdapter for async completion callback | |StaffChatCommand.java| SchedulerAdapter for channel switch | |InventoryManager.java| SchedulerAdapter for autosave | |FormatChatListener.java| SchedulerAdapter for phase renderer | |StaffChatListener.java| SchedulerAdapter for message redirect | |WolfBehaviorListener.java| SchedulerAdapter for delayed task | |CommandManager.java| SchedulerAdapter migration | |ConnectionManager.java| SchedulerAdapter migration, significant refactor | |FreecamDetector.java| SchedulerAdapter migration | |HandcuffsListener.java| SchedulerAdapter migration | |IPNDetector.java| SchedulerAdapter migration | |ModDetectionListener.java| SchedulerAdapter migration | |VanishListener.java| SchedulerAdapter migration | |FaweActivityListener.java| SchedulerAdapter migration, FAWE removal | |OreGenerationListener.java| SchedulerAdapter migration | |AlliumChannelManager.java| SchedulerAdapter migration | |DiscordSrvMessageBridge.java| SchedulerAdapter migration | |GradientNameManager.java| SchedulerAdapter migration | |ModGuardManager.java| SchedulerAdapter migration | |ModGuardTranslationProbe.java| SchedulerAdapter migration | |OreGenerationManager.java| SchedulerAdapter migration | |CrowBarDataSender.java| SchedulerAdapter migration |Technical Details
Scheduler Detection
Folia/Canvas detection usesBukkit.getServer().getClass().getMethods()to findgetGlobalRegionScheduler— more reliable than string-matching server name/version. The detection result is cached in a static final boolean at class load time.TaskHandle
Wraps either aBukkitTask(Paper/Spigot path) or a raw FoliaScheduledTaskobject (accessed via reflection for cancel). Thread-safe cancellation viaAtomicBoolean. TheisScheduled()method returns best-effort status.Vault Initialization Fix
Previously, ifinitializeVault()succeeded on attempt N but the timer tick was already queued,registerVaultDependentListeners()could fire twice. The fix adds aregistered[]boolean flag checked before each registration call, ensuring idempotency.Support and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.2.2aAllium v0.2.2aJun 21, 2026 Wiki: https://github.com/castledking/Allium/wikiHighlights
- Glow command — New/glowcommand for per-player colored glow outlines using PacketEvents virtual team packets. Supports all 16 vanilla colors, rainbow cycling, and per-color permissions. No scoreboard pollution — teams are sent directly to viewers and never registered on any Bukkit scoreboard. - Join/Quit message permissions — Permission-based join/quit message formats viaallium.join_message.<id>andallium.quit_message.<id>. Set custom formats with LuckPerms metadata. - Chat system rewrite —ChatMessageManager,FormatChatListener, andPacketChatTrackerImplsignificantly reworked for better PacketEvents integration and staff moderation features. - Flight restoration refactor —FlightRestorationrewritten for improved Folia compatibility and reliability. - Command management —CommandManagernow caches the server's known commands map with a 2-second TTL, eliminating repeated reflection calls.Helpcommand caches available commands per-player with a 3-second TTL. - TP companion teleportation — Pet/entity teleport-on-TP moved fromTPlistener to dedicatedTeleportBackListenerfor cleaner separation. - Inventory snapshot optimization —InventoryManager.saveSnapshot()simplified to run directly on the calling thread instead of throughSchedulerAdapter.run().New Commands
| Command | Description | Permission | |---------|-------------|------------| |/glow <color> [player]| Set glow color (e.g./glow red,/glow dark_aqua) |allium.glow+allium.glow.<color>| |/glow rainbow [player]| Enable rainbow cycling glow |allium.glow.rainbow| |/glow off [player]| Remove glow effect |allium.glow/allium.glow.others|New Permissions
| Permission | Description | Default | |------------|-------------|---------| |allium.glow| Base glow permission (grants all 16 color sub-permissions) | op | |allium.glow.<color>| Per-color (e.g.allium.glow.red,allium.glow.blue) | via parent | |allium.glow.rainbow| Rainbow glow mode | op | |allium.glow.others| Target other players | op | |allium.join_message.<id>| Custom join message format (value from LP metadata) | — | |allium.quit_message.<id>| Custom quit message format (value from LP metadata) | — | |allium.join_message.*| Wildcard — grants all join message formats | staff | |allium.quit_message.*| Wildcard — grants all quit message formats | staff |New Config
join-and-quit-messages: join: enabled: true format: "&e%name% &7has joined the server" quit: enabled: true format: "&e%name% &7has left the server" broadcast-scope: server # server, world, or globalSetting Custom Join/Quit Messages with LuckPerms
Set a permission with a metadata value on a player:
Supported placeholders:/lp user <player> meta set allium.join_message.vip.a "rift in the universe has spawned %player%" /lp user <player> meta set allium.quit_message.vip.a "%player% has left through a rift"%name%(display name, respects /nick),%player%(real name),%prefix%(Vault prefix),%suffix%(Vault suffix). The first matching permission (alphabetical by suffix) is used. If no permission metadata is found, the default config format is used.Technical Details
Glow
- Uses PacketEventsWrapperPlayServerTeamswithTeamMode.CREATE(includes both team info and player entries in one packet) -TeamMode.UPDATEfor rainbow color changes,TeamMode.REMOVEfor cleanup - Packets sent to all online players including the target (the client needs to receive the team packet to render the colored outline) - NoBukkit.getScoreboardManager()usage — completely virtual - Folia-safe: no region-scheduled tasks neededJoin/Quit Messages
- Handled via standardPlayerJoinEvent/PlayerQuitEvent(main thread on both Bukkit and Folia) - Permission metadata read through Vault Chat'sgetPlayerInfoString()- Falls back to default config format if no permission match found - Vanish check: vanished players don't show join/quit messagesChat System
-ChatMessageManagerrewritten with improved message storage and retrieval -PacketChatTrackerImplupdated for better PacketEvents 2.11.1 compatibility -FormatChatListenersignificantly expanded with new formatting optionsSchedulerAdapter
- All scheduled tasks route throughSchedulerAdapterwhich auto-detects Folia - Folia path: usesEntitySchedulerfor entity-specific tasks,GlobalRegionSchedulerfor global tasks - Bukkit path: uses standardBukkitScheduler- No rawBukkit.getScheduler()calls in new codeFiles Changed (22)
| File | Change | |------|--------| |PluginStart.java| Glow command + listener registration, glow field | |Glow.java| New — /glow command with PacketEvents team packets | |GlowListener.java| New — cleanup on player quit | |JoinQuitMessages.java| New — permission-based join/quit messages | |ChatMessageManager.java| Rewritten — message storage, tracking, moderation | |FormatChatListener.java| Major rewrite — chat formatting, hover, click events | |PacketChatTrackerImpl.java| Major rewrite — PacketEvents 2.11.1 integration | |FlightRestoration.java| Major rewrite — Folia-safe flight state management | |CommandManager.java| Known commands caching,getCachedKnownCommands()API | |Help.java| Available commands caching, usesCommandManagercache | | `TeleportBackListener.java** | Extracted from TP — pet/entity companion teleport | |TP.java| Removed inline teleport listener, now delegates | |InventoryManager.java| Simplified snapshot saving, removed SchedulerAdapter wrapper | |AlliumPlaceholder.java| Import cleanup | |GeneralPlaceholder.java| Import cleanup | |TFlyManager.java| Import cleanup, formatting | |SpawnerChangerManager.java| Minor cleanup | |config.yml| Newjoin-and-quit-messagessection | |lang.yml| Newglow.*message keys | |plugin.yml| Newglowcommand, permission definitions | |pom.xml| Paper version 1.21.8 |Support and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.2.1aAllium v0.2.1aJun 1, 2026 Wiki: https://github.com/castledking/Allium/wikiHighlights
- CrowBar same-world payloads — CrowBar player data is now built per recipient and only includes players in the recipient's current world. - CrowBar quit refresh — Sends an immediate snapshot when a player quits so CrowBar clients can remove stale dots and name tags without waiting for expiry. - CrowBar join/world snapshots — Sends delayed targeted snapshots after joins and world changes so reconnecting CrowBar clients get nearby player positions before movement updates. - NPC filtering — Citizens NPCs are excluded from CrowBar player-data payloads.Support and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.2.0aAllium v0.2.0aJun 1, 2026 Allium v0.2.0a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- CrowBarDataSender NPC Filter — Prevents NPCs from Citizens from having their data sent to CrowBarSupport and Feedback
Issues: https://github.com/castledking/Allium/issues Wiki: https://github.com/castledking/Allium/wiki Discord: https://discord.com/invite/pCKdCX6nYr Website: https://castled.codes/0.1.9aAllium v0.1.9aMay 31, 2026 Wiki: https://github.com/castledking/Allium/wikiHighlights
- Citizens NPC body visibility fixed — Allium no longer strips unlistedPLAYER_INFO_UPDATEentries for unknown player UUIDs. Citizens uses those packets to send player-NPC profile/skin data while keeping NPCs out of the tab list; removing them made the client fail to render NPC bodies. NPCs are also exempted from PartyManager body visibility hiding. - CrowBarDataSender UUID fix — Fixed a bug where the loop variableuuid(a String) was being used instead ofplayer.getUniqueId(), causing all CrowBar clients to receive the same UUID for every player entry.What's Fixed
- Citizens NPC bodies no longer invisible — The confirmed fix is in the PacketEvents tab-list interceptor: unknown/unlisted player-info entries now pass through unchanged instead of being removed. This preserves Citizens' spawn/profile handshake for player-type NPCs. Allium also detects Citizens NPC metadata live and skipshidePlayer()visibility management for those NPC entities. - CrowBarDataSender player UUID — Each player object now correctly callsplayer.getUniqueId().toString(). Previously, a strayuuidvariable from an unrelated context was serialized for every entry, meaning all players appeared as the same entity on the CrowBar locator bar.Internal
-.release/discord-updater-webhook.yml—message_idupdated for the current Discord release post. -.release/src/spigot-release.js— Modrinth download link corrected.Requirements
- Minecraft 1.21.11 - Fabric Loader 0.18.4 or newer - Fabric API 0.141.4 or newer - Java 210.1.8aAllium v0.1.8aMay 31, 2026 Allium v0.1.8
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Citizens NPC locator bar suppression — Allium now setsWAYPOINT_RECEIVE_RANGE = -1on all Citizens NPCs at server startup, preventing them from appearing on the vanilla locator bar without affecting NPC body visibility. - All NPC-specific handling code removed — All hand-rolled NPC filtering code has been stripped from PartyManager and CrowBarDataSender since Citizens API handles suppression natively. - Fly permission fix —/flynow accepts bothallium.flyandallium.tflypermissions, fixing a regression where players with the old permission could not use the command. - Flight restoration on cross-world teleport — Flight state is now captured before teleport and properly restored when the tfy module is active, preventing flight loss when changing worlds via portals or commands.Citizens NPC Waypoint Suppression
- AddedsetupCitizensNpcWaypointRange()in PluginStart that runs 5 seconds after server startup. - Uses reflection to callCitizensAPI.getNPCRegistry()(no compile dependency on Citizens). - Iterates all NPCs and setsAttribute.WAYPOINT_RECEIVE_RANGE = -1on spawned Player-type entities. - NPC bodies remain fully visible — the attribute only suppresses the locator bar waypoint on the client side. - Gracefully handles absence of Citizens plugin with a debug log.Removed NPC Handling Code
- PartyManager: RemovedisCitizensNpcUuid()method. NPCs are no longer special-cased inupdatePlayerVisibilityorhidePlayerAndRefreshTab. - CrowBarDataSender: Removed NPC UUID filter andisCitizensNpcUuid()method. NPC data is now sent to CrowBar clients like any other player (CrowBar client also had its own NPC filtering stripped).Fly Permission Fix
-CommandManager.hasPermissionForCommand()now checks bothallium.flyandallium.tflyfor the/flycommand. - Removed the special-case ingetDerivedPluginCommandPermission()that mappedallium.flytoallium.tfly, since the check now happens at the permission evaluation level instead. - Fixes a regression where players with onlyallium.fly(the documented permission) would get "You don't have permission" when running/fly.Flight Restoration (World Teleport)
- AddedPlayerTeleportEventhandler to captureallowFlightstate before cross-world teleport. -onPlayerChangedWorldnow uses the pre-teleport state for the allow-flight check, fixing false restorations when the player was not flying before the world change. - Added re-check that tfy is still enabled before restoring flight, preventing stale restores if the player toggled tfy off during the world transition.Config And Dependencies
- VaultUnlockedAPI changed fromcompile(shaded) toprovidedscope — the actual plugin's classes are used at runtime, reducing JAR size. - Version bumped to0.1.8a.Internal
- All Citizens/ZNPCs NPC filtering consolidated into a singlesetupCitizensNpcWaypointRange()reflection call, eliminating distributed UUID-pattern checks across PartyManager and CrowBarDataSender.0.1.7aAllium v0.1.7aMay 28, 2026 Allium v0.1.7
Wiki: https://github.com/castledking/Allium/wikiHighlights
- CrowBar mod companion - Allium now sends player position data to CrowBar client mod for enhanced locator bar features, including team colors and hidden player tracking. - Permission-gated CrowBar data - CrowBar player data packets are only sent to players with theallium.crowbarpermission. - Gradient name pause control - Animated gradient names can now be paused programmatically. - WorldEdit integration - Added soft-dependency support for FastAsyncWorldEdit and WorldEdit.CrowBar Mod Companion
- AddedCrowBarDataSenderto broadcast player position data to CrowBar client mod. - Data includes UUID, position, sneaking status, vanish status, carved pumpkin status, and team color. - Packets are sent every 5 ticks (250ms) to connected clients. - Only players with theallium.crowbarpermission receive CrowBar data packets. - Citizens2 and ZNPCsPlus NPCs are automatically filtered from CrowBar data. - Team colors are extracted from scoreboard teams and included in the payload.Animated Gradient Names
- AddedsetPaused(boolean)andisPaused()methods toGradientNameManager. - Gradient phase advancement now respects the paused state. - Allows external control over gradient animation for performance or aesthetic reasons.TAB And NPC Filtering
- Improved TAB integration for far-away players and NPCs. - PacketEvents tab add paths now actively re-add missing entries. - TAB reflection is used where available for better skin/ping/format matching. - Citizens2 NPCs are now filtered from TAB updates more reliably. - Added NPC removal logic to prevent synthetic tab entries for Citizens2 NPCs.Config And Permissions
- Addedallium.crowbarpermission for CrowBar mod companion access. - Added FastAsyncWorldEdit and WorldEdit to soft-dependencies. - Added FastAsyncWorldEdit and WorldEdit to load-after list.Internal
- Package name changes completed fromnet.survivalfun.coretocodes.castled.allium. - Improved TAB packet handling for better performance and reliability.0.1.6aAllium v0.1.6aMay 17, 2026 Allium v0.1.6a
Wiki: https://github.com/castledking/Allium/wikiHighlights
- Chat channels are now split between global and local -/localand/ltoggle radius chat, while/globaland/greturn players to global chat. - Staff locator mode -/locatorbarnow toggles a staff mode that can show every visible player on the party locator bar, regardless of party radius. - ModGuard - new configurable client-mod detection framework with brand, plugin-channel, Forge/FML handshake, evidence logs, alerts, kicks, bypasses, and optional translation probes. - Animated gradient names -%gradientdisplayname%can now be converted into MiniMessage gradient phase output for players withallium.gradientname. - Item Renamer - new single-use custom item that grants temporary/renameaccess and is consumed after a successful rename. - Safer End Gateway teleports - gateway/ender-pearl teleports near the main End island now search for a safe same-level landing instead of relying on vanilla highest-block behavior. - Release automation - new GitHub Actions and.releasetooling for Discord, Modrinth, and Spigot release publishing.Chat Channels
- Added the dedicated local chat channel config atchat.channels.local-channel. - Local chat defaults to a 100 block radius. - Local chat defaults to&8[&eLocal&8] <prefix> &a%allium_nickname%&f: &f<message>. - Local chat is excluded from Discord relay by default. - Global channel aliases are now onlygandglobal;localandlno longer point at global chat. -/localand/ltoggle between local chat and the default/global write channel. -/globaland/galways switch the player back to the default/global write channel. -/channel join <channel> [read|write]still supports explicit read/write channel selection. - Channel state continues to save per player inplayer-channels.yml.DiscordSRV Integration
- Discord inbound messages are routed through Allium's channel system. - Discord messages now use Allium channel formats only for explicitly configured Allium channel formats. - Default or unmapped DiscordSRV channels fall back to DiscordSRV's native message format. - Discord role alias, role color, username, effective name, channel, reply snippet, and message fields now pass through Discord emoji parsing. - Discord inbound messages are mirrored to console because Allium cancels DiscordSRV's normal preprocess/postprocess event handling. - Outbound chat loop detection now tracks local/staff/shortcut context so Allium avoids relaying duplicate chat events. - Local chat is intentionally blocked from Discord relay whenexclude-from-discordis true.Party And Locator Bar
- Added staff locator mode for players withallium.party.staff. -/locatorbarwith no arguments now toggles staff locator mode. - Added/locatorbar help. - Added/locatorbaraliases:/partybar,/lbar, and/pbar. - Addedallium.locatorbar.helpfor viewing locator help. - Staff locator mode lets staff see all non-vanished players on the locator bar, regardless of normal visibility distance. - Locator bar show/hide/reset flows remain available for temporary manual visibility overrides. - Party locator visibility updates keep the existing move throttling to reduce TAB packet pressure.TAB And Far-Away Player Entries
- Improved TAB integration for players Allium hides in the world but keeps visible in tab. - PacketEvents tab add paths now actively re-add missing entries instead of silently no-oping. - TAB reflection is used where available so synthetic tab entries better match TAB-managed skins, ping, game mode, and format. - PacketEvents fallback add packets are still available when TAB reflection cannot add an entry. - TAB group formatting can be read from TAB'sconfig/groups.ymlwhen placeholders are unavailable. - Placeholder fallbacks now reject unresolved%tab_...%strings instead of showing raw placeholder text in tab. - Far-away animated gradient targets are tracked and refreshed withUPDATE_DISPLAY_NAME. - Animated far-away tab entries use the same animated name builder as the normal tab display name.Animated Gradient Names
- AddedGradientNameManager. - Players withallium.gradientnamecan have%gradientdisplayname%rewritten into MiniMessage<gradient:...:phase>output. - The manager extracts first and last colors from existing gradient display names. - Supported color input includes legacy&/section colors,&#RRGGBB, section hex,&x/section-x hex, and MiniMessage hex tags. - The middle named color is chosen from the nearest general color name for MiniMessage's gradient tag. -#phase-mm-g#and#-phase-mm-g#are updated every tick. - The player list name is refreshed every tick for animated gradient players. - TAB prefix and suffix are preserved when Allium builds the animated tab display name. - Prefix/name spacing is normalized so TAB prefixes do not collapse directly into the player name. - Added PlaceholderAPI outputs: -%allium_gradientdisplayname%-%allium_phase-mm-g%-%allium_-phase-mm-g%-%allium_phase_mm_g%-%allium_negative_phase_mm_g%- Addedchat.minimessage.gradient.animationfor animated gradient phase permission checks.Nicknames
- Added/editnickfor editing other players' nicknames through the nickname GUI. - Nicknames now persist more reliably when TAB is installed. - Allium now clears plugin-set tab overrides when TAB owns tab formatting. - Nicknames are blocked when they match an existing online or offline player name. - Addednickname.sync-to-essentials, enabled by default. - When EssentialsX is present, Allium syncs nickname set/reset/restore into Essentials so commands such as/whoissee the same nickname. - Nickname placeholders can read stored offline nicknames by UUID.ModGuard
- Addedplugins/Allium/modguard/config.ymlfrom bundledsrc/main/resources/modguard/config.yml. - Legacymodguardsettings from the rootAllium/config.ymlare migrated intomodguard/config.yml. - Added blacklist/whitelist detection mode. - Added configurable join-time checks withcheck-on-joinandcheck-delay-ticks. - Added configurable punishment mode withkick-on-detection. - Addedmodguard.bypassto skip checks. - Addedmodguard.notifyfor staff notifications. - Addedallium.alertssupport for staff alert delivery. - Added/core alerts modguard [on|off|toggle]. - Added per-player ModGuard evidence logs atplugins/Allium/modguard/logs/{uuid}.yml. - Evidence logs are only created once a player gets a hit, then later hits append to the same UUID file. - Evidence entries include session, timestamp, method, check, raw value, confidence, action, and whether punishment happened. - Brand checks listen forminecraft:brand. - Channel checks inspect client plugin channel registrations. - Forge/FML checks listen onfml:handshake,fml:login,forge:handshake, andforge:login. - Matching supports case-sensitive and regex modes. - Default banned mod patterns include Freecam, Inventory Profiles Next variants, Tweakeroo, Litematica, MiniHUD, Meteor, Wurst, Vape, LiquidBounce, Aristois, Baritone, and related client/mod ids. - Default allowed mod examples for whitelist mode include OptiFabric, Sodium, Lithium, Phosphor, Iris, Fabric API, and Mod Menu.Translation Probe Detection
- Added optional PacketEvents translation-key probing undermodguard/config.yml. - Translation probes are disabled by default. - Probe checks can define translation keys, expected resolved text, action, timeout, fallback prefix, and whether kicks require corroboration. - The default sample check targetskey.meteorclient.openguias an alert-only Meteor probe. - Probe hits feed into ModGuard alerts and evidence logs. - Probe defaults are intentionally alert-based because resource packs can define translation keys and cause false positives.Security Alerts
- Added ModGuard as an individual s
Similar projects
Plugin →VeinMiner
Mine the whole vein on mining a single ore/block. Make the tedious mining experience to something satisfying and fun! FTB Ultimine alternative
99.3MSueta
Simple Voice Chat
A working voice chat in Minecraft!
76.8MSueta
Chunky
Pre-generates chunks, quickly and efficiently
18.4MSueta
VeinMiner Enchantment
Veinminer Addon - Adds vineminer enchantment to enchanting tables. Only tools with the enchantment can veinmine
18MSueta
JourneyMap
Real-time map used for mapping in-game or your browser as you explore.
14.7MSueta
WorldEdit
A Minecraft Map Editor... that runs in-game! With selections, schematics, copy and paste, brushes, and scripting. Use it in creative, or use it temporarily in survival.
11.6MSueta