# 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 ` | `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 ` | `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: 1. `/send Bob lobby` on server A (Bob is on arena-3) → A publishes RefRequest 2. arena-3 receives → calls `bob.referToServer("lobby-1.kweebec.com", 5520)` 3. Bob's client gets the ClientReferral, drops the connection to arena-3, opens a new one to lobby-1 4. 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](03_Configuration.md#server). 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 :`. 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 kill` containers - `docker stop` gives a grace period. - For deploy rollouts, drain (run `/send-all-to lobby` or similar) before shutdown to be doubly sure. ## Presence cache `PlayerPresenceService` keeps an in-memory `UUID → PlayerLocation` map. Maintained by: - Local `PlayerConnectEvent` / `PlayerDisconnectEvent` → publish on `network: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 `LobbyPicker` candidates by a region tag and a `getRegion()` method on `ServerInfo`. - **Queue handling.** A `/send` to 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.getLocation` returns the old server until the new one's connect event fires. - **`/glist` filtering 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()`.