Welcome to the Hangar Open Beta. Please report any issue you encounter on GitHub!
Avatar for castledking

A modern, secure Essentials solution

Report Allium?

R

0.2.18a

Allium v0.2.18a

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

Highlights

Two large features, both developed since the last release and both landing here for the first time.

  • The kitchen — A new harvest/kitchen.yml adds a crafting chain built on physical placed items rather than a recipe book: flour bags that hold 256 flour, a kneading station that turns water plus flour into dough, a placed pie you assemble one ingredient at a time with the instructions floating above it, baking in a Nexo furniture stove, cooling back down if you leave it, and eating it a slice at a time. Nine pie types ship by default, including three-quality berry pies.
  • Trading cards — 36 mobs across five tiers, 180 cards in all. Cards drop from mob kills at a configurable chance, carry an integer quality from 1 to 100 that maps to one of eight named bands, level up on the XP your equipped card earns, and grant stat boosts through AuraSkills. A card is a self-contained item: its whole state lives in the item's own data, so an offline card grants exactly what its lore says it will.
  • Cards equip into a Relique slot — A new card slot definition and validator let players equip one card at a time. Boosts apply on equip and come off on unequip, and XP earned while equipped is written back to the card so it survives relogging.
  • Boost slots roll one at a time — Each card has bonus slots that open with its tier (one on Simple, five on Fabled). They roll, lock and clear individually, each at its own escalating price, so a single bad reroll no longer costs the whole card.
  • /morph on a Fabled card — The reward for merging all the way to the top tier. Wearing the mob's disguise is cosmetic (LibsDisguises is packet-only), and Allium supplies the behaviour: stealth until you strike first, mob AI that targets you like the thing you're wearing, and the mobs that witnessed the attack converging on you.
  • A card frame drawn entirely in the lore — The tier's panel is a bitmap font glyph, not a sprite pair and not a shader. One style id and one name key replace the previous per-tier, per-lore-length scheme.
  • [frame:<tier>] frames any item, from any plugin — Other plugins cannot draw the card frame themselves: they parse their own config text and none can set a tooltip style or blank an item's name. A marker lore line is something any of them can carry, so the frame is applied to the copy of the item sent to the player over a PacketEvents listener. The item on the server is untouched.
  • Chat hovers are framed too — A hover tooltip has no tooltip style, so vanilla's panel is drawn behind it whatever the text asks for. Hover lines step one pixel further left, to the panel sprite's outer pixel, and no panel pixel shows.

Technical Details

The kitchen

Flour bags

A bag is a keyed item with a count in its own data. Right-click with an empty cursor takes one out; right-click while holding the stored item adds one to your cursor stack. Left-click with an item puts it in; shift-left-click takes a whole stack out.

bags:
  flour_bag:
    item: nexo:flour_bag
    stores: nexo:flour
    capacity: 256
    lore: "<gray>Flour: <white><amount></white>/<capacity>"

The click table is deliberate, and one part of it is a bug fix worth knowing about. Plain left-click picks the bag up and moves it around your inventory — it does not withdraw. Shift-left-click is the withdraw gesture. An earlier revision made plain left-click withdraw, which left no way at all to pick a non-empty bag up and move it around an inventory.

Two behaviours are worth calling out because they look like bugs and are not. Creative mode is skipped entirely: creative inventory clicks are client-authoritative and fight any cursor change made on the server. And a bag stack must be exactly one item, because a stack of several bags would multiply whatever went in.

Kneading

Right-click a station holding a water bucket or water bottle, then flour, then more flour. A cauldron keeps its water the vanilla way and its level is the water count, so there is nothing to get out of sync; other stations (Nexo furniture or custom blocks) have no water of their own, so what you pour in is counted instead.

kneading:
  stations:
    blocks: [CAULDRON]
    nexo: []                  # Nexo furniture or custom block ids
  water:  { required: 3, bucket: 3, bottle: 1 }
  flour:  { item: nexo:flour, required: 3 }
  result: { item: nexo:dough, amount: 1 }

You can knead straight out of the bag — each click takes one flour out of it. When both counts are met the station is emptied and dough pops out.

Listing CAULDRON with water.required above 3 is a hard configuration error that disables kneading entirely, because a cauldron can never hold that much.

Pies

Right-click the top of a block with a crust to put a pie down, then right-click the pie with each ingredient in turn. The floating text above it shows what is needed next.

The default step order is egg → sugar → filling → dough. The filling step is what fixes which pie this is — once some filling is in, the rest has to match it — and the hologram cycles through every available flavour until you choose. Chocolate and pumpkin skip the dough top entirely via skip-steps: [4], since neither has a lattice:

pies:
  types:
    blackberry:
      filling: [nexo:blackberries, nexo:blackberries_silver_star, nexo:blackberries_golden_star]
    chocolate:
      filling: minecraft:cocoa_beans
      skip-steps: [4]

A pie is an ItemDisplay showing the current model plus an Interaction hitbox so it can be clicked. Both are persistent entities and all of the pie's state lives in the display's data — there is no database row to fall out of sync with, and a pie is saved and unloaded with its chunk like any other entity. The two halves are cross-linked by UUID and tagged so neither is mistaken for something else on a chunk reload.

Picking a pie up refunds what went into it: an unfinished pie gives back the crust and every ingredient recorded along the way. Breaking the block underneath gives back only the crust — the refunds are not repeated there. A pie that has been partly eaten refuses pickup with "Someone's already eaten some of this."

Baking, cooling, eating

Baking is a normal server cooking recipe, typically a Nexo recipe turning a cold pie into a baked one. The cold pie and the unbaked pie are deliberately the same item, so a pie that has gone cold can be put back in the furnace to warm up again. Nothing in Allium needs configuring for it.

Cooking happens in a Nexo furniture stove. It is a full furnace — real burn time, real fuel, real XP — with the fidelity details that make it read as one:

furnaces:
  stations:
    kitchen_stove:
      title: "Stove"
      type: FURNACE        # FURNACE | SMOKER | BLAST_FURNACE
      speed: 1.0

Recipe lookup walks the server's recipes for one whose input choice matches, so custom recipes work. A lava bucket becomes a bucket, a wet sponge converts a lone bucket in the fuel slot into a water bucket, progress drains twice as fast when the fire goes out, and fractional XP rounds up at random exactly as vanilla does. Progress is pushed to viewers through the inventory properties, so the flame arrow and progress bar animate. Hoppers cannot reach it.

Baked pies carry a cooling deadline. A placed pie that passes it flips to its cold model; a dropped one is swapped for the cold item, preserving the stack size. Pies in inventories keep their clock running but only change when they're put down or dropped, and a pie that went cold in someone's pocket comes out cold. Then four slices, with hunger and saturation configurable, each bite taking the model down one step.

Where kitchen state lives, and what breaks without what
State Where
Kneading progress Chunk data, as "water,flour"
Bag fill level Item 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.

  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.

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.

Information

Published
October 3, 2026
0Downloads

Platforms

Paper
Paper
1.20–26.3