AlliumСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачатьСкачать
ВЕРСИИ (24)
release
Allium v0.2.19a
0.2.19a25 окт. 2026 г.10Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2, 26.2, 26.3bukkit, folia, paper, purpur, spigot
Что нового
# 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.
release
Allium v0.2.18a
0.2.18a3 окт. 2026 г.59Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2, 26.2, 26.3bukkit, folia, paper, purpur, spigot
Что нового
# 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.
```yaml
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.
```yaml
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:
```yaml
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:
```yaml
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 data |
| Pie state (phase, step, type, refunds, baked, bites) | The pie display entity's data |
| Baked-pie cooling deadline | Item data |
| Stove slots, progress, uncollected XP | The stove's display entity's data |
Nothing in the kitchen touches the database.
| Plugin | Without it |
|---|---|
| **Nexo** | **The kitchen is effectively fully inert.** Every default item in `kitchen.yml` is a `nexo:` reference, so bags map to empty and both kneading and pies fail to resolve. This is by design and it does not crash — you get one clear message about the missing resolver per file, not a stack trace. |
| **DecentHolograms** | Everything works except the floating instructions above an assembling pie. |
| **GriefPrevention / WorldGuard** | Build permission is granted. Both are called reflectively, and a failure logs once and allows, rather than locking players out of a kitchen they can otherwise see. |
| **CoreProtect** | Nothing changes — and nothing is logged. Pies are entities, not blocks, so no placement event happens for a protection plugin to cancel. Firing a synthetic one would make block loggers record a placement that never happened. Placing, picking up and eating pies is never logged and pies cannot be rolled back. |
`Nexo` registers its items *after* Allium enables, which used to mean every `nexo:` reference failed at startup — and stored crops whose definition failed to load were deleted from the database when their chunk loaded. The harvest module now waits for Nexo's items-loaded event before enabling and reloads on every later one.
#### Stove scheduling on non-Folia servers
`SchedulerAdapter.runEntityRepeating` — the non-Folia fallback used by the stoves — called `runTask` instead of `runTaskTimer`, so it scheduled a one-shot task. Its sibling repeating methods, `runRepeatingGlobal` and `runAtLocationRepeating`, both paired with `runTaskTimer` correctly.
Cooking still worked on Paper, because `tick()` lights the fuel and advances progress and every interaction re-enters through `open()` and `changed()`. But `ensureTicking()` only schedules when `task` is null and the handle is never cleared, so **a stove left cooking with nobody in the menu advanced one tick per interaction instead of continuously** — it cooked correctly while in use and did not finish unattended. Now fixed; Folia was unaffected, as that path uses `runAtFixedRate`.
### Trading cards
#### Tiers and quality
Five tiers, and **ordinal comparison is the only thing that ever compares them** — nothing weighs them against each other:
| Tier | Colour | Bonus slots | Tier bonus |
|---|---|---|---|
| Simple | gray | 1 | +1 |
| Elite | aqua | 2 | +2 |
| Ultimate | light purple | 3 | +3 |
| Legendary | gold | 4 | +4 |
| Fabled | red | 5 | +5 |
A card's **quality is stored as a plain integer from 1 to 100, never as a band name**, and the band is derived from that integer on read. Storing the name would freeze every card already in circulation into whatever the band boundaries were on the day it dropped. Eight bands partition 1..100 exactly:
| Band | Range | Heads on trade-in | Boost range |
|---|---|---|---|
| rotten | 1–5 | 1 | 1–2 |
| damaged | 6–15 | 1 | 2–3 |
| torn | 16–25 | 2 | 3–4 |
| okay | 26–45 | 3 | 4–5 |
| fine | 46–65 | 5 | 5–6 |
| great | 66–85 | 6 | 6–7 |
| mint | 86–99 | 7 | 7–9 |
| emaculate | 100 | 8 | 7–11 |
`emaculate` is one value in a hundred, so it is a genuine trophy. A gap or an overlap in this table is rejected at load, because a card would otherwise roll a quality with no band and its lore would have no colour and no payout.
#### 180 cards
36 mobs × 5 tiers. Every mob currently ships the same weights — 60 / 25 / 10 / 4.5 / 0.5, totalling 100 — at a `chance` of 0.0015, so any card is roughly 1 in 667 kills and a Fabled card roughly 1 in 133,000.
```yaml
allay:
mob: ALLAY
colour: aqua
chance: 0.0015
tiers:
SIMPLE: { weight: 60.0, item: nexo:allay_trading_card }
ELITE: { weight: 25.0, item: nexo:elite_allay_trading_card }
ULTIMATE: { weight: 10.0, item: nexo:ultimate_allay_trading_card }
LEGENDARY: { weight: 4.5, item: nexo:legendary_allay_trading_card }
FABLED: { weight: 0.5, item: nexo:fabled_allay_trading_card }
```
Drops need a **player** as the killer — dispenser and farm kills produce nothing. A card whose config produces any error is dropped wholesale rather than registered half-configured, and a tier with a positive weight but no item is skipped, because a card that drops an item nobody can hold is worse than no card at all.
One trap: `chance: 0` is an **error**, not a way to disable a mob, and an error removes the card entirely. Delete the entry instead.
#### Starting boosts are derived, never stored
This is the single most important design decision in the feature. A signature's number is recomputed on every read from the card's immutable identity — id, mob, quality, tier — plus its level, through a fixed hash. Nothing is written down.
Two consequences, both intended. **Rebalancing a quality band moves every card in that band at once**, which is the entire point of publishing a range rather than storing a value. And **a card dropped today reads identically in six months**, instead of drifting out of sync with a rebalanced table.
#### Levelling and XP
XP goes to the **equipped** card only. Not a card in the inventory — xp follows the card the player committed to, because a player carrying nine cards cannot bank xp in all of them.
Curve `xp.curve: {10: 25, 25: 120, 50: 600, 75: 2400, 100: 9000}` — levels 0–9 cost 25 each, then 120, 600, 2400, and 9000 past 75. A level with no breakpoint of its own inherits the next one's cost. **302,050 XP to level 100.**
Eight sources ship: kills (4), breeding (40, per mob, 7 days), crop harvests through Allium's own Harvest module (2), advancements (20, once each), ExcellentQuests completions (25), AuraSkills abilities (3, 30s cooldown), EcoJobs work (×1.5), and large fish (12).
Two decisions worth stating outright. **There is deliberately no player-kill source at all** — it is the easiest xp on the server to farm with two accounts. And **"large" means large for you**: LiteFish has no `isLarge()`, its weight bands are a percentage within each species' own range, so a player who has only ever landed 1kg sardines finds 2kg sardines large. Fish percentiles are computed mid-rank against a rolling per-species window, and the catch itself joins the sample before being judged.
Anti-farm is absolute cooldowns rather than a rolling window: advancements and quests pay once ever (daily quests once per day), breeding once per mob per 7 days, abilities once per 30 seconds. Claim and check are a single call, so a caller cannot check, be interrupted, and then award twice.
#### Trade-in is a lookup, not a calculation
A card at 92% quality belongs to the band that covers 92, and that band names its own head value. Rebalancing is an edit to the band table rather than to a formula, **and a card's value can never disagree with the number its lore shows.**
The heads appear in the window and nothing is granted until the player physically takes them out. Closing the window returns the card to the inventory and records anything else as a debt, delivered automatically 20 ticks after the next login (a tick later, so the inventory is populated before the items are added) or on demand via `/tradingcards heads`. Debts are stored as an item *reference* plus an amount rather than as serialized item data, so they survive a retexture.
Default `head-source: SPAWNER_HEADS` pays out Allium's own mob heads, which already have a use — crafting spawner cores and plushies — so trading a card in feeds that progression instead of creating a second currency.
#### Reroll, merge and the workshop
Rerolls are explicitly progressive — `min(cap, round(2000 × 1.6^(n-1) × tierMult))`, rounded to 100 — because flat pricing makes "reroll forever" optimal and linear pricing makes the tail affordable to anyone who saved. A reroll either unlocks a new signature (35%) or re-rolls the bonus boosts; it never replaces a signature it already holds. If a card holds no bonuses, the reroll is **refunded** and the player is pointed at the bonus menu, because a reroll that handed one over free would make the price a suggestion.
**Drops carry no bonuses at all.** Every bonus slot starts empty; a slot is bought with its own money.
Merge takes two same-mob, same-tier cards both at maximum level and gives one card of the next tier at level 0 — worth exactly a fresh drop of the next tier, converting spare cards into progress rather than granting a head start. Every refusal names which of the three conditions failed, and the inputs are consumed only after the result has been successfully built.
#### The Relique slot
Three things must agree, and all three are now enforced and pinned by a test: the slot definition file, the `card` slot being added to the player slot definitions, and the validator key being registered as **`relique:trading_card` — the full key including Relique's namespace**, because Relique resolves it through `Key.asString()` against a live registry.
Registering it as `allium:trading_card` built a two-colon key, Adventure rejected it, and the integration logged a warning and carried on. **Equipping was therefore broken for a long stretch and looked installed the whole time.** The test now asserts the slot JSON and the registered key are the same single-colon string.
Equipped cards are written back through Relique's own `updateItem`, which is the only route that both persists the change and re-syncs modifiers. Without that write-back, xp levels are purely cosmetic and evaporate on relog.
One storage detail that matters: **card xp is written as a string, not a double.** Relique stores an equipped card through AbyssalLib's YAML codec, which reads every number back as an int — a double went in as an `IntTag`, and Paper refuses to read an int tag as a double, so the next read threw. That broke the card's menu and `/cards reload` for any card that had been through the slot.
#### The frame
The tier panel is drawn in the lore itself, as a bitmap font glyph. Every lore line is `IN + <frame glyph> + OUT`: `IN` steps to the left edge of the tooltip box, the glyph draws a full-width slice of the frame, and `OUT` steps back to the text column. The frame never moves the text, and the right border is part of the bitmap, so no line needs padding to a width.
This replaces a `tooltip_style` sprite pair per tier *and per lore line count*, drawn by a core shader. Both were fragile: the style id is a wire format the client turns into an asset path verbatim, so a mismatch silently falls back to vanilla's tooltip with no error anywhere, and the shader was tied to the client's rendering pipeline. The new scheme uses only bitmap and space font providers and the `tooltip_style` component, all stable formats, so nothing needs touching when the pipeline changes between versions.
```bash
python3 tools/generate_card_glyphs.py --out <pack root> # real art
python3 tools/generate_card_glyphs.py --out <pack root> --calibrate # seam test
python3 tools/generate_card_glyphs.py --out <pack root> --width 230 # wider cards
```
The generator fails rather than write assets that disagree with `CardFrame.java`, and the test suite reads the shared constants back out of the generator, so neither half can drift from the other silently.
Two details that took several attempts to get right. The item name must be blanked through the `ITEM_NAME` **component** rather than `ItemMeta.displayName` — the latter writes `custom_name`, and `item_name` takes precedence for the tooltip, so the card's title appeared twice. And the name is a **translatable** the pack maps to nothing, with the plain title as its fallback, so a player without the pack still reads what the card is instead of a blank line.
Chat hovers need their own layout: a hover 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 the bottom line ends with a trim space so the box is exactly the frame's width. Signed player chat is deliberately left alone, since changing it would break the signature.
#### Dependencies and what degrades
| Plugin | Without it |
|---|---|
| **Nexo** | Every card item is a `nexo:` reference, so no card resolves. One clear message per file. |
| **Relique** | No equip slot. Dropping, levelling, rerolling, merging and trading all still work. |
| **LibsDisguises** (premium) | `/morph` still toggles, but the free build shows the disguise only to you. Mob AI behaviour is Allium's own and is unaffected. |
| **AuraSkills** | Stat boosts cannot be applied. It is the only hard requirement beyond Nexo for the boost half. |
| **LiteFish, EcoJobs, ExcellentQuests** | That xp source is skipped and logged once. Never an error — a server without ExcellentQuests should not have to delete its config to keep the file quiet. |
`/tradingcards status` exists for exactly this reason: a source that configures cleanly but never fires is otherwise indistinguishable from a bug.
`quest-complete` needs a patched ExcellentQuests build, because the released plugin has no quest-completion event. It is registered reflectively and reported by `status`.
## Configuration caveats
Straight answers to things that will otherwise cost someone an afternoon. These are all real in this release.
- **`/tradingcards reload` is a partial reload.** It reloads `config.yml`, `cards.yml` and `boosts.yml`, and re-renders the lore of online players' cards. It does **not** rebuild the xp config, the anti-farm store or the fish tracker — **xp changes need a server restart.** Reloading also registers a fresh drop listener without unregistering the old one.
- **`levelling.curve` in `config.yml` is dead config.** Nothing reads it. The curve that actually applies is the flat `xp.curve` map lower in the same file.
- **`reroll.signature-pool` and `reroll.signature-amounts` in `config.yml` are dead config.** The pools and amounts live in `boosts.yml`. The dead copy still lists boosts that no longer exist (`transmit-range`, `jump-height`) and would be rejected if it were ever loaded.
- **`crafting:` is parsed but not implemented.** Nothing in the source reads it. A consequence is that nothing can currently set a card's `bound` flag, so the "traded cards cannot be traded back" rule — which exists to stop a crafted card from closing an infinite head→card→head loop — is currently unreachable.
- **`trade.consume-card` is parsed but never read.** The card is always consumed on confirm. Its config comment describes behaviour that does not exist.
- **`levelling.announce.enabled` is never checked**, and **`levelling.announce.sound` is never read** (the level-up sound is hardcoded). The empty-string trick that turns `equip-message` off *does* work.
- **`boosts.yml` has a duplicate `haste:` key** in `signature-amounts` (`0.5` then `0.2`; last wins). Harmless today because haste is a vanilla attribute and so can never be a signature, but it should be deleted. Two nearby comments also still claim bonuses are "rolled at drop", which stopped being true when drops became bonus-free.
- **`enabled: false` needs a restart** to take effect, in either direction.
## Other fixes
- **`/spawnerhead` is now registered** with a permission node, and spawner-craft locator visibility works at the packet level.
- **`%allium_fly_time_short%`** for the tfly display, so the tab list does not wrap.
- **Chat colour parsing** no longer double-wraps emoji in Discord-relayed names; longest ZWJ and variation-selector sequences match first so compound emoji are not split.
- **One resolver chain is shared across subsystems.** The item resolvers moved out of `harvest/integration` into a top-level `item` package so Harvest and Trading Cards resolve custom items through the same chain, with the Nexo and Oraxen resolvers following.
- **Database calls are guarded on a ready connection**, and DiscordSRV lookups are async.
## Verification
- **Maven suite: 526 tests, 0 failures, 0 errors** (up from 220). New coverage is concentrated in the two features: 27 test classes across `tradingcards` (definition loading, rolling, quality bands, the xp curve and anti-farm, trade quotes, reroll and merge, bonus slots, boost catalogues and display, morph rules, fish percentiles, pending payouts, Relique slot registration, frame and lore rendering, marker framing, xp storage across the Relique round trip) and 10 across `harvest` including a new `KitchenConfigTest`.
- **CI build green** on JDK 25: `mvn install -DskipTests -B`, `Allium.jar` uploaded.
- **Three dependencies are now vendored under `libs/`** — Relique, AbyssalLib and DecentHolograms. None is published anywhere reachable: Relique has no `-api` artifact and its author's repo 404s for the whole `com/github/darksoulq` path, and DecentHolograms was never published for 26.x. Your local builds had been working only because they were already in `~/.m2`. `libs/install.sh` installs them before compiling and the workflow runs it, so a clean checkout and a CI runner both build. All three stay `provided`, so none is shaded into the jar. See `libs/README.md`.
- **`plugin.yml` resolves `0.2.18a`** from the pom through resource filtering — the pom is the only place the version is written.
- **`tools/generate_card_glyphs.py` compiles** and its layout constants agree with `CardFrame.java`, which is asserted by the test suite rather than left as a comment.
- **Not verified here:** no live server run was performed for this release.
release
Allium v0.2.17a
0.2.17a21 сент. 2026 г.21Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2, 26.3bukkit, folia, paper, purpur, spigot
Что нового
# 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.
```yaml
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 DiscordSRV as an unknown slash command and is ignored, and a modal submit becomes a generic event nothing handles.
The dialog pre-fills the player from Allium's chat-message bridge when the message is a relayed chat line, otherwise from the embed author, webhook name or message content, and staff can correct it before submitting. Permission is checked twice (on open and on submit) against `staff-role-id` by role position, with Discord Administrators always allowed. `ban-exempt-groups` matches the target's Vault group and refuses the ban outright.
### Unknown-command helper text
The red line under the chat box is built client-side from the `command.unknown.command` translation key, so the only way to change it is a language override. Allium writes a small pack containing that key in all 142 client locales — the selected locale is loaded over `en_us`, so an English-only override loses. With ResourcePackManager installed the pack is dropped into its `mixer/` folder (`loadbefore` guarantees it lands before the merge); otherwise it is written to `plugins/Allium/generated/` to merge by hand. Colors take `&` codes and MiniMessage, with hex rounded to the nearest of the 16 legacy colors because language files cannot render hex.
### ModGuard probe false kicks
`ModGuardManager.reload()` builds a new probe, but `unregister()` only removed the PacketEvents listener and left the old instance's Bukkit join listener alive. Every reload therefore added another probe that kept sending its own ghost signs at the same position and, having no packet listener, cycled through its batches on timeout. The live probe read those foreign echoes and scored them as detections: 38 of 144 recorded hits carried another probe's fallback token.
`unregister()` now drops the Bukkit handlers and sets a closed flag that stops already-scheduled work. As a second layer, each sign gets its own random fallback and any echo carrying a foreign probe token — or a probe token on a keybind line — is discarded instead of scored, which also covers a late echo arriving after a timeout.
### Auto-restart on Discord
Discord now gets exactly two messages per restart: one warning at `auto-restart.discord-warning-seconds` (default 60) and the final notice, instead of an embed per countdown step. The channel topic carries the countdown through `autorestart.channel-description` / `channel-description-now`, throttled to one edit per five minutes because Discord rate-limits channel edits to roughly two per ten minutes; the final "restarting now" topic is always forced through.
## Other fixes
- **Emoji in Discord-relayed names keep their own colors.** Raw Unicode emoji in Discord nicknames are wrapped as `&r{emoji}{roleColor}`, matching the existing `:alias:` handling, with longest sequences matched first so ZWJ and variation-selector emoji are not double-wrapped.
- **SpawnerMeta spawner delivery uses `/spawnermeta give`** instead of the old `/sm` alias.
- **Handcuffs no longer spam "You caught a fish!"** on every catch.
- **`/core reload` reloads `spawner_heads.yml`** and re-syncs (or removes) the Discord ban command.
## Verification
- **Maven suite: 220 tests, 0 failures, 0 errors.** New coverage for the probe echo rules, the unknown-command pack, the Discord ban command builder and exempt groups, head-name resolution and plushie ids, and `spawner_heads.yml` parsing.
- **Bare-server boot:** Paper 26.3 with only `Allium.jar` in `plugins/` reaches `Done` in ~7s, enables Allium, and registers every listener including the three chat managers. No PacketEvents, Vault, DiscordSRV or Nexo present.
- **Head sources:** all 78 Nexo mob plushies resolve to a craftable head once the 15 configured mobs fall back to Allium's own heads.
release
Allium v0.2.16a
0.2.16a10 сент. 2026 г.18Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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:
```yaml
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, XRay, ChestESP, KillAura, BleachHack, Aristois, Coffee, ...), the IPN inventory family, and misc utilities (Tweakeroo, Litematica, Xaero's, seed-crackers, world downloaders, ...). Brand, plugin-channel, and FML-handshake checks are individually toggleable (`check-brand`, `check-channels`, `check-fml-handshake`). LiquidBounce stays as a documented disabled entry: current builds load their language files into their own `ClientLanguage`, so the vanilla resolver never sees a `liquidbounce.*` key and the sign probe cannot reach them.
### EssentialsX visibility bridge
`PluginStart.getVisibilityApi()` returns an `AlliumVisibilityApi`. EssentialsX's `AlliumVisibilityBridge` looks the object up by reflection and calls `getVisibility(viewer, target)`; the returned decision maps to Essentials' behavior:
| Decision | Meaning |
|---|---|
| `VANISHED` | Target hidden from viewer (Allium's vanish levels say so) |
| `PARTY_ISOLATED` | Target is isolated from the viewer by party rules; Essentials must not let Bukkit's `canSee` undo Allium's hide |
| `VISIBLE` | Sender is the target themselves |
| `DEFAULT` | Let Essentials decide |
This stops the "Essentials could not query AlliumVisibilityApi" warning and makes the two plugins agree on who sees whom. Initialization is ordered after both `VanishManager` and `PartyManager` so the bridge can delegate to them.
### Home limits for offline players
`HomeLimits.getOfflinePermissionMaxHomes(OfflinePlayer)` returns an `OfflinePermissionResult(resolved, maxHomes)`. Online players resolve normally; offline players fall back to LuckPerms, pulled reflectively exactly the way the rest of the plugin talks to it (`LuckPermsProvider.get()` + `getUserManager()`, `getUser` then `loadUser(...).join()`, `resolveInheritedNodes` with non-contextual query options). `resolved()` is false when LuckPerms is absent or the player has no user record, so callers can tell "they genuinely have no homes" from "nobody knows". An `allium.sethome.unlimited` node still wins, a negated base (`!allium.sethome`) yields zero, and the staff `/core sethomes` override is untouched.
`/core sethomes <player>` now shows the resolved permission value even while the player is offline instead of `unknown (offline)`. `HomeLimitsOfflinePermissionTest` covers the whole matrix with LuckPerms users delivered through Java proxies.
### Auto-restart announces on Discord, and a placeholder fix
With `auto-restart.relay-to-discord: true` (default), `DiscordRestartRelay` posts embeds for the schedule, each countdown tick, the vote start/pass/fail, and the final shutdown — colored from `autorestart.discord-stopped-message-embed-color`, footered with the stripped `server-stopped-message`. Channel resolution prefers DiscordSRV's main text channel, then the configured `global` channel, then a raw parse of DiscordSRV's own config. `send-final-server-stopped-message` broadcasts the closing line in-game before shutdown. `/ar now` no longer announces "Server will restart in 0s." — an instant restart skips the scheduled/embeds entirely.
The three restart placeholders (`restart_time`, `restart_seconds`, `restart_scheduled`) delegate to the auto-restart command's live state. They were unprintable at first because of a delegation-chain bug: the master `%allium_%` expansion asks each inner expansion in order and stops at the first non-null reply, and `CommandPlaceholder` returned `""` (not null) for any parameter it did not handle. Because it ran *before* `RestartPlaceholder`, every restart placeholder was swallowed as an empty string. It now returns `null` for unhandled parameters, and `LocationPlaceholder`'s database-unavailable path got the same fix.
## Other fixes
- **`§x§R§R§G§G§B§B` legacy hex in chat no longer crashes the formatter.** `FormatChatListener.convertLegacyToMiniMessage` now rewrites the six-section-code form to `<#RRGGBB>` before the single-code replacements run.
- **Per-gamemode inventories are upserted.** `CreativeManager` now persists the `PlayerInventories` snapshot through `Database.savePlayerInventories` (a `MERGE INTO player_inventories` upsert) when a game-mode inventory is saved, so the creative/survival swap is durable rather than shuttle-only.
- **`/gc` gained an `uptime` alias.**
- **`player-only-command` lang key added** for commands that pivoted to accepting console senders.
## Verification
- **Maven suite: 175 tests, 0 failures, 0 errors** — `mvn test` green, including the new `HomeLimitsOfflinePermissionTest` (offline LuckPerms resolution), the existing `CommandVisibilityRulesTest`, and the harvest suite.
- **Placeholder chain:** `%allium_restart_time%` / `%allium_restart_seconds%` / `%allium_restart_scheduled%` now resolve through to `RestartPlaceholder`; the earlier delegated placeholders (fly, world_time, spawn_location, ...) are unaffected.
- **Time pause:** `/time pause` holds the tick across ticks and survives a restart via the `paused_worlds` table; `/time set` on a paused world is immediately undone by the re-assert task.
release
Allium v0.2.15a
0.2.15a21 авг. 2026 г.29Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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:
```text
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.
release
Allium v0.2.14a
0.2.14a12 авг. 2026 г.31Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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.
release
Allium v0.2.13a
0.2.13a11 авг. 2026 г.14Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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: `C8F9B&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:
```text
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
- **`/spy` said "disabled" without disabling everything.** With both modes active, `/spy` removed 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>` requires `allium.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 holding `allium.spy.exempt`.
- **Spawner cores ignored right-clicks on blocks.** `SpawnerCraftListener` listened for `RIGHT_CLICK_AIR` only, so using a core while looking at any block did nothing. It now handles both actions at `HIGHEST` priority 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.yml` is used to seed the `translation-probe` section 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. `ConnectionManager` now mirrors the existing sync-to-Essentials path in reverse on join, and `nickname.sync-to-essentials: false` disables 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.
release
Allium v0.2.12a
0.2.12a4 авг. 2026 г.22Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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`:
```text
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:
```yaml
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 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.** `ConnectionManager` called `loadUser(...).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 uses `getUserManager().getUser(uuid)`, the same in-memory instance LuckPerms' own commands mutate, and only falls back to `loadUser` when the player is not currently loaded.
- **Leading color codes in language strings.** `Lang` returned a literal `&r` before 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.
release
Allium v0.2.11a
0.2.11a1 авг. 2026 г.8Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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:
```text
<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 overridden per crop by adding the corresponding `interaction:` or `growth:` key to its crop file.
The new defaults are:
```yaml
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:
```yaml
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:
```yaml
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-defaults` and `liquids` sections use the defaults above; adding the blocks is optional unless different behavior is wanted.
- Existing crop files with no `growth.randomness` inherit `0.25`. Add `randomness: 0.0` per crop or under `crop-defaults.growth` to 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.0` remains explicit and will not be overwritten. Change it to `0.43` manually if a `GROUND` model is still buried.
- Crop defaults are re-read by `/harvest reload`. Changes to `liquids.*`, 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:
```text
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 `/pos1` or `worldedit:/pos1` are 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 public `ScheduledTask` interface 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 returns `false`.
- **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.
release
Allium v0.2.10a
0.2.10a20 июл. 2026 г.25Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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 `getstatic`s 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.
release
Allium v0.2.9a
0.2.9a20 июл. 2026 г.8Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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 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.
release
Allium v0.2.8a
0.2.8a19 июл. 2026 г.22Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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.
release
Allium v0.2.7a
0.2.7a19 июл. 2026 г.11Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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`.
release
Allium v0.2.6a
0.2.6a7 июл. 2026 г.20Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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()`:
```java
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/
release
Allium v0.2.5a
0.2.5a6 июл. 2026 г.18Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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/
release
Allium v0.2.4a
0.2.4a30 июн. 2026 г.26Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
# 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/
release
Allium v0.2.3a
0.2.3a29 июн. 2026 г.19Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2, 26.2bukkit, folia, paper, purpur, spigot
Что нового
**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:
```java
// 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/
release
Allium v0.2.2a
0.2.2a21 июн. 2026 г.17Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
**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
```yaml
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/
release
Allium v0.2.1a
0.2.1a1 июн. 2026 г.42Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
**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/
release
Allium v0.2.0a
0.2.0a1 июн. 2026 г.23Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
# 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/
release
Allium v0.1.9a
0.1.9a31 мая 2026 г.13Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
**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
release
Allium v0.1.8a
0.1.8a31 мая 2026 г.13Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
# 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.
release
Allium v0.1.7a
0.1.7a28 мая 2026 г.23Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
# 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.
release
Allium v0.1.6a
0.1.6a17 мая 2026 г.39Minecraft: 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4, 1.21.5, 1.21.6, 1.21.7, 1.21.8, 1.21.9, 1.21.10, 1.21.11, 26.1, 26.1.1, 26.1.2bukkit, folia, paper, purpur, spigot
Что нового
# 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 security alert feed.
- `/core alerts` now shows gamemode audit, chat filter, and ModGuard alert state.
- `/core alerts modguard` toggles whether a staff member sees ModGuard alerts.
- Staff permission bundles now include `allium.alerts` and `modguard.notify`.
## Custom Items
- Added the `item_renamer` custom item.
- Item Renamer uses a name tag item with Oraxen model `oraxen:item_renamer`.
- Holding an Item Renamer grants temporary ItemEdit rename permissions.
- Supported temporary permissions include MiniMessage, format, and color rename access.
- Players with direct ItemEdit rename permission are not managed by the Item Renamer system.
- `/rename <name>` and `/ie rename <name>` are intercepted for item consumption.
- The renamer cannot be used on air or to rename itself.
- One renamer is consumed shortly after a successful rename command.
## World And Ore Generation
- Added the custom ore generation manager and listener for applying configured Oraxen ores to generated chunks.
- Added pre-generation plumbing in the ore manager for processing chunks around a configured center/radius.
- Ore generation remains configurable under `ore-generation`.
- Added safe End Gateway teleport handling near the main End island.
- End Gateway handling is configurable with `end-gateway.max-distance-from-center` and `end-gateway.search-radius`.
- The gateway safety search prioritizes same-Y landing positions, then nearby Y levels, then an expanding perimeter search.
- Gateway handling rejects unsafe ground and blocked feet/head positions.
- A short cooldown prevents repeated gateway handling spam.
## Inventory And Restore QoL
- Offline inventory handling was expanded for restore workflows.
- Restore GUI support now works against target UUIDs so staff can restore offline players more easily.
- Inventory snapshot/restore GUI code was expanded across inventory, ender chest, options, and formatting views.
## Command Hiding And Alt Accounts
- Added an `alt` hide group to `hide.yml`.
- The `alt` group is blacklist mode by default.
- The default blocked alt commands are `pay`, `jobs`, `quests`, `tasks`, `ah`, and `auctionhouse`.
- Tab completion hiding remains configured with `^`.
## Config And Permissions
- Added `allium.gradientname`.
- Added `allium.party.staff`.
- Added `allium.locatorbar.help`.
- Added `chat.minimessage.gradient.animation`.
- Added `modguard.bypass`.
- Added `modguard.notify`.
- Added `nickname.sync-to-essentials`.
- Added `end-gateway.max-distance-from-center`.
- Added `end-gateway.search-radius`.
- Plugin metadata now uses `${project.version}` from Maven.
- Maven project version is now `0.1.6a`.
- Plugin main class moved to `codes.castled.allium.PluginStart`.
- Plugin author metadata changed to `castledking`.
## Fixes
- Fixed `/local` and `/global` alias handling by using the executed command label instead of only the Bukkit command name.
- Fixed local chat accidentally behaving like global chat through shared aliases.
- Fixed DiscordSRV inbound messages not appearing in console after Allium cancels DiscordSRV events.
- Fixed raw `%tab_tabprefix%`, `%tab_customtabname%`, and `%tab_tabsuffix%` placeholders showing in tab when TAB/PAPI could not resolve them.
- Fixed far-away animated names snapshotting once and appearing paused in synthetic tab entries.
- Fixed animated tab display names fighting TAB prefix/suffix formatting by preserving TAB formatting in the animated name builder.
- Fixed missing spacing between TAB prefix and animated name in common prefix formats.
- Fixed nickname persistence/restore behavior when TAB owns the tab list.
- Fixed same-name nicknames being accepted.
- Reduced max database pool connections to lower memory usage.
- Kept party movement/TAB visibility updates throttled to avoid excessive packet churn.
## Internal And Release Engineering
- Java packages were renamed from `net.survivalfun.core` to `codes.castled.allium`.
- Removed the old command bridge manager.
- Added GitHub Actions for continuous delivery and Spigot release publishing.
- Added `.release` scripts for release note conversion, Modrinth publishing, Spigot publishing, cookie handling, and Discord notifications.
- Added a release Dockerfile and Discord webhook template.