# ServerRegistry Every server in the network heartbeats its presence to Redis. Every server keeps a local snapshot of who's online and reacts to changes via Hytale events. ## API ```java public interface ServerRegistry { ServerInfo getSelf(); Collection getServers(); Collection getServersByRole(String role); @Nullable ServerInfo getServer(String id); } ``` All four are local cache reads. No Redis round-trip per call. ```java public final class ServerInfo { String id(); // e.g. "lobby-3f2a91bd" String role(); // e.g. "lobby", "bridge_duel" String address(); // currently unused, reserved for future int players(); // last reported player count long startedAt(); // epoch millis when the server booted long lastSeen(); // epoch millis of last heartbeat we observed } ``` ## How it works On `start()`: 1. Subscribe to `network:events` (presence channel). 2. Write own heartbeat to `network:servers:` (TTL 15s). 3. Scan `network:servers:*` to populate the local cache and fire join events for everyone discovered. 4. Publish a `hello` on `network:events`. 5. Schedule a heartbeat task every 5s. Each tick rewrites the heartbeat (resetting TTL) and re-scans. On `stop()` (clean shutdown only): 1. Publish a `goodbye` on `network:events`. 2. Delete the registry key. If a server hard-crashes (kill -9, OOM, network partition), no goodbye is sent. Other servers learn it's gone when the 15s TTL expires the key and the next scan doesn't see it. ## Detection latency | Event | When other servers learn | |---|---| | New server boots | Within a few hundred ms (immediate hello over pub/sub) | | Server clean-shutdown | Within a few hundred ms (immediate goodbye over pub/sub) | | Server hard-crash | Up to 20s (TTL 15s + up to 5s for the next scan) | ## Events When the registry's view of the network changes, it dispatches these on the Hytale event bus: ```java import net.kewwbec.networkcore.api.events.ServerJoinedNetworkEvent; import net.kewwbec.networkcore.api.events.ServerLeftNetworkEvent; getEventRegistry().registerGlobal(ServerJoinedNetworkEvent.class, event -> { ServerInfo server = event.getServer(); LOGGER.at(Level.INFO).log("Server joined: %s (role=%s)", server.id(), server.role()); }); getEventRegistry().registerGlobal(ServerLeftNetworkEvent.class, event -> { ServerInfo server = event.getServer(); LOGGER.at(Level.INFO).log("Server left: %s", server.id()); }); ``` Event handlers run on the Hytale event-bus dispatch thread. Same world-thread caveat as MessageBus subscribers: if you touch entities, hop to `world.execute(...)`. ## Server id and role The `id` is either the value in `server.id` in `config.json`, or auto-generated as `-<8 hex chars>` on every boot. The `role` is a free-form string from `server.role`. Use snake_case. Examples: - `lobby` - hub/main world - `bridge_duel` - one Bridge Duel match host - `arena` - PvP arena - `staff` - admin-only world Roles are how plugins route. Example: a queue plugin running on a lobby finds an available `bridge_duel` server with: ```java Collection available = core.getServerRegistry() .getServersByRole("bridge_duel") .stream() .filter(s -> s.players() < 20) .toList(); ``` ## Redis layout For reference, what NetworkCore writes: ``` network:servers: HASH server heartbeat record, TTL 15s fields: id, role, players, started_at, last_seen network:events CHANNEL pub/sub for presence (hello/goodbye) ``` `network:` is the default `redis.key_prefix`. Change in config if needed.