Sueta

Allium

0.2.19a

A modern, secure Essentials solution

·Plugin
  1. 0.2.19alatestAllium v0.2.19ayesterday

    Allium v0.2.19a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 updated tradingcards/config.yml and tradingcards/boosts.yml for the revised tier, signature, and bonus-slot settings. - CraftEngine is optional. Crop stages retain the configured fallback behavior when it is not installed.
    4 downloads
  2. 0.2.18aAllium v0.2.18a3 weeks ago

    Allium v0.2.18a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    Two large features, both developed since the last release and both landing here for the first time. - The kitchen — A new harvest/kitchen.yml adds 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 new card slot 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.
    bags:
      flour_bag:
        item: nexo:flour_bag
        stores: nexo:flour
        capacity: 256
        lore: "<gray>Flour: <white><amount></white>/<capacity>"
    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.
    kneading:
      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 }
    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. Listing CAULDRON with water.required above 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 via skip-steps: [4], since neither has a lattice:
    pies:
      types:
        blackberry:
          filling: [nexo:blackberries, nexo:blackberries_silver_star, nexo:blackberries_golden_star]
        chocolate:
          filling: minecraft:cocoa_beans
          skip-steps: [4]
    A pie is an ItemDisplay showing the current model plus an Interaction hitbox 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:
    furnaces:
      stations:
        kitchen_stove:
          title: "Stove"
          type: FURNACE        # FURNACE | SMOKER | BLAST_FURNACE
          speed: 1.0
    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, as "water,flour" | | Bag fill level | Item d
    51 downloads
  3. 0.2.17aAllium v0.2.17aSep 21, 2026

    Allium v0.2.17a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Allium loads and runs with no other plugins installed — The plugin previously died at load with NoClassDefFoundError: com/github/retrooper/packetevents/event/PacketListenerCommon because the main class named a PacketEvents type directly, and the JVM verifies that class before any "is PacketEvents installed?" check can run. Registration moved behind PacketEventsLoader, and three chat listeners that Bukkit was silently skipping on DiscordSRV-less servers now register normally. Verified on a clean Paper 26.3 server whose plugins/ folder contains nothing but Allium.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 set enabled: false) to stop that head dropping, or add a mob by giving it a sound and texture. 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 (default true) removes the datapack's head for any mob in spawner_heads.yml, leaving exactly one source. Set it to false and 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_plushie and 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 by discord-moderation.staff-role-id and a ban-exempt-groups list that refuses to ban owners/admins. - Custom unknown-command helper text — hide.yml gained unknown-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 reload left 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 | PluginStart called PacketEvents.getAPI()...registerListener(...). The JVM verifies the whole main class when Paper loads it, and the verifier must resolve PacketListenerCommon to check the argument. | Registration moved into PacketEventsLoader.registerListener(Object). PluginStart no 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, SpamBlockerManager and ChatFilterManager all 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 to Object with an internal cast. | | SpectatorTeleport always disabled | It reflectively loaded net.survivalfun.core.commands.NV, a package name from before the rename, and failed on every server. | Constructs NV directly. | 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.
    spawner_heads.yml
    The 15 head drops moved out of hardcoded Java into plugins/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..."
    sound doubles as the head's identity: Allium reads the note_block_sound component back to decide which mob a head belongs to, and every configured sound is registered with MobHeadRegistry at load, so a mob added here works for spawner crafting and plushies without a code change. With override-moremobs-head-drop: true, an EntityDeathEvent for 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's note_block_sound, but the datapack borrows sounds: mooshroom uses entity.cow.ambient, cave spider uses entity.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 the item_name the datapack stamps on them ("Red Mooshroom Head", "Cave Spider Head"), generated into spawnercraft/more-mob-heads-names.txt (210 names across 84 mobs). Allium's own heads set no item_name and still resolve through their sound.
    Plushie crafting
    PlushieCraftListener watches 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 and spawnercraft.plushies is 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 no EntityDeathEvent is ever fired. Allium now snapshots the stack size on a lethal hit (priority HIGH) and confirms at MONITOR that 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 flipping RAW_EVENTS on DiscordSRV's live JDA session; a context-menu click reaches Disco
    21 downloads
  4. 0.2.16aAllium v0.2.16aSep 10, 2026

    Allium v0.2.16a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - `/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: doDaylightCycle is 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 shared PlayerMatcher now 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, a key.forward control 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's AlliumVisibilityBridge over reflection, so vanishing and party-only isolation survive Essentials' own visibility decisions instead of being undecided. - Offline home limits resolve through LuckPerms — HomeLimits can now answer how many homes an offline player's permissions grant, so /core sethomes no 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, frozen
    TimePauseManager owns the feature. Pausing a world captures the current tick and stores it; from then on two layers keep the clock pinned: | Layer | Mechanism | |---|---| | Gamerule | doDaylightCycle is set to false for the duration of the pause, so the vanilla tick loop cannot advance the clock. Unpausing restores it to true. | | Re-assertion | A 1-tick repeating task calls setTime(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 new paused_worlds database table (world primary key, paused_tick, paused_by, paused_at); storage migrates the database from version 8 to 9. On startup restorePersisted() replays every recorded pause, and a WorldLoadEvent hook 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 is allium.time.pause (default: op). /time usage strings now advertise the pause subcommand.
    Essentials-style player matching everywhere
    PlayerMatcher centralizes the resolution that commands previously did with bare Bukkit.getPlayer. It mirrors Essentials' matchUser order: 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, so j prefers joe over joey) 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.tabComplete returns real in-game names sorted best-first, with nickname-only matches ranked after direct name matches, and also respect visibility. The roll-out replaced Bukkit.getPlayer/getServer().getPlayer/tab-complete filtering in /tp (every path), /msg and mail recipients, /seen, /fly, /god, /balance, /cheque, /home, /locator bar show|hide|reset, and /core (restore, dialog show, sethomes, homebyplayer). /seen also dropped its player-only restriction: it now runs from console and reports to CommandSender instead of a Player, with sounds/particles skipped for console users.
    Anti-bot
    AntiBotListener tracks players currently in their first-ever session (from PlayerJoinEvent when hasPlayedBefore() is false). If their very first chat command is one of anti-bot.commands-to-punish (default plugins, pl, version), the command is cancelled and every line of anti-bot.punishment-command is dispatched from console with %player% substituted:
    anti-bot:
      commands-to-punish:
        - plugins
        - pl
        - version
      punishment-command:
        - "ban %player% -s [SC] Bots aren't allowed on this server"
    Root-command extraction strips the leading / and any namespace: prefix, so minecraft:plugins triggers too. A player holding allium.antibot.bypass is 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 to CHECKS_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 vanilla key.forward keybind. If it comes back as the raw key.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-check mode: in modguard/config.yml): | Mode | Hit rule | |---|---| | translate | Resolved ≠ fallback prefix → hit. Exact echo of the raw key → protected (the client is not confirming). An expected list 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 as key.forwardX-style padding). isKeyPlusLetter treats key + exactly one letter as a clean echo, which removes the WaylandCraft-style false positive while still catching real keybind resolutions. - Layer 1 expanded. banned-mods now covers the hackops client ids (Meteor, Wurst, LiquidBounce, Autism, Baritone, ...), the CheckHacks list (Freecam
    18 downloads
  5. 0.2.15aAllium v0.2.15aAug 21, 2026

    Allium v0.2.15a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - LiquidBounce's plugin-enumeration probe now times out — Running .serverinfo Plugins against an Allium server produces Plugin 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/rewritten CommandSuggestionsListener intercepts the raw TAB_COMPLETE request/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, because pluginname:command entries are removed wholesale. - The packet-level protection is independent of the existing command lists — With both remove-unsafe-commands disabled and command-suggestion-filters emptied, 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 at ServerObserver.captureCommandSuggestions()) does not rely on /plugins. It sends:
    ServerboundCommandSuggestionPacket(completionId, "/")
    
        → server-side Brigadier completion for the root node
    
        → ClientboundCommandSuggestionsPacket(match[])
    
        → extract every suggestion containing ":"
    
        → "pluginname:command" ⇒ pluginname
    This bypasses DECLARE_COMMANDS filtering 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 that TabCompleteEvent/AsyncTabCompleteEvent handlers such as Allium's CommandManager.onTabComplete can 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

    CommandSuggestionsListener (packet level, rewritten)
    The listener enforces the same visibility policy at the protocol boundary: | Behavior | Detail | |---|---| | Request tracking | Incoming Play.Client.TAB_COMPLETE texts are cached by transaction id (bounded map, TTL pruned). | | Untracked responses cancelled | A Play.Server.TAB_COMPLETE whose 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 receives ClientboundCommandSuggestionsPacket and the enumeration probe times out. | | Namespaced stripping | Every match containing : is removed unless listed in allow-namespaced-suggestions. This prevents jbt:mob_bag and similar identifiers from leaking through suggestions. | | Prefix blocklist kept | command-suggestion-filters still applies as an additional layer on plain suggestions. | | Group rules enforced | Plain suggestions additionally pass through CommandManager.shouldAllowTabComplete(player, cmd) — the same hide.yml group 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. |
    hide.yml
    New settings under hide.settings: - cancel-bare-slash-request - cancel-unknown-suggestion-responses - allow-namespaced-suggestions Comments were updated to document the new packet-level protection. Existing keys remain unchanged and supported.
    Architecture note
    The command visibility policy remains in CommandManager; the PacketEvents listeners remain thin enforcement points. DeclareCommandsListener still reads unsafe-commands-list rather 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 install green. - Black-box: LiquidBounce .serverinfo Plugins against the test server returns the timeout message instead of a plugin list. Confirmed with remove-unsafe-commands: false and an emptied command-suggestion-filters list, 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 as jbt:* are removed from suggestion responses unless explicitly allowlisted.
    29 downloads
  6. 0.2.14aAllium v0.2.14aAug 12, 2026

    Allium v0.2.14a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 the EntityDamageEvent handler, 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
    SlimeJump previously listened for fall damage, multiplied it by damageReductionFactor, 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 a damageReductionFactor that 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, the EntityDamageEvent is cancelled entirely — no damage is taken. - The bounce velocity is computed from the damage that would have been taken (damage × bounceMultiplier, capped at maxBounceVelocity, currently 0.2 and 2.0), preserving 80% of horizontal momentum. - The velocity is applied via SchedulerAdapter-free plugin.getServer().getScheduler().runTask(...) on the next tick, after the server's landing logic has finished, so the launch survives. The damageReductionFactor field and constructor parameter were removed; the registration in PluginStart and the default constructor were updated to match.
    Mob Disarmer: new targets
    The disarm gate used to require EntityEquipment with something on it, which excluded two mobs whose loot lives outside that system: | Target | What changed | |---|---| | Enderman | getCarriedBlock() is read, the block is cleared with setCarriedBlock(null), and it is dropped as an item with a guaranteed 100% chance — it was never in EntityEquipment, 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 when readyToBeSheared() 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 a mob_disarmer_pickup_blocked_until epoch-millis deadline into the mob's persistent data container when the Disarmer fires, and MobDisarmerListener cancels EntityPickupItemEvent for non-player entities at HIGHEST priority 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 vanilla CanPickUpLoot flag 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
    onPlayerDropItem filters 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 plain return, 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
    The PlayerCommandSendEvent filtering was split out of onCommandSend into two static, side-effect-free functions — shouldAllowTabComplete(List<CommandGroup>, String) and filterRootCommands(...) — with CommandGroup loosened to package-private so tests can construct real groups. The extracted code is behaviour-identical; the new CommandVisibilityRulesTest pins down the intended semantics: - A whitelist group's executable commands stay hidden from tab completion unless they are also listed in the group's tabCompletes — being allowed to run a command no longer implies it is visible. - tabCompletes can 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) | stripCarriedBlock clears 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.
    31 downloads
  7. 0.2.13aAllium v0.2.13aAug 11, 2026

    Allium v0.2.13a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Permission migration no longer fires on wildcard grants — The SFCore-era core.* → 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 every core.<node> question and had the entire permission list written onto their account as real allium.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: &#8C8F9B&lVIP was 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, &#RRGGBB and <gradient> freely. - One answer for how many homes a player gets — Numbered permissions and the /core sethomes override 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% reports unlimited instead 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, /spy now 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's core.* nodes forward: performPermissionMigration() walks every Vault group once at startup, and an inner PlayerPermissionMigrationListener walks each player on join until player_migration_completed is set. Both decide what to migrate with playerHas / groupHas. Those are permission checks. LuckPerms resolves them through wildcards, and Bukkit resolves an unregistered node — which every core.<node> is, since only allium.* is declared in plugin.yml — to its OP default. A player holding *, and any op, therefore answered true to all ~120 entries in permissionsToMigrate. The migration dutifully "moved" each one and called playerAdd for its allium. 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 about allium.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:
    Skipping permission migration for Steve (UUID: …): holds a wildcard/op grant that resolves every permission.
    Skipped players are deliberately not marked migrated_perms = TRUE. Once the wildcard is taken away, their real nodes migrate on a later join. Groups are checked the same way with groupHas and skipped individually, so one admin group carrying * no longer poisons the whole pass. Wildcard fan-out. A genuine core.* node was migrated to allium.* and then fell through into the per-node loop, where the not-yet-removed wildcard still answered true for core.feed, core.trash and everything else — producing the same spray of individual nodes. The per-node loop is now the else branch on both the player and group paths: a real core.* migrates on its own and nothing else is written. | Situation | Before | Now | |---|---|---| | Player holds * | Every allium.<node> written explicitly | Skipped, retried after the grant is removed | | Player is op | Every allium.<node> written explicitly | Skipped | | Player holds core.* | allium.* plus every individual node | allium.* only | | Player holds core.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 in items/impl/, register through CustomItemRegistry, 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_id and nexo: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-3 is 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. SwordToolDurabilityListener intercepts the final durability point: the stack is left one point from breaking, flagged worn, and given stick-level attack damage via ApiCompat.ATTACK_DAMAGE (added this release for the 1.21.3 GENERIC_ATTACK_DAMAGE rename). An anvil repair clears only that combat penalty — it does not restore charges, and a broken tool, the 25% roll, stays broken.
    Claim handcuffs
    HandcuffsItem was rewritten from a single PDC flag into four persistent states across two families: | Type | Id | Family | CMD | |---|--
    14 downloads
  8. 0.2.12aAllium v0.2.12aAug 4, 2026

    Allium v0.2.12a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - `/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 is UNKNOWN and Allium allows the command by default, which means its own no-permission message never appears. ExcellentQuests' /quests hit 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 #hello into 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 reported Message 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§x rather 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. /quests from 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 to UNKNOWN. 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 without debug-mode enabled. Two changes address that. Unresolved commands are announced. The first time a command resolves as UNKNOWN in a server session, CommandManager force-logs an ERROR to console regardless of debug-mode:
    Could not resolve a permission for /quests - allowing by default.
    Use /core hide fix quests <permission> to set one.
    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, so /plugin:quests and /quests warn 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 new ResolutionType.MANUAL_OVERRIDE and the player is checked against the recorded node with Player#hasPermission, meaning a denied command now produces Allium's standard no-permission message like any other. fix lives under /core hide for grouping only. It does not touch hide.yml; the whitelist/blacklist group system there is unrelated and unchanged. Alt accounts. -denyalts records a flag alongside the permission and reuses the alt detection already present in ConnectionManager and CommandManager — the Vault/LuckPerms alt group, 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 existing alt-account-restricted message. Storage. Overrides live in a new command_permission_overrides table 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, default nightcore | | set_by | UUID of the staff member who set it, null from console | | updated_at | Timestamp, refreshed on every write | Writes use MERGE ... KEY(command_label) so setting an existing override replaces it in place. CommandPermissionOverrideStore keeps an in-memory copy that the resolver reads on the command path; it is refreshed after every fix/remove and 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:
    format: "&8[&6Member&8] &6%allium_gradientdisplayname%&7: &f<message>"
    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 both GradientNameManager and the mirrored implementation in FormatChatListener: 1. The colors.isEmpty() fallback that read the player's Vault prefix and adopted its trailing color, along with the getPrefix Vault lookup it needed. 2. extractTrailingColor, which existed only to feed that fallback. 3. normalizeColor, whose #FFFFFF default 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 permission
    AlliumChannelManager sees each chat message twice: onPlayerChatEarly at LOWEST for tracking, and onPlayerChat at LOW, which chooses the destination channel. The # one-shot shortcut was gated on allium.staffchat in the first handler but had no permission check at all in the second. Because routing happens in the second handler, any player typing #hello had their message delivered to the staff channel, and the shortcut's addReadChannel call then subscribed them to staff chat so t
    22 downloads
  9. 0.2.11aAllium v0.2.11aAug 1, 2026

    Allium v0.2.11a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 into plugins/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 readable vanilla: 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, @e and @s plus common filters. /tphere accepts multiple names and selectors, while the new /there command 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, /delmsg reliably 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.0 through 1.0 in 0.1 steps, formatted with a US decimal separator and wrapped after 1.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:
    <gradient:#965CEA:#965CEA:blue:phase>Player</gradient>
    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 -0.1 to 0.0 was 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 #965CEA to blue. - 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's playerListName API 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 its listed state 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 under plugins/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 only a-z, 0-9, _ and -. A stored definition cannot take the ID of a built-in behavior item such as tree_axe, spawner_changer or item_renamer. Each generated file contains three layers: 1. snapshot — Bukkit's serialized ItemStack, 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 receiver permission, and max-give. Deleting one property from the vanilla: 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 only material: 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 preserving allium.admin as 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 resolves ci: items without constructing or reporting an invalid item twice.
    Harvest interaction, growth and regrowth
    harvest/config.yml now has crop-defaults:. Every setting can be over
    8 downloads
  10. 0.2.10aAllium v0.2.10aJul 20, 2026

    Allium v0.2.10a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 with NoClassDefFoundError: 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 throwing NoSuchFieldError at 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, every Material, 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 install leaves two jars in target/: | file | size | contents | |---|---|---| | Allium.jar | ~4.99 MB | shaded, HikariCP and H2 relocated under codes/castled/allium/libs/ | | Allium-0.2.9a.jar | ~2.04 MB | thin jar, 571 entries, no bundled libraries | The release workflow selected the upload with find target -name "Allium*.jar" ... | head -n1. That pattern matches both, and find returns them in directory order rather than any meaningful one, so the build published the thin jar. Its class files still reference the un-relocated com.zaxxer.hikari package, hence the failure inside Database.<init> the moment onEnable reached the database layer. The workflow now targets target/Allium.jar — the shade plugin's <finalName> — explicitly, and fails the build outright if that file is missing or if it does not contain codes/castled/allium/libs/hikari/HikariConfig.class. A silently thin jar cannot reach a release page again. Worth knowing for anyone building locally: pom.xml declares <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 take target/Allium.jar.
    NoSuchFieldError on newer-than-runtime registry constants
    Allium compiles against a much newer Paper API than the oldest server it supports at runtime (1.21.1). Every direct reference to a Material, Attribute, GameRule, EntityType or similar constant that only exists in a later version compiles cleanly and then throws NoSuchFieldError the 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/catch around the call does not help: NoSuchFieldError is an Error, not an Exception, and the existing handlers only caught Exception.) 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+). TreeAxeManager built its material sets from hard enum constants in static initialisers, so constructing TreeAxeListener crashed and took PluginStart.registerCommands — and the plugin — down. Now built through a materials(String...) helper that resolves each name with Material.matchMaterial and skips what the server does not know. This also matches how the rest of the class already worked (getValidGroundTypes has always switched on material names). - Locator bar — `GameRule.LOCATOR_BAR` (1.21.6+). Read in four places, including a repeating global task (retryTabListManagerInit) that logged Global task ... generated an exception every cycle, and in PartyManager player-visibility logic. All four now go through ApiCompat.isLocatorBarEnabled(world), which resolves the rule with GameRule.getByName once and reports true (the previous default) when the rule is absent. - *`/heal` and handcuffs — `Attribute.MAX_HEALTH` / `MOVEMENT_SPEED` (renamed from `GENERIC_ in 1.21.3).** ApiCompat resolves 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). CreativeManager now compares event.getEntityType().name() as a string, so it loads on any version; older servers deliver both kinds as POTION and it treats them as splash potions. - Waypoint attribute — `Attribute.WAYPOINT_TRANSMIT_RANGE` (1.21.6+). The Citizens NPC re-enforcement loop resolves it through ApiCompat and simply does nothing on servers that lack it. The shared ApiCompat helper (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 just Material: every getstatic org/bukkit/** reference was extracted from the compiled classes and diffed against the actual 1.21.1 paper-api. That covered Attribute, EntityType, Enchantment, Particle, Sound, Statistic, PotionEffectType, GameRule and ~25 others. Two categories of false positive were ruled out: - javac enum `switch` tables. A switch over an enum compiles to a synthetic $SwitchMap whose every entry is individually wrapped in a NoSuchFieldError handler (e.g. 2,154 such handlers in FireballExplosion), so unknown constants are skipped as the table is built. These getstatics are always immediately followed by ordinal() and were filtered out. - Mixed-case constants like ServicePriority.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.1 paper-api and every previously-crashing class (TreeAxeManager, HandcuffsItem, Heal, CreativeManager) was force-initialised successfully, with ApiCompat resolving MAX_HEALTH → GENERIC_MAX_HEALTH and WAYPOINT_TRANSMIT_RANGE → null as 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.
    25 downloads
  11. 0.2.9aAllium v0.2.9aJul 20, 2026

    Allium v0.2.9a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 matching minecraft.command.* node. - Class preloading — Allium now loads its own classes at startup, removing the NoClassDefFoundError failure 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 under plugins/Allium/harvest/:
    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 overlays
    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. mode: WEIGHTED_ONE draws a single number across the summed weights and walks entries until it lands, so one roll produces one winner. mode: INDEPENDENT rolls 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. Setting INDEPENDENT on a primary: table produces a reload warning naming the exact YAML path. Five loot modes ship: WEIGHTED_ONE, WEIGHTED_MULTIPLE_WITHOUT_REPLACEMENT, INDEPENDENT, GUARANTEED_ALL and SEQUENCE. 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 via footprint:, 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 are REAL_TIME (wall-clock, including while unloaded) or LOADED_TIME (frozen on unload). Real-time crops catch up when their chunk returns, bounded by maximum-catch-up-stages so 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, variation and soil.retain-for. There is no built-in concept of levels; each tier is a separate entry tuned on its own numbers. quality and variation multiply weights before the draw, so they shift the odds toward rare outcomes while it stays one roll. variation and soil only 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. yield deliberately does not touch additional: 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. Once lifetime elapses the block refuses new plantings until fed by a retainer; crops already growing are never destroyed. The record outlives the block for forget-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 — two 0.8 sprinklers give 0.64, not 0.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, and maximum-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.clickable pairs each crop with an invisible Interaction entity sized by interaction-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 in spawner-models.yml, with variants: selecting by stack size (highest qualifying minimum-stack wins). Stack size comes from a SpawnerProvider service; 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 one ItemDisplay positioned 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 by maximum-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 by growth-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.
    /minecraft: commands denied despite a granted permission node
    v0.2.7a introduced a command permission resolver pipeline to stop CommandManager overriding native Brigadier permissions. A player granted minecraft.command.kill through LuckPerms still got `You don't have permi
    8 downloads
  12. 0.2.8aAllium v0.2.8aJul 19, 2026

    Allium v0.2.8a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Deleted chat messages no longer reappear on second deletion — Fixed a thread-safety bug where PacketChatTrackerImpl captured resend packets as new chat history entries, causing deleted messages to resurface on subsequent /delmsg runs. Required a server restart to clear. - Shared `ChatMessage` instances across collections — playerMessages and playerChatHistory now reference the same object for packet-tracked messages, so setDeleted(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 from FormatChatListener, but a second bug remained: PacketChatTrackerImpl used a plain HashSet for playersBeingResent, and removed the player UUID immediately when resendChatHistoryToPlayer() 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, and SYSTEM_UUID as the sender. The next /delmsg marked the original entries deleted, but the replay-generated copies survived and reappeared on the next resend. After 3+ seconds the duplicate-time-window check in deleteMessage could no longer catch them. Fix (three layers): 1. `playersBeingResent`: HashSet → ConcurrentHashMap.newKeySet() (thread-safe). 2. `suppressPacketTracking`: New AtomicBoolean checked by all three packet handlers via shouldIgnoreTracking(). Set true for the duration of resendChatHistoryToAllPlayers() so no packets from the broadcast are tracked, regardless of per-player timing. 3. Delayed cleanup: Player UUID removal from playersBeingResent and guard release (resendAllInProgress, suppressPacketTracking) are delayed 1 tick via SchedulerAdapter.runLater, giving PacketEvents time to process the outgoing resend packets before tracking resumes. DeleteMsg.resendInProgress also releases after a 1-tick delay to prevent rapid successive /delmsg commands from scheduling overlapping resends.
    Shared ChatMessage Instances
    PacketChatTrackerImpl previously called chatMessageManager.storeMessage() (which creates one ChatMessage in playerMessages) and then constructed a separate ChatMessage object for trackMessageForPlayer() (which stores in playerChatHistory + globalChatHistory). Two different objects for the same logical message meant setDeleted(true) on one did not affect the other. New ChatMessageManager.storeMessageObject() returns the actual stored instance. PacketChatTrackerImpl now calls this method and passes the same reference to trackMessageForPlayer(), so both collections share one object. Setting deleted = true on it propagates everywhere. Additionally, ChatMessage.deleted is now volatile for cross-thread visibility (PacketEvents handlers may run off the main thread).
    Batched Chat Clear
    The clear phase previously sent 120-180 individual player.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 \n separators into a single Component.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 the DeleteMsg.clearChatFallback() path. Unused imports (java.util.Collections, java.util.concurrent.ThreadLocalRandom) were cleaned up from both files.
    22 downloads
  13. 0.2.7aAllium v0.2.7aJul 19, 2026

    Allium v0.2.7a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - New pluggable command permission resolution pipeline — The ad-hoc permission heuristics in CommandManager have been replaced by a dedicated codes.castled.allium.permissions.command package. A chain of framework-specific adapters (Paper BasicCommand, 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 in deleteMessage, 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 reference nexo: item models instead of oraxen:.

    Technical Details

    Command Permission Resolver (permissions/command)
    CommandManager previously resolved permissions through a single hasPermissionForCommand() method that had accumulated special cases (VanillaCommandWrapper class-name checks, Paper-internals safety nets, Allium-derived permissions). That logic is now factored into an adapter pipeline:
    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)
    Key contracts: - CommandPermissionAdapter.resolve() returns Optional.empty() when the adapter does not understand the command (next adapter is tried), or a PermissionResult with ResolutionType.UNKNOWN when it owns the command but cannot safely inspect its permission rule. - PermissionResult is a record carrying allowed, the ResolutionType that decided, the matchedPermission (evidence), and the resolved Command. A null command means the command is unknown → Allium's unknown-command message. - PermissionCandidates generates 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". - DefaultCommandPermissionResolver caches the registered-permission snapshot for 2 s and accepts additional adapters via its constructor — the extension point for CommandAPI, ACF, cloud, or other frameworks. CommandManager behavior changes: - Command resolution (including namespaced plugin:command fallback) now happens inside the resolver; resolveCommandForPermissionCheck, hasPermissionForCommand, and getDerivedPluginCommandPermission were removed (~130 lines of heuristics). - With debug-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: /fly passes with allium.fly or allium.tfly, /ping with allium.ping. - The direct net.milkbowl.vault.permission.Permission import was dropped; Vault permission setup remains reflective. Tests: CommandPermissionAdaptersTest (445 lines) and PermissionCandidatesTest (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
    createChatProxy bailed out with null when the Vault Chat class 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 from INFO to WARN and now explains the common cause: permission plugins like LuckPerms need chat: true in their config to provide chat services — Vault alone does not provide chat formatting.
    Chat Message Deletion Fix
    FormatChatListener used to build its own ChatMessageManager.ChatMessage and track it for every online player, in parallel with PacketChatTrackerImpl (packet capture). The listener-built entries were rendered from a different format template, so their plain text did not match the components stored by AlliumChannelManager. Content matching in deleteMessage then failed for those entries, they stayed undeleted, and the "deleted" message reappeared on resend. The listener-side tracking was removed; PacketChatTrackerImpl is now the single owner of per-player chat history. Also in FormatChatListener, hover components are now wrapped explicitly with HoverEvent.showText(...) instead of passing the component directly to hoverEvent().
    Nickname Handling
    - %allium_nickname% in chat format templates is now substituted with the player component (same treatment as <player>/<display_name>), in both FormatChatListener and AlliumChannelManager. - AlliumChannelManager no longer returns the raw stored nickname: an empty/null stored nickname falls back to player.getName(), and a present one is passed through getFormattedNickname(player, stored) so nickname formatting (colors, prefix rules) applies in channel messages.
    /back Cache Invalidation (TP)
    Teleporting a player now removes their entry from locationCache, so the next /back re-evaluates from lastLocation/the database instead of a stale cached location.
    Adventure Component Builders
    ComponentBuilder.build() calls were replaced with asComponent() in Help, Notes, and Warp (deprecation cleanup; behavior unchanged).
    Oraxen → Nexo
    TreeAxeItem, SpawnerChangerItem, and customitems.yml now reference nexo:tree_axe / nexo:spawner_changer item models instead of the oraxen: namespace.
    Build
    - Version bumped 0.2.6a → 0.2.7a. - Added org.junit.jupiter:junit-jupiter:5.12.2 (test scope) and maven-surefire-plugin:3.5.2.
    11 downloads
  14. 0.2.6aAllium v0.2.6aJul 7, 2026

    Allium v0.2.6a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Paper Brigadier compatibility — CommandManager now correctly handles commands registered via Paper's LifecycleEvents.COMMANDS (e.g., AbyssalLib's CommandBus, custom Brigadier dispatcher commands). These commands appear as VanillaCommandWrapper in the command map, whose testPermissionSilent() 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 — whether PluginCommand, VanillaCommandWrapper, or custom wrappers — Allium trusts the command's native permission system.

    Technical Details

    Problem
    Commands registered through Paper's modern LifecycleEvents.COMMANDS API (used by AbyssalLib's CommandBus, among others) do not go through Bukkit's plugin.yml command registration. Instead, they register LiteralArgumentBuilder<CommandSourceStack> nodes directly into Mojang's Brigadier CommandDispatcher. Paper wraps these dispatcher-only nodes in VanillaCommandWrapper (org.bukkit.craftbukkit.command.VanillaCommandWrapper) so they appear in SimpleCommandMap.knownCommands for Bukkit API compatibility. However, VanillaCommandWrapper.testPermissionSilent() does not reliably evaluate the underlying Brigadier node's requires() predicates, often returning false even when the player holds the correct permission. This caused Allium's CommandManager to 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 misses VanillaCommandWrapper in the general case since the wrapper extends Command/PluginCommand via different paths depending on the Paper version. 2. `PluginIdentifiableCommand` instanceof check — Paper's MinecraftCommandMap$CommandWrapper may implement PluginIdentifiableCommand with a null plugin, making the check useless. 3. `PluginCommand` instanceof check — some Paper wrapper variants extend PluginCommand directly, so negating PluginCommand doesn't reliably capture dispatcher-only commands.
    Actual Fix
    The fix uses a class-name-exact check for VanillaCommandWrapper as the first gate in hasPermissionForCommand():
    if (className.contains("VanillaCommandWrapper")) {
        return true;
    }
    This immediately allows all Brigadier-dispatcher-registered commands and defers permission evaluation to the server's native dispatch (which correctly evaluates the CommandNode.canUse() predicates at execution time). For PluginCommand instances, the revised logic is: 1. Explicit permission set — command.getPermission() is non-null → testPermissionSilent already checked it, allow. 2. Derived permission — only computed for commands whose PluginIdentifiableCommand.getPlugin() returns "Allium". The derived string is allium.<baseCommand>. 3. All other commands (PluginCommand from other plugins, vanilla commands with matching Bukkit wrappers) — trust testPermissionSilent.
    getPluginForCommand Fallback Fix
    The fallback in getPluginForCommand previously returned commandName.toLowerCase() when no plugin could be identified, causing the derived permission to be <commandName>.<commandName> (e.g., relique.relique). This is now null, which propagates through getDerivedPluginCommandPermission as null — 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/
    20 downloads
  15. 0.2.5aAllium v0.2.5aJul 6, 2026

    Allium v0.2.5a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Translation probe config corruption fixed — applyBundledDefaults no longer calls config.save(), preventing the old JAR's defaults from being written to disk on every restart. A new seedMissingProbeConfig() method writes the translation-probe section to disk only when the file actually lacks it (using contains(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 root enabled, so the probe registers even when the translation-probe section is entirely absent from the config file. - Stealthier probe — CloseWindow is sent immediately (no 2-tick delay, sign GUI never renders), and the sign position is Y - 30 below the player (out of view). Zero visual flicker. - `/allium modguard reload` — New command unregisters the probe, reloads the config from disk, and re-registers. Requires allium.admin.

    Bug Fixes

    | Bug | Root Cause | Fix | |-----|-----------|------| | Config file overwritten on every restart | applyBundledDefaults called config.save() with copyDefaults(true), writing old JAR defaults back to disk | Removed config.save() and copyDefaults(true) from applyBundledDefaults | | translation-probe section never persisted to disk | seedMissingProbeConfig used config.contains("translation-probe") which returned true from defaults set by applyBundledDefaults — even though the file lacked the section | Changed to config.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 | Added createDefaultChecks() with 16 hardcoded checks as final fallback | | Probe doesn't fire when translation-probe section is missing | registerTranslationProbe() and isEnabled() returned false for config.getBoolean("translation-probe.enabled", false) when the section was absent | Changed fallback to config.getBoolean("translation-probe.enabled", config.getBoolean("enabled", false)) | | Probe delay too slow (3s) and timeout too long (4s) | Config defaults for delay-ticks (60) and timeout-ticks (80) were unnecessarily conservative | Changed to 10 ticks (0.5s) and 30 ticks (1.5s) | | Sign GUI visible to player on join | CloseWindow was sent after a 2-tick delay, giving the client time to render the sign | CloseWindow sent 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 reload which calls ModGuardManager.reload() |

    Files Changed (4)

    | File | Change | |------|--------| | managers/security/ModGuardManager.java | applyBundledDefaults no longer saves to disk; added seedMissingProbeConfig() with contains(path, true) guard; added reload() static method; added /allium modguard reload dispatcher | | managers/security/ModGuardTranslationProbe.java | Added createDefaultChecks() (16 hardcoded checks); loadChecks() now cascades config → bundled → hardcoded; isEnabled() falls back to root enabled; CloseWindow immediate; sign Y -30; delay-ticks 10, timeout-ticks 30 | | modguard/config.yml | Cleaned up: removed debug, regex-matching, case-sensitive, banned-mods, allowed-mods; added translation-probe section with all 16 checks with require-corroboration-for-kick: false | | commands/Core.java | Added handleModGuardCommand() for /allium modguard reload |

    Technical Details

    Config Persistence Chain
    The old code had two bugs forming a corruption loop: 1. applyBundledDefaults called config.save() with the old JAR's bundled config (no translation-probe section), overwriting user changes on every restart 2. Even after removing config.save(), seedMissingProbeConfig checked config.contains("translation-probe") — but applyBundledDefaults had already set the NEW bundled resource as defaults, which DO contain the section. So contains() returned true from defaults and seedMissingProbeConfig returned early without persisting Fix: seedMissingProbeConfig now uses config.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/
    18 downloads
  16. 0.2.4aAllium v0.2.4aJun 30, 2026

    Allium v0.2.4a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Vault softdepend crash fixed — Allium no longer crashes at startup when Vault is absent. All Vault-typed fields and method signatures were changed to Object, with casts at call sites. VaultEconomyProvider is only created when Vault's Economy class is actually loadable. The FormatChatListener is skipped entirely when Vault Chat isn't available. - PacketEvents softdepend crash fixed — Allium no longer crashes at startup when PacketEvents is absent. The Glow command and GlowListener are guarded behind isPacketEventsAvailable(), and the glowCommand field was relaxed from Glow to Object to prevent class loading. - CrowBar 128-block radius limit removed — Added shouldBeVisibleIgnoreDistance to PartyManager so 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.updatePlayerVisibility now checks viewer.canSee(target) before calling showPlayer, 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 cause NoClassDefFoundError during class loading; new VaultEconomyProvider runs before Vault availability check | Changed Vault-typed fields/methods to Object; guarded VaultEconomyProvider creation; skipped FormatChatListener when vaultChat == null | | Plugin fails to enable without PacketEvents | Glow class references PacketEvents types in method signatures; new Glow(this) at unconditional line 1524 forces class loading | Guarded Glow/GlowListener creation behind isPacketEventsAvailable(); changed glowCommand field to Object | | Players beyond 128 blocks hidden on CrowBar | shouldBeVisible applies distance filtering which limits range to 128 blocks | Added shouldBeVisibleIgnoreDistance that skips distance check while keeping same-world and visibility overrides | | NPCs flash on locator bar every 5 ticks | updatePlayerVisibility calls showPlayer unconditionally every interval | Guarded showPlayer behind viewer.canSee(target) |

    Files Changed (8)

    | File | Change | |------|--------| | PluginStart.java | Changed Vault fields/methods to Object; guarded VaultEconomyProvider and FormatChatListener creation; guarded Glow/GlowListener behind isPacketEventsAvailable() | | commands/Glow.java | No structural change (guarded at call site) | | DB/PermissionCache.java | getVaultPermission returns Object | | listeners/security/CommandManager.java | vaultPermission field / setupVaultPermission now Object | | listeners/security/MaintenanceManager.java | perms field / constructor param now Object | | managers/core/PartyManager.java | Added shouldBeVisibleIgnoreDistance; canSee guard in updatePlayerVisibility | | packetevents/CrowBarDataSender.java | Uses shouldBeVisibleIgnoreDistance instead of shouldBeVisible | | listeners/security/ConnectionManager.java | vaultChat null guard in reloadChatFormatter |

    Technical Details

    Vault Class Loading Fix
    Paper 1.21+'s PluginClassLoader eagerly resolves type references during class definition. Having Vault types in field declarations or method return types causes NoClassDefFoundError even when the code path never executes. Fix: all Vault-typed fields and return types are now Object, with casts at every call site.
    PacketEvents Class Loading Fix
    Same mechanism as Vault — PacketEvents types in Glow's method signatures crash class loading. Glow is now only instantiated when isPacketEventsAvailable() 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/
    26 downloads
  17. 0.2.3aAllium v0.2.3aJun 29, 2026
    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 new SchedulerAdapter in the codes.castled.allium.scheduler package, which properly handles Canvas's regionized threading model. - New SchedulerAdapter — Complete rewrite of the scheduling layer. The old util/SchedulerAdapter.java (587 lines of reflection-heavy Folia detection) is replaced by a clean 426-line scheduler/SchedulerAdapter.java with a dedicated TaskHandle abstraction. The old class is kept as a deprecated thin wrapper delegating to the new one, ensuring backward compatibility. - Tab completion API — New AlliumTabCompletionsEvent + AlliumTabCompletionsListener that 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 where registerVaultDependentListeners() could be called multiple times if Vault initialized successfully and then the retry timer fired again. Added a registered boolean flag to ensure one-time registration. - FAWE removed — FastAsyncWorldEdit dependency and repositories dropped from pom.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-api replaced with io.canvasmc.canvas:canvas-api - pom.xml: <paper.version> bumped from 26.2.build.37-alpha to 26.2.build.835-alpha - pom.xml: Added canvasmc-releases and canvasmc-snapshots repositories - pom.xml: Removed FastAsyncWorldEdit-Core dependency - pom.xml: Removed IntellectualSites repositories - plugin.yml: Removed FastAsyncWorldEdit and WorldEdit from softdepend

    New 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 new codes.castled.allium.scheduler.SchedulerAdapter provides:
    // 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()
    All methods return codes.castled.allium.scheduler.TaskHandle which wraps both BukkitTask and Folia's ScheduledTask.

    Deprecated API

    codes.castled.allium.util.SchedulerAdapter is now a deprecated wrapper. All methods delegate to the new codes.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 uses Bukkit.getServer().getClass().getMethods() to find getGlobalRegionScheduler — 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 a BukkitTask (Paper/Spigot path) or a raw Folia ScheduledTask object (accessed via reflection for cancel). Thread-safe cancellation via AtomicBoolean. The isScheduled() method returns best-effort status.
    Vault Initialization Fix
    Previously, if initializeVault() succeeded on attempt N but the timer tick was already queued, registerVaultDependentListeners() could fire twice. The fix adds a registered[] 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/
    19 downloads
  18. 0.2.2aAllium v0.2.2aJun 21, 2026
    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Glow command — New /glow command 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 via allium.join_message.<id> and allium.quit_message.<id>. Set custom formats with LuckPerms metadata. - Chat system rewrite — ChatMessageManager, FormatChatListener, and PacketChatTrackerImpl significantly reworked for better PacketEvents integration and staff moderation features. - Flight restoration refactor — FlightRestoration rewritten for improved Folia compatibility and reliability. - Command management — CommandManager now caches the server's known commands map with a 2-second TTL, eliminating repeated reflection calls. Help command caches available commands per-player with a 3-second TTL. - TP companion teleportation — Pet/entity teleport-on-TP moved from TP listener to dedicated TeleportBackListener for cleaner separation. - Inventory snapshot optimization — InventoryManager.saveSnapshot() simplified to run directly on the calling thread instead of through SchedulerAdapter.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 global

    Setting Custom Join/Quit Messages with LuckPerms

    Set a permission with a metadata value on a player:
    /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"
    Supported placeholders: %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 PacketEvents WrapperPlayServerTeams with TeamMode.CREATE (includes both team info and player entries in one packet) - TeamMode.UPDATE for rainbow color changes, TeamMode.REMOVE for cleanup - Packets sent to all online players including the target (the client needs to receive the team packet to render the colored outline) - No Bukkit.getScoreboardManager() usage — completely virtual - Folia-safe: no region-scheduled tasks needed
    Join/Quit Messages
    - Handled via standard PlayerJoinEvent / PlayerQuitEvent (main thread on both Bukkit and Folia) - Permission metadata read through Vault Chat's getPlayerInfoString() - Falls back to default config format if no permission match found - Vanish check: vanished players don't show join/quit messages
    Chat System
    - ChatMessageManager rewritten with improved message storage and retrieval - PacketChatTrackerImpl updated for better PacketEvents 2.11.1 compatibility - FormatChatListener significantly expanded with new formatting options
    SchedulerAdapter
    - All scheduled tasks route through SchedulerAdapter which auto-detects Folia - Folia path: uses EntityScheduler for entity-specific tasks, GlobalRegionScheduler for global tasks - Bukkit path: uses standard BukkitScheduler - No raw Bukkit.getScheduler() calls in new code

    Files 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, uses CommandManager cache | | `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 | New join-and-quit-messages section | | lang.yml | New glow.* message keys | | plugin.yml | New glow command, 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/
    17 downloads
  19. 0.2.1aAllium v0.2.1aJun 1, 2026
    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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/
    42 downloads
  20. 0.2.0aAllium v0.2.0aJun 1, 2026

    Allium v0.2.0a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - CrowBarDataSender NPC Filter — Prevents NPCs from Citizens from having their data sent to CrowBar

    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/
    23 downloads
  21. 0.1.9aAllium v0.1.9aMay 31, 2026
    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Citizens NPC body visibility fixed — Allium no longer strips unlisted PLAYER_INFO_UPDATE entries 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 variable uuid (a String) was being used instead of player.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 skips hidePlayer() visibility management for those NPC entities. - CrowBarDataSender player UUID — Each player object now correctly calls player.getUniqueId().toString(). Previously, a stray uuid variable 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_id updated 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 21
    13 downloads
  22. 0.1.8aAllium v0.1.8aMay 31, 2026

    Allium v0.1.8

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Citizens NPC locator bar suppression — Allium now sets WAYPOINT_RECEIVE_RANGE = -1 on 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 — /fly now accepts both allium.fly and allium.tfly permissions, 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

    - Added setupCitizensNpcWaypointRange() in PluginStart that runs 5 seconds after server startup. - Uses reflection to call CitizensAPI.getNPCRegistry() (no compile dependency on Citizens). - Iterates all NPCs and sets Attribute.WAYPOINT_RECEIVE_RANGE = -1 on 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: Removed isCitizensNpcUuid() method. NPCs are no longer special-cased in updatePlayerVisibility or hidePlayerAndRefreshTab. - CrowBarDataSender: Removed NPC UUID filter and isCitizensNpcUuid() 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 both allium.fly and allium.tfly for the /fly command. - Removed the special-case in getDerivedPluginCommandPermission() that mapped allium.fly to allium.tfly, since the check now happens at the permission evaluation level instead. - Fixes a regression where players with only allium.fly (the documented permission) would get "You don't have permission" when running /fly.

    Flight Restoration (World Teleport)

    - Added PlayerTeleportEvent handler to capture allowFlight state before cross-world teleport. - onPlayerChangedWorld now 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 from compile (shaded) to provided scope — the actual plugin's classes are used at runtime, reducing JAR size. - Version bumped to 0.1.8a.

    Internal

    - All Citizens/ZNPCs NPC filtering consolidated into a single setupCitizensNpcWaypointRange() reflection call, eliminating distributed UUID-pattern checks across PartyManager and CrowBarDataSender.
    13 downloads
  23. 0.1.7aAllium v0.1.7aMay 28, 2026

    Allium v0.1.7

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - 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 the allium.crowbar permission. - 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

    - Added CrowBarDataSender to 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 the allium.crowbar permission 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

    - Added setPaused(boolean) and isPaused() methods to GradientNameManager. - 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

    - Added allium.crowbar permission 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 from net.survivalfun.core to codes.castled.allium. - Improved TAB packet handling for better performance and reliability.
    23 downloads
  24. 0.1.6aAllium v0.1.6aMay 17, 2026

    Allium v0.1.6a

    Wiki: https://github.com/castledking/Allium/wiki

    Highlights

    - Chat channels are now split between global and local - /local and /l toggle radius chat, while /global and /g return players to global chat. - Staff locator mode - /locatorbar now 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 with allium.gradientname. - Item Renamer - new single-use custom item that grants temporary /rename access 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 .release tooling for Discord, Modrinth, and Spigot release publishing.

    Chat Channels

    - Added the dedicated local chat channel config at chat.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 only g and global; local and l no longer point at global chat. - /local and /l toggle between local chat and the default/global write channel. - /global and /g always 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 in player-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 when exclude-from-discord is true.

    Party And Locator Bar

    - Added staff locator mode for players with allium.party.staff. - /locatorbar with no arguments now toggles staff locator mode. - Added /locatorbar help. - Added /locatorbar aliases: /partybar, /lbar, and /pbar. - Added allium.locatorbar.help for 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's config/groups.yml when 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 with UPDATE_DISPLAY_NAME. - Animated far-away tab entries use the same animated name builder as the normal tab display name.

    Animated Gradient Names

    - Added GradientNameManager. - Players with allium.gradientname can 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% - Added chat.minimessage.gradient.animation for animated gradient phase permission checks.

    Nicknames

    - Added /editnick for 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. - Added nickname.sync-to-essentials, enabled by default. - When EssentialsX is present, Allium syncs nickname set/reset/restore into Essentials so commands such as /whois see the same nickname. - Nickname placeholders can read stored offline nicknames by UUID.

    ModGuard

    - Added plugins/Allium/modguard/config.yml from bundled src/main/resources/modguard/config.yml. - Legacy modguard settings from the root Allium/config.yml are migrated into modguard/config.yml. - Added blacklist/whitelist detection mode. - Added configurable join-time checks with check-on-join and check-delay-ticks. - Added configurable punishment mode with kick-on-detection. - Added modguard.bypass to skip checks. - Added modguard.notify for staff notifications. - Added allium.alerts support for staff alert delivery. - Added /core alerts modguard [on|off|toggle]. - Added per-player ModGuard evidence logs at plugins/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 for minecraft:brand. - Channel checks inspect client plugin channel registrations. - Forge/FML checks listen on fml:handshake, fml:login, forge:handshake, and forge: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 under modguard/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 targets key.meteorclient.opengui as 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
    39 downloads

Similar projects

Plugin →