This commit is contained in:
2026-05-26 00:11:45 -04:00
commit 296c233157
41 changed files with 2829 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
# 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<ServerInfo> getServers();
Collection<ServerInfo> 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:<id>` (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 `<role>-<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<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.