6.1 KiB
Network Features
BungeeCord-style admin commands and cross-server player movement, built on top of NetworkCore's MessageBus and ServerRegistry.
What's in this layer
| Piece | Purpose |
|---|---|
PlayerPresence API + PlayerPresenceService |
Network-wide "who is on which server" tracking. |
RefRequestService |
Cross-server "please send this player to host:port" wrapper around PlayerRef.referToServer. |
AlertService |
Cross-server broadcast to all players. |
LobbyPicker |
Finds a referrable lobby (least-loaded), with role-prefix fallback. |
ShutdownHandoff |
On graceful stop, walks local players and refers each to a lobby before Redis closes. |
| Commands | /alert, /glist, /hub, /send |
Commands
| Command | Permission | What it does |
|---|---|---|
/alert <message> |
networkcore.alert |
Single-token message broadcast to every player on every server. (Multi-word reasons need the UI - see Staff's pattern.) |
/glist |
networkcore.glist |
Lists every server + players currently on each, from the local PlayerPresence cache. |
/hub |
networkcore.hub |
Refers the caller to a lobby. Picks least-loaded. Errors if no lobby is available. |
/send <player> <serverIdOrRole> |
networkcore.send |
Refers the named player (online anywhere) to a server. server can be a literal server id or a role (picks least-loaded with that role). |
How the transfer actually works
Hytale ships PlayerRef.referToServer(host, port) which sends a ClientReferral packet to the player's client. The client disconnects from the current server and connects to the new host:port. Hytale's own /refer command does the same thing for one player at a time on the same server.
For a player who's on this server, we call referToServer directly.
For a player on a different server, we publish a RefRequest on the bus. Every server's RefRequestService listens; the one that has the player locally does the call. The other servers ignore the request.
This whole flow takes a few hundred ms end to end:
/send Bob lobbyon server A (Bob is on arena-3) → A publishes RefRequest- arena-3 receives → calls
bob.referToServer("lobby-1.kweebec.com", 5520) - Bob's client gets the ClientReferral, drops the connection to arena-3, opens a new one to lobby-1
- lobby-1's connect flow runs as normal
Public address setup
Every server needs to know its own public-facing host/port (what other servers should hand to clients in the referral packet). Configure via server.public_address + server.public_port in config.json.
If null, we auto-detect via Java's InetAddress.getLocalHost().getHostName(). On dev boxes that's usually fine. On cloud / hosted setups (Apex etc.) it almost certainly is NOT - you'll get an internal name that clients can't reach. Set it explicitly.
You can confirm it parsed correctly: boot log line Public network address resolved to <host>:<port>. Also /networkcore servers shows host/port for each server in the network now.
A server with no host in its ServerInfo (i.e. its heartbeat didn't carry one) is treated as "not referrable" - /hub and /send will skip it.
Graceful shutdown handoff
In NetworkCore.shutdown(), before closing Redis, we walk Universe.getPlayers() and referToServer(...) each one to a lobby (picked via LobbyPicker).
This means a properly-shut-down server doesn't drop its players - they get teleported away. It happens BEFORE the goodbye announce + Redis close, so the LobbyPicker can still see other servers in the registry.
Ungraceful kills (kill -9, OOM, network partition, hardware failure) bypass this entirely. Players see Hytale's normal disconnect screen and reconnect manually. This is unavoidable without a proxy in front - there's no way for the dying server to refer players after it's dead.
To make the most of this:
- Use systemd / supervisord / similar to send SIGTERM and wait before SIGKILL. Hytale's shutdown hook handles SIGTERM.
- Don't
docker killcontainers -docker stopgives a grace period. - For deploy rollouts, drain (run
/send-all-to lobbyor similar) before shutdown to be doubly sure.
Presence cache
PlayerPresenceService keeps an in-memory UUID → PlayerLocation map. Maintained by:
- Local
PlayerConnectEvent/PlayerDisconnectEvent→ publish onnetwork:presence - Remote presence events on the same channel → update local map
ServerLeftNetworkEvent(server dropped from registry) → purge all players known to be on that server
On server boot, we re-publish our local roster so other servers refresh their view of us after our restart. Type: KIND_SYNC.
The cache is unbounded but bounded in practice by total online player count. No TTL - entries live until a leave event drops them. A lost leave event leaves a stale entry until that player connects somewhere else (which fires a new join) or until the holding server drops from the registry.
What this layer deliberately doesn't do
- Per-world routing. /hub picks a server, not a world within one. If you want world-aware routing, layer it on top.
- Cross-region picking. No notion of latency / geography. If you need region-aware lobby selection, sort
LobbyPickercandidates by a region tag and agetRegion()method onServerInfo. - Queue handling. A
/sendto a full server just succeeds in queuing the referral; the destination might reject the connection. No retry, no queue, no "you're #3 in line". - Mid-flight player tracking. Once we hand off a referral, we don't follow the player until they connect to the destination. There's a window of "between servers" where
presence.getLocationreturns the old server until the new one's connect event fires. /glistfiltering by group/role. Easy follow-up; not in v1.- In-game UI for any of this. Commands only. Build a HyUI hub later if you want.
Migration note for existing plugins
ServerInfo got two new fields (host, port) and the constructor signature changed. Anything depending on the NetworkCore jar (Staff, Ranks) needs to be rebuilt against the new jar. Same NoSuchMethodError pattern we hit before with Universe.getPlayers().