3.6 KiB
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
public interface ServerRegistry {
ServerInfo getSelf();
Collection<ServerInfo> getServers();
Collection<ServerInfo> getServersByRole(String role);
@Nullable ServerInfo getServer(String id);
}
All four are local cache reads. No Redis round-trip per call.
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():
- Subscribe to
network:events(presence channel). - Write own heartbeat to
network:servers:<id>(TTL 15s). - Scan
network:servers:*to populate the local cache and fire join events for everyone discovered. - Publish a
helloonnetwork:events. - Schedule a heartbeat task every 5s. Each tick rewrites the heartbeat (resetting TTL) and re-scans.
On stop() (clean shutdown only):
- Publish a
goodbyeonnetwork:events. - 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:
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 <role>-<8 hex chars> on every boot.
The role is a free-form string from server.role. Use snake_case. Examples:
lobby- hub/main worldbridge_duel- one Bridge Duel match hostarena- PvP arenastaff- admin-only world
Roles are how plugins route. Example: a queue plugin running on a lobby finds an available bridge_duel server with:
Collection<ServerInfo> available = core.getServerRegistry()
.getServersByRole("bridge_duel")
.stream()
.filter(s -> s.players() < 20)
.toList();
Redis layout
For reference, what NetworkCore writes:
network:servers:<id> 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.