From 34eb2177477f7f53d301fc892cf8103382cd8a0c Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Mon, 23 Mar 2026 20:20:04 +0100 Subject: [PATCH] Gif loading --- .../client/player/SaturnPlayer.java | 7 +- .../org/saturnclient/cosmetics/Cloaks.java | 20 +-- .../saturnclient/cosmetics/utils/GifData.java | 18 +++ .../{AnimatedCloakData.java => GifFrame.java} | 4 +- .../cosmetics/utils/GifUtils.java | 151 ++++++++++++++++++ 5 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/saturnclient/cosmetics/utils/GifData.java rename src/main/java/org/saturnclient/cosmetics/utils/{AnimatedCloakData.java => GifFrame.java} (88%) create mode 100644 src/main/java/org/saturnclient/cosmetics/utils/GifUtils.java diff --git a/src/main/java/org/saturnclient/client/player/SaturnPlayer.java b/src/main/java/org/saturnclient/client/player/SaturnPlayer.java index 83c5827..eee47ce 100644 --- a/src/main/java/org/saturnclient/client/player/SaturnPlayer.java +++ b/src/main/java/org/saturnclient/client/player/SaturnPlayer.java @@ -3,9 +3,7 @@ package org.saturnclient.client.player; import java.util.HashMap; import java.util.Map; import java.util.Queue; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import org.saturnclient.client.ServiceClient; @@ -15,7 +13,6 @@ public class SaturnPlayer { private static final Map PLAYERS = new HashMap<>(); private static final Queue QUEUE = new ConcurrentLinkedQueue<>(); - private static final Set QUEUED = ConcurrentHashMap.newKeySet(); private static volatile boolean RUNNING = false; private static Thread WORKER; @@ -45,7 +42,7 @@ public class SaturnPlayer { SaturnPlayer player = PLAYERS.get(uuid); - if (player == null && QUEUED.add(uuid)) { + if (player == null) { PLAYERS.put(uuid, null); QUEUE.add(uuid); startPlayerThread(); @@ -97,8 +94,6 @@ public class SaturnPlayer { } catch (Exception e) { Providers.saturn.logError("Failed to fetch player " + uuid, e); - } finally { - QUEUED.remove(uuid); } lastWorkTime = System.currentTimeMillis(); diff --git a/src/main/java/org/saturnclient/cosmetics/Cloaks.java b/src/main/java/org/saturnclient/cosmetics/Cloaks.java index 1baa2f4..177c866 100644 --- a/src/main/java/org/saturnclient/cosmetics/Cloaks.java +++ b/src/main/java/org/saturnclient/cosmetics/Cloaks.java @@ -11,7 +11,7 @@ import java.io.InputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import org.saturnclient.cosmetics.utils.AnimatedCloakData; +import org.saturnclient.cosmetics.utils.GifFrame; import org.saturnclient.cosmetics.utils.GifDecoder; // import org.saturnclient.cosmetics.utils.IdentifierUtils; @@ -21,18 +21,20 @@ import org.saturnclient.cosmetics.utils.GifDecoder; * Originally created by IIpho3nix and modified for Saturn Client by leo. */ public class Cloaks { - public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas", "ferrari", - "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white", "albania_mark", "end", "spanish_empire", "spain_flag" }; + public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas", + "ferrari", + "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white", "albania_mark", "end", + "spanish_empire", "spain_flag" }; private static final String[] ANIMATED_CLOAKS = { "glitch", "black_hole_amethyst", "black_hole_flame", "black_hole_white" }; private static final String CLOAKS_RESOURCE_PATH = "assets/saturnclient/textures/cloaks/"; public static final List availableCloaks = new ArrayList<>(); public static IdentifierRef cloakCacheIdentifier = null; - public static final Map> animatedCloaks = new ConcurrentHashMap<>(); + public static final Map> animatedCloaks = new ConcurrentHashMap<>(); private static final Map lastFrameTime = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap> CLOAK_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> CLOAK_CACHE = new ConcurrentHashMap<>(); /** * Initializes the cloak system. @@ -88,7 +90,7 @@ public class Cloaks { String fileName = cloakName + ".gif"; // Check cache first - List cached = CLOAK_CACHE.get(fileName); + List cached = CLOAK_CACHE.get(fileName); if (cached != null) { animatedCloaks.put(cloakName, cached); lastFrameTime.put(cloakName, System.currentTimeMillis()); @@ -114,7 +116,7 @@ public class Cloaks { } // Pre-allocate collections with known size - List animatedFrames = new ArrayList<>(frameCount); + List animatedFrames = new ArrayList<>(frameCount); String baseFrameId = fileName.replace(".gif", ""); // Process frames in batch @@ -127,7 +129,7 @@ public class Cloaks { try { Providers.saturn.registerBufferedImageTexture(frameIdentifier, frame); - animatedFrames.add(new AnimatedCloakData(frameIdentifier, delay)); + animatedFrames.add(new GifFrame(frameIdentifier, delay)); } catch (Exception e) { Providers.saturn.logError("Failed to register frame " + i + " for cloak: " + fileName, e); // Continue with other frames instead of failing completely @@ -155,7 +157,7 @@ public class Cloaks { public static IdentifierRef getCurrentCloakTexture(String cloakName) { if (Arrays.asList(ANIMATED_CLOAKS).contains(cloakName)) { - List frames = animatedCloaks.get(cloakName); + List frames = animatedCloaks.get(cloakName); if (frames == null || frames.isEmpty()) { return null; } diff --git a/src/main/java/org/saturnclient/cosmetics/utils/GifData.java b/src/main/java/org/saturnclient/cosmetics/utils/GifData.java new file mode 100644 index 0000000..dc5dd3b --- /dev/null +++ b/src/main/java/org/saturnclient/cosmetics/utils/GifData.java @@ -0,0 +1,18 @@ +package org.saturnclient.cosmetics.utils; + +import java.util.List; + +public class GifData { + public final List frames; + public final int totalDuration; + + public GifData(List frames) { + this.frames = frames; + + int sum = 0; + for (GifFrame f : frames) { + sum += f.getDelayMs(); + } + this.totalDuration = sum; + } +} diff --git a/src/main/java/org/saturnclient/cosmetics/utils/AnimatedCloakData.java b/src/main/java/org/saturnclient/cosmetics/utils/GifFrame.java similarity index 88% rename from src/main/java/org/saturnclient/cosmetics/utils/AnimatedCloakData.java rename to src/main/java/org/saturnclient/cosmetics/utils/GifFrame.java index 70cebdf..10984ea 100644 --- a/src/main/java/org/saturnclient/cosmetics/utils/AnimatedCloakData.java +++ b/src/main/java/org/saturnclient/cosmetics/utils/GifFrame.java @@ -5,7 +5,7 @@ import org.saturnclient.common.ref.asset.IdentifierRef; /** * Represents a single frame of an animated cloak. */ -public class AnimatedCloakData { +public class GifFrame { private final IdentifierRef textureId; private final int delayMs; @@ -16,7 +16,7 @@ public class AnimatedCloakData { * @param textureId The texture identifier for this frame * @param delayMs The delay in milliseconds before the next frame */ - public AnimatedCloakData(IdentifierRef textureId, int delayMs) { + public GifFrame(IdentifierRef textureId, int delayMs) { this.textureId = textureId; this.delayMs = delayMs; } diff --git a/src/main/java/org/saturnclient/cosmetics/utils/GifUtils.java b/src/main/java/org/saturnclient/cosmetics/utils/GifUtils.java new file mode 100644 index 0000000..527d1ad --- /dev/null +++ b/src/main/java/org/saturnclient/cosmetics/utils/GifUtils.java @@ -0,0 +1,151 @@ +package org.saturnclient.cosmetics.utils; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.awt.image.BufferedImage; + +import org.saturnclient.common.provider.Providers; +import org.saturnclient.common.ref.asset.IdentifierRef; + +public class GifUtils { + public static Map loadedCache = new ConcurrentHashMap<>(); + + private static final Queue QUEUE = new ConcurrentLinkedQueue<>(); + private static volatile boolean RUNNING = false; + private static Thread WORKER; + + public static IdentifierRef get(IdentifierRef gif) { + String g = gif.toString(); + + if (!loadedCache.containsKey(g)) { + loadedCache.put(g, null); + } + + if (loadedCache.get(g) == null) { + return IdentifierRef.of(g.replace(".gif", ".png")); + } + + long now = System.currentTimeMillis(); + + GifData data = loadedCache.get(g); + + long t = now % data.totalDuration; + + int accumulated = 0; + + for (GifFrame frame : data.frames) { + accumulated += frame.getDelayMs(); + if (t < accumulated) { + return frame.getTextureId(); + } + } + + return data.frames.get(data.frames.size() - 1).getTextureId(); + } + + public static GifData getData(IdentifierRef id) { + String fileName = id.toString(); + + try (InputStream inputStream = Providers.saturn.getClient().getResource(id)) { + if (inputStream == null) { + Providers.saturn.logError("Cloak resource not found: " + fileName); + return null; + } + + // Read all bytes at once - more efficient than multiple reads + byte[] data = inputStream.readAllBytes(); + GifDecoder.GifImage gif = GifDecoder.read(data); + + int frameCount = gif.getFrameCount(); + if (frameCount == 0) { + Providers.saturn.logError("No frames found in animated cloak: " + fileName); + return null; + } + + // Pre-allocate collections with known size + List animatedFrames = new ArrayList<>(frameCount); + String baseFrameId = fileName.replace(".gif", ""); + + // Process frames in batch + for (int i = 0; i < frameCount; i++) { + BufferedImage frame = gif.getFrame(i); + int delay = Math.max(gif.getDelay(i) * 10, 50); // Minimum 50ms delay + + String frameId = baseFrameId + "_frame_" + i; + IdentifierRef frameIdentifier = IdentifierRef.ofSaturn("cloaks_" + frameId); + + try { + Providers.saturn.registerBufferedImageTexture(frameIdentifier, frame); + animatedFrames.add(new GifFrame(frameIdentifier, delay)); + } catch (Exception e) { + Providers.saturn.logError("Failed to register frame " + i + " for cloak: " + fileName); + e.printStackTrace(); + } + } + + if (!animatedFrames.isEmpty()) { + Providers.saturn.logInfo( + "Loaded " + animatedFrames.size() + " frames for animated cloak: " + fileName + " (cached)"); + + return new GifData(animatedFrames); + } else { + Providers.saturn.logError("No valid frames could be loaded for cloak: " + fileName); + } + + } catch (Exception e) { + Providers.saturn.logError("Failed to load animated cloak from resources: " + fileName, e); + } + + return null; + } + + public static synchronized void startPlayerThread() { + if (RUNNING) + return; + + RUNNING = true; + + WORKER = new Thread(() -> { + long lastWorkTime = System.currentTimeMillis(); + + while (true) { + IdentifierRef id = QUEUE.poll(); + + if (id != null) { + try { + GifData data = getData(id); + + // Apply on main thread + Providers.saturn.getClient().executeOnThread(() -> { + loadedCache.put(id.toString(), data); + }); + + } catch (Exception e) { + Providers.saturn.logError("Failed to load gif " + id, e); + } + + 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(); + } +}