COWMODSКаталог модов

Allium

A modern, secure Essentials solution

Автор: castledking5502обновлён 25 окт. 2026 г.
ЭкономикаУправлениеУтилиты

Описание

Allium

Discord GitHub Issues Wiki CASTLED CODEX

Allium is a modern Essentials-style Paper plugin built for servers that want one cohesive core instead of a pile of loosely connected utility plugins.

It combines everyday player commands, staff tooling, moderation systems, economy utilities, advanced chat handling, creative-mode restrictions, Discord integration, and custom world systems into a single package that is designed to feel practical on a real server.

If you run a survival server, a semi-vanilla community, or a staff-heavy admin environment and keep finding yourself thinking “this should already be built in”, Allium is aimed directly at that problem.

Why Allium

  • One plugin for the core systems most servers actually need
  • Strong admin and moderation tooling, not just player conveniences
  • Modern integrations with Vault, PlaceholderAPI, LuckPerms, DiscordSRV, PacketEvents, Oraxen, and Floodgate
  • Built around real server-owner workflows: hiding commands, creative auditing, chat cleanup, ticket/escalation style tooling, timed fly, vouchers, utility items, and more

Feature Highlights

Core player utilities

  • Homes, bed homes, warps, spawn, /back, offline location tracking, and full teleport request flow
  • Private messages, reply, mail, social spy, notes, and /whois
  • Economy support with balances, payments, cheques, and XP vouchers
  • Utility commands like /heal, /feed, /god, /fly, /nv, /speed, /trash, /more, /itemdb, /skull

Advanced item and reward systems

  • Flexible /give and /i handling
  • Dialog-based /rename, /lore, and nickname editing
  • Voucher items with redeem commands and weighted permission grants
  • Cheques and XP bottles as redeemable item-backed value
  • Spawner core items and spawner-crafting support

Staff and admin tooling

  • Freeze, handcuffs, vanish, creative controls, invsee, ender chest access, and detailed player inspection
  • /delmsg chat moderation with resend support and Discord mirror cleanup
  • Command/plugin hiding via hide.yml
  • Group-based hidden command inheritance with LuckPerms-aware parent support
  • Escalation-ready admin workflows and offline state tracking

Chat system

  • Configurable channel system with built-in global and staff chat
  • Discord-linked channel routing
  • Word/phrase filter with token, phrase, substring, and regex modes
  • Spam blocker with repeated-message detection and normalization
  • Hover/click formatting, placeholder support, and moderation overlays

Server protection and creative controls

  • Creative Manager with per-action permissions for place, break, use, interact, drop, pickup, spawn, and inventory behavior
  • Creative blacklists for blocks and entities
  • Command hiding and tab completion filtering
  • Anti-alt / security-oriented listener support

World and custom content systems

  • Integrated custom ore generation for Oraxen ores
  • Pregeneration command with ore placement support
  • Oraxen custom smelting hooks
  • Note block / custom block protection helpers
  • Creeper explosion regeneration and related world safety logic

Integrations

Allium is designed to work well with common modern server stacks.

  • Vault
  • LuckPerms
  • PlaceholderAPI
  • DiscordSRV
  • PacketEvents
  • Oraxen
  • Floodgate
  • mcMMO

Most integrations are soft dependencies, so you can run a smaller setup and still use the rest of the plugin.

Good Fit For

  • Survival and SMP servers that want a stronger core plugin
  • Servers replacing a pile of Essentials-style utilities with one system
  • Staff teams that need moderation, visibility control, and audit-friendly tools
  • Servers using DiscordSRV, PlaceholderAPI, LuckPerms, and custom content plugins

Quick Examples

  • Give staff their own live in-game + Discord-linked staff chat
  • Hide /plugins, /version, and sensitive tab-completes from normal players
  • Let players use temporary fly time without permanent fly permission
  • Turn XP, money, or perks into redeemable items
  • Run /delmsg and clean up moderated chat quickly
  • Generate custom silver/galena ores directly in world generation

Installation Overview

  1. Install Allium on a Paper server.
  2. Add Vault and LuckPerms if you want economy/chat/group integration.
  3. Add PlaceholderAPI if you want placeholder-driven formatting.
  4. Add DiscordSRV if you want Discord-linked channels and staff relay.
  5. Add PacketEvents if you want the best chat resend/deletion behavior.
  6. Review config.yml, lang.yml, hide.yml, and the chat/ module files.

Documentation

For usage instructions, see the official Allium documentation.

Current Direction

Allium is actively growing beyond a traditional Essentials replacement. The current focus is on:

  • better chat moderation and channel systems
  • stronger admin workflows
  • smoother Discord integration
  • deeper custom content/world integration
  • keeping one coherent core instead of fragmenting into many separate plugins

Галерея

Advanced item givingStaff AlertsDetect Mods

Последние версии

Все версии →
release

Allium v0.2.19a

0.2.19a
25 окт. 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.18a
3 окт. 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.17a
21 сент. 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.16a
10 сент. 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.15a
21 авг. 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.14a
12 авг. 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.13a
11 авг. 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: `&#8C8F9B&lVIP` was pushed through the MiniMessage parser, which printed the codes instead of applying them. Both systems now filter against their own permission sets and fold into a single MiniMessage string that is deserialized once, so one message can mix `&a`, `&#RRGGBB` and `<gradient>` freely. - **One answer for how many homes a player gets** — Numbered permissions and the `/core sethomes` override disagreed depending on which code path asked. The effective limit is now the higher of the two everywhere, `/core sethomes <player>` shows the breakdown, and `%allium_homes_max%` reports `unlimited` instead of a number nobody set. - **`/seen` shared IPs are symmetric** — Shared accounts were matched against the target's *current* address only, so the moment either account changed IP they stopped listing each other. The lookup now walks the full IP history. - **Reliability** — `%allium_nickname%` parses without PlaceholderAPI or Essentials, Essentials-only nicknames are imported on join, `/spy` now clears targeted spying along with global, spawner cores respond to right-clicking a block, and the ModGuard translation probe stops reporting a hit when the client echoes the raw key. ## Technical Details ### Permission migration and blanket grants Two migrations carry SFCore's `core.*` nodes forward: `performPermissionMigration()` walks every Vault group once at startup, and an inner `PlayerPermissionMigrationListener` walks each player on join until `player_migration_completed` is set. Both decide what to migrate with `playerHas` / `groupHas`. Those are permission *checks*. LuckPerms resolves them through wildcards, and Bukkit resolves an unregistered node — which every `core.<node>` is, since only `allium.*` is declared in `plugin.yml` — to its `OP` default. A player holding `*`, and any op, therefore answered true to all ~120 entries in `permissionsToMigrate`. The migration dutifully "moved" each one and called `playerAdd` for its `allium.` equivalent, permanently writing a full permission list onto an account that had none of it. Staff handing out `*` to help a player with something produced exactly this. **The probe.** Before any writes, the account is asked about `allium.migrationprobe.<random UUID>` — a node that cannot legitimately be granted. A true answer means everything resolves true and there is nothing meaningful to read, so migration is skipped: ```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 `&#8C8F9B&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 `&#8C8F9B&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.12a
4 авг. 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.11a
1 авг. 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.10a
20 июл. 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.