Stop per-frame player lookups and make shared state thread-safe
SaturnPlayer stored non-Saturn players as null, which looked the same as "not fetched yet", so every render re-queued them and flooded the server with get_player requests. Remember negative lookups and re-check them after 60 s, retry failed lookups after 5 s, queue each player at most once, skip lookups while disconnected, and replace the hand-rolled worker thread with a single-thread executor. getPlayer now returns null only when the server says the player isn't on Saturn and throws on failure, so failures are retried instead of cached. Make state shared between network callbacks and the render thread safe: ConcurrentHashMap for players, CopyOnWriteArrayList for owned cosmetics, volatile session/uuid and cosmetic fields, and run emote notifications on the client thread. Refs saturnclientmc/saturnclient#2, saturnclientmc/saturnclient#6 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -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)
|
||||||
|
throw new IllegalStateException("Not connected to the Saturn server");
|
||||||
|
|
||||||
|
ServiceMethods.Types.Player player = session.request(ServiceMethods.GetPlayer, uuid.toString()).join();
|
||||||
|
|
||||||
|
if (player == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
try {
|
return player.toSaturnPlayer(uuid, name);
|
||||||
ServiceMethods.Types.Player player = session.request(ServiceMethods.GetPlayer, uuid.toString()).join();
|
|
||||||
|
|
||||||
if (player == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
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();
|
try {
|
||||||
|
SaturnPlayer player = ServiceClient.getPlayer(uuid, name);
|
||||||
|
|
||||||
if (uuid != null) {
|
if (player != null) {
|
||||||
try {
|
set(player);
|
||||||
String name = Providers.saturn.getClient().getPlayerListEntry(uuid);
|
|
||||||
|
|
||||||
SaturnPlayer player = ServiceClient.getPlayer(uuid, name);
|
|
||||||
|
|
||||||
// Apply on main thread
|
|
||||||
Providers.saturn.getClient().executeOnThread(() -> {
|
|
||||||
PLAYERS.put(uuid, player);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Providers.saturn.logError("Failed to fetch player " + uuid, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastWorkTime = System.currentTimeMillis();
|
|
||||||
} else {
|
} else {
|
||||||
if (System.currentTimeMillis() - lastWorkTime > 5000) {
|
NEXT_LOOKUP.put(uuid, System.currentTimeMillis() + NOT_SATURN_RECHECK_MS);
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(50);
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Providers.saturn.logError("Failed to fetch player " + uuid, e);
|
||||||
|
NEXT_LOOKUP.put(uuid, System.currentTimeMillis() + FAILED_LOOKUP_RETRY_MS);
|
||||||
|
} finally {
|
||||||
|
PENDING.remove(uuid);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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