Access Control
Package: com.hypixel.hytale.server.core.modules.accesscontrol
Access control determines who can connect to the server. The built-in system provides bans and a whitelist, both UUID-based. Mods can register custom AccessProvider implementations for additional checks.
How Access Checks Work
Section titled “How Access Checks Work”When a player connects, AccessControlModule handles PlayerSetupConnectEvent and queries all registered AccessProvider instances. Each provider returns a CompletableFuture<Optional<String>> — if any returns a reason, the event is cancelled and the player is disconnected with that message.
The built-in providers are checked in registration order (whitelist first, then bans), but all provider futures are combined with thenCombine — if providers return genuinely async futures, they may execute concurrently.
In singleplayer, a ClientDelegatingProvider is added that always allows access.
Custom Access Providers
Section titled “Custom Access Providers”Register your own access control logic:
@Overrideprotected void setup() { AccessControlModule.get().registerAccessProvider(uuid -> { if (isMaintenanceMode() && !isAdmin(uuid)) { return CompletableFuture.completedFuture( Optional.of("Server is in maintenance mode.") ); } return CompletableFuture.completedFuture(Optional.empty()); });}The AccessProvider interface has a single method:
public interface AccessProvider { CompletableFuture<Optional<String>> getDisconnectReason(UUID uuid);}Returning Optional.empty() means “allow” (no objection from this provider). Returning a reason string means “deny with this message”.
If you need external lookups (e.g. a ban database), cache locally and check the cache in your provider. Sync from the database in the background, so the provider always returns a completed future:
// keep an in-memory cache, synced from database in the backgroundprivate final Set<UUID> bannedUuids = ConcurrentHashMap.newKeySet();
@Overrideprotected void setup() { // initial load + periodic refresh getScheduler().scheduleRepeatingAsync(this::refreshFromDatabase, 0, 60, TimeUnit.SECONDS);
AccessControlModule.get().registerAccessProvider(uuid -> CompletableFuture.completedFuture( bannedUuids.contains(uuid) ? Optional.of("You are banned.") : Optional.empty() ) );}See Scheduling for the full scheduling API.
Kicking Players
Section titled “Kicking Players”Kicking is a disconnect with a reason message:
playerRef.getPacketHandler().disconnect("You were kicked.");The server sends a Disconnect packet with the reason string, then closes the connection. There is no dedicated “kick” method — disconnecting with a message is the kick.
Bans are UUID-based and stored in bans.json. Two ban types exist: permanent and timed.
Ban Types
Section titled “Ban Types”Permanent — InfiniteBan, never expires:
InfiniteBan ban = new InfiniteBan(targetUuid, bannerUuid, Instant.now(), "reason");Disconnect message: "You are permanently banned! Reason: ..."
Timed — TimedBan, expires after a duration:
TimedBan ban = new TimedBan( targetUuid, bannerUuid, Instant.now(), Instant.now().plus(Duration.ofHours(24)), // expires in 24h "reason");Disconnect message: "You are temporarily banned for <duration>! Reason: ..."
Expired timed bans are automatically pruned on load and on connection checks.
Ban Interface
Section titled “Ban Interface”All ban types implement the Ban interface:
| Method | Return Type | Description |
|---|---|---|
getTarget() | UUID | Banned player’s UUID |
getBy() | UUID | UUID of who created the ban |
getTimestamp() | Instant | When the ban was created |
getReason() | Optional<String> | Ban reason |
isInEffect() | boolean | Whether the ban is currently active |
getType() | String | "infinite" or "timed" |
Built-in Ban Limitations
Section titled “Built-in Ban Limitations”The built-in HytaleBanProvider has a public modify() method that allows adding, removing, and updating bans — and both InfiniteBan and TimedBan have public constructors. However, the banProvider field on AccessControlModule is private with no getter, so mods cannot obtain a reference to it through the public API.
The /ban command always creates permanent (InfiniteBan) bans — there is no built-in command for timed bans. TimedBan is registered as a ban parser (so it deserializes from bans.json), but no built-in code ever creates one.
For custom ban logic, register your own AccessProvider (see Custom Access Providers) or use PlayerSetupConnectEvent to block connections.
Custom Ban Parsers
Section titled “Custom Ban Parsers”Mods can register custom ban types that are deserialized from bans.json:
AccessControlModule.get().registerBanParser("myCustomBan", MyCustomBan::fromJsonObject);Whitelist
Section titled “Whitelist”The whitelist is UUID-based and stored in whitelist.json. When enabled, only listed UUIDs can connect.
The HytaleWhitelistProvider is private on AccessControlModule — mods cannot directly modify it. Use the /whitelist commands or register a custom AccessProvider for your own allowlist logic.
Operators
Section titled “Operators”The OP system is not a separate access control feature — it’s a permission group named "OP" with the "*" wildcard. Use PermissionsModule to manage it:
PermissionsModule perms = PermissionsModule.get();
// grant opperms.addUserToGroup(playerUuid, "OP");
// revoke opperms.removeUserFromGroup(playerUuid, "OP");
// check if opSet<String> groups = perms.getGroupsForUser(playerUuid);boolean isOp = groups.contains("OP");See Permissions for the full permission system.
Events
Section titled “Events”Connection events fire in a specific lifecycle order. Note that setup events and game events are mutually exclusive — if a player disconnects during setup (before fully joining), only the setup disconnect fires, not PlayerDisconnectEvent.
Lifecycle
Section titled “Lifecycle”PlayerSetupConnectEvent
Section titled “PlayerSetupConnectEvent”Fired when a player is connecting, before player data is loaded. Cancellable. No PlayerRef exists yet.
Runs on the Netty I/O thread — see threading.
| Method | Return Type | Description |
|---|---|---|
getUuid() | UUID | Player’s UUID |
getUsername() | String | Player’s username |
getAuth() | PlayerAuthentication | Authentication data |
setCancelled(boolean) | void | Block the connection |
setReason(String) | void | Disconnect reason (shown to player) |
referToServer(String host, int port) | void | Redirect to another server |
referToServer(String host, int port, byte[] data) | void | Redirect with up to 4096 bytes of referral data |
isReferralConnection() | boolean | Whether this player was referred from another server |
getReferralData() | byte[] | Referral data from the sending server |
getReferralSource() | HostAddress | Address of the server that referred this player |
eventRegistry.registerGlobal(PlayerSetupConnectEvent.class, event -> { UUID uuid = event.getUuid();
// block connection event.setReason("Not allowed."); event.setCancelled(true);
// or redirect to another server instead of denying event.referToServer("lobby.example.com", 21000);});This is where AccessControlModule checks bans and whitelist.
PlayerConnectEvent
Section titled “PlayerConnectEvent”Fired after player data is loaded, before the player is added to a world. Has a PlayerRef and allows overriding which world the player joins.
Runs on an async thread (CompletableFuture pool) — see threading.
| Method | Return Type | Description |
|---|---|---|
getPlayerRef() | PlayerRef | The connecting player |
getWorld() | World | World the player will join |
setWorld(World) | void | Override the target world |
eventRegistry.registerGlobal(PlayerConnectEvent.class, event -> { // redirect new players to a lobby world if (isFirstJoin(event.getPlayerRef())) { event.setWorld(lobbyWorld); }});PlayerDisconnectEvent
Section titled “PlayerDisconnectEvent”Fired when a fully-connected player leaves. Has PlayerRef, fires before the player is removed from their world.
Runs on the Netty I/O thread (or whichever thread called disconnect()) — see threading.
| Method | Return Type | Description |
|---|---|---|
getPlayerRef() | PlayerRef | The disconnecting player |
getDisconnectReason() | PacketHandler.DisconnectReason | Why the player disconnected |
eventRegistry.registerGlobal(PlayerDisconnectEvent.class, event -> { PacketHandler.DisconnectReason reason = event.getDisconnectReason(); // reason.getServerDisconnectReason() — if kicked/banned (server-initiated) // reason.getClientDisconnectType() — if client left voluntarily});PlayerSetupDisconnectEvent
Section titled “PlayerSetupDisconnectEvent”Fired when a player disconnects during setup — before they had a PlayerRef. Only has username/UUID/auth.
Runs on the Netty I/O thread — see threading.
| Method | Return Type | Description |
|---|---|---|
getUsername() | String | Player’s username |
getUuid() | UUID | Player’s UUID |
getAuth() | PlayerAuthentication | Authentication data |
getDisconnectReason() | PacketHandler.DisconnectReason | Why the connection was dropped |
Threading
Section titled “Threading”All connection events are synchronous (IEvent, not IAsyncEvent). The event bus calls each listener inline on the calling thread — no thread hop occurs.
| Event | Thread |
|---|---|
PlayerSetupConnectEvent | Netty I/O thread |
PlayerConnectEvent | Async thread (CompletableFuture pool) |
PlayerDisconnectEvent | Netty I/O thread (or caller of disconnect()) |
PlayerSetupDisconnectEvent | Netty I/O thread |
Commands
Section titled “Commands”| Command | Description |
|---|---|
/ban <player> [reason] | Permanently ban a player |
/unban <player> | Remove a ban |
/whitelist enable/disable | Toggle whitelist |
/whitelist add/remove <player> | Manage whitelist entries |
/whitelist list/status/clear | View or clear whitelist |
/op add/remove <player> | Grant or revoke operator |
/kick <player> | Kick a player |
See Access Control Commands for details.