EliteEssentials-2.0.10.jar
What's new
Added
/tempmute <player> <time> [reason]- mutes a player for a set duration instead of indefinitely. Accepts the same durations as/tempban(1d,2h,30m,1d12h, and a bare number meaning minutes), works on offline players, and can be run from the console. The mute expires on its own, and one that expired while the server was down is dropped on load./unmutelifts either kind, and/muteis unchanged and still permanent- Permission:
eliteessentials.admin.tempmute. The console is always allowed - Messages: new configurable
tempmuteUsage,tempmuteSelf,tempmuteInvalidTime,tempmuteSuccess,tempmuteAlready,tempmutedNotify,tempmutedNotifyReason,tempmutedBlocked, andplayerinfoTempMutedentries (placeholders:{player},{time},{reason},{by}) - A temp-muted player who tries to talk now gets the time remaining rather than a bare "you are muted", across public chat, group chat,
/msg, and/reply./playerinfoand the/eeadminMutes tab also show the remaining time - Listed in
/eehelpfor admins, alongside/muteand/unmute - Storage:
mutes.jsonentries gain amuteEndTimestampfield. Existing entries do not have it, so Gson leaves it at0, which the plugin reads as permanent. No migration needed and the file stays readable by older builds
- Permission:
- Per-group RTP ranges -
/rtpmin and max distance can now differ per permission group, so a VIP rank can be sent further out than the default rank. Advanced permissions mode only; simple mode is unchanged. Two ways to configure it, and both are empty by default so existing servers keep their current behavior- By permission: define a named range under the new
rtp.permissionRangesconfig map, then granteliteessentials.command.tp.rtp.range.<name>to the group. For example"permissionRanges": {"vip": {"minRange": 500, "maxRange": 10000}}paired witheliteessentials.command.tp.rtp.range.vip. This is a plain boolean node, not a numeric one, so unlike the existing.limit.<n>and.cooldown.<n>nodes it does not require LuckPerms specifically and works through HyperPerms or any other permission source - By group name: put the LuckPerms or HyperPerms group name straight into the new
rtp.groupRangesmap, matched case-insensitively, following the same shape aswarps.groupLimits. No permission node needed - Resolution order:
permissionRangesmatch, thengroupRangesmatch, then the existing per-worldrtp.worldRangesentry, then the globalrtp.minRange/rtp.maxRange. A group range therefore overrides a world range; per-world and per-group at the same time is not supported. When a player matches several entries, the one with the largestmaxRangewins, tie-broken by the smallerminRange - The range belongs to the player being teleported, so
/rtp <player>from an admin or the console uses that player's tier rather than the sender's. Granting theeliteessentials.command.tp.rtp.*wildcard hands out every named range, which is worth knowing if you grant wildcards by category - A range with a negative
minRange, or amaxRangebelow itsminRange, is now clamped to something usable and logged as a warning instead of throwing during the location search. This also covers the pre-existingworldRangesand global values
- By permission: define a named range under the new
{time}and{date}placeholders for chat formats - a requested addition, for servers that want a timestamp on each chat line. Both work anywhere a chat format string is accepted: every entry inchatFormat.groupFormats,chatFormat.defaultFormat, and (whengroupChat.useChatFormattingis enabled) group chat, includinggroupChat.formattedMessageFormat. For example"Admin": "&8[&7{time}&8] &c[Admin] {player}&r: {message}"renders as[14:35] [Admin] Steve: Hello!- Config: three new fields under
chatFormat.timeFormat(defaultHH:mm) anddateFormat(defaultyyyy-MM-dd) are Java DateTimeFormatter patterns, soh:mm agives2:35 PM,HH:mm:ssgives14:35:07, anddd MMMgives01 Aug.timeZone(default empty, meaning the server machine's zone) accepts a zone ID such asAmerica/New_YorkorUTC, which matters because most hosts run their boxes on UTC - An invalid pattern or an unrecognized zone falls back to the default and logs a warning once per bad value, rather than throwing on every chat message. Existing configs need no changes; the fields are added with their defaults on next load
- Both placeholders are resolved from a single timestamp per message, so a message sent at midnight cannot show tomorrow's date beside yesterday's time. Substitution happens before the player's message is inserted, so typing
{time}in chat is not resolved as a placeholder - Naming note:
{time}inmessages.jsonremains a duration (5m 30s) used by cooldown, playtime, and fly messages. The two never share a string, since chat format strings do not use the duration form, but the overlap is worth knowing when reading the docs
- Config: three new fields under
Fixed
- JSON storage could permanently wipe a single player's homes, balance, and playtime - reported as isolated players losing everything at once while the server ran on
storageType: "json".PlayerFileStorage.savePlayeropenednew FileOutputStream({uuid}.json), which truncates the target to zero bytes immediately, and then streamed Gson directly into it. Any failure after that truncation left a zero-length or half-written file where the player's only copy of their data used to be. Two things caused such failures: a server crash, kill, or full disk landing inside the write window, andConcurrentModificationExceptionthrown by Gson partway through serializing, becausePlayerFileholds plainLinkedHashMap/ArrayList/HashSetcollections that are mutated without synchronization from at least three threads (the world thread via commands, theEliteEssentials-PeriodicPlayTimeSavedaemon, and theEliteEssentials-JoinQuitscheduler), and nothing serialized concurrent writers to the same file. The damage then became permanent on the next load:loadFromDiskreturned null for an unreadable file, andgetPlayer(uuid, name)treats null as "brand new player", so it built an emptyPlayerFileand the next save wrote that empty record over the remains. A zero-length file was the quietest case of all, because Gson returns null for it without throwing, so nothing was logged at any point. Four changes close this:- Player files are now serialized to memory first and only written if serialization succeeded. A
ConcurrentModificationExceptioncan no longer touch the file; the previous good copy is left exactly as it was. Serialization retries once, since a CME here is transient - Writes go to
{uuid}.json.tmpand are then atomically renamed over the target. The live file is never truncated, so a crash or full disk mid-write leaves the previous complete copy intact instead of destroying it. Falls back to a plain replace on filesystems that rejectATOMIC_MOVE.player_index.jsonis written the same way - Concurrent writes to the same player file are serialized under a per-player lock, so two threads can no longer interleave their output into one file
- An unreadable player file is now moved to
players/corrupted/{uuid}.json.{timestamp}and logged at severe level instead of being silently replaced. The data cannot be reconstructed programmatically from corrupt JSON, but the original bytes are preserved for manual recovery and the event is no longer invisible. If you have already lost data to this bug, those files are gone; this only protects from here on
- Player files are now serialized to memory first and only written if serialization succeeded. A
- A failed save on disconnect discarded the session's data -
unloadPlayercalledsavePlayerwhen the record was dirty and then removed it from the cache unconditionally. Because the save failure was caught and logged rather than propagated, an IO error or serialization failure meant the entry was evicted while still dirty, so that session's playtime, wallet changes, and homes were dropped with only a severe log line to show for it.savePlayernow reports success, and a player whose save failed is kept in memory and left dirty so it is retried by the next save and at shutdown - The periodic playtime flush raced the shutdown save -
shutdown()calledplayerStorageProvider.saveAll()while theEliteEssentials-PeriodicPlayTimeSavethread was still running, and only stopped that thread roughly fifty lines later.stopPeriodicSave()now runs before the finalsaveAll() updateIndexmutated the name index outside the lock that guards it - only the file write heldindexLock, so two threads updating different players could interleave theirremoveIfandputonnameIndexand then each persist a different view of the map. The read, the mutation, and the write now all happen under the lock. Also fixed a latentNullPointerExceptionwhenupdateIndexwas reached fromgetPlayer(uuid, name)for a stored file with no name recorded- MySQL storage never initialized and silently fell back to JSON on every startup -
SchemaManagercreated all of its indexes withCREATE INDEX IF NOT EXISTS. That clause is a SQLite extension; standard MySQL has never supported it and rejects it with a syntax error. The index onplayers(name)is the second statement increateTables(), immediately after theee_playerstable, so schema creation aborted at the first index on every start.StorageFactorythen closed the connection pool and returnedPlayerFileStorage, and the global and player-warp factories saw the now-null pool and fell back too. The result was a MySQL database holding a single emptyee_playerstable while the server ran entirely on JSON files. All eleven index statements (seven increateTables, four across the v2 and v3 migrations) now route through acreateIndex()helper that keepsIF NOT EXISTSon SQLite and, on MySQL, issues a plainCREATE INDEXand treats error 1061 (duplicate key name) as success. Catching 1061 was chosen over aninformation_schemapre-check because it is race-free and avoids an extra round trip per index. SQLite behaviour is byte-for-byte unchanged- MariaDB was never affected. MariaDB accepts
CREATE INDEX IF NOT EXISTSas an extension of its own (10.0.2 and later), which is why this only ever reproduced on standard MySQL. Reported against MySQL 8.4 - No manual database cleanup is needed. The orphaned
ee_playerstable left behind by earlier failed starts is handled byCREATE TABLE IF NOT EXISTS, and the remaining tables plusee_schema_versionare created on the next startup - Data written while the fallback was active stays in JSON. Anything saved during the period MySQL was silently inactive lives in the plugin's JSON files and does not move to MySQL on its own. Once you have confirmed
SQL storage initialized successfully (mysql)in the startup log, run/eemigration sql forceto bring it across, then/eemigration cleanupto move the old JSON files intobackup/ - The startup failure was not actually silent:
Schema initialization failed,Failed to initialize SQL player storage, andFalling back to JSON storagewere all logged at severe level. If your console showed none of these while MySQL was quietly inactive, that points at a separate log-visibility problem worth reporting
- MariaDB was never affected. MariaDB accepts
Changed
/tempbancan now be run from the console -/tempban <player> <time> [reason]works from the console and from scheduled tasks, so an automated moderation setup no longer has to shell out to/ban. Previously/tempbanextended the engine'sAbstractPlayerCommand, which rejects console senders withSender must be a player or provide the --player option!. It now extendsCommandBaseand parses arguments directly, matching/rtp,/heal,/spawn, and/warp. Player usage is unchanged, including the offline-player lookup and the kick of an online target. Bans placed from the console are attributed toConsoleintempbans.jsonand in the{bannedBy}placeholder- As with
/spawnand/warpin 2.0.9, moving offAbstractPlayerCommanddetaches the engine's built-in--playerflag and drops tab-completion of the player name for this command. Use the plain/tempban <player> <time>form /ban,/unban,/mute, and/unmuteare unchanged and still player-only. Say the word if you want those converted too
- As with
- Migration SQL dialect is now passed in rather than sniffed from the JDBC driver -
SchemaManager.migrate()picked its dialect by testing whetherDatabaseMetaData.getDatabaseProductName()containedmysql. This worked only by accident: the plugin loads MySQL Connector/J for both MySQL and MariaDB, so both reportMySQL. Under MariaDB's own driver the product name isMariaDB, the check would return false, and every schema migration would emit SQLite DDL against a MariaDB server.initialize()already knows the correct backend from the configuredstorageType, so it now passes that flag down instead of re-deriving it. No behaviour change on current builds; this closes a latent break should the driver ever change
All Relations
- All Relations
- Embedded Library
- Optional Dependency
- Required Dependency
- Tool
- Incompatible
- Include
This mod has no related projects

