Compare commits
1
Commits
8cb458c9bd
...
3b4e1ab4cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b4e1ab4cb |
+1
-1
@@ -7,5 +7,5 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.github.selimaj-dev:session-java:0.1.6"
|
||||
implementation "dev.selimaj:session-java:0.2.0"
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.saturnclient.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
@@ -11,209 +14,278 @@ import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.cosmetics.Hats;
|
||||
|
||||
import dev.selimaj.session.Session;
|
||||
import dev.selimaj.session.types.SessionErrorException;
|
||||
|
||||
public class ServiceClient {
|
||||
/** Override with -Dsaturn.serverUri=ws://127.0.0.1:8080 to use a local server. */
|
||||
private static final String SERVER_URI = System.getProperty("saturn.serverUri", "wss://saturn-server.selimaj.dev");
|
||||
private static final long CONNECT_TIMEOUT_SECONDS = 10;
|
||||
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);
|
||||
private static final long MIN_RECONNECT_DELAY_MS = 1_000;
|
||||
private static final long MAX_RECONNECT_DELAY_MS = 60_000;
|
||||
|
||||
/** The authenticated session, or null while disconnected or connecting. */
|
||||
private static volatile Session session;
|
||||
public static volatile UUID uuid;
|
||||
|
||||
private static final AtomicBoolean CONNECTING = new AtomicBoolean();
|
||||
private static volatile boolean stopping = false;
|
||||
|
||||
public static void initialize() {
|
||||
new Thread(() -> {
|
||||
if (authenticate()) {
|
||||
Cloaks.initialize();
|
||||
Hats.initialize();
|
||||
Providers.saturn.logInfo("Saturn Session initialization complete");
|
||||
} else {
|
||||
Providers.saturn.logError("Failed to authenticate with the server");
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
Providers.saturn.getClient().onClientStopping(() -> {
|
||||
stopping = true;
|
||||
Session current = session;
|
||||
session = null;
|
||||
|
||||
public static boolean isConnected() {
|
||||
return session != null;
|
||||
}
|
||||
|
||||
public static boolean connectTimeout() {
|
||||
try {
|
||||
session = Session.connect("wss://saturn-server.selimaj.dev", 2, TimeUnit.MINUTES);
|
||||
return true;
|
||||
} catch (TimeoutException e) {
|
||||
System.out.println("Session Server timeout");
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean connectMultiTimeout() {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (connectTimeout()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Providers.saturn.logError("Failed to authenticate (attempt " + (i + 1) + ")");
|
||||
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean authenticate() {
|
||||
MinecraftClientRef client = Providers.saturn.getClient();
|
||||
|
||||
client.onClientStopping(() -> {
|
||||
if (session != null) {
|
||||
if (current != null) {
|
||||
try {
|
||||
session.close();
|
||||
current.close();
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Unable to close Saturn session", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
String accessToken = client.getAccessToken();
|
||||
uuid = client.getUuid();
|
||||
String username = client.getUsername();
|
||||
connectInBackground();
|
||||
}
|
||||
|
||||
if (accessToken == null || uuid == null || username == null) {
|
||||
Providers.saturn.logError("No active Minecraft session found");
|
||||
return false;
|
||||
}
|
||||
public static boolean isConnected() {
|
||||
return session != null;
|
||||
}
|
||||
|
||||
Providers.saturn.logInfo("Authenticating with UUID: " + uuid);
|
||||
|
||||
if (!connectMultiTimeout()) {
|
||||
Providers.saturn.logError("Unable to authenticate: Session Server Timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
ServiceMethods.Types.Player response = session.request(ServiceMethods.Authenticate, accessToken)
|
||||
.get();
|
||||
|
||||
for (String availableCloak : response.cloaks()) {
|
||||
Cloaks.availableCloaks.add(availableCloak);
|
||||
}
|
||||
|
||||
for (String availableHat : response.hats()) {
|
||||
Hats.availableHats.add(availableHat);
|
||||
}
|
||||
|
||||
eventHandlers();
|
||||
|
||||
SaturnPlayer.set(new SaturnPlayer(uuid, username, response.cloak(), response.hat()));
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Authentication failed", e);
|
||||
return false;
|
||||
/** Starts the connect loop unless one is already running. */
|
||||
private static void connectInBackground() {
|
||||
if (stopping || !CONNECTING.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread(() -> {
|
||||
boolean connected = false;
|
||||
try {
|
||||
connected = connectLoop();
|
||||
} finally {
|
||||
CONNECTING.set(false);
|
||||
}
|
||||
|
||||
// A drop between connecting and clearing CONNECTING couldn't start a new loop.
|
||||
if (connected && session == null) {
|
||||
connectInBackground();
|
||||
}
|
||||
}, "Saturn-Connect");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects and authenticates, retrying with exponential backoff. Returns
|
||||
* whether it connected.
|
||||
*/
|
||||
private static boolean connectLoop() {
|
||||
long delay = MIN_RECONNECT_DELAY_MS;
|
||||
|
||||
while (!stopping) {
|
||||
switch (connectOnce()) {
|
||||
case CONNECTED -> {
|
||||
Providers.saturn.logInfo("Saturn Session initialization complete");
|
||||
return true;
|
||||
}
|
||||
case GIVE_UP -> {
|
||||
Providers.saturn.logError("Failed to authenticate with the server");
|
||||
return false;
|
||||
}
|
||||
case RETRY -> {
|
||||
Providers.saturn.logError("Saturn server unavailable, retrying in " + delay / 1000 + "s");
|
||||
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delay = Math.min(delay * 2, MAX_RECONNECT_DELAY_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private enum ConnectResult {
|
||||
CONNECTED,
|
||||
/** Retrying cannot help, e.g. no Minecraft session or the token was rejected. */
|
||||
GIVE_UP,
|
||||
RETRY
|
||||
}
|
||||
|
||||
private static ConnectResult connectOnce() {
|
||||
MinecraftClientRef client = Providers.saturn.getClient();
|
||||
|
||||
String accessToken = client.getAccessToken();
|
||||
UUID playerUuid = client.getUuid();
|
||||
String username = client.getUsername();
|
||||
|
||||
if (accessToken == null || playerUuid == null || username == null) {
|
||||
Providers.saturn.logError("No active Minecraft session found");
|
||||
return ConnectResult.GIVE_UP;
|
||||
}
|
||||
|
||||
uuid = playerUuid;
|
||||
Providers.saturn.logInfo("Authenticating with UUID: " + playerUuid);
|
||||
|
||||
Session s;
|
||||
try {
|
||||
s = Session.connect(SERVER_URI, CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Unable to connect to the Saturn server: " + rootMessage(e));
|
||||
return ConnectResult.RETRY;
|
||||
}
|
||||
|
||||
ServiceMethods.Types.Player response;
|
||||
try {
|
||||
response = s.request(ServiceMethods.Authenticate, accessToken, REQUEST_TIMEOUT).get();
|
||||
} catch (ExecutionException e) {
|
||||
s.close();
|
||||
|
||||
if (e.getCause() instanceof SessionErrorException rejected) {
|
||||
Providers.saturn.logError("Saturn server rejected authentication: " + rejected.getMessage());
|
||||
return ConnectResult.GIVE_UP;
|
||||
}
|
||||
|
||||
Providers.saturn.logError("Authentication failed: " + rootMessage(e));
|
||||
return ConnectResult.RETRY;
|
||||
} catch (InterruptedException e) {
|
||||
s.close();
|
||||
return ConnectResult.GIVE_UP;
|
||||
}
|
||||
|
||||
Cloaks.availableCloaks.clear();
|
||||
Cloaks.availableCloaks.addAll(java.util.List.of(response.cloaks()));
|
||||
Cloaks.initialize();
|
||||
|
||||
Hats.availableHats.clear();
|
||||
Hats.availableHats.addAll(java.util.List.of(response.hats()));
|
||||
Hats.initialize();
|
||||
|
||||
// Other players may have changed cosmetics while we were disconnected.
|
||||
SaturnPlayer.clear();
|
||||
SaturnPlayer.set(new SaturnPlayer(playerUuid, username, response.cloak(), response.hat()));
|
||||
|
||||
eventHandlers(s);
|
||||
|
||||
if (stopping) {
|
||||
s.close();
|
||||
return ConnectResult.GIVE_UP;
|
||||
}
|
||||
|
||||
session = s;
|
||||
// Registered after publishing so an immediate drop still clears it.
|
||||
s.onClose(() -> onDisconnected(s));
|
||||
return ConnectResult.CONNECTED;
|
||||
}
|
||||
|
||||
private static void onDisconnected(Session closed) {
|
||||
if (session != closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
session = null;
|
||||
|
||||
if (!stopping) {
|
||||
Providers.saturn.logError("Disconnected from the Saturn server, reconnecting");
|
||||
connectInBackground();
|
||||
}
|
||||
}
|
||||
|
||||
private static String rootMessage(Throwable e) {
|
||||
while ((e instanceof CompletionException || e instanceof ExecutionException) && e.getCause() != null) {
|
||||
e = e.getCause();
|
||||
}
|
||||
return e.getClass().getSimpleName() + ": " + e.getMessage();
|
||||
}
|
||||
|
||||
public static void setCloak(String itemId) {
|
||||
if (session == null)
|
||||
Session s = session;
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
session.request(ServiceMethods.SetCloak, itemId).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
} else {
|
||||
try {
|
||||
session.request(ServiceMethods.SendPlayer,
|
||||
new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Failed to set cloak (service): ", e);
|
||||
}
|
||||
s.request(ServiceMethods.SetCloak, itemId, REQUEST_TIMEOUT).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Providers.saturn.logError("Failed to set cloak (service): " + rootMessage(throwable));
|
||||
} else {
|
||||
sendPlayer(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setHat(String itemId) {
|
||||
if (session == null)
|
||||
Session s = session;
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
session.request(ServiceMethods.SetHat, itemId).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
} else {
|
||||
try {
|
||||
session.request(ServiceMethods.SendPlayer,
|
||||
new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
s.request(ServiceMethods.SetHat, itemId, REQUEST_TIMEOUT).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Providers.saturn.logError("Failed to set hat (service): " + rootMessage(throwable));
|
||||
} else {
|
||||
sendPlayer(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Tells nearby Saturn players about our current cosmetics. */
|
||||
private static void sendPlayer(Session s) {
|
||||
s.request(ServiceMethods.SendPlayer,
|
||||
new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()),
|
||||
REQUEST_TIMEOUT).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Providers.saturn.logError("Failed to send player (service): " + rootMessage(throwable));
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Failed to set hat (service): ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void buyCloak(String itemId) {
|
||||
if (session == null)
|
||||
Session s = session;
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
session.request(ServiceMethods.BuyCloak, itemId).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
} else {
|
||||
Cloaks.availableCloaks.add(itemId);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Failed to buy cloak (service): ", e);
|
||||
}
|
||||
s.request(ServiceMethods.BuyCloak, itemId, REQUEST_TIMEOUT).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Providers.saturn.logError("Failed to buy cloak (service): " + rootMessage(throwable));
|
||||
} else if (!Cloaks.availableCloaks.contains(itemId)) {
|
||||
Cloaks.availableCloaks.add(itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void buyHat(String itemId) {
|
||||
if (session == null)
|
||||
Session s = session;
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
session.request(ServiceMethods.BuyHat, itemId).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
} else {
|
||||
Hats.availableHats.add(itemId);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Failed to buy hat (service): ", e);
|
||||
}
|
||||
s.request(ServiceMethods.BuyHat, itemId, REQUEST_TIMEOUT).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Providers.saturn.logError("Failed to buy hat (service): " + rootMessage(throwable));
|
||||
} else if (!Hats.availableHats.contains(itemId)) {
|
||||
Hats.availableHats.add(itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void emote(String emote) {
|
||||
if (session == null)
|
||||
Session s = session;
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
session.request(ServiceMethods.Emote,
|
||||
new ServiceMethods.Types.EmoteRequest(emote, SaturnPlayer.getExternalUUIDAsString()))
|
||||
.whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Failed to emote (service): ", e);
|
||||
}
|
||||
s.request(ServiceMethods.Emote,
|
||||
new ServiceMethods.Types.EmoteRequest(emote, SaturnPlayer.getExternalUUIDAsString()),
|
||||
REQUEST_TIMEOUT).whenComplete((msg, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Providers.saturn.logError("Failed to emote (service): " + rootMessage(throwable));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void eventHandlers() {
|
||||
if (session == null)
|
||||
return;
|
||||
|
||||
session.onNotification(ServiceMethods.EmoteEvent, (data) -> {
|
||||
private static void eventHandlers(Session s) {
|
||||
s.onNotification(ServiceMethods.EmoteEvent, (data) -> {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
@@ -230,23 +302,24 @@ public class ServiceClient {
|
||||
Providers.saturn.getClient().executeOnThread(() -> Providers.saturn.playEmote(from, emote));
|
||||
});
|
||||
|
||||
session.onNotification(ServiceMethods.Player, (player) -> {
|
||||
System.out.println(player);
|
||||
s.onNotification(ServiceMethods.Player, (player) -> {
|
||||
SaturnPlayer.set(player.toSaturnPlayer());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's Saturn data, or null if the server says they aren't
|
||||
* using Saturn. Throws if the lookup itself fails, so callers can retry.
|
||||
* using Saturn. Throws if the lookup itself fails or times out, so callers
|
||||
* can retry.
|
||||
*/
|
||||
public static SaturnPlayer getPlayer(UUID uuid, String name) throws Exception {
|
||||
Session session = ServiceClient.session;
|
||||
Session s = session;
|
||||
|
||||
if (session == null)
|
||||
if (s == null)
|
||||
throw new IllegalStateException("Not connected to the Saturn server");
|
||||
|
||||
ServiceMethods.Types.Player player = session.request(ServiceMethods.GetPlayer, uuid.toString()).join();
|
||||
ServiceMethods.Types.Player player = s.request(ServiceMethods.GetPlayer, uuid.toString(), REQUEST_TIMEOUT)
|
||||
.join();
|
||||
|
||||
if (player == null)
|
||||
return null;
|
||||
|
||||
@@ -78,6 +78,12 @@ public class SaturnPlayer {
|
||||
return get(name, Providers.saturn.getClient().getPlayerListEntryUUID(name));
|
||||
}
|
||||
|
||||
/** Forgets every cached player, e.g. after reconnecting to the server. */
|
||||
public static void clear() {
|
||||
PLAYERS.clear();
|
||||
NEXT_LOOKUP.clear();
|
||||
}
|
||||
|
||||
public static void set(SaturnPlayer player) {
|
||||
PLAYERS.put(player.uuid, player);
|
||||
NEXT_LOOKUP.remove(player.uuid);
|
||||
|
||||
Reference in New Issue
Block a user