1+ package net .swofty .service .orchestrator ;
2+
3+ import net .swofty .commons .ServerType ;
4+
5+ import java .time .Instant ;
6+ import java .util .*;
7+ import java .util .concurrent .ConcurrentHashMap ;
8+ import java .util .concurrent .ThreadLocalRandom ;
9+
10+ public class OrchestratorCache {
11+
12+ private static final Map <String , GameServerState > serversByShortName = new ConcurrentHashMap <>();
13+ private static final long HEARTBEAT_TTL_MS = 20000 ; // 20s
14+
15+ public static void handleHeartbeat (UUID uuid ,
16+ String shortName ,
17+ ServerType type ,
18+ Collection <String > maps ,
19+ int maxPlayers ,
20+ int onlinePlayers ) {
21+ GameServerState state = new GameServerState (
22+ uuid ,
23+ shortName ,
24+ type ,
25+ new HashSet <>(maps ),
26+ maxPlayers ,
27+ onlinePlayers ,
28+ Instant .now ().toEpochMilli ()
29+ );
30+ serversByShortName .put (shortName , state );
31+ }
32+
33+ public static Set <String > getMaps (ServerType type ) {
34+ cleanup ();
35+ Set <String > maps = new HashSet <>();
36+ for (GameServerState s : serversByShortName .values ()) {
37+ if (s .type == type ) maps .addAll (s .maps );
38+ }
39+ return maps ;
40+ }
41+
42+ public static GameServerState pickServerForMap (ServerType type , String map , int neededSlots ) {
43+ cleanup ();
44+ List <GameServerState > candidates = new ArrayList <>();
45+ for (GameServerState s : serversByShortName .values ()) {
46+ if (s .type == type && s .maps .contains (map )) {
47+ if (neededSlots <= 0 || s .availableSlots () >= neededSlots ) {
48+ candidates .add (s );
49+ }
50+ }
51+ }
52+ if (candidates .isEmpty ()) return null ;
53+
54+ // prefer the server with the most available slots.. tie-break randomly
55+ candidates .sort (Comparator .comparingInt (GameServerState ::availableSlots ).reversed ());
56+ int topAvail = candidates .getFirst ().availableSlots ();
57+ List <GameServerState > top = new ArrayList <>();
58+ for (GameServerState c : candidates ) {
59+ if (c .availableSlots () == topAvail ) top .add (c );
60+ else break ;
61+ }
62+ return top .get (ThreadLocalRandom .current ().nextInt (top .size ()));
63+ }
64+
65+ private static void cleanup () {
66+ long now = Instant .now ().toEpochMilli ();
67+ serversByShortName .values ().removeIf (s -> now - s .lastHeartbeat > HEARTBEAT_TTL_MS );
68+ }
69+
70+ public record GameServerState (UUID uuid ,
71+ String shortName ,
72+ ServerType type ,
73+ Set <String > maps ,
74+ int maxPlayers ,
75+ int onlinePlayers ,
76+ long lastHeartbeat ) {
77+ public int availableSlots () {
78+ return Math .max (0 , maxPlayers - onlinePlayers );
79+ }
80+ }
81+ }
0 commit comments