diff --git a/src/main/java/org/auraclient/auraclient/cloaks/Cloaks.java b/src/main/java/org/auraclient/auraclient/cloaks/Cloaks.java index 1d38ca3..7d5bf28 100644 --- a/src/main/java/org/auraclient/auraclient/cloaks/Cloaks.java +++ b/src/main/java/org/auraclient/auraclient/cloaks/Cloaks.java @@ -3,13 +3,17 @@ package org.auraclient.auraclient.cloaks; import net.minecraft.util.Identifier; import org.auraclient.auraclient.AuraClient; import org.auraclient.auraclient.auth.AuraApi; +import org.auraclient.auraclient.cloaks.utils.AnimatedCapeData; import org.auraclient.auraclient.cloaks.utils.IdentifierUtils; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; /** * Manages the cloak/cape system for Aura Client. @@ -20,6 +24,8 @@ public class Cloaks { public static final List availableCloaks = new ArrayList<>(); public static Identifier capeCacheIdentifier = null; public static final Map playerCapes = new HashMap<>(); + public static final Map> animatedCapes = new ConcurrentHashMap<>(); + private static final Map lastFrameTime = new ConcurrentHashMap<>(); /** * Initializes the cloak system. @@ -39,7 +45,11 @@ public class Cloaks { Cloaks.playerCapes.put(uuid, capeName); if (!capeName.isEmpty()) { - loadStaticCape(capeName); + if (capeName.endsWith(".gif")) { + loadAnimatedCape(uuid, capeName); + } else { + loadStaticCape(capeName); + } } } @@ -68,4 +78,97 @@ public class Cloaks { AuraClient.LOGGER.error("Failed to load static cape from resources: " + fileName, e); } } + + private static void loadAnimatedCape(String uuid, String fileName) { + try { + String resourcePath = CAPES_RESOURCE_PATH + fileName; + InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath); + + if (inputStream != null) { + ImageInputStream stream = ImageIO.createImageInputStream(inputStream); + ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next(); + reader.setInput(stream); + + List frames = new ArrayList<>(); + int frameCount = reader.getNumImages(true); + + for (int i = 0; i < frameCount; i++) { + BufferedImage frame = reader.read(i); + String frameId = fileName.replace(".gif", "") + "_frame_" + i; + Identifier frameIdentifier = Identifier.of(AuraClient.MOD_ID, "capes_" + frameId); + IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame); + + // Get frame delay in milliseconds (default to 100ms if not specified) + int delay = 100; + try { + javax.imageio.metadata.IIOMetadata metadata = reader.getImageMetadata(i); + String metaFormatName = metadata.getNativeMetadataFormatName(); + org.w3c.dom.Node root = metadata.getAsTree(metaFormatName); + org.w3c.dom.NodeList children = root.getChildNodes(); + + for (int j = 0; j < children.getLength(); j++) { + org.w3c.dom.Node node = children.item(j); + if (node.getNodeName().equals("GraphicControlExtension")) { + org.w3c.dom.NamedNodeMap attrs = node.getAttributes(); + org.w3c.dom.Node delayNode = attrs.getNamedItem("delayTime"); + if (delayNode != null) { + delay = Integer.parseInt(delayNode.getNodeValue()) * 10; // Convert centiseconds to + // milliseconds + } + break; + } + } + } catch (Exception e) { + AuraClient.LOGGER.warn("Could not read frame delay for " + fileName + " frame " + i); + } + + frames.add(new AnimatedCapeData(frameIdentifier, delay)); + } + + animatedCapes.put(uuid, frames); + lastFrameTime.put(uuid, System.currentTimeMillis()); + } + } catch (IOException e) { + AuraClient.LOGGER.error("Failed to load animated cape from resources: " + fileName, e); + } + } + + public static Identifier getCurrentCapeTexture(String uuid) { + if (!playerCapes.containsKey(uuid)) { + return null; + } + + String capeName = playerCapes.get(uuid); + if (capeName.endsWith(".gif")) { + List frames = animatedCapes.get(uuid); + if (frames == null || frames.isEmpty()) { + return null; + } + + long currentTime = System.currentTimeMillis(); + long lastTime = lastFrameTime.getOrDefault(uuid, currentTime); + int currentFrame = 0; + + // Find the current frame based on elapsed time + long elapsedTime = currentTime - lastTime; + long totalTime = 0; + for (int i = 0; i < frames.size(); i++) { + totalTime += frames.get(i).getDelayMs(); + if (elapsedTime < totalTime) { + currentFrame = i; + break; + } + } + + // Reset animation if we've gone through all frames + if (elapsedTime >= totalTime) { + lastFrameTime.put(uuid, currentTime); + currentFrame = 0; + } + + return frames.get(currentFrame).getTextureId(); + } else { + return Identifier.of(AuraClient.MOD_ID, "textures/capes/" + capeName + ".png"); + } + } } \ No newline at end of file diff --git a/src/main/java/org/auraclient/auraclient/mixin/AbstractClientPlayerEntityMixin.java b/src/main/java/org/auraclient/auraclient/mixin/AbstractClientPlayerEntityMixin.java index 5de953b..e40a7b4 100644 --- a/src/main/java/org/auraclient/auraclient/mixin/AbstractClientPlayerEntityMixin.java +++ b/src/main/java/org/auraclient/auraclient/mixin/AbstractClientPlayerEntityMixin.java @@ -27,18 +27,16 @@ public class AbstractClientPlayerEntityMixin { @Inject(method = "getSkinTextures", at = @At("RETURN"), cancellable = true) private void getSkinTextures(CallbackInfoReturnable cir) { AbstractClientPlayerEntity player = (AbstractClientPlayerEntity) (Object) this; - String uuid = player.getUuidAsString().replace("-", ""); if (Cloaks.playerCapes.containsKey(uuid)) { - String cloak = Cloaks.playerCapes.get(uuid); - - if (!cloak.isEmpty()) { + Identifier capeTexture = Cloaks.getCurrentCapeTexture(uuid); + if (capeTexture != null) { SkinTextures textures = cir.getReturnValue(); cir.setReturnValue(new SkinTextures( textures.texture(), textures.textureUrl(), - Identifier.of("auraclient", "textures/capes/" + cloak + ".png"), + capeTexture, textures.elytraTexture(), textures.model(), textures.secure()));