
Allium
A modern, secure Essentials solution
- Загрузки
- 257
- Подписчики
- 2
- Обновлён
- 21 августа 2026 г.
- Лицензия
- GPL-3.0-only
Опубликован 5 апреля 2026 г.
Allium is a modern Essentials-style Paper plugin built for servers that want one cohesive core instead of a pile of loosely connected utility plugins.
It combines everyday player commands, staff tooling, moderation systems, economy utilities, advanced chat handling, creative-mode restrictions, Discord integration, and custom world systems into a single package that is designed to feel practical on a real server.
If you run a survival server, a semi-vanilla community, or a staff-heavy admin environment and keep finding yourself thinking “this should already be built in”, Allium is aimed directly at that problem.
Why Allium
- One plugin for the core systems most servers actually need
- Strong admin and moderation tooling, not just player conveniences
- Modern integrations with Vault, PlaceholderAPI, LuckPerms, DiscordSRV, PacketEvents, Oraxen, and Floodgate
- Built around real server-owner workflows: hiding commands, creative auditing, chat cleanup, ticket/escalation style tooling, timed fly, vouchers, utility items, and more
Feature Highlights
Core player utilities
- Homes, bed homes, warps, spawn,
/back, offline location tracking, and full teleport request flow - Private messages, reply, mail, social spy, notes, and
/whois - Economy support with balances, payments, cheques, and XP vouchers
- Utility commands like
/heal,/feed,/god,/fly,/nv,/speed,/trash,/more,/itemdb,/skull
Advanced item and reward systems
- Flexible
/giveand/ihandling - Dialog-based
/rename,/lore, and nickname editing - Voucher items with redeem commands and weighted permission grants
- Cheques and XP bottles as redeemable item-backed value
- Spawner core items and spawner-crafting support
Staff and admin tooling
- Freeze, handcuffs, vanish, creative controls, invsee, ender chest access, and detailed player inspection
/delmsgchat moderation with resend support and Discord mirror cleanup- Command/plugin hiding via
hide.yml - Group-based hidden command inheritance with LuckPerms-aware parent support
- Escalation-ready admin workflows and offline state tracking
Chat system
- Configurable channel system with built-in global and staff chat
- Discord-linked channel routing
- Word/phrase filter with token, phrase, substring, and regex modes
- Spam blocker with repeated-message detection and normalization
- Hover/click formatting, placeholder support, and moderation overlays
Server protection and creative controls
- Creative Manager with per-action permissions for place, break, use, interact, drop, pickup, spawn, and inventory behavior
- Creative blacklists for blocks and entities
- Command hiding and tab completion filtering
- Anti-alt / security-oriented listener support
World and custom content systems
- Integrated custom ore generation for Oraxen ores
- Pregeneration command with ore placement support
- Oraxen custom smelting hooks
- Note block / custom block protection helpers
- Creeper explosion regeneration and related world safety logic
Integrations
Allium is designed to work well with common modern server stacks.
- Vault
- LuckPerms
- PlaceholderAPI
- DiscordSRV
- PacketEvents
- Oraxen
- Floodgate
- mcMMO
Most integrations are soft dependencies, so you can run a smaller setup and still use the rest of the plugin.
Good Fit For
- Survival and SMP servers that want a stronger core plugin
- Servers replacing a pile of Essentials-style utilities with one system
- Staff teams that need moderation, visibility control, and audit-friendly tools
- Servers using DiscordSRV, PlaceholderAPI, LuckPerms, and custom content plugins
Quick Examples
- Give staff their own live in-game + Discord-linked staff chat
- Hide
/plugins,/version, and sensitive tab-completes from normal players - Let players use temporary fly time without permanent fly permission
- Turn XP, money, or perks into redeemable items
- Run
/delmsgand clean up moderated chat quickly - Generate custom silver/galena ores directly in world generation
Installation Overview
- Install Allium on a Paper server.
- Add Vault and LuckPerms if you want economy/chat/group integration.
- Add PlaceholderAPI if you want placeholder-driven formatting.
- Add DiscordSRV if you want Discord-linked channels and staff relay.
- Add PacketEvents if you want the best chat resend/deletion behavior.
- Review
config.yml,lang.yml,hide.yml, and thechat/module files.
Documentation
For usage instructions, see the official Allium documentation.
Current Direction
Allium is actively growing beyond a traditional Essentials replacement. The current focus is on:
- better chat moderation and channel systems
- stronger admin workflows
- smoother Discord integration
- deeper custom content/world integration
- keeping one coherent core instead of fragmenting into many separate plugins
Ченджлог
0.2.15aРелиз26.1.1, 26.1.2, 26.2 · 21 августа 2026 г.
Allium v0.2.15a
Wiki: https://github.com/castledking/Allium/wiki
Highlights
LiquidBounce's plugin-enumeration probe now times out — Running
.serverinfo Pluginsagainst an Allium server producesPlugin detection timed out. It may be due to the server blocking the request.instead of a plugin list. This was verified black-box against a live LiquidBounce client.The command-suggestion protocol is now filtered at the packet level — A new/rewritten
CommandSuggestionsListenerintercepts the rawTAB_COMPLETErequest/response pair via PacketEvents and enforces Allium's visibility rules on the wire, where Bukkit events cannot reach.Every namespaced suggestion is stripped unless explicitly allowlisted — Unknown namespaces (
jbt:*,someplugin:*) can never leak regardless of any blocklist configuration, becausepluginname:commandentries are removed wholesale.The packet-level protection is independent of the existing command lists — With both
remove-unsafe-commandsdisabled andcommand-suggestion-filtersemptied, the black-box test still passes. The active protection is the packet-level bare-/response cancellation plus the namespaced-suggestion stripping rule.
Background: how the leak worked
LiquidBounce's primary stealth plugin-detection mechanism (verified in its source 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": 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": the same gap is reported following Paper's command API changes.
PaperMC/Velocity#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-requestcancel-unknown-suggestion-responsesallow-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 installgreen.Black-box: LiquidBounce
.serverinfo Pluginsagainst the test server returns the timeout message instead of a plugin list. Confirmed withremove-unsafe-commands: falseand an emptiedcommand-suggestion-filterslist, isolating the packet-level listener as the effective protection.Legitimate completion:
/he,/help, and/msg <partial>continue to provide normal completion for unprivileged players.Namespaced suggestions: Hidden namespaced entries such as
jbt:*are removed from suggestion responses unless explicitly allowlisted.
0.2.14aРелиз26.1.1, 26.1.2, 26.2 · 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
EntityDamageEventhandler, 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:
- 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.
- The damage reduction silently depended on a
damageReductionFactorthat 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
EntityDamageEventis cancelled entirely — no damage is taken. - The bounce velocity is computed from the damage that would have been taken (
damage × bounceMultiplier, capped atmaxBounceVelocity, currently 0.2 and 2.0), preserving 80% of horizontal momentum. - The velocity is applied via
SchedulerAdapter-freeplugin.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. tabCompletescan expose a command that is not in the execution list at all.- Blacklist tab rules hide roots independently of the command rules.
Verification
The Maven suite passes with 170 tests, up from 161. Four new test classes:
| Test | Coverage |
|---|---|
EndermanCarriedBlockTest (2) |
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.
0.2.13aРелиз26.1.1, 26.1.2, 26.2 · 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 everycore.<node>question and had the entire permission list written onto their account as realallium.feed,allium.trash, … nodes. Handing*to someone temporarily was enough to trigger it. Migration now probes with a node nobody can hold; when that comes back true, the account resolves everything and is left alone until the blanket grant is gone. Groups had the identical defect and got the same guard. - Mob Disarmer and Phantom Obliterator — Two charged Nexo-backed tools. The Disarmer strips a mob's held items and armour and rolls each piece against the mob's own vanilla drop chance; the Obliterator clears unnamed phantoms within 64 blocks. Both carry three charges in the item's PDC rather than in a per-player map, so the cooldown belongs to the tool and survives trading, storage and restarts.
- Claim handcuffs — A second handcuff family for landowners. Restraining an untrusted player inside your own claim starts a five-second countdown that ends in a GriefPrevention claim ban; dropping the cuffs cancels it, and so does the target leaving the claim. All GPExpansion access is reflective, so Allium still starts without it and works against either upstream GriefPrevention or GriefPrevention3D.
- Chat colours are parsed once, against permissions — Legacy codes and MiniMessage tags used to fight over the same message:
C8F9B&lVIPwas pushed through the MiniMessage parser, which printed the codes instead of applying them. Both systems now filter against their own permission sets and fold into a single MiniMessage string that is deserialized once, so one message can mix&a,&#RRGGBBand<gradient>freely. - One answer for how many homes a player gets — Numbered permissions and the
/core sethomesoverride disagreed depending on which code path asked. The effective limit is now the higher of the two everywhere,/core sethomes <player>shows the breakdown, and%allium_homes_max%reportsunlimitedinstead of a number nobody set. /seenshared IPs are symmetric — Shared accounts were matched against the target's current address only, so the moment either account changed IP they stopped listing each other. The lookup now walks the full IP history.- Reliability —
%allium_nickname%parses without PlaceholderAPI or Essentials, Essentials-only nicknames are imported on join,/spynow clears targeted spying along with global, spawner cores respond to right-clicking a block, and the ModGuard translation probe stops reporting a hit when the client echoes the raw key.
Technical Details
Permission migration and blanket grants
Two migrations carry SFCore'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 |
|---|---|---|---|
| Staff | handcuffs |
STAFF | 1012 |
| Staff, restraining | handcuffs_restrained |
STAFF | 1013 |
| Claim | claim_handcuffs |
CLAIM | 1014 |
| Claim, restraining | claim_handcuffs_restrained |
CLAIM | 1015 |
Type resolution reads Allium's id first, then Nexo's, then custom model data, and finally the legacy allium:handcuffs_item byte — cuffs from older builds become ordinary staff cuffs the first time they are rewritten. /core item give <player> claim_handcuffs issues the new family.
Staff cuffs now need a permission. allium.restrain (or allium.admin) is checked both when the rod is cast and again when the bobber lands; allium.handcuffs.resist still exempts a target. Claim cuffs need no Allium permission — the claim itself is the authority.
What claim cuffs check, through the reflective ClaimHandcuffBridge, before a restraint is allowed: the target stands inside a claim, the caster owns it or holds MANAGE trust, the target holds no trust of any kind, and the target is not already banned from it. Any failure sends the reason and nothing happens.
The countdown. A successful restraint blinds and slows both players, titles a five-second countdown at each of them, and pulses particles that tighten each second. It is abandoned if the restraint ends, if the caster drops the cuffs, or if the target leaves the claim; on completion the bridge is re-checked, the target is released and detached from the vehicle stack first, and only then is the ban written to GPExpansion's claim data store. If that write fails, the player is released and both sides are told.
plugin.yml gains GPExpansion and GriefPrevention as softdepends. Every GPX reference is reflective, so a server without them loses claim cuffs and nothing else.
Chat colour parsing
ChatColorParser is a new permission-aware parser shared by FormatChatListener and AlliumChannelManager. It accepts &a, &#RRGGBB, &x&R&R&G&G&B&B, their § equivalents, and MiniMessage tags — in the same message.
The two systems are gated separately, then merged: MiniMessage tags are validated on the text as typed, legacy codes are converted to tags afterwards, and the tags Allium generates itself are never permission-checked. That ordering is what lets a player with colour permissions but no MiniMessage permissions keep their &a codes.
| Node | Grants |
|---|---|
chat.color.<name> |
That single colour (chat.color.red, …) |
chat.color.hex |
&#RRGGBB and &x hex forms |
chat.color / chat.color.* |
Every colour, hex included |
chat.format.<style> |
bold, italic, underline, strikethrough, magic, reset |
chat.format / chat.format.* |
Every style |
chat.minimessage |
MiniMessage tags at all |
chat.minimessage.<tag> |
One tag type |
chat.minimessage.* |
Every tag |
Codes the author may not use are dropped rather than printed, and a parse failure falls back to fully stripped text instead of raw markup. Two related fixes came out of the same pass: legacy hex no longer counts as "needs the MiniMessage parser" (the bug that printed C8F9B&lVIP verbatim), and the sanitiser that was meant to drop non-ASCII characters no longer erases the whole message — it previously matched [^-<DEL>].
AlliumChannelManager.sendPlayerMessage gained an overload that carries the coloured form alongside the plain one: the plain text still drives the Discord relay, the console line and duplicate suppression, while the coloured form is what renders in game.
Home limits
HomeLimits is now the single source of truth. Two systems hand out homes — numbered allium.sethome.<n> nodes and the /core sethomes database override — and the effective limit is the higher of the two, so a staff grant never silently downgrades a rank and a rank upgrade is never swallowed by a stale override. allium.sethome.unlimited and allium.sethome.* resolve to unlimited, which renders as unlimited rather than a number.
The permission side reads effective permissions directly instead of probing 100 nodes, falling back to the probe only for permission plugins that resolve numbered nodes without listing them. Overrides are cached for 5 seconds — they are read on every placeholder resolve, and each read costs two H2 round trips on the main thread — and /core sethomes invalidates the entry as it writes.
| Change | Detail |
|---|---|
/core sethomes <player> |
New: shows effective limit, permission grant, override and homes set |
/core sethomes <player> +N|-N |
Now relative to the effective limit, not to the override alone |
/core sethomes permission |
allium.sethomes accepted alongside allium.admin |
home.delay (config) |
New: teleport delay for /home, previously read from teleport.delay |
allium.home.nodelay |
Now what skips the home delay; /home incorrectly checked allium.tpa.nodelay on all three teleport paths. Not declared in plugin.yml, so grant it through your permission plugin |
home.invalid-name (lang) |
New message; the rejection path passed no {home} placeholder |
An override set below what the player's rank already grants now says so instead of appearing to take effect.
Placeholders
%allium_home% and %allium_time% are delegates of the master %allium_% expansion, and a non-null answer stops the chain. Both returned "" for parameters they did not recognise, silently swallowing every placeholder registered behind them; both now return null and let the chain continue.
| Placeholder | Notes |
|---|---|
%allium_homes_max% |
Effective limit or unlimited; %allium_home_max% still accepted |
%allium_homes_set% |
Homes set; %allium_home_set% still accepted |
%allium_home_<n|name>_<w|world|x|y|z|yaw|pitch>% |
world is new alongside w; the home_ prefix is optional |
%allium_home_<n|name>_location% |
Unchanged |
%allium_time_world_time_24h% |
Accepted alongside world_time_24 |
Home name parsing was rewritten to split on the last underscore and validate the suffix against a known field set, so names containing underscores resolve correctly instead of being mangled by a replace on the coordinate name.
%allium_nickname% and %allium_nickname_raw% are replaced natively by NicknameManager.applyAlliumPlaceholders in chat, channel formats and join/quit messages — no PlaceholderAPI, no Essentials, and no chat.placeholderapi permission required — falling back to the player's real name when no nickname is set. FormatChatListener no longer touches %gradientdisplayname%; that placeholder belongs to GradientPlus, and Allium's animated take on it stays behind %allium_gradientdisplayname%.
allium.gradientname changed meaning: the player's GradientPlus colour is now shown to everyone, and the permission buys the phase animation. Without it the same gradient is rendered static rather than being withheld. GradientNameManager reads GradientPlus through %gradient_<text>% first — that form paints arbitrary text and so works regardless of GradientPlus' name_source_placeholder — with a re-entrancy guard for configurations that point that setting back at Allium, and a 1-second colour cache so a per-tick tab rebuild does not re-resolve placeholders.
Shared-IP accounts in /seen
getPlayersSeenOnIp(ip, uuid) matched other accounts against a single address: the target's current IP when online, their last known IP when offline. Two accounts that had shared a house for months stopped listing each other as soon as either one reconnected on a different address, and the relationship was asymmetric — A could list B while B did not list A.
getSharedIpAccountNames(uuid) self-joins player_ip_history on ip_address, so any address the two accounts ever shared is a match and the result is symmetric by construction. Rows are collapsed by UUID, so an account sharing several addresses is listed once, and names resolve through player_data so a renamed account appears under its current name rather than whichever name was current when the history row was written. Results are sorted case-insensitively.
Other fixes
/spysaid "disabled" without disabling everything. With both modes active,/spyremoved the caller from global spying, reported disabled and returned — leaving their targeted entries in place, so messages kept arriving. It now clears global and targeted spying together, and remains the only entry point that can turn global spying on. Turning a target off by re-running/spy <player>requiresallium.spy.others; without it the command re-affirms the target rather than toggling. Dropping one target no longer discards the whole target set, and tab completion no longer suggests the sender or anyone holdingallium.spy.exempt.- Spawner cores ignored right-clicks on blocks.
SpawnerCraftListenerlistened forRIGHT_CLICK_AIRonly, so using a core while looking at any block did nothing. It now handles both actions atHIGHESTpriority and ignores the off-hand pass so a single use does not fire twice. - ModGuard translation probe false positives. A client echoing the probe key back verbatim was treated as a resolved translation, i.e. a detection. The check now rejects both the fallback string and the key itself before considering the response. Separately, the bundled
modguard/config.ymlis used to seed thetranslation-probesection when it is missing, instead of rebuilding it from hard-coded defaults. - Essentials nickname import. A player with an Essentials nickname but no Allium one had nothing for
%allium_nickname%to return.ConnectionManagernow mirrors the existing sync-to-Essentials path in reverse on join, andnickname.sync-to-essentials: falsedisables the lookup in both directions.
Verification
The Maven suite passes with 161 tests. ChatColorParserTest is new with 15 tests covering the legacy/MiniMessage permission gates, the mixed-markup path that C8F9B&lVIP <gradient:red:blue>hi</gradient> exercises, and the "ampersand that is not a colour code" case. SharedIpAccountsTest adds 5, driving the self-join SQL and the row collapsing against an in-memory H2 database, including the renamed-account and multiple-shared-IP cases. GradientNameManagerTest grew to 6 with coverage for the new static rendering path, confirming it keeps the gradient stops while dropping the phase.
The permission migration guard, the two charged tools, the claim handcuff countdown and the home limit resolution need a live server, a permission provider and (for claim cuffs) GPExpansion, and were verified in game rather than by unit test.
0.2.12aРелиз26.1.1, 26.1.2, 26.2 · 4 августа 2026 г.
Allium v0.2.12a
Wiki: https://github.com/castledking/Allium/wiki
Highlights
/core hide fixrepairs commands Allium cannot classify — Allium's resolver pipeline asks a chain of adapters which permission a command requires. When every adapter declines, the result isUNKNOWNand Allium allows the command by default, which means its own no-permission message never appears. ExcellentQuests'/questshit exactly this. The new/core hide fix <command> <permission>records a permission for that command, and any unresolved command is now announced on console once per session so it can be found in the first place.%allium_gradientdisplayname%is colorable again — The placeholder used to always come back pre-colored, so writing&6%allium_gradientdisplayname%in a chat format or join message had no visible effect. When GradientPlus has no gradient or static color selected, the placeholder now returns a plain, uncolored name and the surrounding format decides the color. A real GradientPlus selection still renders its animated gradient exactly as before.#<message>respectsallium.staffchatagain — The permission check existed on the tracking pass but not on the pass that actually routes the message, so any player could type#hellointo staff chat and be subscribed to staff chat afterwards./delmsgno longer refuses the next identical message — Deleting a message also marked any message with the same text sent within three seconds as deleted, so a repeated line reportedMessage has already been deleted.- Reliability — Alt-account group reads use LuckPerms' live in-memory user instead of a fresh storage copy, and a leading color code in a language string emits
§r§xrather than the literal&r.
Technical Details
Manual command permission overrides
Allium resolves a command's required permission through an adapter pipeline — Paper basic commands, Brigadier, NightCore, vanilla, LuckPerms, and finally registered permission nodes. When no adapter can classify a command, PermissionResult.unknown(true, command) is returned: Allium allows the command and leaves enforcement to the command's own framework.
/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:
- The
colors.isEmpty()fallback that read the player's Vault prefix and adopted its trailing color, along with thegetPrefixVault lookup it needed. extractTrailingColor, which existed only to feed that fallback.normalizeColor, whose#FFFFFFdefault silently turned an unparseable color into white.
Chat's fallback formatter continues to delegate to the same implementation as the tab-name animator, so the two cannot drift apart.
#<message> staff-chat 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 they continued receiving it. The two handlers also disagreed: the early pass recorded the message as global while the routing pass sent it to staff, which left the Discord suppression state inconsistent.
Both handlers now gate on canWrite(player, staffChannelName) — allium.channel.join.staff-chat.write or allium.staffchat — matching the predicate used by /channel itself. A player without either now sends #hello to their normal channel as ordinary text.
/delmsg and repeated messages
ChatMessageManager.deleteMessage matched by message ID and by content: any stored message whose plain text overlapped the target's within a three-second window was marked deleted too. Two identical lines a second apart were therefore deleted together, and deleting the first made the second report Message has already been deleted.
That heuristic predates the logical-ID linking added in v0.2.11a, where every rendered message registers a short-lived ID that its per-viewer packet copies adopt. Those copies are already reachable by ID, so the content sweep was redundant for them and destructive for everything else.
Deletion is now by ID. Content matching survives only as a narrow fallback for packet-captured copies that never adopted an ID — a copy whose sender is the system UUID and whose ID belongs to no real stored message. That is precisely the set that ID matching cannot reach, so a deleted message still cannot reappear during a resend, while two identical messages remain independently deletable.
One related behavior is unchanged: PacketChatTrackerImpl.dedupeResendHistory still collapses adjacent identical lines by content, so if two identical messages both survive a deletion, a chat resend renders them as one line.
Other fixes
- Alt-group reads use LuckPerms' live user.
ConnectionManagercalledloadUser(...).join(), producing a fresh copy from storage. Saving that stale snapshot could resurrect groups another command had just removed, and the write bypassed LuckPerms' command log entirely. It now usesgetUserManager().getUser(uuid), the same in-memory instance LuckPerms' own commands mutate, and only falls back toloadUserwhen the player is not currently loaded. - Leading color codes in language strings.
Langreturned a literal&rbefore the section-coded color when a message began with a color code, so the raw characters appeared in chat. It now returns§r§<code>.
Verification
The Maven suite passes with 139 tests. GradientNameManagerTest continues to cover the two-stop animated gradient and its phase-midpoint continuity, confirming that removing the hard-coded color fallbacks left the GradientPlus rendering path unchanged; the new "unset means uncolored" branch needs a live player and PlaceholderAPI and is covered by in-game verification rather than a unit test. CommandPermissionAdaptersTest and PermissionCandidatesTest cover the adapter pipeline that the override store sits behind.
0.2.11aРелиз26.1.1, 26.1.2, 26.2 · 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 intoplugins/Allium/items/<id>.yml; it can then be listed, updated, removed, reloaded or given without writing Java. The lossless item snapshot retains vanilla components and third-party metadata, while a readablevanilla:block makes common properties editable. Stored items also resolve through/give <player> ci:<id>and/i ci:<id>. - Harvest fields behave more like fields — Crop stage durations can be staggered so a field planted together does not ripen in lockstep. Mature crops can be picked or uprooted with different regrowth rules, growing-crop feedback can be exact, vague or silent, water and lava receive configurable vanilla-like behavior, and pistons now break crops instead of being stopped by them.
- Minecraft-style selectors and destination teleports — The shared selector engine supports
@p,@r,@a,@eand@splus common filters./tphereaccepts multiple names and selectors, while the new/therecommand sends players, entities, selected pets or the sender to the block being looked at. - Chat respects the rest of the server — A cancelled chat event stays cancelled, recipients removed by mute, ignore or range plugins remain removed,
/delmsgreliably removes every per-viewer copy of a message, and Discord-to-game messages resolve Nexo glyph placeholders before delivery. - Reliability pass — Folia repeating tasks can now be cancelled reliably, explicit Bukkit permission nodes win over coarse Brigadier op-level checks, WorldEdit-style
//commands resolve correctly, Floodgate players are excluded from the Java translation probe, and Allium no longer prints Bukkit's raw usage line over its own formatted command errors.
Technical Details
Smooth animated names without losing solid-color movement
The animation counter itself was already correct. It matches the UnlimitedNametags phase cycle exactly: -1.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
#965CEAtoblue. - Chat's fallback formatter delegates to the same implementation as the tab-name animator, so the two cannot drift apart again.
There was a second source of instability in the tab list: multiple systems could write the same frame. GradientNameManager, the PacketEvents tab manager, Bukkit'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
listedstate 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:
snapshot— Bukkit's serializedItemStack, used as the lossless base. This retains components Allium does not model directly, including custom model data and metadata added by item plugins.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.allium— delivery options:notify-receivers, an optional receiverpermission, andmax-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 overridden per crop by adding the corresponding interaction: or growth: key to its crop file.
The new defaults are:
crop-defaults:
interaction:
right-click-harvest: true
break-harvest: true
progress-check: HINT
growth:
randomness: 0.25
right-click-harvest controls whether a mature plant can be picked in place. break-harvest controls whether breaking a mature plant runs its real mature-harvest table instead of the smaller break-drops.mature table. Immature plants always use break-drops.immature because they have no mature produce yet.
Growing-crop feedback has three modes:
| Mode | Result |
|---|---|
TIME |
Shows the exact time until the next stage |
HINT |
Shows a whole-path progress description such as “Coming along” or “Nearly ready” |
OFF |
Right-clicking a growing crop is silent |
HINT is the default because an exact countdown is misleading when randomized stage duration is enabled. Bare YAML OFF is accepted even though YAML 1.1 parses that word as boolean false.
Regrowth may now differ based on how the crop was taken:
regrowth:
enabled: true
stage: 2
on-right-click:
enabled: true
stage: 2
on-break:
enabled: false
The flat enabled/stage form remains the right-click rule for compatibility. Breaking defaults to removing the plant unless on-break.enabled is explicitly enabled. A source-specific block that omits stage inherits the shared stage instead of silently resetting the crop to stage zero.
Staggered crop timing
growth.randomness treats every configured stage duration as a mean. The default 0.25 spreads a 30-minute stage across 22 minutes 30 seconds to 37 minutes 30 seconds. Set it to 0.0 for the previous exact timing; values are clamped to 0.9.
The roll is deterministic for one crop instance and stage but unrelated between crops and stages. It is derived from the crop UUID rather than stored, so the regular growth engine and offline catch-up independently reproduce the same due times across restarts and chunk unloads. Replanting creates a new UUID and therefore a fresh roll; a plot cannot become permanently “fast.” Fertilizer and sprinkler speed multipliers apply after the spread and still scale the complete stage duration.
Liquids, pistons and crop visuals
Display-entity crops occupy air, so vanilla has no block to wash away or burn. Allium now listens for flowing liquids, player buckets and dispensers and applies a configurable response to the crop in the destination cell:
liquids:
enabled: true
water: DROP
lava: BURN
| Response | Behavior |
|---|---|
DROP |
Destroy the crop and return the same item loot a player break would return |
BURN |
Destroy it with a hiss and smoke, returning nothing |
DESTROY |
Destroy it silently with no loot |
PROTECT |
Cancel the fluid movement and leave the crop standing |
Water defaults to DROP; a mature crop with break-harvest enabled returns its mature produce and additional item tables, including seeds. Lava defaults to BURN. Liquid destruction never executes command rewards because there is no player responsible for the harvest. Setting liquids.enabled: false leaves crops untouched and permits them to stand inside fluid.
PROTECT is intentionally not the default. A source fluid repeatedly retries a cancelled spread, so protecting large fields beside running water can create continuing event load.
Pistons no longer stop when a crop is in their path. The crop is destroyed instead, including crops hit by the extending piston head or rooted on a block being moved. Break-item tables can drop, but player-targeted command rewards do not run.
The default GROUND crop display offset changed from 0.0 to 0.43, lifting dropped-item-context models out of the soil. Full-block models authored for display-transform: NONE should generally keep y-offset: 0.0. The shipped tomato example also now guarantees seed return when a mature plant is uprooted, so its plant/harvest/replant loop cannot strand a player.
Upgrade notes for existing Harvest configurations
- Missing
crop-defaultsandliquidssections use the defaults above; adding the blocks is optional unless different behavior is wanted. - Existing crop files with no
growth.randomnessinherit0.25. Addrandomness: 0.0per crop or undercrop-defaults.growthto retain exact durations. - Existing crop files inherit
break-harvest: true, so breaking a mature plant now pays its mature harvest table and then follows the break-specific regrowth rule. - An existing
crop-visuals.y-offset: 0.0remains explicit and will not be overwritten. Change it to0.43manually if aGROUNDmodel is still buried. - Crop defaults are re-read by
/harvest reload. Changes toliquids.*, storage, module enablement and service-owned visual settings require a restart.
Selectors, /here and /there
The selector parser now has one structured result path instead of returning an unexplained empty list. Supported selectors are @p, @r, @a, @e and @s; supported arguments include:
type, name, tag, gamemode/gm, distance, limit, sort, world,
x, y, z, dx, dy, dz
type, name, tag and gamemode accept ! negation. Distance accepts exact values and ranges such as 0..50, ..16 and 10..; dx/dy/dz define a box from the selector origin. Selector-aware tab completion offers argument keys and context-specific values.
/tphere (aliases /here and /s) now accepts any number of player names and selectors and teleports their de-duplicated union to the sender. With no targets, it brings the sender's active /tppet and /tpmob selections to the sender, or performs a harmless self teleport when no selection is armed.
/there (alias /tpthere) ray-traces up to 128 blocks and sends the named/selected targets to a standing position beside the block being looked at. With no arguments it sends active pet/mob selections, or the sender when none are selected.
New nodes default to operators:
| Permission | Grants |
|---|---|
allium.tpthere |
/there and /tpthere |
allium.selectors |
Selector use in supported Allium commands |
allium.command.selector.all |
@a |
allium.command.selector.entities |
@e |
Selected pets and mobs now travel through one shared, Folia-safe path whenever their owner teleports. The selection is atomically claimed before movement, preventing duplicate teleports and duplicate “auto-disabled” messages when more than one teleport event represents the same move.
Chat delivery and message deletion
Allium's channel manager broadcasts manually, but it previously discarded the modern chat event's recipients and did not stop when another plugin cancelled the event. That could bypass soft mutes, ignore lists and range-chat filtering. It now snapshots the remaining player viewers, intersects them with the channel's readers, clears the event viewers only to suppress the vanilla duplicate, and sends exclusively to that intersection. Cancelled chat is not rebroadcast.
Every rendered message now registers a short-lived logical ID before its per-viewer packets are captured. All copies inherit that ID, including copies created during a packet resend, so one /delmsg removes the original and every recipient-specific copy. Lookup prefers the original record so sender and timestamp information remain correct.
Discord-to-game channel messages resolve Nexo's configured glyph placeholders before sending. The in-game component gets Nexo's glyph character and styling, while the console keeps the readable placeholder/emoji. The integration is reflective and cached; absent or unsupported Nexo versions leave the message unchanged rather than preventing delivery.
Permissions, Folia and operational fixes
- Plugin Brigadier commands honor their explicit permission node. When a command exposes a Bukkit permission, that check now wins over a Brigadier predicate that only asks for an op level. This extends the v0.2.9 vanilla-command fix to plugin command wrappers.
- WorldEdit-style double-slash commands resolve. Commands registered internally as
/pos1orworldedit:/pos1are retried with their leading slash restored after parsing//pos1. - Folia tasks really stop. Paper returns package-private scheduled-task implementations; reflecting
cancel()from that implementation could throw an access error and leave a repeating task alive. Allium now resolves the publicScheduledTaskinterface method, caches it, and only latches the handle as cancelled after invocation succeeds. - No duplicate raw command usage. Allium commands already send localized usage and errors. Executors are now wrapped so Bukkit does not add the plain-white
usage:string when a handler returnsfalse. - No Java translation probe for Bedrock clients. Floodgate players are detected through its API, with the UUID marker as a fallback, and skipped before ModGuard opens its Java-only probe UI.
- Quieter Citizens tracking. The waypoint-range scan runs four times per second but now logs only when the tracked NPC set changes, an attribute actually needs correction or the failure changes.
Verification
The full Maven test suite passes with 139 tests. New coverage includes the rendered animation seam, TAB/Bukkit writer ownership, stale visibility-transition invalidation, tab-entry relisting, stored-item round trips and editable metadata, Harvest interaction/regrowth parsing, randomized timing and offline catch-up, liquid defaults, and command-permission precedence.
0.2.10aРелиз26.1.1, 26.1.2, 26.2 · 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
NoSuchFieldErrorat 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+).TreeAxeManagerbuilt its material sets from hard enum constants in static initialisers, so constructingTreeAxeListenercrashed and tookPluginStart.registerCommands— and the plugin — down. Now built through amaterials(String...)helper that resolves each name withMaterial.matchMaterialand skips what the server does not know. This also matches how the rest of the class already worked (getValidGroundTypeshas always switched on material names). - Locator bar —
GameRule.LOCATOR_BAR(1.21.6+). Read in four places, including a repeating global task (retryTabListManagerInit) that loggedGlobal task ... generated an exceptionevery cycle, and inPartyManagerplayer-visibility logic. All four now go throughApiCompat.isLocatorBarEnabled(world), which resolves the rule withGameRule.getByNameonce and reportstrue(the previous default) when the rule is absent. /healand handcuffs —Attribute.MAX_HEALTH/MOVEMENT_SPEED(renamed fromGENERIC_*in 1.21.3).ApiCompatresolves whichever spelling the server has via reflection;/healfalls 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 fromPOTIONin 1.21.5).CreativeManagernow comparesevent.getEntityType().name()as a string, so it loads on any version; older servers deliver both kinds asPOTIONand it treats them as splash potions. - Waypoint attribute —
Attribute.WAYPOINT_TRANSMIT_RANGE(1.21.6+). The Citizens NPC re-enforcement loop resolves it throughApiCompatand simply does nothing on servers that lack it.
The 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
switchtables. Aswitchover an enum compiles to a synthetic$SwitchMapwhose every entry is individually wrapped in aNoSuchFieldErrorhandler (e.g. 2,154 such handlers inFireballExplosion), so unknown constants are skipped as the table is built. Thesegetstatics are always immediately followed byordinal()and were filtered out. - Mixed-case constants 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.
0.2.9aРелиз26.1.1, 26.1.2, 26.2 · 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 matchingminecraft.command.*node.- Class preloading — Allium now loads its own classes at startup, removing the
NoClassDefFoundErrorfailure 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 permission to use /minecraft:kill.
The cause was adapter ordering against two disagreeing permission models. BrigadierCommandPermissionAdapter runs before BukkitCommandPermissionAdapter, and for a vanilla command it reflected out the Brigadier CommandNode and tested its requires predicate. Vanilla's predicate is an op-level check — source.hasPermission(2) — that knows nothing about the minecraft.command.<name> node CraftBukkit registers as the same command's Bukkit permission. So the Brigadier adapter returned a denial before the Bukkit adapter, which would have checked that node and passed, ever ran.
For vanilla commands the Bukkit permission now wins: if command.getPermission() is set and testPermissionSilent(player) passes, the adapter returns allowed with ResolutionType.VANILLA immediately. Otherwise it falls through to the existing requirement test unchanged. Vanilla detection uses three signals — the wrapper class name, a helpCommandNamespace of minecraft, or the pipeline resolving the owning namespace to minecraft — so it works whether the command arrives as /kill or /minecraft:kill.
This was deliberately not wired to allium.hide.bypass. That permission governs tab-completion visibility of namespaced commands, and treating it as a permission override would mean anyone who can see minecraft: commands can also run all of them. Reading the real permission node keeps LuckPerms authoritative.
Covered by a regression test (vanillaBukkitPermissionBeatsOpLevelBrigadierRequirement) reproducing the exact case: a kill node with a false requirement plus a granted minecraft.command.kill.
Class preloading
A plugin jar is read lazily — a class is only pulled off disk the first time it is needed. Replacing Allium.jar on a running server leaves the classloader pointing at a file whose contents changed underneath it, so anything not yet loaded fails with NoClassDefFoundError. The typical victim is a nested class used only when a particular message is first formatted, and the typical moment is inside the restart command being used to apply the update.
ClassPreloader walks the jar during onEnable and resolves every codes.castled.allium.* class with initialize = false. That reads and defines each class — the part that needs the jar — without running static initialisers, so no side effects fire early and nothing observable about startup changes. Initialisation still happens naturally on first real use, by which point the class no longer needs the file on disk. Costs roughly 150ms; toggle with preload-classes in config.yml.
This is damage control, not a licence to hot-swap. It cannot help with classes owned by other plugins or the server, cannot make a running instance pick up new code, and cannot help if the jar is replaced before it runs. The supported update path is still plugins/update/ plus a restart.
Build and dependencies
H2 is the only bundled JDBC driver. It backs both the core plugin database and the default harvest storage, ships shaded and relocated, and works with no downloads. SQLite and MySQL are still supported backends but their drivers are no longer bundled — together they cost roughly 14 MB, most of it SQLite native binaries for CPU architectures a given server will never run. Admins supplying them put the jar in the server's libraries/ folder; a missing driver produces an explicit startup message naming it and where to put it, rather than an opaque connection failure. Hikari and H2 are both relocated so they cannot clash with another plugin's copy.
snakeyaml 2.3 pinned explicitly. DiscordSRV ships a shaded fat jar bundling a snakeyaml predating LoaderOptions#setMaxAliasesForCollections. Declaring the modern version ahead of DiscordSRV keeps Bukkit's YamlConfiguration working on the compile and test classpath.
Nexo added as a provided dependency and softdepend alongside Oraxen. Items resolve through the plugins' public APIs rather than by running /nexo give or similar, and integrations register only when the plugin is actually enabled — Allium runs fine with neither installed. To try Harvest vanilla-only, replace the nexo: references in the shipped configs with items such as minecraft:wheat_seeds.
Commands and permissions
/harvest (alias /hf) with reload, give, crop, spawner, soil and debug subtrees. allium.harvest.admin grants everything. Player-facing nodes default to true: allium.harvest.crop.plant, allium.harvest.crop.harvest, allium.harvest.sprinkler.place. Admin nodes default to op.
/harvest crop plant <crop> [path] forces a specific path, which is the fastest way to test a rare variant without rolling for it.
Reload is best-effort rather than all-or-nothing: definitions that parse cleanly are activated and entries with fatal problems are skipped, so one broken crop does not block every other change in the file. Errors name the file, the exact YAML path and the problem. storage.* and enabled still need a full restart.
0.2.8aРелиз26.1.1, 26.1.2, 26.2 · 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
PacketChatTrackerImplcaptured resend packets as new chat history entries, causing deleted messages to resurface on subsequent/delmsgruns. Required a server restart to clear. - Shared
ChatMessageinstances across collections —playerMessagesandplayerChatHistorynow reference the same object for packet-tracked messages, sosetDeleted(true)on one automatically affects the other. - Faster chat clear — The invisible-character clear phase now batches 20 characters per message instead of sending 120-180 individual packets, cutting the clear from ~180 packets to ~9.
Technical Details
Thread-Safe Resend Suppression
The previous fix in v0.2.7a removed duplicate tracking 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):
playersBeingResent:HashSet→ConcurrentHashMap.newKeySet()(thread-safe).suppressPacketTracking: NewAtomicBooleanchecked by all three packet handlers viashouldIgnoreTracking(). Settruefor the duration ofresendChatHistoryToAllPlayers()so no packets from the broadcast are tracked, regardless of per-player timing.- Delayed cleanup: Player UUID removal from
playersBeingResentand guard release (resendAllInProgress,suppressPacketTracking) are delayed 1 tick viaSchedulerAdapter.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.
Комментарии
Загружаем…
