Allium v0.2.16a
Wiki: https://github.com/castledking/Allium/wiki
Highlights
/time pausefreezes a world's day-night cycle — A world can be pinned to a single tick and persisted there across restarts. Two layers cooperate:doDaylightCycleis 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
PlayerMatchernow 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.forwardcontrol 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'sAlliumVisibilityBridgeover reflection, so vanishing and party-only isolation survive Essentials' own visibility decisions instead of being undecided. - Offline home limits resolve through LuckPerms —
HomeLimitscan now answer how many homes an offline player's permissions grant, so/core sethomesno 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:
- Exact UUID (when the term parses as one)
- Exact real name, case-insensitive
- Real-name prefix — closest match wins (shortest name, then alphabetical, so
jprefersjoeoverjoey) - Nickname prefix
- Nickname contains (Essentials' display-name fallback)
Two vanish rules apply: a player hidden from the sender is never a candidate, and console matches everyone. PlayerMatcher.tabComplete returns real in-game names sorted best-first, with nickname-only matches ranked after direct name matches, and also respect visibility.
The roll-out replaced Bukkit.getPlayer/getServer().getPlayer/tab-complete filtering in /tp (every path), /msg and mail recipients, /seen, /fly, /god, /balance, /cheque, /home, /locator bar show|hide|reset, and /core (restore, dialog show, sethomes, homebyplayer). /seen also dropped its player-only restriction: it now runs from console and reports to CommandSender instead of a Player, with sounds/particles skipped for console users.
Anti-bot
AntiBotListener tracks players currently in their first-ever session (from PlayerJoinEvent when hasPlayedBefore() is false). If their very first chat command is one of anti-bot.commands-to-punish (default plugins, pl, version), the command is cancelled and every line of anti-bot.punishment-command is dispatched from console with %player% substituted:
anti-bot:
commands-to-punish:
- plugins
- pl
- version
punishment-command:
- "ban %player% -s [SC] Bots aren't allowed on this server"
Root-command extraction strips the leading / and any namespace: prefix, so minecraft:plugins triggers too. A player holding allium.antibot.bypass is skipped. The guard fires only once per player (the set entry is consumed by the first command), and leaving the server clears it.
ModGuard translation probe: three modes and a control line
The probe still works through the MC-265322 ghost-sign trick, but the plumbing is different:
- Multi-probe signs. Up to
CHECKS_PER_SIGN(3) checks are packed onto one sign, one per line, and the echoed lines are evaluated as a batch before the next sign is queued. Fewer sign round-trips means faster full sweeps and less disruption to the victim's screen. - Control line. The fourth line always carries the vanilla
key.forwardkeybind. If it comes back as the rawkey.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:inmodguard/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).isKeyPlusLettertreatskey+ exactly one letter as a clean echo, which removes the WaylandCraft-style false positive while still catching real keybind resolutions. - Layer 1 expanded.
banned-modsnow 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 ownClientLanguage, so the vanilla resolver never sees aliquidbounce.*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§Blegacy hex in chat no longer crashes the formatter.FormatChatListener.convertLegacyToMiniMessagenow rewrites the six-section-code form to<#RRGGBB>before the single-code replacements run.- Per-gamemode inventories are upserted.
CreativeManagernow persists thePlayerInventoriessnapshot throughDatabase.savePlayerInventories(aMERGE INTO player_inventoriesupsert) when a game-mode inventory is saved, so the creative/survival swap is durable rather than shuttle-only. /gcgained anuptimealias.player-only-commandlang key added for commands that pivoted to accepting console senders.
Verification
- Maven suite: 175 tests, 0 failures, 0 errors —
mvn testgreen, including the newHomeLimitsOfflinePermissionTest(offline LuckPerms resolution), the existingCommandVisibilityRulesTest, and the harvest suite. - Placeholder chain:
%allium_restart_time%/%allium_restart_seconds%/%allium_restart_scheduled%now resolve through toRestartPlaceholder; the earlier delegated placeholders (fly, world_time, spawn_location, ...) are unaffected. - Time pause:
/time pauseholds the tick across ticks and survives a restart via thepaused_worldstable;/time seton a paused world is immediately undone by the re-assert task.