promotional bannermobile promotional banner

EliteEssentials | Everything Your Server Needs and More!

Homes, warps, player warps, TPA, RTP, kits, economy, bans, mutes, warnings, admin UI, chat formatting, 500+ configurable messages, SQL storage, PlaceholderAPI, LuckPerms & HyperPerms. The most complete essentials mod for Hytale trusted by top servers.
Back to Files

EliteEssentials-2.0.10.jar

File nameEliteEssentials-2.0.10.jar
Uploader
EliteScouterEliteScouter
Uploaded
Aug 2, 2026
Downloads
171
Size
19.5 MB
File ID
8559950
Type
R
Release
Supported game versions
  • 0.5

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. /unmute lifts either kind, and /mute is unchanged and still permanent
    • Permission: eliteessentials.admin.tempmute. The console is always allowed
    • Messages: new configurable tempmuteUsage, tempmuteSelf, tempmuteInvalidTime, tempmuteSuccess, tempmuteAlready, tempmutedNotify, tempmutedNotifyReason, tempmutedBlocked, and playerinfoTempMuted entries (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. /playerinfo and the /eeadmin Mutes tab also show the remaining time
    • Listed in /eehelp for admins, alongside /mute and /unmute
    • Storage: mutes.json entries gain a muteEndTimestamp field. Existing entries do not have it, so Gson leaves it at 0, which the plugin reads as permanent. No migration needed and the file stays readable by older builds
  • Per-group RTP ranges - /rtp min 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.permissionRanges config map, then grant eliteessentials.command.tp.rtp.range.<name> to the group. For example "permissionRanges": {"vip": {"minRange": 500, "maxRange": 10000}} paired with eliteessentials.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.groupRanges map, matched case-insensitively, following the same shape as warps.groupLimits. No permission node needed
    • Resolution order: permissionRanges match, then groupRanges match, then the existing per-world rtp.worldRanges entry, then the global rtp.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 largest maxRange wins, tie-broken by the smaller minRange
    • 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 the eliteessentials.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 a maxRange below its minRange, is now clamped to something usable and logged as a warning instead of throwing during the location search. This also covers the pre-existing worldRanges and global values
  • {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 in chatFormat.groupFormats, chatFormat.defaultFormat, and (when groupChat.useChatFormatting is enabled) group chat, including groupChat.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 (default HH:mm) and dateFormat (default yyyy-MM-dd) are Java DateTimeFormatter patterns, so h:mm a gives 2:35 PM, HH:mm:ss gives 14:35:07, and dd MMM gives 01 Aug. timeZone (default empty, meaning the server machine's zone) accepts a zone ID such as America/New_York or UTC, 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} in messages.json remains 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

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.savePlayer opened new 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, and ConcurrentModificationException thrown by Gson partway through serializing, because PlayerFile holds plain LinkedHashMap/ArrayList/HashSet collections that are mutated without synchronization from at least three threads (the world thread via commands, the EliteEssentials-PeriodicPlayTimeSave daemon, and the EliteEssentials-JoinQuit scheduler), and nothing serialized concurrent writers to the same file. The damage then became permanent on the next load: loadFromDisk returned null for an unreadable file, and getPlayer(uuid, name) treats null as "brand new player", so it built an empty PlayerFile and 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 ConcurrentModificationException can 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.tmp and 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 reject ATOMIC_MOVE. player_index.json is 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
  • A failed save on disconnect discarded the session's data - unloadPlayer called savePlayer when 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. savePlayer now 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() called playerStorageProvider.saveAll() while the EliteEssentials-PeriodicPlayTimeSave thread was still running, and only stopped that thread roughly fifty lines later. stopPeriodicSave() now runs before the final saveAll()
  • updateIndex mutated the name index outside the lock that guards it - only the file write held indexLock, so two threads updating different players could interleave their removeIf and put on nameIndex and 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 latent NullPointerException when updateIndex was reached from getPlayer(uuid, name) for a stored file with no name recorded
  • MySQL storage never initialized and silently fell back to JSON on every startup - SchemaManager created all of its indexes with CREATE 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 on players(name) is the second statement in createTables(), immediately after the ee_players table, so schema creation aborted at the first index on every start. StorageFactory then closed the connection pool and returned PlayerFileStorage, 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 empty ee_players table while the server ran entirely on JSON files. All eleven index statements (seven in createTables, four across the v2 and v3 migrations) now route through a createIndex() helper that keeps IF NOT EXISTS on SQLite and, on MySQL, issues a plain CREATE INDEX and treats error 1061 (duplicate key name) as success. Catching 1061 was chosen over an information_schema pre-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 EXISTS as 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_players table left behind by earlier failed starts is handled by CREATE TABLE IF NOT EXISTS, and the remaining tables plus ee_schema_version are 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 force to bring it across, then /eemigration cleanup to move the old JSON files into backup/
    • The startup failure was not actually silent: Schema initialization failed, Failed to initialize SQL player storage, and Falling back to JSON storage were 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

Changed

  • /tempban can 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 /tempban extended the engine's AbstractPlayerCommand, which rejects console senders with Sender must be a player or provide the --player option!. It now extends CommandBase and 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 to Console in tempbans.json and in the {bannedBy} placeholder
    • As with /spawn and /warp in 2.0.9, moving off AbstractPlayerCommand detaches the engine's built-in --player flag and drops tab-completion of the player name for this command. Use the plain /tempban <player> <time> form
    • /ban, /unban, /mute, and /unmute are unchanged and still player-only. Say the word if you want those converted too
  • Migration SQL dialect is now passed in rather than sniffed from the JDBC driver - SchemaManager.migrate() picked its dialect by testing whether DatabaseMetaData.getDatabaseProductName() contained mysql. This worked only by accident: the plugin loads MySQL Connector/J for both MySQL and MariaDB, so both report MySQL. Under MariaDB's own driver the product name is MariaDB, 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 configured storageType, 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

This mod has no related projects