Stop per-frame player lookups and make shared state thread-safe #1
@@ -13,8 +13,8 @@ import org.saturnclient.cosmetics.Hats;
|
|||||||
import dev.selimaj.session.Session;
|
import dev.selimaj.session.Session;
|
||||||
|
|
||||||
public class ServiceClient {
|
public class ServiceClient {
|
||||||
private static Session session;
|
private static volatile Session session;
|
||||||
public static UUID uuid;
|
public static volatile UUID uuid;
|
||||||
|
|
||||||
public static void initialize() {
|
public static void initialize() {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
@@ -28,6 +28,10 @@ public class ServiceClient {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isConnected() {
|
||||||
|
return session != null;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean connectTimeout() {
|
public static boolean connectTimeout() {
|
||||||
try {
|
try {
|
||||||
session = Session.connect("wss://saturn-server.selimaj.dev", 2, TimeUnit.MINUTES);
|
session = Session.connect("wss://saturn-server.selimaj.dev", 2, TimeUnit.MINUTES);
|
||||||
@@ -220,11 +224,10 @@ public class ServiceClient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.emote() != null && !data.emote().isEmpty()) {
|
String emote = data.emote() != null && !data.emote().isEmpty() ? data.emote() : null;
|
||||||
Providers.saturn.playEmote(from, data.emote());
|
|
||||||
} else {
|
// Notifications arrive on a background thread.
|
||||||
Providers.saturn.playEmote(from, null);
|
Providers.saturn.getClient().executeOnThread(() -> Providers.saturn.playEmote(from, emote));
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
session.onNotification(ServiceMethods.Player, (player) -> {
|
session.onNotification(ServiceMethods.Player, (player) -> {
|
||||||
@@ -233,21 +236,21 @@ public class ServiceClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SaturnPlayer getPlayer(UUID uuid, String name) {
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public static SaturnPlayer getPlayer(UUID uuid, String name) throws Exception {
|
||||||
|
Session session = ServiceClient.session;
|
||||||
|
|
||||||
if (session == null)
|
if (session == null)
|
||||||
return null;
|
throw new IllegalStateException("Not connected to the Saturn server");
|
||||||
|
|
||||||
try {
|
|
||||||
ServiceMethods.Types.Player player = session.request(ServiceMethods.GetPlayer, uuid.toString()).join();
|
ServiceMethods.Types.Player player = session.request(ServiceMethods.GetPlayer, uuid.toString()).join();
|
||||||
|
|
||||||
if (player == null)
|
if (player == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return player.toSaturnPlayer(uuid, name);
|
return player.toSaturnPlayer(uuid, name);
|
||||||
} catch (Exception e) {
|
|
||||||
Providers.saturn.logError("Failed to get player", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,39 @@
|
|||||||
package org.saturnclient.client.player;
|
package org.saturnclient.client.player;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import org.saturnclient.client.ServiceClient;
|
import org.saturnclient.client.ServiceClient;
|
||||||
import org.saturnclient.common.provider.Providers;
|
import org.saturnclient.common.provider.Providers;
|
||||||
|
|
||||||
public class SaturnPlayer {
|
public class SaturnPlayer {
|
||||||
private static final Map<UUID, SaturnPlayer> PLAYERS = new HashMap<>();
|
/** Players known to be using Saturn. */
|
||||||
|
private static final Map<UUID, SaturnPlayer> PLAYERS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static final Queue<UUID> QUEUE = new ConcurrentLinkedQueue<>();
|
/**
|
||||||
|
* Earliest time (ms) a player without a Saturn entry may be looked up again,
|
||||||
|
* so non-Saturn players aren't re-requested every frame.
|
||||||
|
*/
|
||||||
|
private static final Map<UUID, Long> NEXT_LOOKUP = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static volatile boolean RUNNING = false;
|
/** Players queued or being looked up. */
|
||||||
private static Thread WORKER;
|
private static final Set<UUID> PENDING = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
public String cloak = "";
|
private static final long NOT_SATURN_RECHECK_MS = 60_000;
|
||||||
public String hat = "";
|
private static final long FAILED_LOOKUP_RETRY_MS = 5_000;
|
||||||
|
|
||||||
|
private static final ExecutorService LOOKUPS = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "SaturnPlayer-Worker");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
|
||||||
|
public volatile String cloak = "";
|
||||||
|
public volatile String hat = "";
|
||||||
public final UUID uuid;
|
public final UUID uuid;
|
||||||
public final String name;
|
public final String name;
|
||||||
|
|
||||||
@@ -30,12 +45,18 @@ public class SaturnPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SaturnPlayer get() {
|
public static SaturnPlayer get() {
|
||||||
if (ServiceClient.uuid == null) {
|
UUID self = ServiceClient.uuid;
|
||||||
|
if (self == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return PLAYERS.get(ServiceClient.uuid);
|
return PLAYERS.get(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the player if they are known to use Saturn, otherwise null. An
|
||||||
|
* unknown player is looked up in the background, at most once at a time and
|
||||||
|
* no more often than {@link #NEXT_LOOKUP} allows. Safe to call every frame.
|
||||||
|
*/
|
||||||
public static SaturnPlayer get(String name, UUID uuid) {
|
public static SaturnPlayer get(String name, UUID uuid) {
|
||||||
if (uuid == null)
|
if (uuid == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -43,9 +64,7 @@ public class SaturnPlayer {
|
|||||||
SaturnPlayer player = PLAYERS.get(uuid);
|
SaturnPlayer player = PLAYERS.get(uuid);
|
||||||
|
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
PLAYERS.put(uuid, null);
|
requestLookup(name, uuid);
|
||||||
QUEUE.add(uuid);
|
|
||||||
startPlayerThread();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
@@ -61,58 +80,46 @@ public class SaturnPlayer {
|
|||||||
|
|
||||||
public static void set(SaturnPlayer player) {
|
public static void set(SaturnPlayer player) {
|
||||||
PLAYERS.put(player.uuid, player);
|
PLAYERS.put(player.uuid, player);
|
||||||
|
NEXT_LOOKUP.remove(player.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getExternalUUIDAsString() {
|
public static String[] getExternalUUIDAsString() {
|
||||||
return PLAYERS.keySet().stream().filter(id -> ServiceClient.uuid == null || !id.equals(ServiceClient.uuid))
|
UUID self = ServiceClient.uuid;
|
||||||
|
|
||||||
|
return PLAYERS.keySet().stream().filter(id -> self == null || !id.equals(self))
|
||||||
.map(UUID::toString)
|
.map(UUID::toString)
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized void startPlayerThread() {
|
private static void requestLookup(String name, UUID uuid) {
|
||||||
if (RUNNING)
|
if (!ServiceClient.isConnected()) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RUNNING = true;
|
Long next = NEXT_LOOKUP.get(uuid);
|
||||||
|
if (next != null && System.currentTimeMillis() < next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
WORKER = new Thread(() -> {
|
if (!PENDING.add(uuid)) {
|
||||||
long lastWorkTime = System.currentTimeMillis();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
LOOKUPS.execute(() -> {
|
||||||
UUID uuid = QUEUE.poll();
|
|
||||||
|
|
||||||
if (uuid != null) {
|
|
||||||
try {
|
try {
|
||||||
String name = Providers.saturn.getClient().getPlayerListEntry(uuid);
|
|
||||||
|
|
||||||
SaturnPlayer player = ServiceClient.getPlayer(uuid, name);
|
SaturnPlayer player = ServiceClient.getPlayer(uuid, name);
|
||||||
|
|
||||||
// Apply on main thread
|
if (player != null) {
|
||||||
Providers.saturn.getClient().executeOnThread(() -> {
|
set(player);
|
||||||
PLAYERS.put(uuid, player);
|
} else {
|
||||||
});
|
NEXT_LOOKUP.put(uuid, System.currentTimeMillis() + NOT_SATURN_RECHECK_MS);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Providers.saturn.logError("Failed to fetch player " + uuid, e);
|
Providers.saturn.logError("Failed to fetch player " + uuid, e);
|
||||||
|
NEXT_LOOKUP.put(uuid, System.currentTimeMillis() + FAILED_LOOKUP_RETRY_MS);
|
||||||
|
} finally {
|
||||||
|
PENDING.remove(uuid);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
lastWorkTime = System.currentTimeMillis();
|
|
||||||
} else {
|
|
||||||
if (System.currentTimeMillis() - lastWorkTime > 5000) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(50);
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RUNNING = false;
|
|
||||||
}, "SaturnPlayer-Worker");
|
|
||||||
|
|
||||||
WORKER.setDaemon(true);
|
|
||||||
WORKER.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.saturnclient.client.player.SaturnPlayer;
|
|||||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
public class Cloaks {
|
public class Cloaks {
|
||||||
public static final String[] ALL_CLOAKS = {
|
public static final String[] ALL_CLOAKS = {
|
||||||
@@ -30,7 +31,8 @@ public class Cloaks {
|
|||||||
"black_hole_flame",
|
"black_hole_flame",
|
||||||
"black_hole_white" };
|
"black_hole_white" };
|
||||||
|
|
||||||
public static final List<String> availableCloaks = new ArrayList<>();
|
// Written by network callbacks, read by the UI thread.
|
||||||
|
public static final List<String> availableCloaks = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public static void initialize() {
|
public static void initialize() {
|
||||||
availableCloaks.add(0, "");
|
availableCloaks.add(0, "");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.saturnclient.cosmetics;
|
package org.saturnclient.cosmetics;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
import org.saturnclient.client.ServiceClient;
|
import org.saturnclient.client.ServiceClient;
|
||||||
import org.saturnclient.client.player.SaturnPlayer;
|
import org.saturnclient.client.player.SaturnPlayer;
|
||||||
@@ -9,7 +9,8 @@ import org.saturnclient.client.player.SaturnPlayer;
|
|||||||
public class Hats {
|
public class Hats {
|
||||||
public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo_white", "halo_black", "horns_end",
|
public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo_white", "halo_black", "horns_end",
|
||||||
"halo_end", "bucket_black", "bucket_end" };
|
"halo_end", "bucket_black", "bucket_end" };
|
||||||
public static List<String> availableHats = new ArrayList<>();
|
// Written by network callbacks, read by the UI thread.
|
||||||
|
public static List<String> availableHats = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public static void initialize() {
|
public static void initialize() {
|
||||||
availableHats.add(0, "");
|
availableHats.add(0, "");
|
||||||
|
|||||||
Reference in New Issue
Block a user