Below are the known problems from all systems: the symptom as an admin sees it, then
the cause and what to do about it. Every entry is checked against the sources, Issues_Discovered.md or
the "Fixed" entries in the changelog — no guesses make it here.
Entries are grouped by system; the same entries are repeated at the end of each system's own page, next
to its commands.
/payconfirmtoggle does nothing to /pay#
Economy
A comment in MiscItemCommands promises the behaviour of Essentials Commandpayconfirmtoggle: "toggle whether /pay asks for confirmation". The command puts the UUID into an in-memory Set, and the getter isPayConfirmDisabled is not called anywhere else. PayCommand has neither a confirmation nor any reference to this Set — the transfer goes through right after the balance, tax and paytoggle checks. The flag is not even written to disk; a restart clears it.
What to do: Do not promise players "confirm the transfer first". If you need confirmations, this is not the command; currently it only prints a chat line about its internal flag.
/pwarp does not teleport to the player warp#
Teleportation
Issues_Discovered.md: "Player Warps having issues". In PwarpCommands.executePwarp, after a successful getPlayerWarp, warpManager.teleportToWarp(player, warpName) is called — a lookup in the server warp map. The code comment: "Reuse teleport logic (may need adjustment)". The teleportToPlayerWarp method exists and is marked admin-only (OP 4), but neither /pwarp nor /warp calls it. Symptom: /setpwarp shop + /pwarps show shop, /pwarp shop replies not_found or takes you to the server /warp shop if the name matches.
What to do: Do not use pwarps as players' public points. For a working teleport — /home or /warp. Fix: in executePwarp teleport by the TeleportLocation from getPlayerWarp, not teleportToWarp.
Two different /tpo under one literal#
Teleportation
DirectTeleportCommands.registerTpoCommand (if the teleportation module and commands.tpo are on) attaches /tpo <player> → teleportToOfflinePlayer (PlayerData.lastLocation), permission neoessentials.teleport.admin.tpo. Later ServerAdminCommands.registerTpo attaches /tpo <target> → online teleportTo bypassing tptoggle, permission neoessentials.teleport.tpo. The argument names differ (player vs target), both are word: the tpo root has two children. Which execute /tpo Steve reaches on a particular Brigadier version cannot be determined unambiguously from the source.
What to do: For online bypassing toggle, rely on /tpo only if commands.tpo is off (then the ServerAdmin node remains). Offline — always /tpoffline. Do not document /tpo as an "offline teleport".
/tpaall sends a request only to the first player#
Teleportation
WorldInteractionCommands.executeTpaAll calls sendTeleportRequest(hub, online, TPAHERE) in a loop. The manager keeps one outgoing request per UUID (sentRequests): the second iteration replies already_sent and rolls back. Result: "sent N" is almost always 1 (or 0 if the only candidate has tptoggle off). This is not the maxPendingRequests queue.
What to do: To gather everyone — /tpall (forced) or send /tpahere by hand. /tpaall in its current form is not a mass invitation.
An IP mute from the panel does not block chat#
Moderation
MuteManager.muteIP / the ip_mutes collection / the /api/moderation/ipmute routes exist, and isIPMuted is declared. ChatMuteGuard and ChatHandler check only isMuted by player name; there are no other isIPMuted calls in the mod. There is no in-game IP mute command either. A record appears in storage, while chat, /msg and mail work as usual.
What to do: Mute the name via /mute or POST /api/moderation/mute. An IP ban (/banip) still keeps the player off the server — that is a different mechanism.
The staff ping for /report is not localized#
Moderation
/modhistory uses localized strings and a calm format with the reason, issuer and date. After /report, online players with neoessentials.moderation.reports still get the raw line [Report] <nick> reported <nick>: <reason>, bypassing lang.
What to do: View punishment history with /modhistory <player>. For pings about new reports use the panel if you need a Russian-language interface.
/whisper is in the registry but not in Brigadier#
Chat and messages
registry.registerCommandWithPermission("whisper", …) ends up in the inventory and in the site's command table, but MsgCommand.register attaches trees only to msg, tell, w, message, pm. There is no whisper literal — the command does not run and does not complete. /pm, on the contrary, works but is not in the mod's registry.
What to do: Use /msg / /tell / /w / /message. Do not promise /whisper until it is registered in MsgCommand.
/ignore does not hide public chat#
Chat and messages
IgnoreManager is called from /msg, /reply and the online branch of /mail send. ChatHandler does not read the ignore list on ServerChatEvent — the player keeps seeing channel messages (local/global/staff/team). There is no /ignorelist command: getIgnoreList() is not exposed anywhere.
What to do: Ignore does not work for "not seeing someone in chat". To remove an ignore — /unignore with an online name; you need both nodes neoessentials.chat.unignore and neoessentials.chat.ignore. A forgotten name — edit DataStore by hand.
/ac is helpop, not admin chat#
Chat and messages
The HelpopCommand comment calls the alias "admin chat", the inventory says "Request help from staff". The actual registration: the same helpop/ac/amsg → sendHelpRequest. The staff channel from chat.channels is registered by ChannelCommands as /staff (aliases mod, admin, s) and requires neoessentials.chat.staff.
What to do: Staff channel: /staff or the @ prefix. Calling for help: /helpop//ac//amsg and the neoessentials.helpop.receive permission for moderators.
Mail sendall does not write offline mailboxes#
Chat and messages
The comment promises a broadcast "to all players", but the loop runs over server.getPlayerList().getPlayers(). Anyone not online at that second does not get the mail and will not see it on join. clearall, on the contrary, clears the whole loaded MAIL_BOX (the entire file).
What to do: Do mass mailing to offline players with an external script over mail_data.json on a stopped server, or /mail send to each player.
The shipped team command breaks vanilla /team#
Chat channels
chat.channels.team.command = team. dispatcher.register(Commands.literal("team")) takes over the name of the vanilla scoreboard /team. This is not a parser bug — the conflict is built into the default config. The aliases t and teamchat do not clash with vanilla.
What to do: Rename the channel's command (keep teamchat) or set team.enabled: false if you do not use FTB Teams, then restart.
Chat channels
The Javadoc of getChannelDisplayName still claims that one broken literal aborts registration of all channels after it. ChannelCommands already has a per-channel try/catch. This does not affect behaviour, only reading the sources.
What to do: Go by ChannelCommands.java, not by that comment.
`permissionSync.permissionMappings` does not grant permission nodes#
Discord integration
DiscordPermissionSync.mapDiscordRoleToPermissionGroup takes permissions.get(0) and passes it to user.setGroup. Group existence is not checked. The shipped discord_auth.json starts the lists with neoessentials.*, neoessentials.dashboard.temppass etc. — these are not group names. Also, reloadConfig() is never called: after editing the file the singleton keeps the old mapping until a restart, even though /neoe reload clears the JSON cache.
What to do: Disable permissionSync.enabled and use discordrolesync.json. If you keep this path, the first list element must be a group name from permissions.json, and edits need a restart, not just /neoe reload.
Enabled `discordRoleSync.enabled` on a running server — the periodic pass stays silent#
Discord integration
DiscordRoleSyncTask.start() is called from ServerStartedEvent and exits immediately if enabled was false then. The join handler reads the flag each time and works after /neoe reload. But nobody starts the ScheduledExecutorService — a role change without rejoining is picked up only after a restart (or not at all while the player is online).
What to do: Enable role sync for the first time with a restart. After that /neoe reload is enough for the role map; changing intervalSeconds again needs a restart.
The native relay detector does not refresh on `/neoe reload`#
Discord integration
simple-discord-link.toml / mc2discord.toml are parsed in the adapter's initialize() at startup. Editing the bridge's own config or toggling its native keys without a restart keeps the old suppression set: either duplicates come back, or NeoEssentials stays silent although native is already off.
What to do: Restart the server after editing the bridge config. Check the WARN at startup and nativeRelayWarnings in the panel status.
The bridge's native relay can still double the prefix#
Discord integration
Messages that SDLink sends itself from chat.playerMessages read Player.getDisplayName() (which already contains the tablist scoreboard team prefix) and separately add the rank from LuckPerms/FTB Ranks. That is the bridge's closed code. NeoEssentials adapter messages since build 68 take the prefix from PermissionAPI and do not double it. The detector mutes the NeoEssentials default route exactly when native is on — so what remains in Discord is the doubling native one.
What to do: Turn off the bridge's native relay for that event and restart, so only the NeoEssentials adapter posts to the channel. tablist.json → nametagSettings.enabled: false removes the prefix from the tablist/nametag entirely — it is not a targeted Discord fix.
kits.pastebinCreatekit returns a fake URL and does not create the kit#
Kits
uploadToPastebin in CreateKitCommand is a stub: it always returns https://pastebin.com/mock/…, with no HTTP call. With the flag on, the branch that saves to the DataStore is skipped entirely, so /kit does not see this kit.
What to do: Keep kits.pastebinCreatekit: false. If you need the JSON, take it from the kits collection in storage.
/invsee used to copy items into the viewer's inventory (fixed)#
Items
Before build.214 /invsee//inv//ec//enderchest opened a ChestMenu over a SimpleContainer with .copy() of the stack. The client could pull out copies while the originals stayed with the target. Now read-only goes through buildReadOnlyMenu (mayPickup/mayPlace = false). Editing went through a menu without the viewer's hotbar slots — a desync; viewer slots have been added. The aliases are no longer redirect() without requires() (the permission bypass on /inv//ec is closed).
What to do: On the current build duplication through the read-only window does not reproduce. If a player "took" items out of /invsee, it is either an old build or they have neoessentials.invsee.edit.
/ciconfirmtoggle has no effect#
Items
MiscItemCommands.isCiConfirmDisabled is not read by any /clearinventory//ci call: there is no confirmation, the toggle only prints "enabled/disabled" in chat. The state is in memory, without DataStore.
What to do: Do not promise players a preview before clearing. If you need a safeguard, do not grant neoessentials.item.clearinventory.
/pong and /fw are in /help but not in Brigadier#
Utility commands
NeoEssentials.java calls registerCommand("pong") and registerCommand("fw"), so the lines end up in the HelpCommand catalog. Neither PingCommand nor FunCommands creates Commands.literal("pong") / literal("fw"). Completion and parsing follow the tree: /pong and /fw are unknown commands (or vanilla/another mod, if someone else took them).
What to do: Ping — /ping. Firework — /firework clear|power|fire|color. Do not promise players aliases from /help until the literals appear in the source.
/info is labelled as a whois alias in the catalog but does something else#
Utility commands
registerCommand("info", "Get player information (alias)") describes whois, but WhoisCommand does not register an info literal. FunCommands attaches /info to printing the MOTD + a footer about /rules. Permission neoessentials.info, no arguments — /info Steve does not work as whois.
What to do: Player data — only /whois . Treat /info as a short MOTD.
/compass prints the card twice#
Utility commands
In the no-target branch CompassCommand calls showCompassInfo(...) twice in a row. The player sees a duplicated "facing / coordinates / biome" block. The [player] branch is called once.
What to do: Until it is fixed, read the first card. /compass for someone else has no duplicate.
/whois does not show offline players#
Utility commands
The word branch calls showOfflinePlayerInfo → getOfflinePlayerSeenInfo, which always returns Optional.empty() (an "integrate with SeenCommand" stub). Even with neoessentials.whois.detailed an offline name ends in player_not_found. Online — EntityArgument.player() — works.
What to do: Check offline status with /seen (seen_data.json). /whois only for those currently in the world.
Bare /fireball requires the nonexistent node fireball.fireball#
Utility commands
The root requires checks neoessentials.fireball. executeFireball for the default type "fireball" additionally requires neoessentials.fireball.fireball or neoessentials.fireball.*. There is no fireball.fireball node in PermissionRegistry. A player with only fireball (and no OP bypass) gets no_perm_type. The trident suggestion is in PROJECTILE_TYPES but not in the switch — a large fireball spawns.
What to do: Grant neoessentials.fireball.* (or OP with opsBypassPermissions). For the trident do not expect a separate entity.
/recipe, /give, /kill, /xp, /effect override vanilla#
Utility commands
The mod registers the same literals after vanilla. Lost: /recipe give, /give with selectors and components, /kill @e, full /xp add/query, vanilla /effect. The mod removes /help via reflection and installs its own. This is not a parsing failure — the last tree wins.
What to do: Vanilla parsing — with the namespace prefix: /minecraft:give, /minecraft:kill, /minecraft:recipe, /minecraft:xp, /minecraft:effect, /minecraft:help.
/rest does not skip the night#
Utility commands
The upstream wiki says "Skip the night (vote)". The implementation is resetStat(TIME_SINCE_REST): it zeroes the insomnia counter that makes phantoms spawn. The game day, weather and sleep vote do not change.
What to do: Night — /time night / /day or vanilla sleeping. /rest — anti-phantom only.
/away does not clear AFK while trackCommands is on#
AFK
CommandEvent fires before executes. For /away (not in the default excludedCommands) AfkCommandHandler calls updateActivity: if the player is already AFK, the status is cleared. Then toggleAfk sets AFK again — chat may get both the return message and "is now AFK", and the player stays AFK. /afk is in the exclusion list, so toggling back and forth works.
What to do: Return with /afk or by moving. Or add away to afk.excludedCommands.
The `titleFrames` key from the original wiki is not read#
Scoreboard
ScoreboardManager.loadConfig() takes title frames only from title (string or array). The loader has no titleFrames field. Title animation is defined by an array in title. The shipped comment and the ScoreboardSystem.md page disagree with the code.
What to do: Write frames like this: "title": ["frame1", "frame2"]. Do not add a separate titleFrames — it will be ignored.
`toggleCommandEnabled` has no effect#
Scoreboard
The original wiki describes a toggleCommandEnabled key. The loader does not read it; the /scoreboard toggle branch is registered with the rest of the command as long as the module and commands.scoreboard are on.
What to do: To take toggle away from players, revoke the neoessentials.scoreboard.toggle node. To remove the whole command — commands.scoreboard: false and a restart.
Saving a board through the web panel drops `refreshMultiplier`#
Scoreboard
toConfigJson() / boardToJson() serialize name, priority, conditions, title, lines — and do not write refreshMultiplier. The next save from the panel overwrites scoreboard.json without that field; after reload the multiplier is 1 again.
What to do: Set refreshMultiplier manually in the file and do not save that board with the panel button, or restore the field in the file after saving.
Scoreboard
resolveText substitutes both {player} and {displayname} via player.getName(). /nick does not affect this line. The placeholder name promises a display name; the code returns the registry name.
What to do: Do not expect a nickname in {displayname} on the sidebar. For lines that need a nickname, use the nickname placeholder that PlaceholderAPI actually resolves and check it with /scoreboard preview.
Leaderboards
The shipped comment and LeaderboardSystem.md say entryFormat controls both chat and GUI item names. LeaderboardMenu.buildEntryStack always builds Component.literal("#" + rank + " " + name) and never reads definition.entryFormat(). On the shop_sales board, chat has medals and colours; the chest does not.
What to do: Treat /leaderboard <board> chat as the only place the template works. The GUI shows only rank, name and the value in lore.
Economy invalidation is tied to the `money` id#
Leaderboards
BaltopCommand.invalidateCache() invalidates LeaderboardManager.getBoard("money") after /pay and /eco. An economy board with another id is not found by this call. /baltop itself still updates.
What to do: Keep the money id, or accept the delay until refreshInterval / a manual /leaderboard reload.
`{network_online}`, `{server_online:X}` and `{current_server}` always show local data; `/tablist proxy status` is forever `detected=false`#
Tab list
The proxy integration does not work in either direction. Outgoing messages: sendBungeeMessage is a stub; its javadoc says outright that encoding via StreamCodec on NeoForge 1.21.1 is not done and postponed; it only writes a debug line to the log and sends nothing. Incoming: the onPluginMessage handler that parses GetServer/GetServers/PlayerCount replies is not called from anywhere — the plugin message channel is never subscribed (the onServerStarted handler in TablistEventHandler is empty, just a comment). So serverPlayerCounts stays empty, {network_online} falls back to the local counter, {server_online:ANYTHING} gives 0, and {current_server} gives the proxy.serverLabel value.
What to do: Do not rely on network placeholders. If you need to show a number right now, set it manually: /tablist proxy setserver Lobby 12 (this is the only working data source for {server_online:Lobby} and the {network_online} sum). proxy.enabled: false is the shipped default for a reason.
`excludeServers`, `hiddenServers`, `playersByServer` and `showNetworkPlayers` have no effect#
Tab list
The values are read from tablist.json and printed in /tablist layout info, but the methods that should apply them (isHiddenByServer, isExcludedServer, groupedByServer, isShowNetworkPlayers) are not called from anywhere in the mod. Layout by proxy server is not wired into rendering, and since there is no data from the proxy anyway (see above), filters by server name are doubly pointless.
What to do: Treat these four keys as declared for the future. Group players via layout.groupSections by permission groups — that branch is fully implemented.
A removed or renamed `fakePlayers` line keeps hanging in the tab after `/tablist reload`#
Tab list
On reload the in-memory entry list is cleared and re-read from the config, after which only new entries are sent to players. No removal packet (ClientboundPlayerInfoRemovePacket) is sent, and the per-viewer set of already inserted UUIDs is not cleared, so the client keeps drawing an entry that is no longer in the config. Changing the id of an existing entry has the same effect: the UUID is derived from id, the old one stays and a new one appears next to it. The /tablist fakeplayer remove <id> command does not have this problem — it properly sends the removal packet to all online players.
What to do: After any edit to the fakePlayers section run /tablist fakeplayer refresh — it removes all entries for everyone and inserts them again. A player rejoining the server also clears their tab.
`/tablist layout sort` toggles nothing#
Tab list
In the command tree the layout sort branch has no on|off argument — the node simply runs and prints an informational message. The class's own header javadoc and the original wiki page claim /tablist layout sort [on|off], but no such branch is registered, and typing on/off is rejected as an unknown argument.
What to do: Change layout.sortByGroupWeight in tablist.json and run /tablist reload. You can check the current state with /tablist layout info or /tablist info.
Manual `/tablist proxy setserver` counters survive `/tablist reload`#
Tab list
ProxyIntegration has a reset() method with the comment "called on reload", but it has no callers at all. loadConfig() re-reads only the proxy section settings, while the accumulated serverPlayerCounts, the networkOnline sum and the "player → server" map stay in memory.
What to do: Overwrite the value with the same /tablist proxy setserver <server> 0 or restart the server — there is no other way to reset them.
The prefix/suffix disappeared for all players after a reload or after a new player joined#
Tab list
Historically this was caused by two independent bugs, both already fixed and described in detail in code comments. First: before the main team update, a separate sorting pass ran that moved players to a neL_... team without a prefix; the "already sent" cache did not notice and did not resend the prefix, so after the very first update cycle everyone was left with an empty prefix until /tablist reload. Second: the team was named just by group, so all players in a group shared one team object with one shared prefix/suffix — whoever's update ran last, that text was seen by everyone else (new connections are processed last, so it looked like "a player joined and everyone's suffix reset"). Now the team name includes a hash of the actually built prefix+suffix, and the extra sorting pass is gone.
What to do: If the symptom reproduces, you have a build from before the fix — update. If prefixes still drop on a fresh build, check whether another mod manages scoreboard teams: a player can be in only one team, and any third-party /team join overwrites what the tablist set.
`/tablist disable` does not turn off everything, and not permanently#
Tab list
The disable branch sets a flag in memory and sends an empty header/footer packet. It does not remove fake lines or grid filler lines, does not dismantle scoreboard teams and does not touch tablist.json. From then on updatePlayer exits on its first line, so nobody will remove the leftover entries. Any /tablist reload, /neoe reload or restart restores the enabled value from the config.
What to do: Before /tablist disable run /tablist fakeplayer refresh… or rather, to really turn the system off, set "enabled": false in tablist.json (or modules.tablistEnabled: false in config.json) and restart the server. Leftover decorative lines disappear for a player when they rejoin.
"You have no keys" when opening by command, although a key item is in your hand#
Crates
Contrary to the English wiki and the changelog, /crate open <crate> does not take key items into account: the command handler checks only the virtual balance and passes null instead of the item in hand. A physical key works only when right-clicking a block registered with /crate admin setblock.
What to do: Either place a physical block (then key items work as intended) or give virtual keys with /crate key give. If a crate is opened only by command, do not give key items for it.
Closing the window during a roulette animation before the reel stops eats the key without a reward#
Crates
The key is deducted before the GUI opens, and the reward is given at the end of the animation. In roulette each next step is scheduled from the previous one, and closing the window stops the scheduler — the chain breaks, the finale never comes, grantReward is not called (a RejectedExecutionException may appear in the console). In sequential all frames are scheduled up front, so the reward is given even if the window is closed early, and instant gives it before the window even appears.
What to do: Warn players not to close the window until the scroll ends; for an impatient audience use /crate admin setanimation <crate> sequential or instant. To return the key to an affected player: /crate key give <player> <crate> 1.
A typo in the crate id in votifier.json sends keys nowhere#
Crates
The comment in votifier.json promises that crediting is "silently skipped" without a matching crate, but there is no check in the code: addKeys creates a balance record for any identifier string. Keys for a nonexistent vote_crate really accumulate in storage, but are shown neither in /crate keys nor in the placeholder (both iterate only declared crates) and are never spent.
What to do: Compare the keys in the keys section with the output of /crate list — case does not matter, spelling does. Already "lost" keys are easier to reissue manually with /crate key give.
The open history file grows forever and nobody reads it#
Crates
Each opening writes a record to the crate_history collection (player, crate, reward, time). The mod has no command, web panel page or report that reads it, and on the default JSON backend each record rewrites the whole neoessentials/store/crate_history.json file — on an active server that is noticeable disk load and an ever-growing file.
What to do: Either switch storage to SQLite/MySQL, or periodically delete neoessentials/store/crate_history.json on a stopped server — it does not affect how crates work.
All crate system messages are in English#
Crates
The shipped ru_ru.json has no commands.neoessentials.crate.* keys at all. At startup missing keys are merged in from en_us, so on a server with localization.language: ru_ru players see English texts ("You don't have any keys for ...") rather than an untranslated key.
What to do: Translate them directly in your neoessentials/languages/custom/ru_ru.json — after the first start the English values are already there; the merge touches only missing keys and does not overwrite your edits.
The voting site reports "vote sent", but nothing happens on the server and the log is empty#
Votifier
All vote rejections are logged via NeoLog.debug(..., LogCategory.VOTIFIER, ...): an invalid V2 HMAC signature, a mismatched challenge, a broken 256-byte V1 block, any exception while parsing the connection. In config.json logging.categories.votifier.debug is false by default, and the debug level is written only to logs/debug.log, not the console. As a result the most common setup error — a V2 token or V1 public key copied incompletely — looks like total silence.
What to do: Set logging.categories.votifier.debug: true in config.json, run /neoessentials reload and check logs/debug.log; it will show "Invalid V2 vote signature" or "Invalid V2 vote challenge". A successfully accepted vote, in contrast, is logged at info level with the line "Received V1/V2 vote from ... for player ...".
A typo in sites.*.keys silently gives a key for a nonexistent crate#
Votifier
The comment in votifier.json promises that without a matching crate in crates.json the key is "silently skipped". In reality VoteRewardManager calls CrateKeyManager.addKeys() directly and does not check that the crate exists: the key balance is written to the crate_keys collection under the given id. The player sees the keys in /crate keys, but /crate open <id> replies "crate not found", and there is no automatic way to move those keys to a real crate.
What to do: Match the ids in sites.<site>.keys with the crate ids in crates.json character for character. After editing the config test the whole chain: /votifier testvote <site> <name> → /crate keys <name> → /crate open <id>.
/votifier genkeys generates nothing#
Votifier
Both the subcommand name and its description in the command registry promise key generation. The implementation is a single sendSuccess with a hint text: keys are created only inside RSAKeyManager.load(), which runs at server start and only if config/neoessentials/votifier/public.pem and private.pem are not on disk.
What to do: Treat genkeys as help. Real regeneration: stop the server, delete both .pem files, start the server, take the new public key from the console and paste it into every voting site panel.
V2 votes stop arriving after every server restart#
Votifier
If v2Token in votifier.json is empty, the server generates a random UUID and tries to save it back to the config. When the write fails (file permissions, a read-only config, a read-only volume), the log gets an ERROR "Failed to save auto-generated Votifier V2 token — it will regenerate every restart until this is fixed", and on the next start the token is different — the one you pasted into the voting site panel becomes invalid.
What to do: Find this ERROR in the log, fix the permissions on config/neoessentials/votifier.json or write a permanent v2Token by hand, and restart the server.
/votifier testvote is not a safe check but a real vote#
Votifier
The subcommand builds a Vote object and publishes PlayerVoteEvent directly, bypassing the whole protocol layer. All subscribers run for real: reward commands execute (including giving money and items), crate keys are credited, the player's statistics grow, the vote party counter grows and may cross the threshold right there, and all online players get the "X voted" broadcast. There is no confirmation and no undo.
What to do: Test on a service account, not a real player. Undoing the effects is manual: neoessentials/store/votifier_stats.json and votifier_voteparty.json on a stopped server.
The V1 protocol has no replay protection#
Votifier
In V2 the server sends a one-time challenge in the greeting and rejects the vote if the received challenge does not match — protection against resending an intercepted packet. V1 has no such field in the protocol, and handleV1() checks nothing similar: the same 256-byte RSA block can be sent to the port again and earn the reward as many times as you like.
What to do: Where the voting site supports NuVotifier (V2), configure that. Do not expose the listener port (8192 by default) to the internet more widely than necessary — restrict access with a firewall to the voting sites' addresses.
Votifier messages are not translated into Russian#
Votifier
The shipped data/lang/ru_ru.json has no commands.neoessentials.votifier.* keys — all 10 of the system's strings exist only in en_us.json. With localization.language: ru_ru the loader fills in missing keys with the English fallback, so both players and admins see "Vote Party: 3/50", "Claimed 1 pending vote reward(s)!" and the other messages in English.
What to do: These strings are automatically added to neoessentials/languages/custom/ru_ru.json during the merge at startup — translate them right there and restart the server. Do not enable localization.preserveCustomTranslations until the translation is done, otherwise merging new keys is disabled.
With LuckPerms/FTB almost all /permissions mutations fail with an NPE#
Permission system
The getManager() == null guard exists only in setPrefix/setSuffix. addGroupPermission, setUserGroup, createGroup, temp and context immediately call PermissionAPI.getManager().getGroup/getUser. In external mode the manager is null on purpose — you get unexpected_error with a NullPointerException, even though the log says "internal groups loaded but NOT USED". PermissionStorage.save is a no-op anyway, even if the NPE did not happen.
What to do: Do not use /permissions group|user|create|delete with an active adapter. Grant the node in LuckPerms/FTB Ranks. The prefix is set there too, not via setprefix.
FakePermissionsEX and pex-bridge are dead code#
Permission system
registerFakePexCommand was meant to attach a trimmed /pex group|user add|remove with a simulation_notice message and an OP 2 requirement. injectPermissionCommands — a separate neoessentials-pex-bridge command with OP 4. A grep of the repository finds no calls. The generator inventory marks /pex as implementedIn FakePermissionsEXCommand.java only because the file contains Commands.literal("pex").
What to do: Ignore javadoc/wiki about "fake PEX until the real plugin loads". The working /pex = /permissions.
canModifyPermissions is not wired to anything#
Permission system
PermissionManager has a 1000 ms cooldown on "permission modifications", but canModifyPermissions is not called from PermissionsCommand or anywhere else. You can spam group add without that pause; the audit is written to permissions_audit.log with permissions.auditLogging: true.
What to do: Do not count on a server-side rate limit for the command. If you need one, restrict it by permission and OP, not by this method.
An unknown role in /apikey create still creates an ADMIN key#
Web dashboard
On garbage like admins, parseRole sends sendFailure "expected ADMIN, OPERATOR, MODERATOR, or VIEWER. Defaulting to ADMIN" and returns User.Role.ADMIN. Brigadier then calls create anyway — the key appears and the token is printed. The failure does not roll back the branch.
What to do: Run /apikey list right away: if the role is ADMIN and you wanted VIEWER, /apikey revoke <id> and create it again. Do not take the red line in chat as a sign that no key exists.
/apikey cannot be disabled through the commands section#
Web dashboard
The neighbouring dashboard commands check isCommandEnabled. ApiKeyCommand.register does not: with the mod loaded, /apikey is always in the tree, gated only by the neoessentials.dashboard.apikeys node (plus the PermissionAPI OP bypass for players and OP 2 for the console).
What to do: Remove the node from non-admins. The current build cannot disable the command with commands.apikey: false.
On old builds /dashboard start was dead code#
Dashboard connectivity
The class existed but was not registered in the dispatcher — the command was "unknown". In this branch /dashboard is wired up; if the server runs an ancient jar, status/start will be unknown regardless of the config.
What to do: Check the mod version. On the current fork the command should appear in /help when the module is enabled.
A single quote in a string with {0} prints a literal {0}#
Localization
This is MessageFormat: ' starts an escaped section and swallows the placeholder. en_us/ru_ru already account for this by doubling; your own override is easy to break.
What to do: Write '' instead of '. After the edit, run /language override reload or /language reload.
{neoessentials_tps} from the upstream wiki is not in the registry#
API and placeholders
APISystem.md and the «Server-wide» table promise {neoessentials_tps} / {tps}. DefaultPlaceholderExpansion.initializePlaceholders has no tps key and no switch branch for it. TPS is computed by TablistManager.getTps and substituted only as the short {tps} in the tablist header and in chat short-form. /placeholder test {neoessentials_tps} and REST resolve leave the token untouched. Same with {player}: in the expansion it is name/username/displayname, and {player} is handled only by the tablist.
What to do: For PlaceholderAPI/REST use {neoessentials_name} and accept that TPS is not served through this registry. In tablist.json write {tps}.
The /placeholder test [player] javadoc does not match the tree#
API and placeholders
The PlaceholderCommand class header promises an optional target. Registration: test → greedy text → executes. Everything after the first word goes into the text; there is no separate player argument. /placeholder test {neoessentials_name} Steve simply resolves the string «{neoessentials_name} Steve» for the executor.
What to do: Another player's context — only via GET /api/placeholders/resolve?player=Steve&text=….
/vault convert silently skips offline accounts#
API and placeholders
This is not an unfinished TODO in the wiki: the method deliberately iterates only online players. An admin comparing baltop from SQLite across two providers will see convert_complete with the online count (often 0 to a few) and conclude the conversion «broke», although offline players are simply out of scope.
What to do: Treat the command as an online-session reconciliation, not a database migration. For a full transfer use the economy's own export/import.