
Список изменений
Allium v0.2.12a
Wiki: https://github.com/castledking/Allium/wiki
Highlights
/core hide fixrepairs commands Allium cannot classify — Allium's resolver pipeline asks a chain of adapters which permission a command requires. When every adapter declines, the result isUNKNOWNand Allium allows the command by default, which means its own no-permission message never appears. ExcellentQuests'/questshit exactly this. The new/core hide fix <command> <permission>records a permission for that command, and any unresolved command is now announced on console once per session so it can be found in the first place.%allium_gradientdisplayname%is colorable again — The placeholder used to always come back pre-colored, so writing&6%allium_gradientdisplayname%in a chat format or join message had no visible effect. When GradientPlus has no gradient or static color selected, the placeholder now returns a plain, uncolored name and the surrounding format decides the color. A real GradientPlus selection still renders its animated gradient exactly as before.#<message>respectsallium.staffchatagain — The permission check existed on the tracking pass but not on the pass that actually routes the message, so any player could type#hellointo staff chat and be subscribed to staff chat afterwards./delmsgno longer refuses the next identical message — Deleting a message also marked any message with the same text sent within three seconds as deleted, so a repeated line reportedMessage has already been deleted.- Reliability — Alt-account group reads use LuckPerms' live in-memory user instead of a fresh storage copy, and a leading color code in a language string emits
§r§xrather than the literal&r.
Technical Details
Manual command permission overrides
Allium resolves a command's required permission through an adapter pipeline — Paper basic commands, Brigadier, NightCore, vanilla, LuckPerms, and finally registered permission nodes. When no adapter can classify a command, PermissionResult.unknown(true, command) is returned: Allium allows the command and leaves enforcement to the command's own framework.
/quests from ExcellentQuests resolved this way. The NightCore adapter correctly determined that the command needs no permission, but did not treat itself as authoritative enough to claim the result, so the pipeline fell through to UNKNOWN. Nothing was insecure — the framework still enforced its own rule — but Allium could not substitute its own formatted no-permission message, and there was no way to tell this was happening without debug-mode enabled.
Two changes address that.
Unresolved commands are announced. The first time a command resolves as UNKNOWN in a server session, CommandManager force-logs an ERROR to console regardless of debug-mode:
Could not resolve a permission for /quests - allowing by default.
Use /core hide fix quests <permission> to set one.
The warning is emitted once per command label per session, and the set is cleared on config reload. Namespaced invocations are reduced to their bare label, so /plugin:quests and /quests warn once between them.
Overrides can be set manually.
| Command | Purpose |
|---|---|
/core hide fix <command> <permission> [-denyalts] | Set or replace the permission for a command |
/core hide fix remove <command> | Delete an override |
/core hide fix check <command> | Show an override's permission, alt rule, source and author |
/core hide fix list | List every stored override |
An override is consulted only after every adapter has declined. It cannot override a real adapter decision, so setting one on a command Allium already understands has no effect on that command's permission. When an override does apply, the result carries the new ResolutionType.MANUAL_OVERRIDE and the player is checked against the recorded node with Player#hasPermission, meaning a denied command now produces Allium's standard no-permission message like any other.
fix lives under /core hide for grouping only. It does not touch hide.yml; the whitelist/blacklist group system there is unrelated and unchanged.
Alt accounts. -denyalts records a flag alongside the permission and reuses the alt detection already present in ConnectionManager and CommandManager — the Vault/LuckPerms alt group, minus players exempted in the database. No new alt-detection logic was added. Unlike the permission itself, the alt rule is evaluated for any command carrying an override, not only unresolved ones: a player who is alt-restricted, not exempt, and would otherwise be allowed is cancelled with the existing alt-account-restricted message.
Storage. Overrides live in a new command_permission_overrides table created at startup:
| Column | Notes |
|---|---|
command_label | Primary key, lowercased |
permission | The node checked against the player |
deny_alts | Boolean, default false |
source | Free-form origin tag, default nightcore |
set_by | UUID of the staff member who set it, null from console |
updated_at | Timestamp, refreshed on every write |
Writes use MERGE ... KEY(command_label) so setting an existing override replaces it in place. CommandPermissionOverrideStore keeps an in-memory copy that the resolver reads on the command path; it is refreshed after every fix/remove and on config reload, so no restart is needed. Every database method degrades to empty/false if the table is missing rather than throwing.
Gradient display names accept an external color
GradientPlus emits one color code per character. When a player has not selected a gradient or static color with /gradient, it emits its default white for every character, which is indistinguishable from deliberately choosing white.
Allium previously handled that badly in two ways. If no colors were extracted at all, it fell back to the trailing color of the player's Vault prefix and applied that to the name. If white was extracted, it built a white-to-white animated gradient. Either way %allium_gradientdisplayname% returned a string that already carried a color, so a format like &6%allium_gradientdisplayname% was overridden by the placeholder's own output.
The placeholder now treats "no extracted colors" and "every extracted color is #FFFFFF" as unset and returns the escaped visible name with no color codes and no MiniMessage tags. The surrounding format applies:
format: "&8[&6Member&8] &6%allium_gradientdisplayname%&7: &f<message>"
A real GradientPlus selection is untouched. Multi-color presets still use their first and last character colors, solid presets still pair with their nearest named Minecraft color, and both keep the two-stop phase animation introduced in v0.2.11a.
Three hard-coded color paths were removed to get there, in both GradientNameManager and the mirrored implementation in FormatChatListener:
- The
colors.isEmpty()fallback that read the player's Vault prefix and adopted its trailing color, along with thegetPrefixVault lookup it needed. extractTrailingColor, which existed only to feed that fallback.normalizeColor, whose#FFFFFFdefault silently turned an unparseable color into white.
Chat's fallback formatter continues to delegate to the same implementation as the tab-name animator, so the two cannot drift apart.
#<message> staff-chat permission
AlliumChannelManager sees each chat message twice: onPlayerChatEarly at LOWEST for tracking, and onPlayerChat at LOW, which chooses the destination channel. The # one-shot shortcut was gated on allium.staffchat in the first handler but had no permission check at all in the second.
Because routing happens in the second handler, any player typing #hello had their message delivered to the staff channel, and the shortcut's addReadChannel call then subscribed them to staff chat so they continued receiving it. The two handlers also disagreed: the early pass recorded the message as global while the routing pass sent it to staff, which left the Discord suppression state inconsistent.
Both handlers now gate on canWrite(player, staffChannelName) — allium.channel.join.staff-chat.write or allium.staffchat — matching the predicate used by /channel itself. A player without either now sends #hello to their normal channel as ordinary text.
/delmsg and repeated messages
ChatMessageManager.deleteMessage matched by message ID and by content: any stored message whose plain text overlapped the target's within a three-second window was marked deleted too. Two identical lines a second apart were therefore deleted together, and deleting the first made the second report Message has already been deleted.
That heuristic predates the logical-ID linking added in v0.2.11a, where every rendered message registers a short-lived ID that its per-viewer packet copies adopt. Those copies are already reachable by ID, so the content sweep was redundant for them and destructive for everything else.
Deletion is now by ID. Content matching survives only as a narrow fallback for packet-captured copies that never adopted an ID — a copy whose sender is the system UUID and whose ID belongs to no real stored message. That is precisely the set that ID matching cannot reach, so a deleted message still cannot reappear during a resend, while two identical messages remain independently deletable.
One related behavior is unchanged: PacketChatTrackerImpl.dedupeResendHistory still collapses adjacent identical lines by content, so if two identical messages both survive a deletion, a chat resend renders them as one line.
Other fixes
- Alt-group reads use LuckPerms' live user.
ConnectionManagercalledloadUser(...).join(), producing a fresh copy from storage. Saving that stale snapshot could resurrect groups another command had just removed, and the write bypassed LuckPerms' command log entirely. It now usesgetUserManager().getUser(uuid), the same in-memory instance LuckPerms' own commands mutate, and only falls back toloadUserwhen the player is not currently loaded. - Leading color codes in language strings.
Langreturned a literal&rbefore the section-coded color when a message began with a color code, so the raw characters appeared in chat. It now returns§r§<code>.
Verification
The Maven suite passes with 139 tests. GradientNameManagerTest continues to cover the two-stop animated gradient and its phase-midpoint continuity, confirming that removing the hard-coded color fallbacks left the GradientPlus rendering path unchanged; the new "unset means uncolored" branch needs a live player and PlaceholderAPI and is covered by in-game verification rather than a unit test. CommandPermissionAdaptersTest and PermissionCandidatesTest cover the adapter pipeline that the override store sits behind.
