Skip to content

Commit f9c9aff

Browse files
committed
feat: begin game orchestrator
1 parent a5ad8a0 commit f9c9aff

34 files changed

Lines changed: 1614 additions & 820 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ jobs:
5454
service.api/build/libs/*.jar
5555
service.datamutex/build/libs/*.jar
5656
service.party/build/libs/*.jar
57+
service.orchestrator/build/libs/*.jar
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package net.swofty.commons;
22

33
public enum ServiceType {
4-
AUCTION_HOUSE,
5-
BAZAAR,
6-
ITEM_TRACKER,
7-
API,
8-
DATA_MUTEX,
9-
PARTY,
10-
;
4+
AUCTION_HOUSE,
5+
BAZAAR,
6+
ITEM_TRACKER,
7+
API,
8+
DATA_MUTEX,
9+
PARTY,
10+
ORCHESTRATOR
1111
}

commons/src/main/java/net/swofty/commons/proxy/FromProxyChannels.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public enum FromProxyChannels {
1515
RUN_EVENT_ON_SERVER("run-event", new RunEventRequirements()),
1616
PING_SERVER("ping-server", new PingServerRequirements()),
1717
GIVE_PLAYERS_ORIGIN_TYPE("give-players-origin-type", new GivePlayersOriginTypeRequirements()),
18+
BEDWARS_JOIN_PREFERENCE("bedwars-join-preference", new BedWarsJoinPreferenceRequirements()),
1819
;
1920

2021
private final String channelName;

commons/src/main/java/net/swofty/commons/proxy/requirements/to/PlayerHandlerRequirements.java

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,30 @@
55
import java.util.List;
66

77
public class PlayerHandlerRequirements extends ProxyChannelRequirements {
8-
@Override
9-
public List<RequiredKey> getRequiredKeysForProxy() {
10-
return List.of();
11-
}
8+
@Override
9+
public List<RequiredKey> getRequiredKeysForProxy() {
10+
return List.of();
11+
}
1212

13-
@Override
14-
public List<RequiredKey> getRequiredKeysForServer() {
15-
return List.of(
16-
new RequiredKey("uuid"), // The UUID of the player
17-
new RequiredKey("action") // see {@link PlayerHandlerActions}
18-
);
19-
}
13+
@Override
14+
public List<RequiredKey> getRequiredKeysForServer() {
15+
return List.of(
16+
new RequiredKey("uuid"), // The UUID of the player
17+
new RequiredKey("action") // see {@link PlayerHandlerActions}
18+
);
19+
}
2020

21-
public enum PlayerHandlerActions {
22-
TRANSFER,
23-
TELEPORT,
24-
BANK_HASH,
25-
VERSION,
26-
IS_ONLINE,
27-
EVENT,
28-
REFRESH_COOP_DATA,
29-
MESSAGE,
30-
TRANSFER_WITH_UUID,
31-
GET_SERVER
32-
;
33-
}
21+
public enum PlayerHandlerActions {
22+
TRANSFER,
23+
TELEPORT,
24+
BANK_HASH,
25+
VERSION,
26+
IS_ONLINE,
27+
EVENT,
28+
REFRESH_COOP_DATA,
29+
MESSAGE,
30+
TRANSFER_WITH_UUID,
31+
GET_SERVER,
32+
BEDWARS_SET_PREFERENCE;
33+
}
3434
}

proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,20 @@ public CompletableFuture<UUID> getBankHash() {
186186
return future;
187187
}
188188

189+
public void setBedWarsJoinPreference(String mode, String map) {
190+
JSONObject json = new JSONObject();
191+
json.put("uuid", uuid.toString());
192+
json.put("mode", mode == null ? "" : mode);
193+
json.put("map", map == null ? "" : map);
194+
195+
PlayerHandlerRequirements.PlayerHandlerActions action =
196+
PlayerHandlerRequirements.PlayerHandlerActions.BEDWARS_SET_PREFERENCE;
197+
json.put("action", action.name());
198+
199+
ServerOutboundMessage.sendMessageToProxy(ToProxyChannels.PLAYER_HANDLER,
200+
json, (s) -> {});
201+
}
202+
189203
public void refreshCoopData(String datapoint) {
190204
JSONObject json = new JSONObject();
191205
json.put("uuid", uuid.toString());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
3+
plugins {
4+
java
5+
application
6+
id("io.github.goooler.shadow") version "8.1.7"
7+
}
8+
9+
group = "net.swofty"
10+
version = "3.0"
11+
12+
java {
13+
sourceCompatibility = JavaVersion.VERSION_21
14+
targetCompatibility = JavaVersion.VERSION_21
15+
toolchain {
16+
languageVersion.set(JavaLanguageVersion.of(21))
17+
}
18+
}
19+
20+
repositories {
21+
maven("https://jitpack.io")
22+
mavenCentral()
23+
}
24+
25+
dependencies {
26+
implementation(project(":service.generic"))
27+
implementation(project(":commons"))
28+
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
29+
implementation("org.tinylog:tinylog-api:2.7.0")
30+
implementation("org.tinylog:tinylog-impl:2.7.0")
31+
}
32+
33+
application {
34+
mainClass.set("net.swofty.service.orchestrator.OrchestratorService")
35+
}
36+
37+
tasks {
38+
named<ShadowJar>("shadowJar") {
39+
archiveBaseName.set("ServiceOrchestrator")
40+
archiveClassifier.set("")
41+
archiveVersion.set("")
42+
}
43+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package net.swofty.service.orchestrator;
2+
3+
import net.swofty.commons.ServiceType;
4+
import net.swofty.service.generic.SkyBlockService;
5+
import net.swofty.service.generic.redis.ServiceEndpoint;
6+
7+
import java.util.List;
8+
9+
public class OrchestratorService implements SkyBlockService {
10+
11+
public static void main(String[] args) {
12+
SkyBlockService.init(new OrchestratorService());
13+
}
14+
15+
@Override
16+
public ServiceType getType() {
17+
return ServiceType.ORCHESTRATOR;
18+
}
19+
20+
@Override
21+
public List<ServiceEndpoint> getEndpoints() {
22+
return loopThroughPackage("net.swofty.service.orchestrator.endpoints", ServiceEndpoint.class).toList();
23+
}
24+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package net.swofty.service.orchestrator.endpoints;
2+
3+
import net.swofty.commons.impl.ServiceProxyRequest;
4+
import net.swofty.commons.protocol.ProtocolObject;
5+
import net.swofty.commons.protocol.objects.orchestrator.GameHeartbeatProtocolObject;
6+
import net.swofty.service.generic.redis.ServiceEndpoint;
7+
import net.swofty.service.orchestrator.OrchestratorCache;
8+
9+
public class GameHeartbeatEndpoint implements ServiceEndpoint
10+
<GameHeartbeatProtocolObject.HeartbeatMessage,
11+
GameHeartbeatProtocolObject.HeartbeatResponse> {
12+
13+
@Override
14+
public ProtocolObject<GameHeartbeatProtocolObject.HeartbeatMessage, GameHeartbeatProtocolObject.HeartbeatResponse> associatedProtocolObject() {
15+
return new GameHeartbeatProtocolObject();
16+
}
17+
18+
@Override
19+
public GameHeartbeatProtocolObject.HeartbeatResponse onMessage(ServiceProxyRequest message, GameHeartbeatProtocolObject.HeartbeatMessage body) {
20+
OrchestratorCache.handleHeartbeat(
21+
body.uuid(),
22+
body.shortName(),
23+
body.type(),
24+
body.maps(),
25+
body.maxPlayers(),
26+
body.onlinePlayers()
27+
);
28+
return new GameHeartbeatProtocolObject.HeartbeatResponse(true);
29+
}
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package net.swofty.service.orchestrator.endpoints;
2+
3+
import net.swofty.commons.impl.ServiceProxyRequest;
4+
import net.swofty.commons.protocol.ProtocolObject;
5+
import net.swofty.commons.protocol.objects.orchestrator.GetMapsProtocolObject;
6+
import net.swofty.service.generic.redis.ServiceEndpoint;
7+
import net.swofty.service.orchestrator.OrchestratorCache;
8+
9+
public class GetMapsEndpoint implements ServiceEndpoint
10+
<GetMapsProtocolObject.GetMapsMessage,
11+
GetMapsProtocolObject.GetMapsResponse> {
12+
13+
@Override
14+
public ProtocolObject<GetMapsProtocolObject.GetMapsMessage, GetMapsProtocolObject.GetMapsResponse> associatedProtocolObject() {
15+
return new GetMapsProtocolObject();
16+
}
17+
18+
@Override
19+
public GetMapsProtocolObject.GetMapsResponse onMessage(ServiceProxyRequest message, GetMapsProtocolObject.GetMapsMessage body) {
20+
return new GetMapsProtocolObject.GetMapsResponse(OrchestratorCache.getMaps(body.type()).stream().sorted().toList());
21+
}
22+
}

0 commit comments

Comments
 (0)