package org.saturnclient.client.player; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.saturnclient.client.ServiceClient; import org.saturnclient.common.provider.Providers; public class SaturnPlayer { /** Players known to be using Saturn. */ private static final Map PLAYERS = new ConcurrentHashMap<>(); /** * 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 NEXT_LOOKUP = new ConcurrentHashMap<>(); /** Players queued or being looked up. */ private static final Set PENDING = ConcurrentHashMap.newKeySet(); private static final long NOT_SATURN_RECHECK_MS = 60_000; 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 String name; public SaturnPlayer(UUID uuid, String name, String cloak, String hat) { this.cloak = cloak; this.hat = hat; this.uuid = uuid; this.name = name; } public static SaturnPlayer get() { UUID self = ServiceClient.uuid; if (self == null) { return null; } 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) { if (uuid == null) return null; SaturnPlayer player = PLAYERS.get(uuid); if (player == null) { requestLookup(name, uuid); } return player; } public static SaturnPlayer get(UUID uuid) { return get(Providers.saturn.getClient().getPlayerListEntry(uuid), uuid); } public static SaturnPlayer get(String name) { 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); } public static String[] getExternalUUIDAsString() { UUID self = ServiceClient.uuid; return PLAYERS.keySet().stream().filter(id -> self == null || !id.equals(self)) .map(UUID::toString) .toArray(String[]::new); } private static void requestLookup(String name, UUID uuid) { if (!ServiceClient.isConnected()) { return; } Long next = NEXT_LOOKUP.get(uuid); if (next != null && System.currentTimeMillis() < next) { return; } if (!PENDING.add(uuid)) { return; } LOOKUPS.execute(() -> { try { SaturnPlayer player = ServiceClient.getPlayer(uuid, name); if (player != null) { set(player); } else { NEXT_LOOKUP.put(uuid, System.currentTimeMillis() + NOT_SATURN_RECHECK_MS); } } 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); } }); } }