diff --git a/versions/1.21.4/src/main/java/org/saturnclient/SaturnClient.java b/versions/1.21.4/src/main/java/org/saturnclient/SaturnClient.java
deleted file mode 100644
index 12f9cc3..0000000
--- a/versions/1.21.4/src/main/java/org/saturnclient/SaturnClient.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.saturnclient;
-
-import net.fabricmc.api.ModInitializer;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class SaturnClient implements ModInitializer {
- public static final String MOD_ID = "saturn-client";
-
- // This logger is used to write text to the console and the log file.
- // It is considered best practice to use your mod id as the logger's name.
- // That way, it's clear which mod wrote info, warnings, and errors.
- public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
-
- @Override
- public void onInitialize() {
- // This code runs as soon as Minecraft is in a mod-load-ready state.
- // However, some things (like resources) may still be uninitialized.
- // Proceed with mild caution.
-
- LOGGER.info("Hello Fabric world!");
- }
-}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/Emotes.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/Emotes.java
new file mode 100644
index 0000000..bc0353b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/Emotes.java
@@ -0,0 +1,17 @@
+package org.saturnclient.cosmetics;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Emotes {
+ public static List availableEmotes = new ArrayList<>();
+
+ public static void initialize() {
+ availableEmotes.add("tpose");
+ availableEmotes.add("back_flip");
+ availableEmotes.add("facepalm");
+ availableEmotes.add("cry");
+ availableEmotes.add("giga_chad");
+ availableEmotes.add("front_flip");
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/CloakFeatureRenderer.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/CloakFeatureRenderer.java
new file mode 100644
index 0000000..a91ff29
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/CloakFeatureRenderer.java
@@ -0,0 +1,343 @@
+package org.saturnclient.cosmetics.cloak;
+
+import java.util.Arrays;
+
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+import org.saturnclient.saturnclient.config.Config;
+
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.equipment.EquipmentModel;
+import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
+import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
+import net.minecraft.client.render.entity.feature.FeatureRenderer;
+import net.minecraft.client.render.entity.feature.FeatureRendererContext;
+import net.minecraft.client.render.entity.model.PlayerEntityModel;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.component.type.EquippableComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.equipment.EquipmentAsset;
+import net.minecraft.registry.RegistryKey;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.RotationAxis;
+import net.minecraft.util.math.Vec3d;
+
+/**
+ * The cloak renderer
+ * - **Efficient**: uses a image only consisting of the cloak texture, 16x8
+ * whitespace.
+ * - **Curving**: Curves when player velocity increases.
+ * - **Wavey**: Makes the cloak feel like wind, when jump sprinting.
+ */
+public class CloakFeatureRenderer extends FeatureRenderer {
+ private final EquipmentModelLoader equipmentModelLoader;
+ private static final int PARTS = 24;
+ private final float[] segmentValues = new float[PARTS];
+
+ private static final float TEX_W = 176f;
+ private static final float TEX_H = 138f;
+
+ private static final class PxRect {
+ final int x, y, w, h;
+
+ PxRect(int x, int y, int w, int h) {
+ this.x = x;
+ this.y = y;
+ this.w = w;
+ this.h = h;
+ }
+
+ float u1() {
+ return x / TEX_W;
+ }
+
+ float v1() {
+ return y / TEX_H;
+ }
+
+ float u2() {
+ return (x + w) / TEX_W;
+ }
+
+ float v2() {
+ return (y + h) / TEX_H;
+ }
+ }
+
+ private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128);
+ private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128);
+ private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128);
+ private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128);
+ private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
+ private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
+
+ public CloakFeatureRenderer(FeatureRendererContext context,
+ EquipmentModelLoader equipmentModelLoader) {
+ super(context);
+ this.equipmentModelLoader = equipmentModelLoader;
+ Arrays.fill(segmentValues, 0.0f);
+ }
+
+ private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
+ EquippableComponent equippableComponent = (EquippableComponent) stack
+ .get(DataComponentTypes.EQUIPPABLE);
+ if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
+ EquipmentModel equipmentModel = this.equipmentModelLoader
+ .get((RegistryKey) equippableComponent.assetId().get());
+ return !equipmentModel.getLayers(layerType).isEmpty();
+ } else {
+ return false;
+ }
+ }
+
+ private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
+ Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
+ float u1, float v1, float u2, float v2, int light, int overlay,
+ float normalX, float normalY, float normalZ, boolean flipWinding) {
+ MatrixStack.Entry entry = matrixStack.peek();
+
+ if (!flipWinding) {
+ // CCW
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
+ (float) bottomLeft.z)
+ .color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
+ (float) bottomRight.z)
+ .color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
+ (float) topRight.z)
+ .color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
+ (float) topLeft.z)
+ .color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ } else {
+ // CW
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
+ (float) topLeft.z)
+ .color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
+ (float) topRight.z)
+ .color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
+ (float) bottomRight.z)
+ .color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
+ (float) bottomLeft.z)
+ .color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
+ .normal(normalX, normalY, normalZ);
+ }
+ }
+
+ private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer, PlayerEntityRenderState playerState,
+ int light, int overlay) {
+ float capeWidth = 10.0f / 16.0f;
+ float capeHeight = 16.0f / 16.0f;
+ float capeDepth = 1.0f / 16.0f;
+ float capePartHeight = capeHeight / PARTS;
+
+ float x1 = -capeWidth / 2.0f;
+ float x2 = capeWidth / 2.0f;
+
+ final float frontU1 = FRONT_RECT.u1();
+ final float frontU2 = FRONT_RECT.u2();
+ final float frontV1 = FRONT_RECT.v1();
+ final float frontV2 = FRONT_RECT.v2();
+ final float frontPartV = (frontV2 - frontV1) / PARTS;
+ final float backU1 = BACK_RECT.u1();
+ final float backU2 = BACK_RECT.u2();
+ final float backV1 = BACK_RECT.v1();
+ final float backV2 = BACK_RECT.v2();
+ final float backPartV = (backV2 - backV1) / PARTS;
+ final float leftU1 = LEFT_EDGE_RECT.u1();
+ final float leftU2 = LEFT_EDGE_RECT.u2();
+ final float leftV1 = LEFT_EDGE_RECT.v1();
+ final float leftV2 = LEFT_EDGE_RECT.v2();
+ final float leftPartV = (leftV2 - leftV1) / PARTS;
+ final float rightU1 = RIGHT_EDGE_RECT.u1();
+ final float rightU2 = RIGHT_EDGE_RECT.u2();
+ final float rightV1 = RIGHT_EDGE_RECT.v1();
+ final float rightV2 = RIGHT_EDGE_RECT.v2();
+ final float rightPartV = (rightV2 - rightV1) / PARTS;
+ final float topU1 = TOP_RECT.u1();
+ final float topU2 = TOP_RECT.u2();
+ final float topV1 = TOP_RECT.v1();
+ final float topV2 = TOP_RECT.v2();
+ final float bottomU1 = BOTTOM_RECT.u1();
+ final float bottomU2 = BOTTOM_RECT.u2();
+ final float bottomV1 = BOTTOM_RECT.v1();
+ final float bottomV2 = BOTTOM_RECT.v2();
+
+ float curY = 0.0f;
+ float curZ = 0.0f;
+
+ for (int i = 0; i < PARTS; i++) {
+ float angle = (2.0f - segmentValues[i]) * ((float) Math.PI / 2f);
+ float dirY = -(float) Math.cos(angle);
+ float dirZ = (float) Math.sin(angle);
+
+ // 1. Thickness at the START of this segment (Must match PREVIOUS segment's end)
+ float prevAngle = (i > 0) ? (2.0f - segmentValues[i - 1]) * ((float) Math.PI / 2f) : angle;
+ float startAvgAngle = (angle + prevAngle) / 2.0f;
+ float thickYStart = (float) Math.sin(startAvgAngle) * capeDepth;
+ float thickZStart = (float) Math.cos(startAvgAngle) * capeDepth;
+
+ // 2. Thickness at the END of this segment (Must match NEXT segment's start)
+ float nextAngle = (i < PARTS - 1) ? (2.0f - segmentValues[i + 1]) * ((float) Math.PI / 2f) : angle;
+ float endAvgAngle = (angle + nextAngle) / 2.0f;
+ float thickYEnd = (float) Math.sin(endAvgAngle) * capeDepth;
+ float thickZEnd = (float) Math.cos(endAvgAngle) * capeDepth;
+
+ float nextY = curY + dirY * capePartHeight;
+ float nextZ = curZ + dirZ * capePartHeight;
+
+ // Inner Vertices (The spine of the cape)
+ Vec3d innerTopLeft = new Vec3d(x2, curY, curZ);
+ Vec3d innerTopRight = new Vec3d(x1, curY, curZ);
+ Vec3d innerBotLeft = new Vec3d(x2, nextY, nextZ);
+ Vec3d innerBotRight = new Vec3d(x1, nextY, nextZ);
+
+ // Outer Vertices (Offset by the specific joint thickness)
+ Vec3d outerTopLeft = new Vec3d(x2, curY - thickYStart, curZ - thickZStart);
+ Vec3d outerTopRight = new Vec3d(x1, curY - thickYStart, curZ - thickZStart);
+ Vec3d outerBotLeft = new Vec3d(x2, nextY - thickYEnd, nextZ - thickZEnd);
+ Vec3d outerBotRight = new Vec3d(x1, nextY - thickYEnd, nextZ - thickZEnd);
+
+ // FRONT (facing camera)
+ this.renderCapeQuad(vertexConsumer, matrixStack,
+ outerBotRight, outerBotLeft, outerTopLeft, outerTopRight,
+ frontU2, frontV1 + (frontPartV * i), frontU1, frontV1 + (frontPartV * (i + 1)),
+ light, overlay, 0, 0, 1, false);
+
+ // BACK (Facing player)
+ this.renderCapeQuad(vertexConsumer, matrixStack,
+ innerBotLeft, innerBotRight, innerTopRight, innerTopLeft,
+ backU2, backV1 + (backPartV * i), backU1, backV1 + (backPartV * (i + 1)),
+ light, overlay, 0, 0, -1, false);
+
+ // LEFT EDGE
+ this.renderCapeQuad(vertexConsumer, matrixStack,
+ innerBotLeft, outerBotLeft, outerTopLeft, innerTopLeft,
+ leftU1, leftV1 + (leftPartV * i), leftU2, leftV1 + (leftPartV * (i + 1)),
+ light, overlay, 1, 0, 0, false);
+
+ // RIGHT EDGE
+ this.renderCapeQuad(vertexConsumer, matrixStack,
+ outerBotRight, innerBotRight, innerTopRight, outerTopRight,
+ rightU1, rightV1 + (rightPartV * i), rightU2, rightV1 + (rightPartV * (i + 1)),
+ light, overlay, -1, 0, 0, false);
+
+ // TOP FACE (Only on first segment)
+ if (i == 0) {
+ this.renderCapeQuad(vertexConsumer, matrixStack,
+ outerTopLeft, outerTopRight, innerTopRight, innerTopLeft,
+ topU1, topV1, topU2, topV2, light, overlay, 0, 1, 0, false);
+ }
+
+ // BOTTOM FACE (Only on last segment)
+ if (i == PARTS - 1) {
+ this.renderCapeQuad(vertexConsumer, matrixStack,
+ innerBotLeft, innerBotRight, outerBotRight, outerBotLeft,
+ bottomU1, bottomV1, bottomU2, bottomV2, light, overlay, 0, -1, 0, false);
+ }
+
+ curY = nextY;
+ curZ = nextZ;
+ }
+ }
+
+ // Shift array left and insert new value at the start
+ private void updateSegmentValues(float value) {
+ // Shift all elements one position to the right (from end to start)
+ for (int i = segmentValues.length - 1; i > 0; i--) {
+ segmentValues[i] = segmentValues[i - 1];
+ }
+
+ // Insert the new value at index 0
+ segmentValues[0] = value;
+ }
+
+ private long lastUpdate = 0;
+
+ public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
+ PlayerEntityRenderState playerEntityRenderState, float f, float g) {
+ if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible
+ || playerEntityRenderState.skinTextures.capeTexture() != null) {
+ return;
+ }
+
+ SaturnPlayer player = SaturnPlayer.get(playerEntityRenderState.name);
+
+ if (player == null) {
+ return;
+ }
+
+ Identifier customCape = Cloaks.getCurrentCloakTexture(player.uuid);
+ if (customCape == null
+ || this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
+ return;
+ }
+
+ int minBrightness = 7;
+ int blockLight = (light >> 4) & 0xF;
+ int skyLight = (light >> 20) & 0xF;
+ blockLight = Math.max(blockLight, minBrightness);
+ skyLight = Math.max(skyLight, minBrightness);
+ light = (skyLight << 20) | (blockLight << 4);
+
+ VertexConsumer vertexConsumer = vertexConsumerProvider
+ .getBuffer(RenderLayer.getEntityAlpha(customCape));
+
+ matrixStack.push();
+
+ matrixStack.translate(0.0f, 0.0f, 0.12f);
+
+ if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) {
+ matrixStack.translate(0.0F, -0.053125F, 0.06875F);
+ }
+
+ if (playerEntityRenderState.isInSneakingPose) {
+ matrixStack.translate(0, 0.16f, 0.0f);
+ matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f));
+ }
+
+ long now = System.currentTimeMillis();
+ if (now - lastUpdate >= 20) {
+ float velX = Math.min(1.0f, playerEntityRenderState.field_53537 / 108.0f);
+ float rawVelY = playerEntityRenderState.field_53536;
+ float velY = (rawVelY > 4.0f ? rawVelY : 0.0f) / 16;
+
+ float value = playerEntityRenderState.isSwimming ? 0.0f : velX + velY;
+
+ if (Config.cloakPhysics.value) {
+ updateSegmentValues(value);
+ } else {
+ float last = segmentValues[0];
+ float maxStep = 0.02f;
+ float next = value;
+
+ if (next > last)
+ next = Math.min(next, last + maxStep);
+ else if (next < last)
+ next = Math.max(next, last - maxStep);
+
+ Arrays.fill(segmentValues, next);
+ }
+ lastUpdate = now;
+ }
+
+ renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
+
+ matrixStack.pop();
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/Cloaks.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/Cloaks.java
new file mode 100644
index 0000000..ef54f85
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/Cloaks.java
@@ -0,0 +1,265 @@
+package org.saturnclient.cosmetics.cloak;
+
+import net.minecraft.util.Identifier;
+
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.saturnclient.cosmetics.cloak.utils.AnimatedCloakData;
+import org.saturnclient.cosmetics.cloak.utils.IdentifierUtils;
+import org.saturnclient.saturnclient.SaturnClient;
+
+/**
+ * Manages the cloak system for Saturn Client.
+ * Handles loading and caching of player cloaks.
+ * 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" };
+ 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 Identifier cloakCacheIdentifier = null;
+ public static final Map> animatedCloaks = new ConcurrentHashMap<>();
+ private static final Map lastFrameTime = new ConcurrentHashMap<>();
+
+ private static final ExecutorService CLOAK_LOADER_EXECUTOR = Executors.newFixedThreadPool(
+ Math.min(4, Runtime.getRuntime().availableProcessors()),
+ r -> {
+ Thread t = new Thread(r, "cloak-loader");
+ t.setDaemon(true);
+ t.setPriority(Thread.NORM_PRIORITY - 1);
+ return t;
+ });
+
+ private static final ConcurrentHashMap> CLOAK_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * Initializes the cloak system.
+ * Loads cloak textures from resources.
+ */
+ public static void initialize() {
+ availableCloaks.add(0, "");
+ }
+
+ /**
+ * Sets cloak of the current player
+ *
+ * @param cloakName Name of the cloak file to load
+ */
+ public static void setCloak(String cloakName) {
+ if (availableCloaks.contains(cloakName)) {
+ setCloak(ServiceClient.uuid, cloakName);
+ ServiceClient.setCloak(cloakName);
+ }
+ }
+
+ /**
+ * Handles loading and caching of a new cloak texture, without connecting to the
+ * server.
+ *
+ * @param cloakName Name of the cloak file to load
+ */
+ public static void setCloak(UUID uuid, String cloakName) {
+ SaturnPlayer player = SaturnPlayer.get(uuid);
+
+ if (player != null) {
+ player.cloak = cloakName;
+ }
+
+ loadCloak(uuid);
+ }
+
+ /**
+ * Handles loading and caching of a new cloak texture, without connecting to the
+ * server.
+ *
+ * @param cloakName Name of the cloak file to load
+ */
+ public static void loadCloak(UUID uuid) {
+ SaturnPlayer player = SaturnPlayer.get(uuid);
+
+ if (player != null && player.cloak != null) {
+ if (!player.cloak.isEmpty()) {
+ if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) {
+ loadAnimatedCloakAsync(uuid, player.cloak)
+ .exceptionally(throwable -> {
+ SaturnClient.LOGGER.error("Failed to load animated cloak for player: " + player.cloak,
+ throwable);
+ return null;
+ });
+
+ } else {
+ SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png"));
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads and processes a static cloak from a PNG file in resources.
+ *
+ * @param fileName Name of the PNG file to load
+ */
+ private static void loadStaticCloak(String fileName) {
+ try {
+ String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
+ InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath);
+
+ if (inputStream != null) {
+ BufferedImage image = ImageIO.read(inputStream);
+
+ // Add debug logging for static cloak
+ SaturnClient.LOGGER.info("Static cloak dimensions: " + image.getWidth() + "x" + image.getHeight());
+
+ String safeFileName = fileName.toLowerCase(Locale.ROOT)
+ .replace(' ', '_')
+ .replaceAll("[^a-z0-9/._-]", "");
+
+ cloakCacheIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + safeFileName);
+ IdentifierUtils.registerBufferedImageTexture(cloakCacheIdentifier, image);
+ }
+ } catch (IOException e) {
+ SaturnClient.LOGGER.error("Failed to load static cloak from resources: " + fileName, e);
+ }
+ }
+
+ public static CompletableFuture loadAnimatedCloakAsync(UUID uuid, String cloakName) {
+ return CompletableFuture.runAsync(() -> {
+ String fileName = cloakName + ".gif";
+
+ // Check cache first
+ List cached = CLOAK_CACHE.get(fileName);
+ if (cached != null) {
+ animatedCloaks.put(uuid, cached);
+ lastFrameTime.put(uuid, System.currentTimeMillis());
+ return;
+ }
+
+ String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
+
+ try (InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath)) {
+ if (inputStream == null) {
+ SaturnClient.LOGGER.warn("Cloak resource not found: " + resourcePath);
+ return;
+ }
+
+ // 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) {
+ SaturnClient.LOGGER.warn("No frames found in animated cloak: " + fileName);
+ return;
+ }
+
+ // 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;
+ Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId);
+
+ try {
+ IdentifierUtils.registerBufferedImageTextureFast(frameIdentifier, frame);
+ // Register texture on main thread if required by the graphics system
+ if (SaturnClient.client.isOnThread()) {
+ // IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame);
+ } else {
+ // Queue for main thread execution
+ // SaturnClient.client.execute(
+ // () -> IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame));
+ }
+
+ animatedFrames.add(new AnimatedCloakData(frameIdentifier, delay));
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to register frame " + i + " for cloak: " + fileName, e);
+ // Continue with other frames instead of failing completely
+ }
+ }
+
+ if (!animatedFrames.isEmpty()) {
+ // Cache the result for future use
+ CLOAK_CACHE.put(fileName, animatedFrames);
+ animatedCloaks.put(uuid, animatedFrames);
+ lastFrameTime.put(uuid, System.currentTimeMillis());
+
+ SaturnClient.LOGGER.info("Loaded {} frames for animated cloak: {} (cached)",
+ animatedFrames.size(), fileName);
+ } else {
+ SaturnClient.LOGGER.error("No valid frames could be loaded for cloak: " + fileName);
+ }
+
+ } catch (IOException e) {
+ SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + fileName, e);
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Unexpected error loading animated cloak: " + fileName, e);
+ }
+ }, CLOAK_LOADER_EXECUTOR);
+ }
+
+ public static Identifier getCurrentCloakTexture(UUID uuid) {
+ SaturnPlayer player = SaturnPlayer.get(uuid);
+
+ if (player == null || player.cloak.isEmpty()) {
+ return null;
+ }
+
+ String cloakName = player.cloak;
+
+ if (Arrays.asList(ANIMATED_CLOAKS).contains(cloakName)) {
+ List frames = animatedCloaks.get(uuid);
+ if (frames == null || frames.isEmpty()) {
+ return null;
+ }
+
+ long currentTime = System.currentTimeMillis();
+ long lastTime = lastFrameTime.getOrDefault(uuid, currentTime);
+ int currentFrame = 0;
+
+ 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;
+ }
+ }
+
+ if (elapsedTime >= totalTime) {
+ lastFrameTime.put(uuid, currentTime);
+ currentFrame = 0;
+ }
+
+ Identifier identifier = frames.get(currentFrame).getTextureId();
+
+ if (identifier == null) {
+ return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
+ }
+
+ return identifier;
+ } else {
+ return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
+ }
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/GifDecoder.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/GifDecoder.java
new file mode 100644
index 0000000..dcb4b2b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/GifDecoder.java
@@ -0,0 +1,791 @@
+package org.saturnclient.cosmetics.cloak;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/*
+ * Copyright 2014 Dhyan Blum
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ *
+ * A thread-safe decoder capable of processing a GIF data stream to render the graphics
+ * contained in it. This implementation follows the official
+ * GIF
+ * specification .
+ *
+ *
+ *
+ * Example usage:
+ *
+ *
+ *
+ *
+ *
+ * final GifImage gifImage = GifDecoder.read(int[] data);
+ * final int width = gifImage.getWidth();
+ * final int height = gifImage.getHeight();
+ * final int frameCount = gifImage.getFrameCount();
+ * for (int i = 0; i < frameCount; i++) {
+ * final BufferedImage image = gifImage.getFrame(i);
+ * final int delay = gif.getDelay(i);
+ * }
+ *
+ *
+ *
+ *
+ * @author Dhyan Blum
+ * @version 1.09 November 2017 (Thread-safe version)
+ */
+public final class GifDecoder {
+
+ // Thread-local storage for reusable objects to avoid synchronization overhead
+ private static final ThreadLocal DECODING_CONTEXT =
+ ThreadLocal.withInitial(DecodingContext::new);
+
+ /**
+ * Thread-local context containing reusable objects for decoding operations.
+ * This eliminates the need for synchronization while maintaining performance benefits
+ * of object reuse within each thread.
+ */
+ static final class DecodingContext {
+ final BitReader bitReader = new BitReader();
+ final CodeTable codeTable = new CodeTable();
+ int[] pixelBuffer;
+ int[] prevPixelBuffer;
+ int[] tempBuffer;
+
+ // Get or create a pixel buffer of the specified size
+ int[] getPixelBuffer(int size) {
+ if (pixelBuffer == null || pixelBuffer.length < size) {
+ pixelBuffer = new int[Math.max(size, 1024)]; // Minimum size to reduce allocations
+ }
+ return pixelBuffer;
+ }
+
+ // Get or create a previous pixel buffer of the specified size
+ int[] getPrevPixelBuffer(int size) {
+ if (prevPixelBuffer == null || prevPixelBuffer.length < size) {
+ prevPixelBuffer = new int[Math.max(size, 1024)];
+ }
+ return prevPixelBuffer;
+ }
+
+ // Get or create a temporary buffer of the specified size
+ int[] getTempBuffer(int size) {
+ if (tempBuffer == null || tempBuffer.length < size) {
+ tempBuffer = new int[Math.max(size, 1024)];
+ }
+ return tempBuffer;
+ }
+ }
+
+ static final class BitReader {
+ private int nextBitToRead;
+ private int numberOfBitsToRead;
+ private int bitMask; // Used to kill unwanted higher bits
+ private byte[] bytes; // Data array
+
+ // To avoid costly bounds checks, 'in' needs 2 more 0-bytes at the end
+ private void init(final byte[] bytes) {
+ this.bytes = bytes;
+ nextBitToRead = 0;
+ }
+
+ private int read() {
+ // Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
+ final int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8
+ final int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8
+
+ // Use int operations to avoid repeated array access
+ final int byte0 = bytes[byteIndex] & 0xFF;
+ final int byte1 = bytes[byteIndex + 1] & 0xFF;
+ final int byte2 = bytes[byteIndex + 2] & 0xFF;
+
+ // Optimized bit manipulation
+ final int buffer = ((byte2 << 16) | (byte1 << 8) | byte0) >>> bitsToShiftRight;
+ nextBitToRead += numberOfBitsToRead;
+ return buffer & bitMask; // Kill the unwanted higher bits
+ }
+
+ private void setNumberOfBitsToRead(final int numberOfBitsToRead) {
+ this.numberOfBitsToRead = numberOfBitsToRead;
+ bitMask = (1 << numberOfBitsToRead) - 1;
+ }
+ }
+
+ static final class CodeTable {
+ private final int[][] table; // Maps codes to lists of colors
+ private int initTableSize; // Number of colors +2 for CLEAR + EOI
+ private int initCodeSize; // Initial code size
+ private int initCodeLimit; // First code limit
+ private int codeSize; // Current code size, maximum is 12 bits
+ private int nextCode; // Next available code for a new entry
+ private int nextCodeLimit; // Increase codeSize when nextCode == limit
+ private BitReader bitReader; // Notify when code sizes increases
+
+ public CodeTable() {
+ table = new int[4096][];
+ }
+
+ private int add(final int[] indices) {
+ if (nextCode < 4096) {
+ if (nextCode == nextCodeLimit && codeSize < 12) {
+ codeSize++; // Max code size is 12
+ bitReader.setNumberOfBitsToRead(codeSize);
+ nextCodeLimit = (1 << codeSize) - 1; // 2^codeSize - 1
+ }
+ table[nextCode++] = indices;
+ }
+ return codeSize;
+ }
+
+ private int clear() {
+ codeSize = initCodeSize;
+ bitReader.setNumberOfBitsToRead(codeSize);
+ nextCodeLimit = initCodeLimit;
+ nextCode = initTableSize; // Don't recreate table, reset pointer
+ return codeSize;
+ }
+
+ private void init(final GifFrame fr, final int[] activeColTbl, final BitReader br) {
+ this.bitReader = br;
+ final int numColors = activeColTbl.length;
+ initCodeSize = fr.firstCodeSize;
+ initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1
+ initTableSize = fr.endOfInfoCode + 1;
+ nextCode = initTableSize;
+
+ // Initialize single color entries
+ for (int c = 0; c < numColors; c++) {
+ table[c] = new int[]{activeColTbl[c]}; // Single element array
+ }
+
+ table[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR
+ table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
+
+ // Locate transparent color in code table and set to 0
+ if (fr.transpColFlag && fr.transpColIndex < numColors) {
+ table[fr.transpColIndex] = new int[]{0};
+ }
+ }
+ }
+
+ final class GifFrame {
+ // Graphic control extension (optional)
+ // Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
+ private volatile int disposalMethod; // 0-3 as above, 4-7 undefined
+ private volatile boolean transpColFlag; // 1 Bit
+ private volatile int delay; // Unsigned, LSByte first, n * 1/100 * s
+ private volatile int transpColIndex; // 1 Byte
+ // Image descriptor
+ private volatile int x; // Position on the canvas from the left
+ private volatile int y; // Position on the canvas from the top
+ private volatile int w; // May be smaller than the base image
+ private volatile int h; // May be smaller than the base image
+ private volatile int wh; // width * height
+ private volatile boolean hasLocColTbl; // Has local color table? 1 Bit
+ private volatile boolean interlaceFlag; // Is an interlace image? 1 Bit
+ @SuppressWarnings("unused")
+ private volatile boolean sortFlag; // True if local colors are sorted, 1 Bit
+ private volatile int sizeOfLocColTbl; // Size of the local color table, 3 Bits
+ private volatile int[] localColTbl; // Local color table (optional)
+ // Image data
+ private volatile int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
+ private volatile int clearCode;
+ private volatile int endOfInfoCode;
+ private volatile byte[] data; // Holds LZW encoded data
+ private volatile BufferedImage img; // Full drawn image, not just the frame area
+
+ // Synchronization for frame image creation
+ private final Object imageLock = new Object();
+ }
+
+ public final class GifImage {
+ public volatile String header; // Bytes 0-5, GIF87a or GIF89a
+ private volatile int w; // Unsigned 16 Bit, the least significant byte first
+ private volatile int h; // Unsigned 16 Bit, the least significant byte first
+ private volatile int wh; // Image width * image height
+ public volatile boolean hasGlobColTbl; // 1 Bit
+ public volatile int colorResolution; // 3 Bits
+ public volatile boolean sortFlag; // True if global colors are sorted, 1 Bit
+ public volatile int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
+ public volatile int bgColIndex; // Background color index, 1 Byte
+ public volatile int pxAspectRatio; // Pixel aspect ratio, 1 Byte
+ public volatile int[] globalColTbl; // Global color table
+ private final List frames = new ArrayList<>(64);
+ public volatile String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
+ public volatile String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
+ public volatile int repetitions = 0; // 0: infinite loop, N: number of loops
+
+ // Thread-safe frame rendering state
+ private volatile BufferedImage img = null; // Currently, drawn frame
+ private volatile Graphics2D g;
+ private final ReadWriteLock renderLock = new ReentrantReadWriteLock();
+
+ // Synchronization objects
+ private final Object initLock = new Object();
+
+ private int[] decode(final GifFrame fr, final int[] activeColTbl) {
+ final DecodingContext ctx = DECODING_CONTEXT.get();
+ ctx.codeTable.init(fr, activeColTbl, ctx.bitReader);
+ ctx.bitReader.init(fr.data); // Incoming codes
+ final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
+
+ // Use thread-local pixel buffer
+ final int[] out = ctx.getPixelBuffer(wh);
+
+ final int[][] tbl = ctx.codeTable.table; // Code table
+ int outPos = 0; // Next pixel position in the output image array
+ ctx.codeTable.clear(); // Init code table
+ ctx.bitReader.read(); // Skip leading clear code
+ int code = ctx.bitReader.read(); // Read first code
+ int[] pixels = tbl[code]; // Output pixel for first code
+
+ // Use System.arraycopy for better performance
+ System.arraycopy(pixels, 0, out, outPos, pixels.length);
+ outPos += pixels.length;
+
+ try {
+ while (true) {
+ final int prevCode = code;
+ code = ctx.bitReader.read(); // Get next code in stream
+ if (code == clearCode) { // After a CLEAR table, there is
+ ctx.codeTable.clear(); // no previous code, we need to read
+ code = ctx.bitReader.read(); // a new one
+ pixels = tbl[code]; // Output pixels
+ System.arraycopy(pixels, 0, out, outPos, pixels.length);
+ outPos += pixels.length;
+ continue; // Back to the loop with a valid previous code
+ } else if (code == endCode) {
+ break;
+ }
+ final int[] prevVals = tbl[prevCode];
+ final int[] prevValsAndK = new int[prevVals.length + 1];
+ System.arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
+ if (code < ctx.codeTable.nextCode) { // Code table contains code
+ pixels = tbl[code]; // Output pixels
+ System.arraycopy(pixels, 0, out, outPos, pixels.length);
+ outPos += pixels.length;
+ prevValsAndK[prevVals.length] = tbl[code][0]; // K
+ } else {
+ prevValsAndK[prevVals.length] = prevVals[0]; // K
+ System.arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
+ outPos += prevValsAndK.length;
+ }
+ ctx.codeTable.add(prevValsAndK); // Previous indices + K
+ }
+ } catch (final ArrayIndexOutOfBoundsException ignored) {
+ }
+
+ // Return a properly sized array
+ return Arrays.copyOf(out, outPos);
+ }
+
+ private int[] deinterlace(final int[] src, final GifFrame fr) {
+ final int w = fr.w, h = fr.h, wh = fr.wh;
+ final DecodingContext ctx = DECODING_CONTEXT.get();
+ final int[] dest = ctx.getTempBuffer(src.length);
+
+ // Interlaced images are organized in 4 sets of pixel lines
+ final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0)
+ final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
+ final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
+
+ // Sets' start indices in source array
+ final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
+
+ // Line skips in destination array - use bit shifts for multiplication
+ final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
+
+ // Group 1 contains every 8th line starting from 0
+ int from = 0, to = 0;
+ for (; from < set2; from += w, to += w8) {
+ System.arraycopy(src, from, dest, to, w);
+ }
+ // Group 2 contains every 8th line starting from 4
+ for (to = w4; from < set3; from += w, to += w8) {
+ System.arraycopy(src, from, dest, to, w);
+ }
+ // Group 3 contains every 4th line starting from 2
+ for (to = w2; from < set4; from += w, to += w4) {
+ System.arraycopy(src, from, dest, to, w);
+ }
+ // Group 4 contains every 2nd line starting from 1 (biggest group)
+ for (to = w; from < wh; from += w, to += w2) {
+ System.arraycopy(src, from, dest, to, w);
+ }
+ return Arrays.copyOf(dest, src.length); // Return a copy, not the shared buffer
+ }
+
+ private void drawFrame(final GifFrame fr) {
+ renderLock.writeLock().lock();
+ try {
+ // Determine the color table that will be active for this frame
+ final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
+
+ // Get pixels from data stream
+ int[] pixels = decode(fr, activeColTbl);
+ if (fr.interlaceFlag) {
+ pixels = deinterlace(pixels, fr); // Rearrange pixel lines
+ }
+
+ // Create image of type TYPE_INT_ARGB for frame area
+ final BufferedImage frame = new BufferedImage(fr.w, fr.h, BufferedImage.TYPE_INT_ARGB);
+ final int[] frameData = ((DataBufferInt) frame.getRaster().getDataBuffer()).getData();
+ System.arraycopy(pixels, 0, frameData, 0, Math.min(pixels.length, fr.wh));
+
+ // Draw frame area on top of working image
+ g.drawImage(frame, fr.x, fr.y, null);
+
+ // Keep a copy of the previous frame's pixels
+ final DecodingContext ctx = DECODING_CONTEXT.get();
+ final int[] prevPixelBuffer = ctx.getPrevPixelBuffer(wh);
+ final int[] imgData = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
+ System.arraycopy(imgData, 0, prevPixelBuffer, 0, wh);
+
+ // Create another copy for the end user to not expose internal state
+ synchronized (fr.imageLock) {
+ if (fr.img == null) { // Double-checked locking
+ fr.img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+ final int[] frameImgData = ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData();
+ System.arraycopy(prevPixelBuffer, 0, frameImgData, 0, wh);
+ }
+ }
+
+ // Handle disposal of current frame
+ if (fr.disposalMethod == 2) {
+ // Restore to background color (clear frame area only)
+ g.clearRect(fr.x, fr.y, fr.w, fr.h);
+ } else if (fr.disposalMethod == 3) {
+ // Restore previous frame
+ System.arraycopy(prevPixelBuffer, 0, imgData, 0, wh);
+ }
+ } finally {
+ renderLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Returns the background color of the first frame in this GIF image. If
+ * the frame has a local color table, the returned color will be from
+ * that table. If not, the color will be from the global color table.
+ * Returns 0 if there is neither a local nor a global color table.
+ *
+ * @return 32 bit ARGB color in the form 0xAARRGGBB
+ */
+ public final int getBackgroundColor() {
+ renderLock.readLock().lock();
+ try {
+ final GifFrame frame = frames.get(0);
+ if (frame.hasLocColTbl) {
+ return frame.localColTbl[bgColIndex];
+ } else if (hasGlobColTbl) {
+ return globalColTbl[bgColIndex];
+ }
+ return 0;
+ } finally {
+ renderLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * If not 0, the delay specifies how many hundredths (1/100) of a second
+ * to wait before displaying the frame after the current frame.
+ *
+ * @param index Index of the current frame, 0 to N-1
+ * @return Delay as number of hundredths (1/100) of a second
+ */
+ public final int getDelay(final int index) {
+ renderLock.readLock().lock();
+ try {
+ return frames.get(index).delay;
+ } finally {
+ renderLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * @param index Index of the frame to return as image, starting from 0.
+ * For incremental calls such as [0, 1, 2, ...] the method's
+ * run time is O(1) as only one frame is drawn per call. For
+ * random access calls such as [7, 12, ...] the run time is
+ * O(N+1) with N being the number of previous frames that
+ * need to be drawn before N+1 can be drawn on top. Once a
+ * frame has been drawn it is being cached and the run time
+ * is more or less O(0) to retrieve it from the list.
+ * @return A BufferedImage for the specified frame.
+ */
+ public BufferedImage getFrame(final int index) {
+ // Initialize rendering context if needed
+ if (img == null) {
+ synchronized (initLock) {
+ if (img == null) { // Double-checked locking
+ img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+ g = img.createGraphics();
+ g.setBackground(new Color(0, true)); // Transparent color
+ // Set rendering hints for better performance
+ g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
+ }
+ }
+ }
+
+ GifFrame fr = frames.get(index);
+
+ // Check if frame is already rendered using double-checked locking
+ if (fr.img == null) {
+ synchronized (fr.imageLock) {
+ if (fr.img == null) {
+ // Draw all frames until and including the requested frame
+ for (int i = 0; i <= index; i++) {
+ fr = frames.get(i);
+ if (fr.img == null) {
+ drawFrame(fr);
+ }
+ }
+ }
+ }
+ }
+
+ return fr.img;
+ }
+
+ /**
+ * @return The number of frames contained in this GIF image
+ */
+ public final int getFrameCount() {
+ return frames.size();
+ }
+
+ /**
+ * @return The height of the GIF image
+ */
+ public final int getHeight() {
+ return h;
+ }
+
+ /**
+ * @return The width of the GIF image
+ */
+ public final int getWidth() {
+ return w;
+ }
+ }
+
+ static final boolean DEBUG_MODE = false;
+
+ /**
+ * @param in Raw image data as a byte[] array
+ * @return A GifImage object exposing the properties of the GIF image.
+ * @throws IOException If the image violates the GIF specification or is
+ * truncated.
+ */
+ public static GifImage read(final byte[] in) throws IOException {
+ final GifDecoder decoder = new GifDecoder();
+ final GifImage img = decoder.new GifImage();
+ GifFrame frame = null; // Currently open frame
+ int pos = readHeader(in, img); // Read header, get next byte position
+ pos = readLogicalScreenDescriptor(img, in, pos);
+ if (img.hasGlobColTbl) {
+ img.globalColTbl = new int[img.sizeOfGlobColTbl];
+ pos = readColTbl(in, img.globalColTbl, pos);
+ }
+
+ // Main parsing loop with bounds checking optimization
+ while (pos < in.length) {
+ final int block = in[pos] & 0xFF;
+ switch (block) {
+ case 0x21: // Extension introducer
+ if (pos + 1 >= in.length) {
+ throw new IOException("Unexpected end of file.");
+ }
+ switch (in[pos + 1] & 0xFF) {
+ case 0xFE: // Comment extension
+ pos = readTextExtension(in, pos);
+ break;
+ case 0xFF: // Application extension
+ pos = readAppExt(img, in, pos);
+ break;
+ case 0x01: // Plain text extension
+ frame = null; // End of current frame
+ pos = readTextExtension(in, pos);
+ break;
+ case 0xF9: // Graphic control extension
+ if (frame == null) {
+ frame = decoder.new GifFrame();
+ img.frames.add(frame);
+ }
+ pos = readGraphicControlExt(frame, in, pos);
+ break;
+ default:
+ throw new IOException("Unknown extension at " + pos);
+ }
+ break;
+ case 0x2C: // Image descriptor
+ if (frame == null) {
+ frame = decoder.new GifFrame();
+ img.frames.add(frame);
+ }
+ pos = readImgDescr(frame, in, pos);
+ if (frame.hasLocColTbl) {
+ frame.localColTbl = new int[frame.sizeOfLocColTbl];
+ pos = readColTbl(in, frame.localColTbl, pos);
+ }
+ pos = readImgData(frame, in, pos);
+ frame = null; // End of current frame
+ break;
+ case 0x3B: // GIF Trailer
+ return img; // Found trailer, finished reading.
+ default:
+ // Unknown block. The image is corrupted. Strategies: a) Skip
+ // and wait for a valid block. Experience: It'll get worse. b)
+ // Throw exception. c) Return gracefully if we are almost done
+ // processing. The frames we have so far should be error-free.
+ final double progress = (double) pos / in.length;
+ if (progress < 0.9) {
+ throw new IOException("Unknown block at: " + pos);
+ }
+ pos = in.length; // Exit loop
+ }
+ }
+ return img;
+ }
+
+ /**
+ * @param is Image data as input stream. This method will read from the
+ * input stream's current position. It will not reset the
+ * position before reading and won't reset or close the stream
+ * afterwards. Call these methods before and after calling this
+ * method as needed.
+ * @return A GifImage object exposing the properties of the GIF image.
+ * @throws IOException If an I/O error occurs, the image violates the GIF
+ * specification or the GIF is truncated.
+ */
+ public static GifImage read(final InputStream is) throws IOException {
+ final byte[] data = new byte[is.available()];
+ is.read(data, 0, data.length);
+ return read(data);
+ }
+
+ /**
+ * @param img GIF image
+ * @param in Raw data
+ * @param i Index of the first byte of the application extension
+ * @return Index of the first byte after this extension
+ */
+ static int readAppExt(final GifImage img, final byte[] in, int i) {
+ // Use StandardCharsets for better performance if available, otherwise fallback
+ img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
+ img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
+ i += 14; // Go to sub-block size, it's value should be 3
+ final int subBlockSize = in[i] & 0xFF;
+ // The only app extension widely used is NETSCAPE, it's got 3 data bytes
+ if (subBlockSize == 3) {
+ // in[i+1] should have value 01, in[i+5] should be block terminator
+ img.repetitions = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // Short
+ return i + 5;
+ } // Skip unknown application extensions
+ while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
+ i += (in[i] & 0xFF) + 1; // Skip to next sub-block
+ }
+ return i + 1;
+ }
+
+ /**
+ * @param in Raw data
+ * @param colors Pre-initialized target array to store ARGB colors
+ * @param i Index of the color table's first byte
+ * @return Index of the first byte after the color table
+ */
+ static int readColTbl(final byte[] in, final int[] colors, int i) {
+ final int numColors = colors.length;
+ for (int c = 0; c < numColors; c++) {
+ final int r = in[i++] & 0xFF; // 1st byte is red
+ final int g = in[i++] & 0xFF; // 2nd byte is green
+ final int b = in[i++] & 0xFF; // 3rd byte is blue
+ // Optimized color packing - alpha is always 0xFF for opaque
+ colors[c] = 0xFF000000 | (r << 16) | (g << 8) | b;
+ }
+ return i;
+ }
+
+ /**
+ * @param fr GIF frame
+ * @param in Raw data
+ * @param i Index of the extension introducer
+ * @return Index of the first byte after this block
+ */
+ static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
+ final int packed = in[i + 3] & 0xFF;
+ fr.disposalMethod = (packed & 0b00011100) >>> 2; // Bits 4-2
+ fr.transpColFlag = (packed & 1) == 1; // Bit 0
+ fr.delay = (in[i + 4] & 0xFF) | ((in[i + 5] & 0xFF) << 8); // 16 bit LSB
+ fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
+ return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
+ }
+
+ /**
+ * @param in Raw data
+ * @param img The GifImage object that is currently read
+ * @return Index of the first byte after this block
+ * @throws IOException If the GIF header/trailer is missing, incomplete or
+ * unknown
+ */
+ static int readHeader(final byte[] in, final GifImage img) throws IOException {
+ if (in.length < 6) { // Check first 6 bytes
+ throw new IOException("Image is truncated.");
+ }
+ // Use ASCII encoding for header - faster than default charset
+ img.header = new String(in, 0, 6);
+ if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
+ throw new IOException("Invalid GIF header.");
+ }
+ return 6;
+ }
+
+ /**
+ * @param fr The GIF frame to whom this image descriptor belongs
+ * @param in Raw data
+ * @param i Index of the first byte of this block, i.e. the minCodeSize
+ * @return Byte index
+ */
+ static int readImgData(final GifFrame fr, final byte[] in, int i) {
+ final int fileSize = in.length;
+ final int minCodeSize = in[i++] & 0xFF; // Read code size, go to block
+ final int clearCode = 1 << minCodeSize; // CLEAR = 2^minCodeSize
+ fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
+ fr.clearCode = clearCode;
+ fr.endOfInfoCode = clearCode + 1;
+
+ // Pre-calculate image data size for better memory allocation
+ final int imgDataSize = readImgDataSize(in, i);
+ final byte[] imgData = new byte[imgDataSize + 2];
+ int imgDataPos = 0;
+ int subBlockSize = in[i] & 0xFF;
+
+ while (subBlockSize > 0) { // While block has data
+ try { // Next line may throw exception if sub-block size is fake
+ final int nextSubBlockSizePos = i + subBlockSize + 1;
+ final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
+ System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
+ imgDataPos += subBlockSize; // Move output data position
+ i = nextSubBlockSizePos; // Move to next sub-block size
+ subBlockSize = nextSubBlockSize;
+ } catch (final Exception e) {
+ // Sub-block exceeds file end, only use remaining bytes
+ subBlockSize = fileSize - i - 1; // Remaining bytes
+ if (subBlockSize > 0) {
+ System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
+ imgDataPos += subBlockSize; // Move output data position
+ }
+ i += subBlockSize + 1; // Move to next sub-block size
+ break;
+ }
+ }
+ fr.data = imgData; // Holds LZW encoded data
+ i++; // Skip last sub-block size, should be 0
+ return i;
+ }
+
+ static int readImgDataSize(final byte[] in, int i) {
+ final int fileSize = in.length;
+ int imgDataPos = 0;
+ int subBlockSize = in[i] & 0xFF;
+ while (subBlockSize > 0) { // While block has data
+ try { // Next line may throw exception if sub-block size is fake
+ final int nextSubBlockSizePos = i + subBlockSize + 1;
+ final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
+ imgDataPos += subBlockSize; // Move output data position
+ i = nextSubBlockSizePos; // Move to next sub-block size
+ subBlockSize = nextSubBlockSize;
+ } catch (final Exception e) {
+ // Sub-block exceeds file end, only use remaining bytes
+ subBlockSize = fileSize - i - 1; // Remaining bytes
+ if (subBlockSize > 0) {
+ imgDataPos += subBlockSize; // Move output data position
+ }
+ break;
+ }
+ }
+ return imgDataPos;
+ }
+
+ /**
+ * @param fr The GIF frame to whom this image descriptor belongs
+ * @param in Raw data
+ * @param i Index of the image separator, i.e. the first block byte
+ * @return Index of the first byte after this block
+ */
+ static int readImgDescr(final GifFrame fr, final byte[] in, int i) {
+ fr.x = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 1-2: left
+ fr.y = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 3-4: top
+ fr.w = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 5-6: width
+ fr.h = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 7-8: height
+ fr.wh = fr.w * fr.h;
+ final int packed = in[++i] & 0xFF; // Byte 9 is a packed byte
+ fr.hasLocColTbl = (packed & 0b10000000) != 0; // Bit 7
+ fr.interlaceFlag = (packed & 0b01000000) != 0; // Bit 6
+ fr.sortFlag = (packed & 0b00100000) != 0; // Bit 5
+ final int colTblSizePower = (packed & 7) + 1; // Bits 2-0
+ fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
+ return ++i;
+ }
+
+ /**
+ * @param img GIF image
+ * @param i Start index of this block.
+ * @return Index of the first byte after this block.
+ */
+ static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) {
+ img.w = (in[i] & 0xFF) | ((in[i + 1] & 0xFF) << 8); // 16 bit, LSB 1st
+ img.h = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // 16 bit
+ img.wh = img.w * img.h;
+ final int packed = in[i + 4] & 0xFF; // Byte 4 is a packed byte
+ img.hasGlobColTbl = (packed & 0b10000000) != 0; // Bit 7
+ final int colResPower = ((packed & 0b01110000) >>> 4) + 1; // Bits 6-4
+ img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
+ img.sortFlag = (packed & 0b00001000) != 0; // Bit 3
+ final int globColTblSizePower = (packed & 7) + 1; // Bits 0-2
+ img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
+ img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
+ img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
+ return i + 7;
+ }
+
+ /**
+ * @param in Raw data
+ * @param pos Index of the extension introducer
+ * @return Index of the first byte after this block
+ */
+ static int readTextExtension(final byte[] in, final int pos) {
+ int i = pos + 2; // Skip extension introducer and label
+ int subBlockSize = in[i++] & 0xFF;
+ while (subBlockSize != 0 && i < in.length) {
+ i += subBlockSize;
+ if (i >= in.length) break; // Safety check
+ subBlockSize = in[i++] & 0xFF;
+ }
+ return i;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/AnimatedCloakData.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/AnimatedCloakData.java
new file mode 100644
index 0000000..ca92aa0
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/AnimatedCloakData.java
@@ -0,0 +1,36 @@
+package org.saturnclient.cosmetics.cloak.utils;
+
+import net.minecraft.util.Identifier;
+
+/**
+ * Represents a single frame of an animated cloak.
+ */
+public class AnimatedCloakData {
+ private final Identifier textureId;
+ private final int delayMs;
+
+ /**
+ * Creates a new animated cloak frame.
+ *
+ * @param textureId The texture identifier for this frame
+ * @param delayMs The delay in milliseconds before the next frame
+ */
+ public AnimatedCloakData(Identifier textureId, int delayMs) {
+ this.textureId = textureId;
+ this.delayMs = delayMs;
+ }
+
+ /**
+ * @return The texture identifier for this frame
+ */
+ public Identifier getTextureId() {
+ return textureId;
+ }
+
+ /**
+ * @return The delay in milliseconds before the next frame
+ */
+ public int getDelayMs() {
+ return delayMs;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/IdentifierUtils.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/IdentifierUtils.java
new file mode 100644
index 0000000..93fb940
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/IdentifierUtils.java
@@ -0,0 +1,98 @@
+package org.saturnclient.cosmetics.cloak.utils;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import net.minecraft.client.texture.NativeImage;
+import net.minecraft.client.texture.NativeImageBackedTexture;
+import net.minecraft.util.Identifier;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+
+public class IdentifierUtils {
+
+ /**
+ * ORIGINAL METHOD - EXTREMELY SLOW - causes 10-20 second freezes
+ * This converts BufferedImage -> PNG bytes -> ByteBuffer -> NativeImage ->
+ * Texture
+ * This is completely unnecessary and wastes massive amounts of CPU time!
+ */
+ public static void registerBufferedImageTexture(Identifier i, BufferedImage bi) {
+ try {
+ // This is the performance killer:
+ // 1. Converts BufferedImage to PNG (expensive compression)
+ // 2. Creates ByteBuffer copy (memory allocation)
+ // 3. NativeImage.read() parses PNG back to raw pixels (expensive decompression)
+ // This is doing compression -> decompression for no reason!
+
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
+ javax.imageio.ImageIO.write(bi, "png", baos);
+ byte[] bytes = baos.toByteArray();
+ java.nio.ByteBuffer bb = org.lwjgl.BufferUtils.createByteBuffer(bytes.length).put(bytes);
+ bb.flip();
+ NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb));
+ SaturnClient.client.getTextureManager().registerTexture(i, nibt);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * OPTIMIZED METHOD - Direct pixel conversion without unnecessary PNG
+ * encoding/decoding
+ * This should reduce loading time from 10-20 seconds to under 1 second
+ */
+ public static void registerBufferedImageTextureFast(Identifier identifier, BufferedImage bufferedImage) {
+ try {
+ int width = bufferedImage.getWidth();
+ int height = bufferedImage.getHeight();
+
+ // Create NativeImage directly with the correct format
+ NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
+
+ // Direct pixel copy - much faster than PNG conversion
+ if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
+ // Fast path for ARGB images - direct memory access
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int pixel = pixels[y * width + x];
+ nativeImage.setColorArgb(x, y, pixel);
+ }
+ }
+ } else {
+ // Fallback for other image types - still faster than PNG conversion
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int rgb = bufferedImage.getRGB(x, y);
+ nativeImage.setColorArgb(x, y, rgb);
+ }
+ }
+ }
+
+ // Register the texture
+ NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
+ SaturnClient.client.execute(
+ () -> SaturnClient.client.getTextureManager().registerTexture(identifier, texture));
+
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to register texture: " + identifier, e);
+ // Fallback to slow method if fast method fails
+ registerBufferedImageTexture(identifier, bufferedImage);
+ }
+ }
+
+ /**
+ * EVEN FASTER VERSION - Bulk texture registration
+ * Use this when registering many textures at once (like animated cloak frames)
+ */
+ public static void registerBufferedImageTexturesBulk(java.util.Map textures) {
+ long startTime = System.currentTimeMillis();
+
+ for (java.util.Map.Entry entry : textures.entrySet()) {
+ registerBufferedImageTextureFast(entry.getKey(), entry.getValue());
+ }
+
+ long endTime = System.currentTimeMillis();
+ SaturnClient.LOGGER.info("Registered {} textures in {}ms", textures.size(), (endTime - startTime));
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/RandomUtils.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/RandomUtils.java
new file mode 100644
index 0000000..b555d83
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/cloak/utils/RandomUtils.java
@@ -0,0 +1,48 @@
+package org.saturnclient.cosmetics.cloak.utils;
+
+import java.util.Random;
+
+/**
+ * Utility class for generating random strings and numbers.
+ */
+public final class RandomUtils {
+ private static final String ALPHANUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ private static final String ALPHANUMERIC_LOWERCASE = "0123456789abcdefghijklmnopqrstuvwxyz";
+ private static final Random RANDOM = new Random();
+
+ private RandomUtils() {
+ // Prevent instantiation
+ }
+
+ /**
+ * Generates a random string of specified length using given characters.
+ */
+ private static String random(final int length, final char[] chars) {
+ StringBuilder sb = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ sb.append(chars[RANDOM.nextInt(chars.length)]);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generates a random string of specified length using alphanumeric characters.
+ */
+ public static String randomString(final int length) {
+ return random(length, ALPHANUMERIC.toCharArray());
+ }
+
+ /**
+ * Generates a random lowercase string of specified length.
+ */
+ public static String randomStringLowercase(final int length) {
+ return random(length, ALPHANUMERIC_LOWERCASE.toCharArray());
+ }
+
+ /**
+ * Generates a random integer between min and max (inclusive).
+ */
+ public static int randBetween(int min, int max) {
+ return RANDOM.nextInt(max - min + 1) + min;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/hat/HatFeatureRenderer.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/hat/HatFeatureRenderer.java
new file mode 100644
index 0000000..98b536e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/hat/HatFeatureRenderer.java
@@ -0,0 +1,39 @@
+package org.saturnclient.cosmetics.hat;
+
+import org.saturnclient.cosmetics.obj.MtlLoader;
+import org.saturnclient.cosmetics.obj.ObjModel;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.feature.FeatureRenderer;
+import net.minecraft.client.render.entity.feature.FeatureRendererContext;
+import net.minecraft.client.render.entity.model.PlayerEntityModel;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import net.minecraft.client.util.math.MatrixStack;
+
+public class HatFeatureRenderer extends FeatureRenderer {
+ public HatFeatureRenderer(FeatureRendererContext context) {
+ super(context);
+ }
+
+ @Override
+ public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
+ PlayerEntityRenderState state, float limbAngle, float limbDistance) {
+ SaturnPlayer player = SaturnPlayer.get(state.name);
+
+ if (state.invisible || player == null || player.hat.isEmpty()) {
+ return;
+ }
+
+ matrices.push();
+
+ this.getContextModel().head.rotate(matrices);
+
+ ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
+ matrices, vertexConsumers, light,
+ OverlayTexture.DEFAULT_UV);
+
+ matrices.pop();
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/hat/Hats.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/hat/Hats.java
new file mode 100644
index 0000000..6aee225
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/hat/Hats.java
@@ -0,0 +1,31 @@
+package org.saturnclient.cosmetics.hat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+
+public class Hats {
+ public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo", "halo_black" };
+ public static List availableHats = new ArrayList<>();
+
+ public static void initialize() {
+ availableHats.add(0, "");
+ }
+
+ public static void setHat(String hatName) {
+ if (availableHats.contains(hatName)) {
+ setHat(ServiceClient.uuid, hatName);
+ ServiceClient.setHat(hatName);
+ }
+ }
+
+ public static void setHat(UUID uuid, String hatName) {
+ SaturnPlayer player = SaturnPlayer.get(uuid);
+ if (player != null) {
+ player.hat = hatName;
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/MtlLoader.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/MtlLoader.java
new file mode 100644
index 0000000..22ce3b9
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/MtlLoader.java
@@ -0,0 +1,57 @@
+package org.saturnclient.cosmetics.obj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.javagl.obj.*;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.Identifier;
+
+public class MtlLoader {
+ static Map> loadedMtls = new HashMap<>();
+
+ static Map loadMtl(Identifier mtlId) throws IOException {
+ try (InputStream is = MinecraftClient.getInstance().getResourceManager()
+ .getResource(mtlId).get().getInputStream()) {
+ List mtlList = MtlReader.read(is);
+ Map mtlMap = new java.util.LinkedHashMap<>();
+ for (Mtl mtl : mtlList) {
+ mtlMap.put(mtl.getName(), mtl);
+ }
+ return mtlMap;
+ }
+ }
+
+ public static Map of(Identifier mtlId) {
+ System.out.println(mtlId);
+ if (loadedMtls.containsKey(mtlId))
+ return loadedMtls.get(mtlId);
+
+ try {
+ Map mtl = loadMtl(mtlId);
+ loadedMtls.put(mtlId, mtl);
+
+ return mtl;
+ } catch (IOException e) {
+ e.printStackTrace();
+
+ return null;
+ }
+
+ }
+
+ public static Map cosmetic(String cosmeticId) {
+ int index = cosmeticId.indexOf('_');
+
+ String mtlId = (index == -1) ? "" : cosmeticId.substring(index + 1);
+
+ if (mtlId.isEmpty())
+ mtlId = "white";
+
+ return of(Identifier.of("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl"));
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/ObjModel.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/ObjModel.java
new file mode 100644
index 0000000..bfe2ac1
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/ObjModel.java
@@ -0,0 +1,102 @@
+package org.saturnclient.cosmetics.obj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.javagl.obj.Mtl;
+import de.javagl.obj.Obj;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.resource.Resource;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+
+public class ObjModel {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ public static final Map loadedObjModels = new HashMap<>();
+
+ public record Config(
+ @JsonProperty("translate") Vec3d transform,
+ @JsonProperty("scale") Vec3d scale,
+ @JsonProperty("rotation") Vec3d rotation) {
+
+ public Config {
+ transform = transform == null ? new Vec3d(0, 0, 0) : transform;
+ scale = scale == null ? new Vec3d(1, 1, 1) : scale;
+ rotation = rotation == null ? new Vec3d(0, 0, 0) : rotation;
+ }
+
+ @JsonFormat(shape = JsonFormat.Shape.ARRAY)
+ public record Vec3d(float x, float y, float z) {
+ }
+ }
+
+ Obj obj;
+ Config config;
+
+ public static ObjModel cosmetic(String cosmeticKind, String id) {
+ return of(Identifier.of("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model"));
+ }
+
+ public static ObjModel of(Identifier obj) {
+ if (loadedObjModels.containsKey(obj))
+ return loadedObjModels.get(obj);
+
+ ObjModel o = new ObjModel(obj);
+ loadedObjModels.put(obj, o);
+ return o;
+ }
+
+ ObjModel(Identifier identifier) {
+ Identifier objId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".obj");
+ Identifier jsonId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".json");
+
+ try {
+ this.obj = ObjRenderer.loadObj(objId);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ // Try loading JSON config if it exists
+ ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager();
+ Optional resource = resourceManager.getResource(jsonId);
+
+ if (resource.isPresent()) {
+ try (InputStream stream = resource.get().getInputStream()) {
+ this.config = MAPPER.readValue(stream, Config.class);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void render(Map mtlMap, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
+ int overlay) {
+ matrices.push();
+
+ matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
+ matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
+
+ if (this.config != null) {
+ matrices.translate(config.transform.x, config.transform.y, config.transform.z);
+
+ matrices.scale(config.scale.x, config.scale.y, config.scale.z);
+
+ matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(config.rotation.x));
+ matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(config.rotation.y));
+ matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Z.rotationDegrees(config.rotation.z));
+ }
+
+ ObjRenderer.renderObj(this.obj, mtlMap, matrices, vertexConsumers, light, overlay);
+
+ matrices.pop();
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/ObjRenderer.java b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/ObjRenderer.java
new file mode 100644
index 0000000..498e4f0
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/cosmetics/obj/ObjRenderer.java
@@ -0,0 +1,111 @@
+package org.saturnclient.cosmetics.obj;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+import de.javagl.obj.*;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.util.Identifier;
+
+public class ObjRenderer {
+ public static Obj loadObj(Identifier objId) throws IOException {
+ try (InputStream is = MinecraftClient.getInstance().getResourceManager()
+ .getResource(objId).get().getInputStream()) {
+ return ObjReader.read(is);
+ }
+ }
+
+ public static void renderObj(Obj obj, Map mtlMap, MatrixStack matrices,
+ VertexConsumerProvider vertexConsumers, int light, int overlay) {
+ MatrixStack.Entry entry = matrices.peek();
+
+ Map materialGroups = ObjSplitting.splitByMaterialGroups(obj);
+
+ for (Map.Entry group : materialGroups.entrySet()) {
+ String materialName = group.getKey();
+ Obj groupObj = group.getValue();
+
+ Mtl mtl = mtlMap != null ? mtlMap.get(materialName) : null;
+
+ // Resolve texture identifier using same logic as ObjUnbakedModelModel
+ Identifier texture = Identifier.of("minecraft:textures/misc/white.png");
+
+ if (mtl != null) {
+ String mapKd = mtl.getMapKd();
+ if (mapKd != null) {
+ // "#key" references are not resolvable at runtime without a texture map,
+ // so we only support direct resource location strings here
+ if (!mapKd.startsWith("#")) {
+ texture = Identifier.of(mapKd);
+ }
+ }
+ }
+
+ // Diffuse color (Kd)
+ float r = 1f, g = 1f, b = 1f;
+ if (mtl != null) {
+ FloatTuple kd = mtl.getKd();
+ if (kd != null) {
+ r = kd.getX();
+ g = kd.getY();
+ b = kd.getZ();
+ }
+ }
+
+ VertexConsumer consumer = vertexConsumers.getBuffer(RenderLayer.getEntityAlpha(texture));
+
+ int faces = groupObj.getNumFaces();
+ final float fr = r, fg = g, fb = b;
+
+ for (int f = 0; f < faces; f++) {
+ ObjFace face = groupObj.getFace(f);
+ int verts = face.getNumVertices();
+
+ // Your original winding order logic, untouched
+ for (int i = verts - 1; i >= 0; i--) {
+ writeVertex(groupObj, face, i, consumer, entry, light, overlay, fr, fg, fb);
+ }
+ }
+ }
+ }
+
+ private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
+ MatrixStack.Entry matrix, int light, int overlay,
+ float r, float g, float b) {
+
+ FloatTuple pos = obj.getVertex(face.getVertexIndex(index));
+ float x = pos.getX();
+ float y = pos.getY();
+ float z = pos.getZ();
+
+ float nx = 0f, ny = 1f, nz = 0f;
+ int normalIndex = face.getNormalIndex(index);
+ if (normalIndex >= 0) {
+ FloatTuple n = obj.getNormal(normalIndex);
+ nx = n.getX();
+ ny = n.getY();
+ nz = n.getZ();
+ }
+
+ float u = 0f, v = 0f;
+ int texIndex = face.getTexCoordIndex(index);
+ if (texIndex >= 0) {
+ FloatTuple tc = obj.getTexCoord(texIndex);
+ u = tc.getX();
+ v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left
+ }
+
+ consumer.vertex(matrix, x, y, z)
+ .color(r, g, b, 1f)
+ .texture(u, v)
+ .overlay(overlay)
+ .light(light)
+ .normal(matrix, nx, ny, nz);
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/mixin/ExampleMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/mixin/ExampleMixin.java
deleted file mode 100644
index b457fb4..0000000
--- a/versions/1.21.4/src/main/java/org/saturnclient/mixin/ExampleMixin.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.saturnclient.mixin;
-
-import net.minecraft.server.MinecraftServer;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-
-@Mixin(MinecraftServer.class)
-public class ExampleMixin {
- @Inject(at = @At("HEAD"), method = "loadWorld")
- private void init(CallbackInfo info) {
- // This code is injected into the start of MinecraftServer.loadWorld()V
- }
-}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/HudMod.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/HudMod.java
new file mode 100644
index 0000000..fa5b280
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/HudMod.java
@@ -0,0 +1,11 @@
+package org.saturnclient.modules;
+
+import org.saturnclient.ui2.RenderScope;
+
+public interface HudMod {
+ public ModDimensions getDimensions();
+
+ public void renderDummy(RenderScope scope);
+
+ public void renderHud(RenderScope scope);
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/ModDimensions.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/ModDimensions.java
new file mode 100644
index 0000000..77f6e6f
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/ModDimensions.java
@@ -0,0 +1,54 @@
+package org.saturnclient.modules;
+
+import java.util.Map;
+
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.saturnclient.config.manager.Property.NamedProperty;
+
+public class ModDimensions {
+ public Property x = Property.integer(0);
+ public Property y = Property.integer(0);
+ public Property scale = Property.floatProp(1.0f);
+ public Property bgColor = Property.color(0x00000000);
+ public Property fgColor = Property.color(0xFFffffff);
+ public Property radius = Property.integer(0);
+ public Property font = Property.font(0);
+ public boolean renderBackground = true;
+
+ public int width = 0;
+ public int height = 0;
+
+ public NamedProperty>> prop() {
+ return Property.namespace(Map.of(
+ "X", x,
+ "Y", y,
+ "Scale", scale,
+ "Background Color", bgColor,
+ "Foreground Color", fgColor,
+ "Corner Radius", radius,
+ "Font", font)).named("In-Game Display");
+ }
+
+ public ModDimensions() {
+ }
+
+ public ModDimensions(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public ModDimensions(int x, int y, int width, int height) {
+ this.x.value = x;
+ this.y.value = y;
+ this.width = width;
+ this.height = height;
+ }
+
+ public ModDimensions(int x, int y, int width, int height, float scale) {
+ this.x.value = x;
+ this.y.value = y;
+ this.scale.value = scale;
+ this.width = width;
+ this.height = height;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/ModManager.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/ModManager.java
new file mode 100644
index 0000000..9a2ef26
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/ModManager.java
@@ -0,0 +1,60 @@
+package org.saturnclient.modules;
+
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.saturnclient.modules.mods.*;
+
+public class ModManager {
+ public static final List ENABLED_MODS = new ArrayList<>();
+ public static final Module[] ALL_MODS = {
+ new Crosshair(),
+ new AutoSprint(),
+ new ArmorDisplay(),
+ new Fps(),
+ new StatusEffects(),
+ new Coordinates(),
+ new Freelook(),
+ new Keystrokes(),
+ new Fullbright(),
+ new NoFog(),
+ new DayCounter(),
+ new Speedometer(),
+ new Zoom(),
+ new HealthDisplay(),
+ new Nametags(),
+ new Ping(),
+ new Tps(),
+ new Clock()
+ };
+
+ public static void init() {
+ // Pre-filter enabled modules to avoid checking every tick
+ updateEnabledModules();
+
+ ClientTickEvents.END_CLIENT_TICK.register(client -> {
+ // Only iterate over enabled modules
+ for (Module m : ENABLED_MODS) {
+ m.tick();
+ }
+ });
+ }
+
+ public static synchronized void updateEnabledModules() {
+ ENABLED_MODS.clear();
+ for (Module m : ALL_MODS) {
+ if (m.isEnabled()) {
+ ENABLED_MODS.add(m);
+ }
+ }
+ }
+
+ public static Module[] getAllMods() {
+ return ALL_MODS;
+ }
+
+ public static void refreshEnabledModules() {
+ updateEnabledModules();
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/Module.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/Module.java
new file mode 100644
index 0000000..6c719d6
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/Module.java
@@ -0,0 +1,60 @@
+package org.saturnclient.modules;
+
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property.NamedProperty;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.util.Identifier;
+
+public abstract class Module {
+ private final ModuleDetails details;
+ private final ConfigManager configManager;
+
+ public Module(ModuleDetails details, NamedProperty>... props) {
+ this.details = details;
+ configManager = new ConfigManager(details.name);
+ for (NamedProperty> prop : props) {
+ configManager.property(prop.name, prop.prop);
+ }
+ }
+
+ public void render(RenderScope scope) {
+ }
+
+ public void tick() {
+ }
+
+ public abstract boolean isEnabled();
+
+ public final void setEnabled(boolean e) {
+ onEnabled(e);
+ ModManager.updateEnabledModules();
+ }
+
+ public abstract void onEnabled(boolean e);
+
+ public final String getName() {
+ return details.name;
+ }
+
+ public Identifier getIconTexture() {
+ return Textures.getModIcon(details.namespace);
+ }
+
+ public ConfigManager getConfig() {
+ return configManager;
+ }
+
+ public String getDescription() {
+ return details.description;
+ }
+
+ public String[] getTags() {
+ return details.tags;
+ }
+
+ public String getVersion() {
+ return details.version;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/ModuleDetails.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/ModuleDetails.java
new file mode 100644
index 0000000..3cce480
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/ModuleDetails.java
@@ -0,0 +1,39 @@
+package org.saturnclient.modules;
+
+public class ModuleDetails {
+ public String name;
+ public String namespace;
+ public String description;
+ public String version;
+ public String[] tags = {};
+
+ public ModuleDetails(String name, String namespace) {
+ this.name = name;
+ this.namespace = namespace;
+ }
+
+ public ModuleDetails name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ModuleDetails namespace(String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ public ModuleDetails description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public ModuleDetails version(String version) {
+ this.version = version;
+ return this;
+ }
+
+ public ModuleDetails tags(String... tags) {
+ this.tags = tags;
+ return this;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/README.md b/versions/1.21.4/src/main/java/org/saturnclient/modules/README.md
new file mode 100644
index 0000000..0eb81ca
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/README.md
@@ -0,0 +1,6 @@
+# Credits
+### Freelook
+- https://github.com/Celibistrial/freelook
+- https://modrinth.com/mod/freelook
+
+**The other mods are coded by us, if you have a concern or we missed something feel free to submit a issue with the credits tag**
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java
new file mode 100644
index 0000000..5778b71
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java
@@ -0,0 +1,102 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.entity.EquipmentSlot;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+
+public class ArmorDisplay extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(40, 75);
+ private static Property useMainHand = Property.bool(false);
+
+ public ArmorDisplay() {
+ super(new ModuleDetails("Armor Display", "armor")
+ .description("Displays armor durability")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ useMainHand.named("Use Main Hand"),
+ dimensions.prop());
+ }
+
+ public void renderArmor(RenderScope scope, ItemStack mainHand, ItemStack helmet, ItemStack chestplate,
+ ItemStack leggings, ItemStack boots) {
+ int row = 0;
+
+ if (useMainHand.value && !mainHand.isEmpty()) {
+ scope.drawItem(mainHand, 0, 15 * row);
+ renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
+ row++;
+ }
+
+ if (!helmet.isEmpty()) {
+ scope.drawItem(helmet, 0, 15 * row);
+ renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
+ row++;
+ }
+
+ if (!chestplate.isEmpty()) {
+ scope.drawItem(chestplate, 0, 15 * row);
+ renderHealth(scope, row, chestplate.getMaxDamage(), chestplate.getDamage());
+ row++;
+ }
+ if (!leggings.isEmpty()) {
+ scope.drawItem(leggings, 0, 15 * row);
+ renderHealth(scope, row, leggings.getMaxDamage(), leggings.getDamage());
+ row++;
+ }
+
+ if (!boots.isEmpty()) {
+ scope.drawItem(boots, 0, 15 * row);
+ renderHealth(scope, row, boots.getMaxDamage(), boots.getDamage());
+ row++;
+ }
+ }
+
+ public void renderHealth(RenderScope scope, int i, int max_damage, int damage) {
+ if (max_damage > 0) {
+ scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value,
+ dimensions.fgColor.value);
+ }
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ renderArmor(scope,
+ SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND),
+ SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD),
+ SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST),
+ SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS),
+ SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET));
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderArmor(scope, new ItemStack(Items.DIAMOND_SWORD), new ItemStack(Items.DIAMOND_HELMET),
+ new ItemStack(Items.DIAMOND_CHESTPLATE),
+ new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS));
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/AutoSprint.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/AutoSprint.java
new file mode 100644
index 0000000..7a413b4
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/AutoSprint.java
@@ -0,0 +1,46 @@
+package org.saturnclient.modules.mods;
+
+import java.util.Objects;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+public class AutoSprint extends Module {
+ private static Property enabled = Property.bool(false);
+
+ public AutoSprint() {
+ super(
+ new ModuleDetails("Auto Sprint", "sprint")
+ .description("Makes the player always sprint")
+ .tags("Movement")
+ .version("v0.1.0"),
+ enabled.named("Enabled"));
+ }
+
+ @Override
+ public void tick() {
+ if (SaturnClient.client.player == null || SaturnClient.client.getNetworkHandler() == null) {
+ return;
+ }
+ if (SaturnClient.client.options.forwardKey.isPressed()
+ && !SaturnClient.client.options.backKey.isPressed()
+ && !SaturnClient.client.player.isSneaking()
+ && !SaturnClient.client.player.horizontalCollision
+ && !SaturnClient.client.player.isUsingItem()) {
+
+ Objects.requireNonNull(SaturnClient.client.player).setSprinting(true);
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Clock.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Clock.java
new file mode 100644
index 0000000..2492162
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Clock.java
@@ -0,0 +1,77 @@
+package org.saturnclient.modules.mods;
+
+import java.time.format.DateTimeFormatter;
+import java.time.LocalTime;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class Clock extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static Property format = Property.select(0, "24 hour", "12 hour");
+ private static Property seconds = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
+
+ public Clock() {
+ super(new ModuleDetails("Clock", "clock")
+ .description("Displays the current real-world time")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ format.named("Format"),
+ seconds.named("Show seconds"),
+ dimensions.prop()
+ );
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderClock(scope);
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ renderClock(scope);
+ }
+
+ public void renderClock(RenderScope scope) {
+ String text = "";
+ LocalTime currentTime = LocalTime.now();
+
+ switch (format.value) {
+ case 0:
+ if (seconds.value) { text = currentTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")); }
+ else { text = currentTime.format(DateTimeFormatter.ofPattern("HH:mm")); }
+ break;
+ case 1:
+ if (seconds.value) { text = currentTime.format(DateTimeFormatter.ofPattern("hh:mm:ss a")); }
+ else { text = currentTime.format(DateTimeFormatter.ofPattern("hh:mm a")); }
+ break;
+ }
+
+ scope.drawText(text,
+ 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ dimensions.height = 18 * text.split("\n").length;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Coordinates.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Coordinates.java
new file mode 100644
index 0000000..7ad5c0b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Coordinates.java
@@ -0,0 +1,75 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+import net.minecraft.util.math.Vec3d;
+
+public class Coordinates extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static Property displayMethod = Property.select(0, "Flat", "Flat annotated", "Newline", "Newline annotated");
+ private static ModDimensions dimensions = new ModDimensions(40, 18);
+
+ public Coordinates() {
+ super(new ModuleDetails("Coordinates", "coords")
+ .description("Displays your current coordinates")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ displayMethod.named("Display Method"),
+ dimensions.prop()
+ );
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderCoords(532, 69, 253, scope);
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ Vec3d pos = SaturnClient.client.player.getPos();
+ renderCoords((int) pos.x, (int) pos.y, (int) pos.z, scope);
+ }
+
+ public void renderCoords(int x, int y, int z, RenderScope scope) {
+ String f = "%d %d %d"; // Flat
+ switch (displayMethod.value) {
+ case 1: // Flat annotated
+ f = "X: %d Y: %d Z: %d";
+ break;
+ case 2: // Newline
+ f = "%d\n%d\n%d";
+ break;
+ case 3: // Newline annotated
+ f = "X: %d\nY: %d\nZ: %d";
+ break;
+ }
+ String text = String.format(f, x, y, z);
+ scope.drawText(text,
+ 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ dimensions.height = 18 * text.split("\n").length;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Crosshair.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Crosshair.java
new file mode 100644
index 0000000..5a4ed46
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Crosshair.java
@@ -0,0 +1,48 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Textures;
+
+import net.minecraft.client.render.RenderLayer;
+
+public class Crosshair extends Module {
+ public static Property enabled = Property.bool(false);
+
+ public Crosshair() {
+ super(
+ new ModuleDetails("Crosshair", "crosshair")
+ .description("Changes the crosshair if you're aiming at a entity")
+ .tags("Visuals", "Utility")
+ .version("v0.1.0"),
+ enabled.named("Enabled"));
+ }
+
+ @Override
+ public void render(RenderScope scope) {
+ if (enabled.value
+ && SaturnClient.client.targetedEntity != null
+ && SaturnClient.client.targetedEntity.isAlive()) {
+ int scaledWidth = 15;
+ int scaledHeight = 15;
+
+ scope.setRenderLayer(RenderLayer::getCrosshair);
+ scope.drawTexture(Textures.CROSSHAIR_RANGE,
+ (scope.getScaledWindowWidth() - scaledWidth) / 2,
+ (scope.getScaledWindowHeight() - scaledHeight) / 2, 0.0F, 0.0F, scaledWidth, scaledHeight);
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/DayCounter.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/DayCounter.java
new file mode 100644
index 0000000..bbb0b25
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/DayCounter.java
@@ -0,0 +1,61 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class DayCounter extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(40, 18);
+ long day = 0;
+
+ public DayCounter() {
+ super(new ModuleDetails("DayCounter", "day")
+ .description("Displays the number of days that have passed")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ dimensions.prop()
+ );
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderDay(271, scope);
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ day = (SaturnClient.client.world.getTimeOfDay() / 24000L);
+ renderDay(day, scope);
+ }
+
+ public void renderDay(long day, RenderScope scope) {
+ String text = "Day: " + day;
+ scope.drawText(text,
+ 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ dimensions.height = 18 * text.split("\n").length;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
+
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Fps.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Fps.java
new file mode 100644
index 0000000..f788e71
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Fps.java
@@ -0,0 +1,53 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class Fps extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
+
+ public Fps() {
+ super(new ModuleDetails("FPS Display", "fps")
+ .description("Displays current FPS")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ dimensions.prop());
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ String text = String.valueOf(SaturnClient.client.getCurrentFps()) + " FPS";
+ scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ String text = "369 FPS";
+ scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Freelook.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Freelook.java
new file mode 100644
index 0000000..e894211
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Freelook.java
@@ -0,0 +1,71 @@
+package org.saturnclient.modules.mods;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+import net.minecraft.client.option.Perspective;
+
+public class Freelook extends Module {
+ private static Property enabled = Property.bool(false);
+ private static Property toggle = Property.bool(true);
+ public static Property freelookKey = Property.keybinding(GLFW.GLFW_KEY_H);
+ public static boolean isFreeLooking = false;
+ private static boolean was1stPerson = false;
+
+ public Freelook() {
+ super(
+ new ModuleDetails("Freelook", "freelook")
+ .description("Allows you to look around freely without moving your character")
+ .tags("Movement", "Camera")
+ .version("0.1.0"),
+ enabled.named("Enabled"),
+ toggle.named("Toggle freelook"),
+ freelookKey.named("Freelook Keybinding")
+ );
+ }
+
+ @Override
+ public void tick() {
+ if (toggle.value) {
+ if (freelookKey.wasKeyPressed()) {
+ if (isFreeLooking) {
+ stopFreelook();
+ } else {
+ startFreelook();
+ }
+ }
+ } else if (freelookKey.isKeyPressed()) {
+ if (!isFreeLooking) {
+ startFreelook();
+ }
+ } else if (isFreeLooking) {
+ stopFreelook();
+ }
+ }
+
+ private void startFreelook() {
+ isFreeLooking = true;
+ was1stPerson = SaturnClient.client.options.getPerspective() == Perspective.FIRST_PERSON;
+ SaturnClient.client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
+ }
+
+ private void stopFreelook() {
+ if (was1stPerson) {
+ SaturnClient.client.options.setPerspective(Perspective.FIRST_PERSON);
+ }
+ isFreeLooking = false;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Fullbright.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Fullbright.java
new file mode 100644
index 0000000..7c4a3d3
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Fullbright.java
@@ -0,0 +1,39 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+public class Fullbright extends Module {
+ private static Property enabled = Property.bool(false);
+ private static Property brightness = Property.integer(100);
+
+ public Fullbright() {
+ super(
+ new ModuleDetails("Fullbright", "fullbright")
+ .description("Allows you to see in the dark")
+ .tags("Camera")
+ .version("v0.1.0"),
+ enabled.named("Enabled"),
+ brightness.named("Brightness %"));
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ public static boolean shouldOverrideBrightness() {
+ return enabled.value;
+ }
+
+ public static float getBrightnessValue() {
+ return brightness.value / 10;
+ }
+
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/HealthDisplay.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/HealthDisplay.java
new file mode 100644
index 0000000..e831ec2
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/HealthDisplay.java
@@ -0,0 +1,85 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+import net.minecraft.util.math.Vec3d;
+
+public class HealthDisplay extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static Property displayMode = Property.select(0, "Health", "Hearts");
+ private static Property decimals = Property.select(0, "0", "1", "2");
+ private static ModDimensions dimensions = new ModDimensions(40, 18);
+ double speed = 0.;
+ Vec3d velocity;
+ double y = 0.;
+
+ public HealthDisplay() {
+ super(new ModuleDetails("Health display", "health")
+ .description("Displays your current health")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ displayMode.named("Display mode"),
+ decimals.named("Decimals"),
+ dimensions.prop()
+ );
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderHealth(10, scope);
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ float health = SaturnClient.client.player.getHealth();
+ switch (displayMode.value) {
+ case 1:
+ health = health / 2;
+ }
+ renderHealth(health, scope);
+ }
+
+ public void renderHealth(float health, RenderScope scope) {
+ String text = "";
+
+ switch (decimals.value) {
+ case 0:
+ text = String.format("%.0f ", health);
+ break;
+ case 1:
+ text = String.format("%.1f ", health);
+ break;
+ case 2:
+ text = String.format("%.2f ", health);
+ break;
+ }
+
+ scope.drawText(text,
+ 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ dimensions.height = 18 * text.split("\n").length;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Keystrokes.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Keystrokes.java
new file mode 100644
index 0000000..f67e755
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Keystrokes.java
@@ -0,0 +1,153 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.modules.ModuleDetails;
+
+public class Keystrokes extends Module implements HudMod {
+
+ private static final Property enabled = Property.bool(false);
+ private static final Property showMouse = Property.bool(true);
+ private static final Property showSpace = Property.bool(false);
+
+ private static final Property clickBg = Property.color(0xFFCCCCCC);
+ private static final Property clickFg = Property.color(0xFFFFFFFF);
+
+ private static final ModDimensions dimensions = new ModDimensions(78, 54);
+
+ // Key states
+ private boolean w, a, s, d, lmb, rmb, space;
+
+ // Layout constants
+ private static final int KEY_SIZE = 24;
+ private static final int KEY_SPACING = 3;
+ private static final int MOUSE_WIDTH = 38;
+ private static final int MOUSE_HEIGHT = 24;
+ private static final int SPACE_HEIGHT = 19;
+
+ public Keystrokes() {
+ super(new ModuleDetails("Keystrokes", "keystrokes")
+ .description("Displays movement keystrokes")
+ .version("v0.2.0")
+ .tags("Utility"),
+
+ enabled.named("Enabled"),
+ showMouse.named("Show mouse clicks"),
+ showSpace.named("Show space"),
+ dimensions.prop(),
+ clickFg.named("Clicked fg"),
+ clickBg.named("Clicked bg")
+ );
+
+ dimensions.renderBackground = false;
+ }
+
+ @Override
+ public void tick() {
+ w = SaturnClient.client.options.forwardKey.isPressed();
+ a = SaturnClient.client.options.leftKey.isPressed();
+ s = SaturnClient.client.options.backKey.isPressed();
+ d = SaturnClient.client.options.rightKey.isPressed();
+ lmb = SaturnClient.client.options.attackKey.isPressed();
+ rmb = SaturnClient.client.options.useKey.isPressed();
+ space = SaturnClient.client.options.jumpKey.isPressed();
+
+ updateHeight();
+ }
+
+ private void updateHeight() {
+ int height = KEY_SIZE * 2 + KEY_SPACING;
+
+ if (showSpace.value)
+ height += SPACE_HEIGHT + KEY_SPACING;
+
+ if (showMouse.value)
+ height += MOUSE_HEIGHT + KEY_SPACING;
+
+ dimensions.height = height;
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ render(scope, false);
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ render(scope, true);
+ }
+
+ private void render(RenderScope scope, boolean live) {
+
+ int yOffset = 0;
+
+ // --- W ---
+ renderKey(scope, live && w, 'W', KEY_SIZE + KEY_SPACING, yOffset);
+
+ // --- A S D ---
+ yOffset += KEY_SIZE + KEY_SPACING;
+
+ renderKey(scope, live && a, 'A', 0, yOffset);
+ renderKey(scope, live && s, 'S', KEY_SIZE + KEY_SPACING, yOffset);
+ renderKey(scope, live && d, 'D', (KEY_SIZE + KEY_SPACING) * 2, yOffset);
+
+ yOffset += KEY_SIZE + KEY_SPACING;
+
+ // --- SPACE ---
+ if (showSpace.value) {
+ renderKeySpace(scope, live && space, 0, yOffset);
+ yOffset += SPACE_HEIGHT + KEY_SPACING;
+ }
+
+ // --- MOUSE ---
+ if (showMouse.value) {
+ renderKeyMouse(scope, live && lmb, "LMB", 0, yOffset);
+ renderKeyMouse(scope, live && rmb, "RMB", MOUSE_WIDTH + KEY_SPACING, yOffset);
+ }
+ }
+
+ private void renderKey(RenderScope scope, boolean pressed, char c, int x, int y) {
+ int bg = pressed ? clickBg.value : dimensions.bgColor.value;
+ int fg = pressed ? clickFg.value : dimensions.fgColor.value;
+
+ scope.drawRoundedRectangle(x, y, KEY_SIZE, KEY_SIZE, dimensions.radius.value, bg);
+ scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, fg);
+ }
+
+ private void renderKeyMouse(RenderScope scope, boolean pressed, String text, int x, int y) {
+ int bg = pressed ? clickBg.value : dimensions.bgColor.value;
+ int fg = pressed ? clickFg.value : dimensions.fgColor.value;
+
+ scope.drawRoundedRectangle(x, y, MOUSE_WIDTH, MOUSE_HEIGHT, dimensions.radius.value, bg);
+ scope.drawText(0.6f, text, x + 9, y + 7, dimensions.font.value, fg);
+ }
+
+ private void renderKeySpace(RenderScope scope, boolean pressed, int x, int y) {
+ int bg = pressed ? clickBg.value : dimensions.bgColor.value;
+ int fg = pressed ? clickFg.value : dimensions.fgColor.value;
+
+ scope.drawRoundedRectangle(x, y, dimensions.width, SPACE_HEIGHT, dimensions.radius.value, bg);
+
+ int lineY = y + SPACE_HEIGHT / 2;
+ scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, fg);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Nametags.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Nametags.java
new file mode 100644
index 0000000..5f4948f
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Nametags.java
@@ -0,0 +1,148 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.modules.mods.utils.HealthRenderState;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+import net.minecraft.client.render.entity.state.LivingEntityRenderState;
+
+// todo: rendering modes, custom names
+
+public class Nametags extends Module {
+ private static Property enabled = Property.bool(false);
+ private static Property healthDisplay = Property.bool(false);
+ private static Property players = Property.bool(true);
+ private static Property hostile = Property.bool(false);
+ private static Property passive = Property.bool(false);
+ private static Property heartEmoji = Property.bool(true);
+ private static Property obfuscate = Property.bool(false);
+ private static Property unit = Property.select(0, "Health", "Hearts");
+ private static Property format = Property.select(0, "Value", "Value / Total", "%");
+ private static Property nameColor = Property.select(15,
+ "Black",
+ "Dark Blue",
+ "Dark Green",
+ "Dark Aqua",
+ "Dark Red",
+ "Dark Purple",
+ "Gold",
+ "Gray",
+ "Dark Gray",
+ "Blue",
+ "Green",
+ "Aqua",
+ "Red",
+ "Light Purple",
+ "Yellow",
+ "White"
+ );
+ private static Property healthColor = Property.select(12,
+ "Black",
+ "Dark Blue",
+ "Dark Green",
+ "Dark Aqua",
+ "Dark Red",
+ "Dark Purple",
+ "Gold",
+ "Gray",
+ "Dark Gray",
+ "Blue",
+ "Green",
+ "Aqua",
+ "Red",
+ "Light Purple",
+ "Yellow",
+ "White"
+ );
+
+ private static final String[] COLOR_CODES = {
+ "§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7",
+ "§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f"
+ };
+
+ public Nametags() {
+ super(
+ new ModuleDetails("Nametags", "nametags")
+ .description("Allows you to modify entity nametags")
+ .tags("Camera", "Utility")
+ .version("0.1.0"),
+ enabled.named("Enabled"),
+ healthDisplay.named("Display health"),
+ players.named("Players"),
+ hostile.named("Hostile"),
+ passive.named("Passive"),
+ heartEmoji.named("Heart emoji"),
+ obfuscate.named("Obfuscate enemy names"),
+ unit.named("Unit"),
+ format.named("Format"),
+ nameColor.named("Name color"),
+ healthColor.named("Health color")
+ );
+ }
+
+ @Override
+ public void tick() {
+
+ }
+
+ public static String getHealthString(LivingEntityRenderState state, HealthRenderState hrs, float health, float maxHealth) {
+ String ret = "";
+
+ switch (format.value){
+ case 0: // value
+ ret = String.format("%.1f", health / (unit.value + 1));
+ break;
+
+ case 1: // value / total
+ ret = String.format("%.1f", health / (unit.value + 1)) + " / " + String.format("%.1f", maxHealth / (unit.value + 1));
+ break;
+
+ case 2: // %
+ ret = String.format("%.0f%%", (health / maxHealth) * 100);
+ break;
+ }
+ return ret;
+ }
+
+ public static String getNametagString(LivingEntityRenderState state) {
+ if (state.customName == null) return null;
+ if (!healthDisplay.value) return state.customName.getString(); // name only
+ if (!(state instanceof HealthRenderState hrs)) return null;
+
+ float health = hrs.saturn$getHealth();
+ float maxHealth = hrs.saturn$getMaxHealth();
+
+ HealthRenderState.EntityType type = hrs.saturn$getEntityType();
+ if (type == HealthRenderState.EntityType.PASSIVE && !passive.value) return null;
+ if (type == HealthRenderState.EntityType.HOSTILE && !hostile.value) return null;
+ if (type == HealthRenderState.EntityType.PLAYER && !players.value) return null;
+ if (type == HealthRenderState.EntityType.OTHER) return null;
+
+ if (health < 0f || maxHealth < 0f || health > maxHealth) return null;
+
+ String hpColor = COLOR_CODES[healthColor.value];
+ String color = COLOR_CODES[nameColor.value];
+ String heart = heartEmoji.value ? " ❤" : "";
+ String obf = obfuscate.value ? "§k" : "";
+ String reset = "§r";
+
+ String ret = color + obf + state.customName.getString() + reset + " " + hpColor + getHealthString(state, hrs, health, maxHealth) + heart;
+
+ return ret;
+ }
+
+ public static boolean shouldReplaceName() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/NoFog.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/NoFog.java
new file mode 100644
index 0000000..bd8d17e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/NoFog.java
@@ -0,0 +1,39 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+public class NoFog extends Module {
+ private static Property enabled = Property.bool(false);
+ private static Property liquids = Property.bool(false);
+
+ public NoFog() {
+ super(
+ new ModuleDetails("No Fog", "nofog")
+ .description("Stops fog from rendering")
+ .tags("Camera")
+ .version("v0.1.0"),
+ enabled.named("Enabled"),
+ liquids.named("See through liquids"));
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ // static for the mixin
+ public static boolean isActive() {
+ return enabled.value;
+ }
+
+ public static boolean liquids() {
+ return liquids.value;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Ping.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Ping.java
new file mode 100644
index 0000000..06c103d
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Ping.java
@@ -0,0 +1,60 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+
+public class Ping extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
+
+ public Ping() {
+ super(new ModuleDetails("Ping Display", "ping")
+ .description("Displays ping")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ dimensions.prop());
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ ClientPlayNetworkHandler handler = client.getNetworkHandler();
+ String text = "0 ms";
+ if (handler != null) {
+ text = String.valueOf(handler.getPlayerListEntry(client.player.getUuid()).getLatency()) + " ms";
+ }
+ scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ String text = "10 ms";
+ scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Speedometer.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Speedometer.java
new file mode 100644
index 0000000..ec3663c
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Speedometer.java
@@ -0,0 +1,114 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.entity.Entity;
+
+public class Speedometer extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static Property axis = Property.select(1, "Absolute", "X axis", "Y axis");
+ private static Property unitText = Property.select(0, "None", "Blocks/s", "blocks/s", "b/s");
+ private static Property speedText = Property.bool(true);
+ private static ModDimensions dimensions = new ModDimensions(40, 18);
+ double speed = 0.;
+ Vec3d velocity;
+ double y = 0.;
+
+ public Speedometer() {
+ super(new ModuleDetails("Speedometer", "speed")
+ .description("Displays your current speed")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ axis.named("Speed type"),
+ unitText.named("Unit text"),
+ speedText.named("Show speed text"),
+ dimensions.prop()
+ );
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderSpeed(2.71, scope);
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ Entity vehicle = SaturnClient.client.player.getVehicle();
+ Entity player = SaturnClient.client.player;
+
+ if (vehicle == null) {
+ velocity = player.getVelocity();
+ y = player.isOnGround() ? 0. : velocity.y;
+ } else {
+ velocity = vehicle.getVelocity();
+ y = vehicle.isOnGround() ? 0. : velocity.y;
+ }
+
+ switch (axis.value) {
+ case 0: // Absolute
+ speed = (Math.sqrt(Math.pow(velocity.x, 2) + Math.pow(y, 2) + Math.pow(velocity.z, 2)));
+ break;
+
+ case 1: // x axis
+ speed = (Math.sqrt(Math.pow(velocity.x, 2) + Math.pow(velocity.z, 2)));
+ break;
+
+ case 2: // y axis
+ speed = (Math.abs(y));
+ break;
+ }
+ renderSpeed(speed * 20, scope); // blocks/tick to blocks/s
+ }
+
+ public void renderSpeed(double speed, RenderScope scope) {
+ String f = String.format("%.2f ", speed);
+
+ switch (unitText.value) {
+ case 1:
+ f += "Blocks/s";
+ break;
+
+ case 2:
+ f += "blocks/s";
+ break;
+ case 3:
+ f += "b/s";
+ break;
+ }
+
+ String text = f;
+
+ if (speedText.value) {
+ text = "Speed: " + text;
+ }
+
+ scope.drawText(text,
+ 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ dimensions.height = 18 * text.split("\n").length;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/StatusEffects.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/StatusEffects.java
new file mode 100644
index 0000000..3d4a1e0
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/StatusEffects.java
@@ -0,0 +1,105 @@
+package org.saturnclient.modules.mods;
+
+import java.util.Arrays;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.texture.StatusEffectSpriteManager;
+import net.minecraft.entity.effect.StatusEffect;
+import net.minecraft.entity.effect.StatusEffectInstance;
+import net.minecraft.registry.Registries;
+import net.minecraft.registry.entry.RegistryEntry;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+
+public class StatusEffects extends Module implements HudMod {
+ public static Property enabled = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(60, 0);
+
+ private final StatusEffectInstance[] dummyEffects = {
+ new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
+ new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
+ new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2),
+ };
+
+ public StatusEffects() {
+ super(new ModuleDetails("Status effects", "effect")
+ .description("Displays status effects")
+ .version("v0.1.0")
+ .tags("Utility"),
+
+ enabled.named("Enabled"),
+ dimensions.prop());
+ }
+
+ private void renderEffects(RenderScope scope, Iterable effects) {
+ int row = 0;
+
+ StatusEffectSpriteManager statusEffectSpriteManager = SaturnClient.client.getStatusEffectSpriteManager();
+
+ for (StatusEffectInstance effect : effects) {
+ if (effect.shouldShowIcon()) {
+ RegistryEntry registryEntry = effect.getEffectType();
+ Sprite sprite = statusEffectSpriteManager.getSprite(registryEntry);
+ scope.drawSpriteStretched(sprite, 0, 18 * row, 16, 16);
+ scope.drawText(0.5f, getDurationAsString(effect), 18, 18 * row + 3, dimensions.font.value, -1);
+ row++;
+ }
+ }
+
+ dimensions.height = 18 * row;
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ renderEffects(scope, SaturnClient.client.player.getStatusEffects());
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ renderEffects(scope, Arrays.asList(dummyEffects));
+ }
+
+ // This code was from: https://github.com/magicus/statuseffecttimer/blob/master/src/main/java/se/icus/mag/statuseffecttimer/mixin/StatusEffectTimerMixin.java
+ private String getDurationAsString(StatusEffectInstance statusEffectInstance) {
+ if (statusEffectInstance.isInfinite()) {
+ return I18n.translate("effect.duration.infinite");
+ }
+
+ int ticks = MathHelper.floor((float) statusEffectInstance.getDuration());
+ int totalSeconds = ticks / 20;
+
+ if (totalSeconds >= 3600) {
+ return totalSeconds / 3600 + "h";
+ } else if (totalSeconds >= 60) {
+ int minutes = totalSeconds / 60;
+ int seconds = totalSeconds % 60;
+ return String.format("%d:%02d", minutes, seconds);
+ } else {
+ return String.valueOf(totalSeconds) + "s";
+ }
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Tps.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Tps.java
new file mode 100644
index 0000000..a8eb977
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Tps.java
@@ -0,0 +1,99 @@
+package org.saturnclient.modules.mods;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class Tps extends Module implements HudMod {
+ private static Property enabled = Property.bool(false);
+ private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
+
+ private static long lastWorldAge = -1;
+ private static long lastRealTime = -1;
+ private static double currentTps = 20.0;
+
+ private static final int SAMPLE_COUNT = 2;
+ private static final double[] samples = new double[SAMPLE_COUNT];
+ static {
+ java.util.Arrays.fill(samples, 20.0);
+ }
+ private static int sampleIndex = 0;
+ private static int samplesFilled = 0;
+
+ public Tps() {
+ super(new ModuleDetails("TPS Display", "tps")
+ .description("Displays the TPS")
+ .version("v0.1.0")
+ .tags("Utility"),
+ enabled.named("Enabled"),
+ dimensions.prop()
+ );
+ }
+
+ public static void onTimePacket(long worldAge) {
+ long now = System.currentTimeMillis();
+
+ if (lastWorldAge != -1 && lastRealTime != -1) {
+ long tickDelta = worldAge - lastWorldAge;
+ long timeDelta = now - lastRealTime;
+
+ if (timeDelta > 0 && tickDelta > 0) {
+ double sample = (tickDelta / (double) timeDelta) * 1000.0;
+ sample = Math.min(sample, 20.0);
+
+ samples[sampleIndex % SAMPLE_COUNT] = sample;
+ sampleIndex++;
+ samplesFilled = Math.min(samplesFilled + 1, SAMPLE_COUNT);
+
+ double sum = 0;
+ for (int i = 0; i < samplesFilled; i++) sum += samples[i];
+ currentTps = sum / samplesFilled;
+ }
+ }
+
+ lastWorldAge = worldAge;
+ lastRealTime = now;
+ }
+
+ public static void reset() {
+ lastWorldAge = -1;
+ lastRealTime = -1;
+ currentTps = 20.0;
+ sampleIndex = 0;
+ samplesFilled = SAMPLE_COUNT;
+ for (int i = 0; i < SAMPLE_COUNT; i++) samples[i] = 20.0;
+ }
+
+ @Override
+ public void renderHud(RenderScope scope) {
+ String text = String.format("%.1f TPS", currentTps);
+ scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ }
+
+ @Override
+ public void renderDummy(RenderScope scope) {
+ String text = "20.0 TPS";
+ scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
+ dimensions.width = Fonts.getWidth(text, dimensions.font.value);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ }
+
+ @Override
+ public ModDimensions getDimensions() {
+ return dimensions;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Zoom.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Zoom.java
new file mode 100644
index 0000000..cd9d28a
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/Zoom.java
@@ -0,0 +1,60 @@
+package org.saturnclient.modules.mods;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.modules.Module;
+import org.saturnclient.modules.ModuleDetails;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+public class Zoom extends Module {
+ private static Property enabled = Property.bool(false);
+ private static Property toggle = Property.bool(false);
+ public static Property zoomKey = Property.keybinding(GLFW.GLFW_KEY_C);
+ public static Property zoomLevel = Property.floatProp(3.0f);
+
+ public static boolean isZooming = false;
+
+ public Zoom() {
+ super(
+ new ModuleDetails("Zoom", "zoom")
+ .description("Allows you to zoom in for a closer view")
+ .tags("Camera", "Utility")
+ .version("0.1.0"),
+ enabled.named("Enabled"),
+ toggle.named("Toggle zoom"),
+ zoomKey.named("Zoom Keybinding"),
+ zoomLevel.named("Zoom Level")
+ );
+ }
+
+ @Override
+ public void tick() {
+ if (toggle.value) {
+ if (zoomKey.wasKeyPressed()) {
+ isZooming = !isZooming;
+ }
+ } else {
+ isZooming = zoomKey.isKeyPressed();
+ }
+ }
+
+ public static float getZoomLevel() {
+ return zoomLevel.value;
+ }
+
+ public static boolean shouldZoom() {
+ return enabled.value;
+ }
+
+ @Override
+ public void onEnabled(boolean e) {
+ enabled.value = e;
+ if (!e) {
+ isZooming = false;
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled.value;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/BackgroundRendererMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/BackgroundRendererMixin.java
new file mode 100644
index 0000000..b34bf23
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/BackgroundRendererMixin.java
@@ -0,0 +1,33 @@
+package org.saturnclient.modules.mods.mixins;
+
+import net.minecraft.client.render.BackgroundRenderer;
+
+import org.saturnclient.modules.mods.NoFog;
+import net.minecraft.entity.Entity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(BackgroundRenderer.class)
+public class BackgroundRendererMixin {
+
+ @ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
+ private static float modifyFogStart(float viewDistance) {
+ if (NoFog.isActive()) {
+ return Float.MAX_VALUE; // this could be customised
+ }
+ return viewDistance;
+ }
+ // liquids
+ @Redirect(
+ method = "applyFog",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isSpectator()Z")
+ )
+ private static boolean redirectIsSpectator(Entity entity) {
+ if (NoFog.isActive() && NoFog.liquids()) {
+ return true; // pretend to be spectator
+ }
+ return entity.isSpectator();
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/CameraMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/CameraMixin.java
new file mode 100644
index 0000000..7c2cabc
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/CameraMixin.java
@@ -0,0 +1,44 @@
+package org.saturnclient.modules.mods.mixins;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.render.Camera;
+import net.minecraft.entity.Entity;
+import net.minecraft.world.BlockView;
+
+import org.saturnclient.modules.mods.Freelook;
+import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(Camera.class)
+public abstract class CameraMixin {
+ @Unique
+ boolean firstTime = true;
+
+ @Shadow
+ protected abstract void setRotation(float yaw, float pitch);
+
+ @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
+ public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing, float tickDelta, CallbackInfo ci) {
+ if (Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
+ CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
+
+ if (firstTime && MinecraftClient.getInstance().player != null) {
+ cameraOverriddenEntity.freelook$setCameraPitch(MinecraftClient.getInstance().player.getPitch());
+ cameraOverriddenEntity.freelook$setCameraYaw(MinecraftClient.getInstance().player.getYaw());
+ firstTime = false;
+ }
+ this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(), cameraOverriddenEntity.freelook$getCameraPitch());
+
+ }
+ if (!Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
+ firstTime = true;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/EntityMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/EntityMixin.java
new file mode 100644
index 0000000..8442b43
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/EntityMixin.java
@@ -0,0 +1,62 @@
+package org.saturnclient.modules.mods.mixins;
+
+
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.math.MathHelper;
+
+import org.saturnclient.modules.mods.Freelook;
+import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(Entity.class)
+public class EntityMixin implements CameraOverriddenEntity {
+ @Unique
+ private float cameraPitch;
+
+ @Unique
+ private float cameraYaw;
+
+ @Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
+ public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
+ //noinspection ConstantValue// IntelliJ is incorrect here, this code block is reachable
+ if (Freelook.isFreeLooking && (Object) this instanceof ClientPlayerEntity) {
+ double pitchDelta = (yDelta * 0.15);
+ double yawDelta = (xDelta * 0.15);
+
+ this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
+ this.cameraYaw += (float) yawDelta;
+
+ ci.cancel();
+
+ }
+ }
+
+ @Override
+ @Unique
+ public float freelook$getCameraPitch() {
+ return this.cameraPitch;
+ }
+
+ @Override
+ @Unique
+ public float freelook$getCameraYaw() {
+ return this.cameraYaw;
+ }
+
+ @Override
+ @Unique
+ public void freelook$setCameraPitch(float pitch) {
+ this.cameraPitch = pitch;
+ }
+
+ @Override
+ @Unique
+ public void freelook$setCameraYaw(float yaw) {
+ this.cameraYaw = yaw;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/GameRendererMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/GameRendererMixin.java
new file mode 100644
index 0000000..2518f06
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/GameRendererMixin.java
@@ -0,0 +1,22 @@
+package org.saturnclient.modules.mods.mixins;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.saturnclient.modules.mods.Zoom;
+
+import net.minecraft.client.render.GameRenderer;
+
+@Mixin(GameRenderer.class)
+public class GameRendererMixin {
+
+ @Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
+ private void onGetFov(CallbackInfoReturnable cir) {
+ if (Zoom.isZooming && Zoom.shouldZoom()) {
+ float originalFov = cir.getReturnValue();
+ float zoomedFov = originalFov / Zoom.getZoomLevel();
+ cir.setReturnValue(zoomedFov);
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LightmapTextureManagerMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LightmapTextureManagerMixin.java
new file mode 100644
index 0000000..34289f6
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LightmapTextureManagerMixin.java
@@ -0,0 +1,23 @@
+// code modified from Gamma utils: https://github.com/Sjouwer/gamma-utils
+package org.saturnclient.modules.mods.mixins;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import net.minecraft.client.render.LightmapTextureManager;
+import org.saturnclient.modules.mods.Fullbright;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(LightmapTextureManager.class)
+public class LightmapTextureManagerMixin {
+
+ @ModifyExpressionValue(
+ method = "update",
+ at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)
+ )
+ private float modifyGamma(float original) {
+ if (Fullbright.shouldOverrideBrightness()) {
+ return Fullbright.getBrightnessValue();
+ }
+ return original;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LivingEntityRenderStateMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LivingEntityRenderStateMixin.java
new file mode 100644
index 0000000..dacee20
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LivingEntityRenderStateMixin.java
@@ -0,0 +1,26 @@
+package org.saturnclient.modules.mods.mixins;
+
+import net.minecraft.client.render.entity.state.LivingEntityRenderState;
+
+import org.saturnclient.modules.mods.utils.HealthRenderState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+
+@Mixin(LivingEntityRenderState.class)
+public class LivingEntityRenderStateMixin implements org.saturnclient.modules.mods.utils.HealthRenderState {
+
+ @Unique private float saturn$health = 0f;
+ @Unique private float saturn$maxHealth = 1f;
+ @Unique private HealthRenderState.EntityType saturn$entityType = HealthRenderState.EntityType.OTHER;
+
+ public float saturn$getHealth() { return saturn$health; }
+ public float saturn$getMaxHealth() { return saturn$maxHealth; }
+ public void saturn$setHealth(float health, float maxHealth) {
+ this.saturn$health = health;
+ this.saturn$maxHealth = maxHealth;
+ }
+
+ public HealthRenderState.EntityType saturn$getEntityType() { return saturn$entityType; }
+ public void saturn$setEntityType(HealthRenderState.EntityType type) { this.saturn$entityType = type; }
+
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LivingEntityRendererMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LivingEntityRendererMixin.java
new file mode 100644
index 0000000..34e5d1f
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/LivingEntityRendererMixin.java
@@ -0,0 +1,34 @@
+package org.saturnclient.modules.mods.mixins;
+
+import net.minecraft.client.render.entity.LivingEntityRenderer;
+import net.minecraft.client.render.entity.state.LivingEntityRenderState;
+import org.saturnclient.modules.mods.utils.HealthRenderState;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.mob.HostileEntity;
+import net.minecraft.entity.passive.PassiveEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(LivingEntityRenderer.class)
+public abstract class LivingEntityRendererMixin {
+
+ @Inject(method = "updateRenderState", at = @At("TAIL"))
+ private void saturn$captureHealth(T entity, S state, float tickDelta, CallbackInfo ci) {
+ if (!(state instanceof HealthRenderState hrs)) return;
+
+ hrs.saturn$setHealth(entity.getHealth(), entity.getMaxHealth());
+
+ if (entity instanceof PlayerEntity) {
+ hrs.saturn$setEntityType(HealthRenderState.EntityType.PLAYER);
+ } else if (entity instanceof HostileEntity) {
+ hrs.saturn$setEntityType(HealthRenderState.EntityType.HOSTILE);
+ } else if (entity instanceof PassiveEntity) {
+ hrs.saturn$setEntityType(HealthRenderState.EntityType.PASSIVE);
+ } else {
+ hrs.saturn$setEntityType(HealthRenderState.EntityType.OTHER);
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/NametagsMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/NametagsMixin.java
new file mode 100644
index 0000000..be17d91
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/NametagsMixin.java
@@ -0,0 +1,48 @@
+package org.saturnclient.modules.mods.mixins;
+
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.EntityRenderer;
+import net.minecraft.client.render.entity.state.EntityRenderState;
+import net.minecraft.client.render.entity.state.LivingEntityRenderState;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.Text;
+import org.saturnclient.modules.mods.Nametags;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(EntityRenderer.class)
+public abstract class NametagsMixin {
+
+ @Unique
+ private static final ThreadLocal saturn$rendering = ThreadLocal.withInitial(() -> false);
+
+ @Shadow
+ protected abstract void renderLabelIfPresent(
+ S state, Text text, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light
+ );
+
+ @Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
+ private void saturn$replaceNametag(S state, Text text,
+ MatrixStack matrices, VertexConsumerProvider vertexConsumers,
+ int light, CallbackInfo ci) {
+
+ if (saturn$rendering.get()) return;
+ if (!Nametags.shouldReplaceName()) return;
+ if (!(state instanceof LivingEntityRenderState living)) return;
+
+ String replacement = Nametags.getNametagString(living);
+ if (replacement == null) return;
+
+ ci.cancel();
+ saturn$rendering.set(true);
+ try {
+ renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
+ } finally {
+ saturn$rendering.set(false);
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java
new file mode 100644
index 0000000..a552698
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java
@@ -0,0 +1,58 @@
+package org.saturnclient.modules.mods.mixins;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.ModManager;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.menus.HudEditor;
+import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
+import org.saturnclient.ui2.RenderScope;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.hud.InGameHud;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.RenderTickCounter;
+
+@Mixin(InGameHud.class)
+public class RenderMixin {
+ @Inject(method = "renderChat", at = @At("TAIL"))
+ public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
+ TextRenderer textRenderer = SaturnClient.client.textRenderer;
+
+ if (textRenderer != null &&
+ !(SaturnClient.client.currentScreen instanceof HudEditor)) {
+ RenderScope renderScope = new RenderScope(context.getMatrices(),
+ ((DrawContextAccessor) context).getVertexConsumers());
+
+ for (org.saturnclient.modules.Module m : ModManager.ENABLED_MODS) {
+ if (m instanceof HudMod && m.isEnabled()) {
+ ModDimensions dim = ((HudMod) m).getDimensions();
+
+ renderScope.matrices.push();
+
+ renderScope.matrices.translate(dim.x.value, (float) dim.y.value, 0);
+
+ renderScope.matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
+
+ if (dim.renderBackground) {
+ renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value,
+ dim.bgColor.value);
+ }
+
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ ((HudMod) m).renderHud(renderScope);
+
+ renderScope.matrices.pop();
+ } else {
+ m.render(renderScope);
+ }
+ }
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/TpsMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/TpsMixin.java
new file mode 100644
index 0000000..7a9d15e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/mixins/TpsMixin.java
@@ -0,0 +1,18 @@
+package org.saturnclient.modules.mods.mixins;
+
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
+import org.saturnclient.modules.mods.Tps;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ClientPlayNetworkHandler.class)
+public class TpsMixin {
+
+ @Inject(method = "onWorldTimeUpdate", at = @At("HEAD"))
+ private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) {
+ Tps.onTimePacket(packet.time());
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/utils/CameraOverriddenEntity.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/utils/CameraOverriddenEntity.java
new file mode 100644
index 0000000..169608f
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/utils/CameraOverriddenEntity.java
@@ -0,0 +1,9 @@
+package org.saturnclient.modules.mods.utils;
+
+public interface CameraOverriddenEntity {
+ float freelook$getCameraPitch();
+ float freelook$getCameraYaw();
+
+ void freelook$setCameraPitch(float pitch);
+ void freelook$setCameraYaw(float yaw);
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/utils/HealthRenderState.java b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/utils/HealthRenderState.java
new file mode 100644
index 0000000..4853754
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/modules/mods/utils/HealthRenderState.java
@@ -0,0 +1,14 @@
+package org.saturnclient.modules.mods.utils;
+
+public interface HealthRenderState {
+ float saturn$getHealth();
+ float saturn$getMaxHealth();
+ void saturn$setHealth(float health, float maxHealth);
+
+ EntityType saturn$getEntityType();
+ void saturn$setEntityType(EntityType type);
+
+ enum EntityType {
+ PLAYER, HOSTILE, PASSIVE, OTHER
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/SaturnClient.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/SaturnClient.java
new file mode 100644
index 0000000..ba767c9
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/SaturnClient.java
@@ -0,0 +1,85 @@
+package org.saturnclient.saturnclient;
+
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.util.math.MatrixStack;
+
+import org.saturnclient.cosmetics.Emotes;
+import org.saturnclient.cosmetics.cloak.Cloaks;
+import org.saturnclient.cosmetics.hat.Hats;
+import org.saturnclient.modules.ModManager;
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.event.KeyInputHandler;
+import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.screens.TitleMenu;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Main class for the Saturn Client mod.
+ */
+public class SaturnClient implements ModInitializer {
+
+ public static final String MOD_ID = "saturnclient";
+ public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
+ public static MinecraftClient client;
+
+ @Override
+ public void onInitialize() {
+ LOGGER.info("Initializing " + MOD_ID);
+ client = MinecraftClient.getInstance();
+ Config.init();
+ ModManager.init();
+
+ client.execute(() -> {
+ SaturnScreen.preload(client);
+ });
+
+ ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save());
+
+ ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ if (screen != null && !(screen instanceof TitleMenu)) {
+ if (screen instanceof SaturnScreen) {
+ ((SaturnScreen) screen).draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18)
+ .position(screen.width - 180 - 20, screen.height - 18 - 20));
+ } else {
+ ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, tickDelta) -> {
+ RenderScope renderScope = new RenderScope(context.getMatrices(),
+ ((DrawContextAccessor) context).getVertexConsumers());
+
+ int width = context.getScaledWindowWidth();
+ int height = context.getScaledWindowHeight();
+
+ float scaleX = (float) screen1.width / width;
+ float scaleY = (float) screen1.height / height;
+
+ MatrixStack matrixStack = context.getMatrices();
+ matrixStack.push();
+ matrixStack.translate(width - 95, height - 14, 0);
+ matrixStack.scale(0.5f / scaleX, 0.5f / scaleY, 1.0f);
+ renderScope.drawTexture(Textures.LOGO_TEXT_BIG, 0, 0, 0, 0, 180, 18);
+ matrixStack.pop();
+ });
+ }
+ }
+ });
+
+ KeyInputHandler.register();
+ Emotes.initialize();
+ if (ServiceClient.authenticate()) {
+ Cloaks.initialize();
+ Hats.initialize();
+ LOGGER.info(MOD_ID + " initialization complete");
+ } else {
+ LOGGER.error("Failed to authenticate with the server");
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/ServiceClient.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/ServiceClient.java
new file mode 100644
index 0000000..4102704
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/ServiceClient.java
@@ -0,0 +1,205 @@
+package org.saturnclient.saturnclient.client;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.saturnclient.cosmetics.cloak.Cloaks;
+import org.saturnclient.cosmetics.hat.Hats;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+
+import dev.kosmx.playerAnim.api.layered.AnimationStack;
+import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
+import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
+import dev.selimaj.session.Session;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.util.Identifier;
+
+public class ServiceClient {
+ private static Session session;
+ public static UUID uuid;
+
+ public static boolean connectTimeout() {
+ try {
+ session = Session.connect("wss://saturn-server.selimaj.dev", 10, TimeUnit.SECONDS);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static boolean authenticate() {
+ ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> {
+ if (session != null)
+ try {
+ session.close();
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Unable to close Saturn session");
+
+ }
+ });
+
+ try {
+ var mcSession = SaturnClient.client.getSession();
+ if (mcSession == null) {
+ SaturnClient.LOGGER.error("No active Minecraft session found");
+ return false;
+ }
+
+ String accessToken = mcSession.getAccessToken();
+ uuid = mcSession.getUuidOrNull();
+ String username = mcSession.getUsername();
+
+ SaturnClient.LOGGER.info("Authenticating with UUID: " + uuid);
+
+ if (!connectTimeout()) {
+ SaturnClient.LOGGER.error("Unable to authenticate: Session Server Timeout");
+ return false;
+ }
+
+ ServiceMethods.Types.Player response = session.request(ServiceMethods.Authenticate, accessToken)
+ .get();
+
+ for (String availableCloak : response.cloaks()) {
+ Cloaks.availableCloaks.add(availableCloak);
+ }
+
+ for (String availableHat : response.hats()) {
+ Hats.availableHats.add(availableHat);
+ }
+
+ eventHandlers();
+
+ SaturnPlayer.set(new SaturnPlayer(uuid, username, response.cloak(), response.hat()));
+
+ Cloaks.loadCloak(uuid);
+
+ return true;
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Authentication failed", e);
+ return false;
+ }
+ }
+
+ public static void setCloak(String itemId) {
+ try {
+ session.request(ServiceMethods.SetCloak, itemId).whenComplete((msg, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ } else {
+ try {
+ session.request(ServiceMethods.SendPlayer,
+ new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to set cloak (service): ", e);
+ }
+ }
+
+ public static void setHat(String itemId) {
+ try {
+ session.request(ServiceMethods.SetHat, itemId).whenComplete((msg, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ } else {
+ try {
+ session.request(ServiceMethods.SendPlayer,
+ new ServiceMethods.Types.SendPlayerRequest(SaturnPlayer.getExternalUUIDAsString()));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to set hat (service): ", e);
+ }
+ }
+
+ public static void buyCloak(String itemId) {
+ try {
+ session.request(ServiceMethods.BuyCloak, itemId).whenComplete((msg, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ } else {
+ Cloaks.availableCloaks.add(itemId);
+ }
+ });
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to buy cloak (service): ", e);
+ }
+ }
+
+ public static void buyHat(String itemId) {
+ try {
+ session.request(ServiceMethods.BuyHat, itemId).whenComplete((msg, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ } else {
+ Hats.availableHats.add(itemId);
+ }
+ });
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to buy hat (service): ", e);
+ }
+ }
+
+ public static void emote(String emote) {
+ try {
+ session.request(ServiceMethods.Emote,
+ new ServiceMethods.Types.EmoteRequest(emote, SaturnPlayer.getExternalUUIDAsString()))
+ .whenComplete((msg, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ }
+ });
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to emote (service): ", e);
+ }
+ }
+
+ public static void eventHandlers() {
+ session.onNotification(ServiceMethods.EmoteEvent, (data) -> {
+ for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
+ if (player.getUuidAsString().equals(data.from())) {
+ AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
+ if (data.emote() != null && !data.emote().isEmpty()) {
+ if (animationStack.isActive() && animationStack.getPriority() == 1000) {
+ animationStack.removeLayer(1000);
+ }
+ animationStack.addAnimLayer(1000,
+ PlayerAnimationRegistry
+ .getAnimation(Identifier.of("saturnclient", data.emote()))
+ .playAnimation());
+ } else {
+ animationStack.removeLayer(1000);
+ }
+ }
+ }
+ });
+
+ session.onNotification(ServiceMethods.Player, (player) -> {
+ System.out.println(player);
+ SaturnPlayer.set(player.toSaturnPlayer());
+ });
+ }
+
+ public static void player(UUID uuid, String name) {
+ try {
+ session.request(ServiceMethods.GetPlayer, uuid.toString())
+ .whenComplete((user, throwable) -> {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ } else if (user != null) {
+ SaturnPlayer.set(user.toSaturnPlayer(uuid, name));
+ }
+ });
+ } catch (Exception e) {
+ SaturnClient.LOGGER.error("Failed to get player", e);
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/ServiceMethods.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/ServiceMethods.java
new file mode 100644
index 0000000..0a09c53
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/ServiceMethods.java
@@ -0,0 +1,113 @@
+package org.saturnclient.saturnclient.client;
+
+import java.util.UUID;
+
+import org.saturnclient.saturnclient.client.ServiceMethods.Types.SendPlayerRequest;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+
+import dev.selimaj.session.types.Method;
+
+public class ServiceMethods {
+
+ // =========================
+ // Types (Rust structs)
+ // =========================
+ public static class Types {
+
+ public record EmoteRequest(String emote, String[] targets) {
+ }
+
+ public record EmoteEvent(String emote, String from) {
+ }
+
+ public record Player(
+ String cloak,
+ String hat,
+ String[] cloaks,
+ String[] hats) {
+ public SaturnPlayer toSaturnPlayer(UUID uuid, String name) {
+ return new SaturnPlayer(uuid, name, cloak, hat);
+ }
+ }
+
+ public record PlayerStream(Player player, String name, String uuid) {
+ public SaturnPlayer toSaturnPlayer() {
+ return new SaturnPlayer(UUID.fromString(uuid), name, this.player.cloak, this.player.hat);
+ }
+ }
+
+ public record SendPlayerRequest(String[] targets) {
+ }
+ }
+
+ // =========================
+ // AUTH
+ // =========================
+ public static final Method Authenticate = new Method<>("auth",
+ String.class,
+ Types.Player.class,
+ String.class);
+
+ // =========================
+ // EQUIP
+ // =========================
+ public static final Method SetCloak = new Method<>("set_cloak",
+ String.class,
+ String.class,
+ String.class);
+
+ public static final Method SetHat = new Method<>("set_hat",
+ String.class,
+ String.class,
+ String.class);
+
+ // =========================
+ // BUY
+ // =========================
+ public static final Method BuyCloak = new Method<>("buy_cloak",
+ String.class,
+ String.class,
+ String.class);
+
+ public static final Method BuyHat = new Method<>("buy_hat",
+ String.class,
+ String.class,
+ String.class);
+
+ // =========================
+ // EMOTE
+ // =========================
+ public static final Method Emote = new Method<>("emote",
+ Types.EmoteRequest.class,
+ Void.class,
+ String.class);
+
+ public static final Method EmoteEvent = new Method<>("emote_event",
+ Types.EmoteEvent.class,
+ Void.class,
+ Void.class);
+
+ // =========================
+ // GET PLAYER
+ // =========================
+ public static final Method GetPlayer = new Method<>("get_player",
+ String.class,
+ Types.Player.class,
+ String.class);
+
+ // =========================
+ // PLAYER (Server Notification)
+ // =========================
+ public static final Method Player = new Method<>("player",
+ Types.PlayerStream.class,
+ Void.class,
+ Void.class);
+
+ // =========================
+ // SEND PLAYER
+ // =========================
+ public static final Method SendPlayer = new Method<>("send_player",
+ SendPlayerRequest.class,
+ Void.class,
+ String.class);
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/PlayerTracker.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/PlayerTracker.java
new file mode 100644
index 0000000..f43347f
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/PlayerTracker.java
@@ -0,0 +1,52 @@
+package org.saturnclient.saturnclient.client.player;
+
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.ServiceClient;
+
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.client.MinecraftClient;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+import com.mojang.authlib.GameProfile;
+
+public class PlayerTracker {
+ private static Set trackedPlayers = new HashSet<>();
+
+ public static void initialize() {
+ // When client joins a server, reset tracked players
+ ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
+ trackedPlayers.clear();
+ client.execute(() -> {
+ checkForNewPlayers(SaturnClient.client);
+ });
+ });
+
+ // Check for new players every tick
+ ClientTickEvents.END_CLIENT_TICK.register(client -> checkForNewPlayers(client));
+ }
+
+ private static void checkForNewPlayers(MinecraftClient client) {
+ if (client.getNetworkHandler() == null)
+ return;
+
+ for (PlayerListEntry player : client.getNetworkHandler().getPlayerList()) {
+ UUID playerUUID = player.getProfile().getId();
+
+ if (!trackedPlayers.contains(playerUUID)) {
+ // A new player has joined
+ onPlayerJoin(client, player);
+ trackedPlayers.add(playerUUID);
+ }
+ }
+ }
+
+ private static void onPlayerJoin(MinecraftClient client, PlayerListEntry player) {
+ GameProfile profile = player.getProfile();
+ ServiceClient.player(profile.getId(), profile.getName());
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/Roles.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/Roles.java
new file mode 100644
index 0000000..219174b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/Roles.java
@@ -0,0 +1,61 @@
+package org.saturnclient.saturnclient.client.player;
+
+import java.util.Map;
+import java.util.UUID;
+
+import org.saturnclient.saturnclient.config.Config;
+
+import net.minecraft.util.Formatting;
+
+public class Roles {
+ static enum Role {
+ OWNER,
+ ADMIN,
+ PARTNER,
+ CONTRIBUTOR
+ }
+
+ private static final Map ROLES = Map.of(
+ UUID.fromString("d362a042-28bd-49e1-a807-ae74dbe8aba9"),
+ Role.OWNER,
+ UUID.fromString("d8dc7023-179b-4460-bcf6-493fbf1289a6"),
+ Role.CONTRIBUTOR);
+
+ /*
+ * Gets the icon color of a specific individual, here are the different colors
+ *
+ * - Owner: Dark Red
+ * - Admin: Red
+ * - Partners: Gold
+ * - Contributor: Aqua
+ * - Other/player: White
+ */
+ public static Formatting getIconColor(UUID uuid) {
+ Role role = ROLES.get(uuid);
+
+ if (role == null) {
+ return Formatting.WHITE;
+ }
+
+ switch (role) {
+ case OWNER:
+ return Formatting.DARK_RED;
+
+ case ADMIN:
+ return Formatting.RED;
+
+ case PARTNER:
+ return Formatting.GOLD;
+
+ case CONTRIBUTOR:
+ return Formatting.AQUA;
+
+ default:
+ return Formatting.WHITE;
+ }
+ }
+
+ public static String getSaturnIndicator() {
+ return Config.realisticLogo.value ? "" : "";
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/SaturnPlayer.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/SaturnPlayer.java
new file mode 100644
index 0000000..f4c935e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/client/player/SaturnPlayer.java
@@ -0,0 +1,54 @@
+package org.saturnclient.saturnclient.client.player;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.jetbrains.annotations.Nullable;
+import org.saturnclient.saturnclient.client.ServiceClient;
+
+public class SaturnPlayer {
+ private static Map PLAYERS = new HashMap<>();
+ private static Map PLAYER_NAMES = new HashMap<>();
+
+ public String cloak = "";
+ public 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;
+ }
+
+ @Nullable
+ public static SaturnPlayer get() {
+ return PLAYERS.get(ServiceClient.uuid);
+ }
+
+ @Nullable
+ public static SaturnPlayer get(UUID uuid) {
+ return PLAYERS.get(uuid);
+ }
+
+ @Nullable
+ public static SaturnPlayer get(String name) {
+ UUID uuid = PLAYER_NAMES.get(name);
+ if (uuid == null)
+ return null;
+ return PLAYERS.get(uuid);
+ }
+
+ public static void set(SaturnPlayer player) {
+ PLAYER_NAMES.put(player.name, player.uuid);
+ PLAYERS.put(player.uuid, player);
+ }
+
+ public static String[] getExternalUUIDAsString() {
+ return PLAYERS.keySet().stream().filter(id -> !id.equals(ServiceClient.uuid)).map(UUID::toString)
+ .toArray(String[]::new);
+ }
+
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/AnimationConfig.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/AnimationConfig.java
new file mode 100644
index 0000000..0c76f73
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/AnimationConfig.java
@@ -0,0 +1,55 @@
+package org.saturnclient.saturnclient.config;
+
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+public class AnimationConfig {
+ public static ConfigManager config;
+
+ // Logo
+ public static final AnimationConfig logo = new AnimationConfig(true, 700, 0);
+
+ // Main menu
+ public static final AnimationConfig mainMenu = new AnimationConfig(true, 700, 120);
+
+ // Mod menu
+ public static final AnimationConfig modMenu = new AnimationConfig(true, 700, 20);
+
+ // Shift menu
+ public static final AnimationConfig shiftMenu = new AnimationConfig(true, 700, 100);
+
+ // Cosmetics menu
+ public static final AnimationConfig cosmeticsMenu = new AnimationConfig(true, 700, 40);
+
+ // Emote wheel
+ public static final AnimationConfig emoteWheel = new AnimationConfig(true, 700, 40);
+
+ public static void init(ConfigManager parent) {
+ config = new ConfigManager(parent, "Animations");
+
+ logo.init("Logo");
+ mainMenu.init("Main Menu");
+ modMenu.init("Mod Menu");
+ shiftMenu.init("Shift Menu");
+ cosmeticsMenu.init("Cosmetics Menu");
+ emoteWheel.init("Emote Wheel");
+ }
+
+ // Animation properties
+ public final Property duration;
+ public final Property stagger;
+ public final Property curve;
+
+ public AnimationConfig(boolean enabled, int delay, int stagger) {
+ this.duration = Property.integer(delay);
+ this.stagger = Property.integer(stagger);
+ this.curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
+ }
+
+ public void init(String name) {
+ // config.property("Enable " + name + " Animations", this.enabled);
+ config.property(name + " Duration", this.duration);
+ config.property(name + " Stagger", this.stagger);
+ config.property(name + " Curve", this.curve);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/Config.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/Config.java
new file mode 100644
index 0000000..109deea
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/Config.java
@@ -0,0 +1,37 @@
+package org.saturnclient.saturnclient.config;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.resources.Textures;
+
+import net.minecraft.util.Identifier;
+
+public class Config {
+ public static ConfigManager config;
+
+ public static Property realisticLogo = Property.bool(false);
+ public static Property saturnTitleScreen = Property.bool(true);
+ public static Property cloakPhysics = Property.bool(true);
+ public static Property openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B);
+ public static Property stagger = Property.bool(true);
+
+ public static Identifier getLogo() {
+ return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
+ }
+
+ public static void init() {
+ config = new ConfigManager("Saturn Client");
+ config.property("Realistic logo", realisticLogo);
+ config.property("Saturn client title screen", saturnTitleScreen);
+ config.property("Open Emote Wheel", openEmoteWheel);
+ config.property("Cloak Physics", cloakPhysics);
+ config.property("Stagger Animations", stagger);
+
+ // Initialize a sub namespace for theme
+ Theme.init(config);
+
+ // Initialize a sub namespace for animations
+ AnimationConfig.init(config);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/Theme.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/Theme.java
new file mode 100644
index 0000000..6da4e1a
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/Theme.java
@@ -0,0 +1,91 @@
+package org.saturnclient.saturnclient.config;
+
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property;
+
+public class Theme {
+ // Main UI background (slightly warm dark to match Saturn tone)
+ public static final Property BACKGROUND = Property.color(0xFF12100D);
+
+ // Default text color
+ public static final Property FOREGROUND = Property.color(0xFFF2E6D2);
+
+ // Widget default background (normal state)
+ public static final Property PRIMARY = Property.color(0xFF1C1915);
+
+ // Widget text color
+ public static final Property PRIMARY_FG = Property.color(0xFFEAD9B8);
+
+ // Saturn brand color (hovered / enabled / selected)
+ public static final Property ACCENT = Property.color(0xFFE2BF7D);
+
+ // Text/icons shown on accent background
+ public static final Property ACCENT_FG = Property.color(0xFF1A140A);
+
+ // Misc colors
+ public static final Property SCROLL = Property.color(0xFFE2BF7D);
+
+ // UI metrics
+ public static final Property BG_RADIUS = Property.integer(12);
+ public static final Property WIDGET_RADIUS = Property.integer(10);
+
+ // Font
+ public static final Property FONT = Property.font(2);
+
+ public static void init(ConfigManager parent) {
+ ConfigManager config = new ConfigManager(parent, "UI Style");
+ config.property("Background", BACKGROUND);
+ config.property("Foreground", FOREGROUND);
+ config.property("Primary", PRIMARY);
+ config.property("Primary Foreground", PRIMARY_FG);
+ config.property("Accent", ACCENT);
+ config.property("Accent Foreground", ACCENT_FG);
+ config.property("Scrollbar Color", SCROLL);
+
+ config.property("Background Radius", BG_RADIUS);
+ config.property("Widget Radius", WIDGET_RADIUS);
+
+ config.property("Font", FONT);
+ }
+
+ /**
+ * primary by default, accent if true
+ */
+ public static int getBg(boolean hover) {
+ return hover ? ACCENT.value : PRIMARY.value;
+ }
+
+ /**
+ * (Foreground) primary by default, accent if true
+ */
+ public static int getFg(boolean hover) {
+ return hover ? ACCENT_FG.value : PRIMARY_FG.value;
+ }
+
+ /**
+ * Multiplies existing alpha with 0–255 alpha.
+ */
+ public static int withAlpha(int alpha, int color) {
+ alpha = Math.max(0, Math.min(255, alpha));
+
+ int originalAlpha = (color >>> 24) & 0xFF;
+
+ // Merge (multiply) alphas
+ int merged = (originalAlpha * alpha) / 255;
+
+ return (color & 0x00FFFFFF) | (merged << 24);
+ }
+
+ /**
+ * Multiplies existing alpha with 0.0–1.0 alpha.
+ */
+ public static int withAlpha(float alpha, int color) {
+ alpha = Math.max(0f, Math.min(1f, alpha));
+
+ int originalAlpha = (color >>> 24) & 0xFF;
+
+ int merged = (int) (originalAlpha * alpha);
+
+ return (color & 0x00FFFFFF) | (merged << 24);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/manager/ConfigManager.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/manager/ConfigManager.java
new file mode 100644
index 0000000..9bdc10d
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/manager/ConfigManager.java
@@ -0,0 +1,181 @@
+package org.saturnclient.saturnclient.config.manager;
+
+import com.google.gson.*;
+import org.saturnclient.saturnclient.SaturnClient;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ConfigManager {
+
+ private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json");
+
+ // SINGLE ROOT TREE
+ private static final Map> ROOT = new LinkedHashMap<>();
+
+ private static JsonObject cachedJson = null;
+
+ private final Map> currentMap;
+
+ private final String[] path;
+
+ /**
+ * Creates a root namespace
+ */
+ public ConfigManager(String namespace) {
+ this.currentMap = new LinkedHashMap<>();
+ this.path = new String[] { namespace }; // Path is just ["namespace"]
+ ROOT.put(namespace, Property.namespace(currentMap));
+ }
+
+ /**
+ * Creates a sub-namespace inside a parent
+ */
+ public ConfigManager(ConfigManager parent, String namespace) {
+ this.currentMap = new LinkedHashMap<>();
+
+ // Build new path: parent path + current namespace
+ this.path = new String[parent.path.length + 1];
+ System.arraycopy(parent.path, 0, this.path, 0, parent.path.length);
+ this.path[this.path.length - 1] = namespace;
+
+ parent.property(namespace, Property.namespace(currentMap));
+ }
+
+ /**
+ * Add a property
+ */
+ public Property property(String name, Property value) {
+ currentMap.put(name, value);
+ loadSingleProperty(name, value);
+ return value;
+ }
+
+ /**
+ * Add empty namespace
+ */
+ public ConfigManager sub(String name) {
+ return new ConfigManager(this, name);
+ }
+
+ /**
+ * Load entire config
+ */
+ public static void load() {
+ SaturnClient.LOGGER.info("Loading config...");
+
+ JsonObject json = loadAndCache();
+ if (json == null)
+ return;
+
+ loadRecursive(json, ROOT);
+ }
+
+ /**
+ * Save entire config
+ */
+ public static void save() {
+ SaturnClient.LOGGER.info("Saving config...");
+
+ JsonObject rootJson = new JsonObject();
+ saveRecursive(rootJson, ROOT);
+
+ try {
+ String formatted = new GsonBuilder()
+ .setPrettyPrinting()
+ .create()
+ .toJson(rootJson);
+
+ Files.writeString(configFile.toPath(), formatted);
+ SaturnClient.LOGGER.info("Config saved successfully.");
+ } catch (IOException e) {
+ SaturnClient.LOGGER.error("Error saving config file", e);
+ }
+ }
+
+ /**
+ * Recursive save
+ */
+ private static void saveRecursive(JsonObject json, Map> map) {
+ map.forEach((name, prop) -> {
+ if (prop.isNamespace()) {
+ JsonObject child = new JsonObject();
+ saveRecursive(child, prop.getNamespaceMap());
+ json.add(name, child);
+ } else {
+ json.add(name, prop.toJson());
+ }
+ });
+ }
+
+ /**
+ * Recursive load
+ */
+ private static void loadRecursive(JsonObject json, Map> map) {
+ map.forEach((name, prop) -> {
+ JsonElement element = json.get(name);
+ if (element == null)
+ return;
+
+ if (prop.isNamespace() && element.isJsonObject()) {
+ loadRecursive(element.getAsJsonObject(), prop.getNamespaceMap());
+ } else {
+ prop.loadFromJson(element);
+ }
+ });
+ }
+
+ /**
+ * Load only a single property immediately after creation,
+ * traversing the JSON tree based on this manager's path.
+ */
+ private void loadSingleProperty(String name, Property> prop) {
+ JsonObject source = cachedJson != null ? cachedJson : loadAndCache();
+ if (source == null)
+ return;
+
+ // Traverse the tree to the correct namespace
+ JsonObject currentScope = source;
+ for (String segment : path) {
+ JsonElement next = currentScope.get(segment);
+ if (next != null && next.isJsonObject()) {
+ currentScope = next.getAsJsonObject();
+ } else {
+ return; // Path doesn't exist in config yet
+ }
+ }
+
+ // Now look for the property inside the correctly nested object
+ JsonElement element = currentScope.get(name);
+ if (element != null) {
+ prop.loadFromJson(element);
+ }
+ }
+
+ /**
+ * Reads file and caches json
+ */
+ private static JsonObject loadAndCache() {
+ try {
+ if (!configFile.exists()) {
+ Files.writeString(configFile.toPath(), "{}");
+ return new JsonObject();
+ }
+
+ String json = Files.readString(configFile.toPath());
+ cachedJson = JsonParser.parseString(json).getAsJsonObject();
+ return cachedJson;
+
+ } catch (IOException e) {
+ SaturnClient.LOGGER.error("Error reading config file", e);
+ return null;
+ }
+ }
+
+ public Map> getProperties() {
+ return currentMap;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/manager/Property.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/manager/Property.java
new file mode 100644
index 0000000..45ef2b5
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/config/manager/Property.java
@@ -0,0 +1,298 @@
+package org.saturnclient.saturnclient.config.manager;
+
+import java.util.Map;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.saturnclient.SaturnClient;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+public class Property {
+ public static class NamedProperty {
+ public String name;
+ public Property prop;
+
+ public NamedProperty(String name, Property value) {
+ this.name = name;
+ this.prop = value;
+ }
+ }
+
+ public enum PropertyType {
+ BOOLEAN,
+ INTEGER,
+ FLOAT,
+ STRING,
+ HEX,
+ NAMESPACE,
+ SELECT,
+ KEYBINDING,
+ }
+
+ public T value;
+ public boolean isReset = false;
+ private T defaultValue;
+ private String[] availableValues;
+ private final PropertyType type;
+ private boolean wasPressedLastTick = false;
+
+ private Property(T value, PropertyType type) {
+ this.value = value;
+ this.defaultValue = value;
+ this.type = type;
+ }
+
+ // ---------- Factory Methods ----------
+ public static Property from(T value) {
+ if (value instanceof Boolean)
+ return new Property<>(value, PropertyType.BOOLEAN);
+ if (value instanceof Integer)
+ return new Property<>(value, PropertyType.INTEGER);
+ if (value instanceof Float)
+ return new Property<>(value, PropertyType.FLOAT);
+ if (value instanceof String)
+ return new Property<>(value, PropertyType.STRING);
+ if (valueIsNamespace(value))
+ return new Property<>(value, PropertyType.NAMESPACE);
+ return null;
+ }
+
+ public static Property font(int value) {
+ return select(value, "Default", "Inter", "Inter bold");
+ }
+
+ public static Property select(Integer value, String... availableValues) {
+ Property property = new Property<>(value, PropertyType.SELECT);
+ property.availableValues = availableValues;
+ return property;
+ }
+
+ public static Property>> namespace(Map> value) {
+ return new Property<>(value, PropertyType.NAMESPACE);
+ }
+
+ public static Property bool(boolean value) {
+ return new Property<>(value, PropertyType.BOOLEAN);
+ }
+
+ public static Property integer(int value) {
+ return new Property<>(value, PropertyType.INTEGER);
+ }
+
+ public static Property floatProp(float value) {
+ return new Property<>(value, PropertyType.FLOAT);
+ }
+
+ public static Property string(String value) {
+ return new Property<>(value, PropertyType.STRING);
+ }
+
+ public static Property color(int value) {
+ return new Property<>(value, PropertyType.HEX);
+ }
+
+ public static Property keybinding(int value) {
+ return new Property<>(value, PropertyType.KEYBINDING);
+ }
+
+ // ---------- Select Helpers ----------
+ public void next() {
+ if (type == PropertyType.SELECT) {
+ int i = (Integer) value;
+ setValue((i < availableValues.length - 1) ? i + 1 : 0);
+ }
+ }
+
+ public void prev() {
+ if (type == PropertyType.SELECT) {
+ int i = (Integer) value;
+ setValue((i > 0) ? i - 1 : availableValues.length - 1);
+ }
+ }
+
+ public void setSelection(int selection) {
+ if (type == PropertyType.SELECT && selection >= 0 && selection < availableValues.length) {
+ setValue(selection);
+ }
+ }
+
+ public String getSelection() {
+ return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null;
+ }
+
+ // ---------- Lifecycle ----------
+ public void reset() {
+ value = defaultValue;
+ isReset = true;
+ }
+
+ public PropertyType getType() {
+ return type;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map> getNamespaceValue() {
+ if (type == PropertyType.NAMESPACE && value instanceof Map) {
+ return (Map>) value;
+ }
+ throw new IllegalStateException("Property is not a namespace");
+ }
+
+ // ---------- JSON Serialization ----------
+ /*
+ * BOOLEAN,
+ * INTEGER,
+ * FLOAT,
+ * STRING,
+ * HEX,
+ * NAMESPACE,
+ * SELECT,
+ * KEYBINDING,
+ */
+ public JsonElement toJson() {
+ switch (type) {
+ case BOOLEAN:
+ return new JsonPrimitive((Boolean) value);
+ case INTEGER:
+ return new JsonPrimitive((Integer) value);
+ case FLOAT:
+ return new JsonPrimitive((Float) value);
+ case NAMESPACE:
+ JsonObject nested = new JsonObject();
+ getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson()));
+ return nested;
+ case HEX:
+ return new JsonPrimitive(String.format("#%08X", (Integer) value));
+ case KEYBINDING:
+ return new JsonPrimitive((Integer) value);
+ case SELECT:
+ return new JsonPrimitive((Integer) value);
+ default:
+ return new JsonPrimitive(String.valueOf(value));
+ }
+ }
+
+ public void loadFromJson(JsonElement element) {
+ if (element == null)
+ return;
+
+ switch (type) {
+ case BOOLEAN:
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean())
+ setValue(element.getAsBoolean());
+ break;
+ case INTEGER:
+ if (element.isJsonPrimitive()) {
+ JsonPrimitive p = element.getAsJsonPrimitive();
+ if (p.isNumber())
+ setValue(p.getAsInt());
+ else if (p.isString())
+ setValue(parseHexToInt(p.getAsString()));
+ }
+ break;
+ case FLOAT:
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
+ setValue(element.getAsFloat());
+ break;
+ case STRING:
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
+ setValue(element.getAsString());
+ break;
+ case HEX:
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
+ setValue(parseHexToInt(element.getAsString()));
+ break;
+ case NAMESPACE:
+ if (element.isJsonObject()) {
+ JsonObject obj = element.getAsJsonObject();
+ getNamespaceValue().forEach((k, v) -> v.loadFromJson(obj.get(k)));
+ }
+ break;
+ case SELECT:
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
+ setSelection(element.getAsInt());
+ break;
+ case KEYBINDING:
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
+ setValue(element.getAsInt());
+ break;
+ }
+ }
+
+ // ---------- Utility ----------
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ public static boolean valueIsNamespace(Object obj) {
+ return obj instanceof Map;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void setValue(Object value) {
+ this.value = (T) value;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void setValue(Object value, Object defaultValue) {
+ this.value = (T) value;
+ this.defaultValue = (T) defaultValue;
+ }
+
+ public NamedProperty named(String name) {
+ return new NamedProperty<>(name, this);
+ }
+
+ // ---------- Keybindings ----------
+ public boolean isKeyPressed() {
+ return (Integer) value != -1 &&
+ GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS &&
+ SaturnClient.client.currentScreen == null;
+ }
+
+ public boolean wasKeyPressed() {
+ boolean pressed = isKeyPressed();
+ boolean result = pressed && !wasPressedLastTick;
+ wasPressedLastTick = pressed;
+ return result;
+ }
+
+ // ---------- HEX ----------
+ public static int parseHexToInt(String hex) {
+ hex = hex.replace("#", "");
+ if (hex.length() == 6)
+ hex = "FF" + hex;
+ if (hex.length() != 8)
+ throw new IllegalArgumentException("Hex must be 6 or 8 chars long, got \'" + hex + "\'");
+ return (int) Long.parseLong(hex, 16);
+ }
+
+ // Misc
+ public Property copy() {
+ return new Property<>(this.value, this.type);
+ }
+
+ public void load(String name, JsonElement element) {
+ if (element != null && element.isJsonObject()) {
+ load(name, element.getAsJsonObject());
+ }
+ }
+
+ public void load(String name, JsonObject element) {
+ JsonElement value = element.get(name);
+ if (value != null)
+ this.loadFromJson(value);
+ }
+
+ public boolean isNamespace() {
+ return type == PropertyType.NAMESPACE;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map> getNamespaceMap() {
+ return isNamespace() ? (Map>) (Object) value : null;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/event/KeyInputHandler.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/event/KeyInputHandler.java
new file mode 100644
index 0000000..2a3e970
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/event/KeyInputHandler.java
@@ -0,0 +1,55 @@
+package org.saturnclient.saturnclient.event;
+
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
+import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
+import net.minecraft.client.option.KeyBinding;
+import net.minecraft.client.util.InputUtil;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.ui2.screens.EmoteWheel;
+import org.saturnclient.ui2.screens.ShiftMenu;
+
+import dev.kosmx.playerAnim.api.layered.AnimationStack;
+import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
+
+import org.lwjgl.glfw.GLFW;
+
+/**
+ * Handles keyboard input events for the Saturn Client.
+ * Manages keybindings and their associated actions.
+ */
+public class KeyInputHandler {
+ private static final String KEY_CATEGORY = "key.category.saturnclient";
+ private static final String MAIN_MENU_KEY_ID = "key.saturnclient.main_menu";
+ private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT;
+ private static KeyBinding mainMenuKeyBinding;
+
+ public static void register() {
+ mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ MAIN_MENU_KEY_ID,
+ InputUtil.Type.KEYSYM,
+ DEFAULT_MENU_KEY,
+ KEY_CATEGORY));
+
+ // Register the event handler for the main menu key
+ ClientTickEvents.END_CLIENT_TICK.register(client -> {
+ if (mainMenuKeyBinding.wasPressed()) {
+ client.setScreen(new ShiftMenu());
+ }
+
+ if (Config.openEmoteWheel.wasKeyPressed() && client.currentScreen == null && !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) {
+ client.setScreen(new EmoteWheel());
+ }
+
+ if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
+ AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
+ if (animationStack.isActive() && animationStack.getPriority() == 1000) {
+ animationStack.removeLayer(1000);
+ ServiceClient.emote("");
+ }
+ }
+ });
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java
new file mode 100644
index 0000000..5e8e214
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java
@@ -0,0 +1,140 @@
+package org.saturnclient.saturnclient.menus;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.Text;
+
+import org.saturnclient.modules.HudMod;
+import org.saturnclient.modules.ModDimensions;
+import org.saturnclient.modules.ModManager;
+import org.saturnclient.modules.Module;
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.SaturnScreen;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HudEditor extends Screen {
+ private final List hudMods = new ArrayList<>();
+ private HudMod draggingMod = null;
+ private boolean resizing = false;
+ private int offsetX, offsetY;
+ private static final int RESIZE_MARGIN = 3;
+
+ public HudEditor() {
+ super(Text.literal("HUD Editor"));
+ for (Module m : ModManager.ENABLED_MODS) {
+ if (m.isEnabled() && m instanceof HudMod) {
+ hudMods.add((HudMod) m);
+ }
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (button == 0) {
+ for (int m = hudMods.size() - 1; m >= 0; m--) {
+ HudMod mod = hudMods.get(m);
+ ModDimensions dim = mod.getDimensions();
+
+ // Check if clicking the corner for resizing
+ if (isCorner(mouseX, mouseY, dim)) {
+ draggingMod = mod;
+ resizing = true;
+ offsetX = (int) (mouseX - (dim.x.value + dim.width * dim.scale.value));
+ offsetY = (int) (mouseY - (dim.y.value + dim.height * dim.scale.value));
+ return true;
+ }
+
+ // Check if clicking inside the mod
+ if (isInside(mouseX, mouseY, dim)) {
+ draggingMod = mod;
+ resizing = false;
+ offsetX = (int) (mouseX - dim.x.value);
+ offsetY = (int) (mouseY - dim.y.value);
+ return true;
+ }
+ }
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ if (button == 0) {
+ draggingMod = null;
+ resizing = false;
+ }
+ return super.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ if (draggingMod != null) {
+ ModDimensions dim = draggingMod.getDimensions();
+
+ if (resizing) {
+ // Scale based on cursor movement
+ float newScale = Math.max(0.5f, ((float) (mouseX - dim.x.value) / dim.width));
+ dim.scale.value = newScale;
+ } else {
+ // Move the mod
+ dim.x.value = (int) mouseX - offsetX;
+ dim.y.value = (int) mouseY - offsetY;
+ }
+ }
+ return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (client.world == null && client.getCurrentServerEntry() == null) {
+ SaturnScreen.ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, 1.0F, delta);
+ }
+
+ RenderScope scope = new RenderScope(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers());
+
+ for (HudMod mod : hudMods) {
+ ModDimensions dim = mod.getDimensions();
+ MatrixStack matrices = context.getMatrices();
+ matrices.push();
+
+ matrices.translate(dim.x.value, dim.y.value, 0);
+ matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
+
+ // Render the mod
+ mod.renderDummy(scope);
+
+ matrices.pop();
+
+ // Draw border around mod
+ int borderX = dim.x.value;
+ int borderY = dim.y.value;
+ int borderW = (int) (dim.width * dim.scale.value);
+ int borderH = (int) (dim.height * dim.scale.value);
+
+ context.drawBorder(borderX, borderY, borderW, borderH, 0xFFFFFFFF); // White border
+ }
+ }
+
+ private boolean isInside(double mouseX, double mouseY, ModDimensions dim) {
+ return mouseX >= dim.x.value && mouseX <= dim.x.value + (dim.width * dim.scale.value) &&
+ mouseY >= dim.y.value && mouseY <= dim.y.value + (dim.height * dim.scale.value);
+ }
+
+ private boolean isCorner(double mouseX, double mouseY, ModDimensions dim) {
+ float rightX = dim.x.value + dim.width * dim.scale.value;
+ float bottomY = dim.y.value + dim.height * dim.scale.value;
+
+ return Math.abs(mouseX - rightX) < RESIZE_MARGIN && Math.abs(mouseY - bottomY) < RESIZE_MARGIN;
+ }
+
+ @Override
+ public void close() {
+ ConfigManager.save();
+ super.close();
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/DrawContextAccessor.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/DrawContextAccessor.java
new file mode 100644
index 0000000..f049e40
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/DrawContextAccessor.java
@@ -0,0 +1,12 @@
+package org.saturnclient.saturnclient.mixin;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.render.VertexConsumerProvider;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(DrawContext.class)
+public interface DrawContextAccessor {
+ @Accessor("vertexConsumers")
+ VertexConsumerProvider.Immediate getVertexConsumers();
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/NameTagMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/NameTagMixin.java
new file mode 100644
index 0000000..6cb2d0b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/NameTagMixin.java
@@ -0,0 +1,93 @@
+package org.saturnclient.saturnclient.mixin;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.font.TextRenderer.TextLayerType;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.EntityRenderDispatcher;
+import net.minecraft.client.render.entity.EntityRenderer;
+import net.minecraft.client.render.entity.state.EntityRenderState;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.Vec3d;
+
+import org.saturnclient.saturnclient.client.player.Roles;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+
+import java.util.UUID;
+
+import org.joml.Matrix4f;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(value = EntityRenderer.class, priority = 1800)
+public abstract class NameTagMixin {
+ @Shadow
+ public EntityRenderDispatcher dispatcher;
+
+ /**
+ * @author HexLeo
+ * @reason Adds the Saturn Client icon to the player's name if they are online
+ * with Saturn Client
+ */
+ @Inject(method = "renderLabelIfPresent(Lnet/minecraft/client/render/entity/state/EntityRenderState;Lnet/minecraft/text/Text;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At("HEAD"), cancellable = true)
+ private void onRenderLabelIfPresent(S state, Text text, MatrixStack matrices,
+ VertexConsumerProvider vertexConsumers, int light, CallbackInfo ci) {
+
+ UUID uuid = isSaturn(state);
+
+ if (uuid != null) {
+ MutableText iconText = Text.literal(Roles.getSaturnIndicator())
+ .styled(style -> style.withColor(Roles.getIconColor(uuid)));
+
+ Text nameText = text.copy().styled(style -> style.withColor(Formatting.WHITE));
+
+ renderLabel(state, iconText.append(nameText), matrices,
+ vertexConsumers, light);
+
+ ci.cancel();
+ }
+ }
+
+ private UUID isSaturn(S state) {
+ if (state instanceof PlayerEntityRenderState) {
+ String name = ((PlayerEntityRenderState) state).name;
+ SaturnPlayer player = SaturnPlayer.get(name);
+ if (player != null) {
+ return player.uuid;
+ }
+ }
+
+ return null;
+ }
+
+ private void renderLabel(S state, Text text, MatrixStack matrices,
+ VertexConsumerProvider vertexConsumers, int light) {
+ Vec3d vec3d = state.nameLabelPos;
+ if (vec3d != null) {
+ boolean bl = !state.sneaking;
+ int i = "deadmau5".equals(text.getString()) ? -10 : 0;
+ matrices.push();
+ matrices.translate(vec3d.x, vec3d.y + 0.5, vec3d.z);
+ matrices.multiply(this.dispatcher.getRotation());
+ matrices.scale(0.025F, -0.025F, 0.025F);
+ Matrix4f matrix4f = matrices.peek().getPositionMatrix();
+ TextRenderer textRenderer = SaturnClient.client.textRenderer;
+ float f = (float) (-textRenderer.getWidth(text)) / 2.0F;
+ int j = (int) (SaturnClient.client.options.getTextBackgroundOpacity(0.25F) * 255.0F) << 24;
+ textRenderer.draw(text, f, (float) i, -2130706433, false, matrix4f, vertexConsumers,
+ bl ? TextLayerType.SEE_THROUGH : TextLayerType.NORMAL, j, light);
+ if (bl) {
+ textRenderer.draw(text, f, (float) i, -1, false, matrix4f, vertexConsumers, TextLayerType.NORMAL, 0,
+ light);
+ }
+ matrices.pop();
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java
new file mode 100644
index 0000000..5a9228e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java
@@ -0,0 +1,30 @@
+package org.saturnclient.saturnclient.mixin;
+
+import net.minecraft.client.render.entity.EntityRendererFactory;
+import net.minecraft.client.render.entity.LivingEntityRenderer;
+import net.minecraft.client.render.entity.PlayerEntityRenderer;
+import net.minecraft.client.render.entity.model.PlayerEntityModel;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import net.minecraft.entity.player.PlayerEntity;
+
+import org.saturnclient.cosmetics.cloak.CloakFeatureRenderer;
+import org.saturnclient.cosmetics.hat.HatFeatureRenderer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(PlayerEntityRenderer.class)
+public abstract class PlayerEntityRendererMixin
+ extends LivingEntityRenderer {
+
+ protected PlayerEntityRendererMixin() {
+ super(null, null, 0.0f);
+ }
+
+ @Inject(method = "", at = @At("TAIL"))
+ private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) {
+ this.addFeature(new HatFeatureRenderer(this));
+ this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader()));
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/SetScreenMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/SetScreenMixin.java
new file mode 100644
index 0000000..e549083
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/SetScreenMixin.java
@@ -0,0 +1,26 @@
+package org.saturnclient.saturnclient.mixin;
+
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.ui2.screens.TitleMenu;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.TitleScreen;
+
+@Mixin(MinecraftClient.class)
+public class SetScreenMixin {
+ @ModifyVariable(method = "setScreen", at = @At("HEAD"), argsOnly = true)
+ private Screen replaceTitleScreen(Screen screen) {
+ if (Config.saturnTitleScreen.value) {
+ if (screen instanceof TitleScreen)
+ return new TitleMenu();
+ if (screen == null && ((MinecraftClient) (Object) this).world == null)
+ return new TitleMenu();
+ }
+
+ return screen;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/SplashOverlayMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/SplashOverlayMixin.java
new file mode 100644
index 0000000..912fa5c
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/SplashOverlayMixin.java
@@ -0,0 +1,160 @@
+package org.saturnclient.saturnclient.mixin;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Textures;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Overwrite;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Final;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.SplashOverlay;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.resource.ResourceReload;
+import net.minecraft.util.Util;
+import net.minecraft.util.math.MathHelper;
+
+@Mixin(SplashOverlay.class)
+public abstract class SplashOverlayMixin {
+
+ @Shadow
+ private MinecraftClient client;
+ @Shadow
+ private ResourceReload reload;
+
+ @Shadow
+ private long reloadCompleteTime;
+
+ @Shadow
+ private long reloadStartTime;
+
+ @Shadow @Final
+ private boolean reloading;
+
+ @Shadow @Final
+ private Consumer> exceptionHandler;
+
+ @Shadow
+ private float progress;
+
+ /**
+ * @author SaturnClient
+ * @reason Replace vanilla splash rendering completely but keep exit logic
+ */
+ @Overwrite
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ int screenWidth = context.getScaledWindowWidth();
+ int screenHeight = context.getScaledWindowHeight();
+ long currentTime = Util.getMeasuringTimeMs();
+
+ // Initialize reload start time if not set
+ if (reloadStartTime == -1L) {
+ reloadStartTime = currentTime;
+ }
+
+ // Compute fade-out progress after reload completes
+ float fadeOutProgress = reloadCompleteTime > -1L
+ ? (float) (currentTime - reloadCompleteTime) / 1000f
+ : -1f;
+
+ int alpha = fadeOutProgress >= 0f
+ ? MathHelper.ceil((1.0f - MathHelper.clamp(fadeOutProgress - 1f, 0f, 1f)) * 255f)
+ : 255;
+
+ // Compute fade-in progress while reloading
+ float reloadProgress = reloadStartTime > -1L
+ ? (float) (currentTime - reloadStartTime) / 500f
+ : -1f;
+
+ // Get render scope for better UI
+ RenderScope scope = new RenderScope(context);
+ scope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ // Draw background overlay
+ if (fadeOutProgress >= 1.0F) {
+ renderCurrentScreen(context, mouseX, mouseY, delta);
+ int overlayAlpha = computeOverlayAlpha(fadeOutProgress - 1f);
+ scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
+ Theme.withAlpha(overlayAlpha, 0xFFFFFFFF));
+ } else if (reloading) {
+ if (reloadProgress < 1.0F) {
+ renderCurrentScreen(context, mouseX, mouseY, delta);
+ }
+ scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
+ Theme.withAlpha(alpha, 0xFFFFFFFF));
+ } else {
+ scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight,
+ Theme.withAlpha(alpha, 0xFFFFFFFF));
+ }
+
+ float u = this.reload.getProgress();
+ this.progress = MathHelper.clamp(this.progress * 0.95F + u * 0.050000012F, 0.0F, 1.0F);
+
+ // Draw central logo
+ drawLogo(scope, screenWidth, screenHeight, alpha);
+ renderProgressBar(scope, context, screenWidth, screenHeight, alpha);
+
+ // Remove overlay after fade-out completes
+ if (fadeOutProgress >= 2.0F) {
+ client.setOverlay(null);
+ }
+
+ // Handle reload completion and exceptions
+ handleReloadCompletion(context);
+ }
+
+ private void renderCurrentScreen(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (client.currentScreen != null) {
+ client.currentScreen.render(context, mouseX, mouseY, delta);
+ }
+ }
+
+ /** Draw the main logo centered */
+ private void drawLogo(RenderScope scope, int screenWidth, int screenHeight, int alpha) {
+ scope.drawTexture(Textures.LOGO, (screenWidth - 60) / 2, ((screenHeight - 60) / 2) - 20, 0, 0, 60, 60,
+ Theme.withAlpha(alpha, 0xFFFFFFFF));
+ }
+
+ private void renderProgressBar(RenderScope scope, DrawContext context, int screenWidth, int screenHeight,
+ int alpha) {
+ int progressY = ((screenHeight - 5) / 2) + 25;
+ context.drawBorder((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
+
+ int progressWidth = Math.min((int) (progress * 120), 120);
+ int progressX = (screenWidth - 120) / 2;
+
+ context.fill(progressX, progressY, progressX + progressWidth, progressY + 5,
+ Theme.withAlpha(alpha, 0xFFFFFFFF));
+ }
+
+ /** Compute overlay alpha from fade progress */
+ private int computeOverlayAlpha(float progress) {
+ return MathHelper.ceil((1.0F - MathHelper.clamp(progress, 0.0F, 1.0F)) * 255.0F);
+ }
+
+ /** Handle reload completion logic */
+ private void handleReloadCompletion(DrawContext context) {
+ float reloadProgress = reloadStartTime > -1L ? (float) (Util.getMeasuringTimeMs() - reloadStartTime) / 500f
+ : -1f;
+
+ if (reloadCompleteTime == -1L && reload.isComplete() && (!reloading || reloadProgress >= 2.0F)) {
+ try {
+ reload.throwException();
+ exceptionHandler.accept(Optional.empty());
+ } catch (Throwable ex) {
+ exceptionHandler.accept(Optional.of(ex));
+ }
+
+ reloadCompleteTime = Util.getMeasuringTimeMs();
+
+ if (client.currentScreen != null) {
+ client.currentScreen.init(client, context.getScaledWindowWidth(), context.getScaledWindowHeight());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/TabListMixin.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/TabListMixin.java
new file mode 100644
index 0000000..e830a3e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/TabListMixin.java
@@ -0,0 +1,43 @@
+package org.saturnclient.saturnclient.mixin;
+
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.UUID;
+
+import org.jetbrains.annotations.Nullable;
+import org.saturnclient.saturnclient.client.player.Roles;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Overwrite;
+import org.spongepowered.asm.mixin.Shadow;
+
+import com.mojang.authlib.GameProfile;
+
+@Mixin(PlayerListEntry.class)
+public abstract class TabListMixin {
+
+ @Shadow
+ private GameProfile profile;
+
+ /**
+ * @author HexLeo
+ * @reason Adds the Saturn Client icon to the player's name if they are online
+ * with Saturn Client
+ */
+ @Overwrite
+ @Nullable
+ public Text getDisplayName() {
+ GameProfile profile = ((PlayerListEntry) (Object) this).getProfile();
+ String name = profile.getName();
+ UUID uuid = profile.getId();
+
+ if (SaturnPlayer.get(uuid) != null) {
+ return Text.literal(Roles.getSaturnIndicator())
+ .styled(style -> style.withColor(Roles.getIconColor(uuid)))
+ .append(Text.literal(name).styled(style -> style.withColor(Formatting.WHITE)));
+ }
+ return Text.literal(name);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/WhoAmI.java b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/WhoAmI.java
new file mode 100644
index 0000000..041e0cf
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/saturnclient/mixin/WhoAmI.java
@@ -0,0 +1,32 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+/*
+ * Original code from FokshaWasTaken (GitHub),
+ * Repository: https://github.com/FokshaWasTaken/who-am-i/
+*/
+
+package org.saturnclient.saturnclient.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import net.minecraft.client.render.entity.LivingEntityRenderer;
+import net.minecraft.entity.Entity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(LivingEntityRenderer.class)
+public class WhoAmI {
+ @ModifyExpressionValue(method = "hasLabel(Lnet/minecraft/entity/LivingEntity;D)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getCameraEntity()Lnet/minecraft/entity/Entity;"))
+ public Entity obfuscateCameraEntity(Entity original) {
+ // hasLabel checks if the rendered entity is equal to the camera entity (aka our
+ // player),
+ // so by setting the camera entity to null in this instance makes the check
+ // always succeed
+ // this also allows to keep the other checks in mojang's code, such as checking
+ // if the hud is enabled etc.
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/Element.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/Element.java
new file mode 100644
index 0000000..49e8a16
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/Element.java
@@ -0,0 +1,89 @@
+package org.saturnclient.ui2;
+
+import org.saturnclient.ui2.anim.Animation;
+
+public class Element {
+ public int x;
+ public int y;
+ public int width;
+ public int height;
+ public float opacity = 1.0f;
+ public boolean focused;
+ public Animation animation;
+ public float scale = 1.0f;
+ public Integer duration = null; // Element duration, will fade out if not null
+
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ }
+
+ public void click(int mouseX, int mouseY) {
+ }
+
+ public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) {
+ }
+
+ public void keyPressed(int keyCode, int scanCode, int modifiers) {
+ }
+
+ public void charTyped(char typedChar) {
+ }
+
+ public void mouseClicked(double mouseX, double mouseY, int button) {
+ }
+
+ public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ }
+
+ public void mouseReleased(double mouseX, double mouseY, int button) {
+ }
+
+ public void playAnimationFrame(long elapsed) {
+ if (this.animation != null && elapsed >= this.animation.delay) {
+ this.animation.tick(this.animation.curve
+ .apply(Math.min(1.0, (double) (elapsed - this.animation.delay) / this.animation.duration)), this);
+ }
+ }
+
+ public Element dimensions(int width, int height) {
+ this.width = width;
+ this.height = height;
+
+ return this;
+ }
+
+ public Element position(int x, int y) {
+ this.x = x;
+ this.y = y;
+
+ return this;
+ }
+
+ public Element centerHorizontal(int w, int h, int offsetX, int offsetY) {
+ this.x = (w - (int) (width * scale)) / 2 + offsetX;
+ this.y = offsetY;
+
+ return this;
+ }
+
+ public Element center(int w, int h) {
+ this.centerOffset(w, h, 0, 0);
+ return this;
+ }
+
+ public Element centerOffset(int w, int h, int offsetX, int offsetY) {
+ this.x = (w - (int) (width * scale)) / 2 + offsetX;
+ this.y = (h - (int) (height * scale)) / 2 + offsetY;
+
+ return this;
+ }
+
+ public Element animation(Animation animation) {
+ this.animation = animation;
+ return this;
+ }
+
+ public Element scale(float scale) {
+ this.scale = scale;
+ return this;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/ElementContext.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/ElementContext.java
new file mode 100644
index 0000000..def84dd
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/ElementContext.java
@@ -0,0 +1,25 @@
+package org.saturnclient.ui2;
+
+public class ElementContext {
+ public int mouseX, mouseY;
+ public int elementWidth, elementHeight;
+ public float elementScale;
+ public long elapsed;
+
+ public ElementContext(long elapsed, int mouseX, int mouseY, Element e) {
+ this.mouseX = mouseX - e.x;
+ this.mouseY = mouseY - e.y;
+ this.elementWidth = e.width;
+ this.elementHeight = e.height;
+ this.elementScale = e.scale;
+ this.elapsed = elapsed;
+ }
+
+ public boolean isHovering() {
+ return Utils.isHovering(mouseX, mouseY, elementWidth, elementHeight, elementScale);
+ }
+
+ public boolean isHovering(int x, int y, int width, int height) {
+ return Utils.isHovering(mouseX - x, mouseY - y, width, height, elementScale);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/RenderScope.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/RenderScope.java
new file mode 100644
index 0000000..1f846e6
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/RenderScope.java
@@ -0,0 +1,450 @@
+package org.saturnclient.ui2;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.jetbrains.annotations.Nullable;
+import org.joml.Matrix4f;
+import org.saturnclient.ui2.resources.Fonts;
+import org.saturnclient.ui2.resources.SvgTexture;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.font.TextRenderer.TextLayerType;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.ScreenRect;
+import net.minecraft.client.render.DiffuseLighting;
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.item.ItemRenderState;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.Window;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.ModelTransformationMode;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.crash.CrashException;
+import net.minecraft.util.crash.CrashReport;
+import net.minecraft.util.crash.CrashReportSection;
+import net.minecraft.util.math.RotationAxis;
+import net.minecraft.world.World;
+
+public class RenderScope {
+ public MatrixStack matrices;
+ public VertexConsumerProvider.Immediate vertexConsumers;
+ // private Function renderLayers;
+ private ScissorStack scissorStack = new ScissorStack();
+ private int opacity = 255 << 24;
+ private final ItemRenderState itemRenderState;
+
+ public RenderScope(DrawContext context) {
+ this(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers());
+ }
+
+ public RenderScope(MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers) {
+ this.matrices = matrices;
+ this.vertexConsumers = vertexConsumers;
+ this.scissorStack = new ScissorStack();
+ this.itemRenderState = new ItemRenderState();
+ }
+
+ public void setOpacity(float alpha) {
+ this.opacity = (int) (alpha * 255) << 24;
+ }
+
+ public int getColor(int color) {
+ int originalAlpha = (color >>> 24) & 0xFF;
+ int newAlpha = (opacity >>> 24) & 0xFF;
+ int mixedAlpha = (originalAlpha * newAlpha) / 255;
+ return (mixedAlpha << 24) | (color & 0x00FFFFFF);
+ }
+
+ public void setRenderLayer(Function renderLayers) {
+ // this.renderLayers = renderLayers;
+ }
+
+ public void drawRect(int x, int y, int width, int height, int color) {
+ if (color == 0)
+ return;
+ color = getColor(color);
+ Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
+
+ int x1 = x;
+ int x2 = x + width;
+ int y1 = y;
+ int y2 = y + height;
+
+ int i;
+ if (x1 < x2) {
+ i = x1;
+ x1 = x2;
+ x2 = i;
+ }
+
+ if (y1 < y2) {
+ i = y1;
+ y1 = y2;
+ y2 = i;
+ }
+
+ VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(RenderLayer.getGui());
+ vertexConsumer.vertex(matrix4f, (float) x1, (float) y1, 0).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x1, (float) y2, 0).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x2, (float) y2, 0).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, 0).color(color);
+ }
+
+ public void drawText(String text, int x, int y, int font, int color) {
+ drawText(1.0f, text, x, y, font, color);
+ }
+
+ public void drawText(float scale, String text, int x, int y, int font, int color) {
+ if (color == 0)
+ return;
+ if (font == 0) {
+ scale *= 2;
+ }
+ int i = 0;
+ for (String line : text.split("\n")) {
+ color = getColor(color);
+ matrices.push();
+ matrices.translate(x, y + (i * Fonts.getHeight()), 0);
+ matrices.scale(scale, scale, 1.0f);
+ TextRenderer textRenderer = SaturnClient.client.textRenderer;
+ textRenderer.draw(Fonts.setFont(line, Fonts.getFont(font)), 0, font == 0 ? 1 : 7, color, false,
+ this.matrices.peek().getPositionMatrix(),
+ this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
+ matrices.pop();
+ i++;
+ }
+ }
+
+ private void drawRoundedCorner(int width, int height, int radius, int color) {
+ int w = width * 20;
+ int h = height * 20;
+
+ for (int y = 0; y < h; y++) {
+ int startX = 0;
+
+ if (y < radius) {
+ double dy = radius - y - 0.5;
+ double dx = Math.sqrt(Math.max(0, radius * radius - dy * dy));
+ startX = radius - (int) dx;
+ }
+
+ this.drawRect(startX, y, w - startX, 1, color);
+ }
+ }
+
+ private void drawRoundedSide(int cornerWidth, int cornerHeight, int radius, int color) {
+ // Top
+ this.matrices.push();
+
+ this.matrices.scale(0.05f, 0.05f, 1.0f);
+
+ this.drawRoundedCorner(cornerWidth, cornerHeight, radius * 10, color);
+
+ this.matrices.pop();
+
+ // Bottom
+ this.matrices.push();
+
+ this.matrices.translate(cornerWidth, cornerHeight * 2, 0);
+
+ this.matrices.multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(90));
+
+ this.matrices.translate(0, -cornerWidth, 0);
+
+ this.matrices.scale(0.05f, 0.05f, 1.0f);
+
+ this.drawRoundedCorner(cornerHeight, cornerWidth, radius * 10, color);
+
+ this.matrices.pop();
+ }
+
+ public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) {
+ if (color == 0)
+ return;
+ radius = Math.min(radius, Math.min(width, height));
+ int cornerWidth = width / 2;
+ int cornerHeight = height / 2;
+
+ this.matrices.push();
+
+ this.matrices.translate(x, y, 0);
+
+ this.drawRoundedSide(cornerWidth, cornerHeight, radius, color);
+
+ this.matrices.translate(cornerWidth * 2, cornerHeight * 2, 0);
+
+ this.matrices.multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(180));
+
+ this.matrices.translate(0, 0, 0);
+
+ this.drawRoundedSide(cornerWidth, cornerHeight, radius, color);
+
+ this.matrices.pop();
+ }
+
+ public int getScaledWindowWidth() {
+ return SaturnClient.client.getWindow().getScaledWidth();
+ }
+
+ public int getScaledWindowHeight() {
+ return SaturnClient.client.getWindow().getScaledHeight();
+ }
+
+ public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int color) {
+ this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
+ }
+
+ public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height) {
+ this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height);
+ }
+
+ public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith,
+ int regionHeight, int textureWidth, int textureHeight) {
+ this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1);
+ }
+
+ public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth,
+ int regionHeight, int textureWidth, int textureHeight, int color) {
+ this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float) textureWidth,
+ (u + (float) regionWidth) / (float) textureWidth, (v + 0.0F) / (float) textureHeight,
+ (v + (float) regionHeight) / (float) textureHeight, color);
+ }
+
+ private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1,
+ float v2, int color) {
+
+ if (sprite.toString().endsWith(".svg")) {
+ sprite = SvgTexture.getSvg(MinecraftClient.getInstance(), sprite, (x2 - x1) * 2, (y2 - y1) * 2);
+ }
+
+ if (color == 0)
+ return;
+ x1 *= 4;
+ x2 *= 4;
+ y1 *= 4;
+ y2 *= 4;
+ color = getColor(color);
+
+ matrices.push();
+ matrices.scale(0.25f, 0.25f, 1.0f);
+ RenderSystem.setShaderTexture(0, sprite);
+ RenderLayer renderLayer = (RenderLayer) RenderLayer.getGuiTextured(sprite);
+ Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
+ VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(renderLayer);
+ vertexConsumer.vertex(matrix4f, (float) x1, (float) y1, 0.0F).texture(u1, v1).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x1, (float) y2, 0.0F).texture(u1, v2).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x2, (float) y2, 0.0F).texture(u2, v2).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, 0.0F).texture(u2, v1).color(color);
+ matrices.pop();
+ }
+
+ public void enableScissor(int x1, int y1, int x2, int y2) {
+ ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1))
+ .transform(this.matrices.peek().getPositionMatrix());
+ this.setScissor(this.scissorStack.push(screenRect));
+ }
+
+ public void disableScissor() {
+ this.setScissor(this.scissorStack.pop());
+ }
+
+ public boolean scissorContains(int x, int y) {
+ return this.scissorStack.containsPoint(x, y);
+ }
+
+ private void setScissor(@Nullable ScreenRect rect) {
+ this.draw();
+ if (rect != null) {
+ Window window = SaturnClient.client.getWindow();
+ int i = window.getFramebufferHeight();
+ double d = window.getScaleFactor();
+ double e = (double) rect.getLeft() * d;
+ double f = (double) i - (double) rect.getBottom() * d;
+ double g = (double) rect.width() * d;
+ double h = (double) rect.height() * d;
+ RenderSystem.enableScissor((int) e, (int) f, Math.max(0, (int) g), Math.max(0, (int) h));
+ } else {
+ RenderSystem.disableScissor();
+ }
+
+ }
+
+ public void draw() {
+ this.vertexConsumers.draw();
+ }
+
+ public void draw(Consumer drawer) {
+ drawer.accept(this.vertexConsumers);
+ this.vertexConsumers.draw();
+ }
+
+ static class ScissorStack {
+ private final Deque stack = new ArrayDeque<>();
+
+ public ScreenRect push(ScreenRect p_281812_) {
+ ScreenRect screenrectangle = this.stack.peekLast();
+ if (screenrectangle != null) {
+ ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
+ ScreenRect.empty());
+ this.stack.addLast(screenrectangle1);
+ return screenrectangle1;
+ } else {
+ this.stack.addLast(p_281812_);
+ return p_281812_;
+ }
+ }
+
+ @Nullable
+ public ScreenRect pop() {
+ if (this.stack.isEmpty()) {
+ throw new IllegalStateException("Scissor stack underflow");
+ } else {
+ this.stack.removeLast();
+ return this.stack.peekLast();
+ }
+ }
+
+ public boolean containsPoint(int p_329411_, int p_333404_) {
+ return this.stack.isEmpty() ? true : this.stack.peek().contains(p_329411_, p_333404_);
+ }
+ }
+
+ public void drawItem(ItemStack item, int x, int y) {
+ this.drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0);
+ }
+
+ public void drawItem(ItemStack stack, int x, int y, int seed) {
+ this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed);
+ }
+
+ public void drawItem(ItemStack stack, int x, int y, int seed, int z) {
+ this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed, z);
+ }
+
+ public void drawItemWithoutEntity(ItemStack stack, int x, int y) {
+ this.drawItemWithoutEntity(stack, x, y, 0);
+ }
+
+ public void drawItemWithoutEntity(ItemStack stack, int x, int y, int seed) {
+ this.drawItem((LivingEntity) null, SaturnClient.client.world, stack, x, y, seed);
+ }
+
+ public void drawItem(LivingEntity entity, ItemStack stack, int x, int y, int seed) {
+ this.drawItem(entity, entity.getWorld(), stack, x, y, seed);
+ }
+
+ private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y,
+ int seed) {
+ this.drawItem(entity, world, stack, x, y, seed, 0);
+ }
+
+ private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
+ int z) {
+ if (!stack.isEmpty()) {
+ SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI,
+ false, world, entity, seed);
+ this.matrices.push();
+ this.matrices.translate((float) (x + 8), (float) (y + 8),
+ (float) (150 + (this.itemRenderState.hasDepth() ? z : 0)));
+
+ try {
+ this.matrices.scale(16.0F, -16.0F, 16.0F);
+ boolean bl = !this.itemRenderState.isSideLit();
+ if (bl) {
+ this.draw();
+ DiffuseLighting.disableGuiDepthLighting();
+ }
+
+ this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV);
+ this.draw();
+ if (bl) {
+ DiffuseLighting.enableGuiDepthLighting();
+ }
+ } catch (Throwable var11) {
+ CrashReport crashReport = CrashReport.create(var11, "Rendering item");
+ CrashReportSection crashReportSection = crashReport.addElement("Item being rendered");
+ crashReportSection.add("Item Type", () -> {
+ return String.valueOf(stack.getItem());
+ });
+ crashReportSection.add("Item Components", () -> {
+ return String.valueOf(stack.getComponents());
+ });
+ crashReportSection.add("Item Foil", () -> {
+ return String.valueOf(stack.hasGlint());
+ });
+ throw new CrashException(crashReport);
+ }
+
+ this.matrices.pop();
+ }
+ }
+
+ public void drawSpriteStretched(Sprite sprite, int x, int y, int width, int height) {
+ this.drawSpriteStretched(sprite, x, y, width, height, -1);
+ }
+
+ public void drawSpriteStretched(Sprite sprite, int x, int y, int width, int height, int color) {
+ if (color == 0)
+ return;
+ if (width != 0 && height != 0) {
+ this.drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height, sprite.getMinU(), sprite.getMaxU(),
+ sprite.getMinV(), sprite.getMaxV(), color);
+ }
+ }
+
+ public void fill(int x1, int y1, int x2, int y2, int color) {
+ this.fill(x1, y1, x2, y2, 0, color);
+ }
+
+ public void fill(int x1, int y1, int x2, int y2, int z, int color) {
+ this.fill(RenderLayer.getGui(), x1, y1, x2, y2, z, color);
+ }
+
+ public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int color) {
+ this.fill(layer, x1, y1, x2, y2, 0, color);
+ }
+
+ public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int z, int color) {
+ Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
+ int i;
+ if (x1 < x2) {
+ i = x1;
+ x1 = x2;
+ x2 = i;
+ }
+
+ if (y1 < y2) {
+ i = y1;
+ y1 = y2;
+ y2 = i;
+ }
+
+ VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(layer);
+ vertexConsumer.vertex(matrix4f, (float) x1, (float) y1, (float) z).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x1, (float) y2, (float) z).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x2, (float) y2, (float) z).color(color);
+ vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, (float) z).color(color);
+ }
+
+ public void drawBorder(int x, int y, int width, int height, int color) {
+ this.fill(x, y, x + width, y + 1, color);
+ this.fill(x, y + height - 1, x + width, y + height, color);
+ this.fill(x, y + 1, x + 1, y + height - 1, color);
+ this.fill(x + width - 1, y + 1, x + width, y + height - 1, color);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/SaturnScreen.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/SaturnScreen.java
new file mode 100644
index 0000000..29651de
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/SaturnScreen.java
@@ -0,0 +1,169 @@
+package org.saturnclient.ui2;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
+import org.saturnclient.ui2.components.ElementRenderer;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gl.PostEffectProcessor;
+import net.minecraft.client.gui.CubeMapRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.RotatingCubeMapRenderer;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.render.DefaultFramebufferSet;
+import net.minecraft.client.texture.ResourceTexture;
+import net.minecraft.client.texture.TextureManager;
+import net.minecraft.client.util.Pool;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public abstract class SaturnScreen extends Screen {
+ private final Pool pool = new Pool(3);
+ protected List elements = new ArrayList<>();
+ protected float backgroundOpacity = 1.0f;
+ protected Instant start = null;
+ public int backgroundBlur = 10;
+
+ protected static final Identifier PANORAMA = Identifier.of("saturnclient",
+ "textures/gui/title/background/panorama");
+ protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(PANORAMA);
+
+ public static final RotatingCubeMapRenderer ROTATING_PANORAMA_RENDERER;
+
+ static {
+ ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER);
+ }
+
+ public static void preload(MinecraftClient client) {
+ TextureManager textureManager = client.getTextureManager();
+
+ // Force upload each texture to GPU
+ for (int i = 0; i < 6; i++) {
+ String var10001 = PANORAMA.getPath();
+ Identifier tex = PANORAMA.withPath(var10001 + "_" + i + ".png");
+ ResourceTexture resourceTexture = new ResourceTexture(tex);
+ textureManager.registerTexture(tex, resourceTexture);
+ }
+ }
+
+ public SaturnScreen(String title) {
+ super(Text.literal(title));
+ }
+
+ public void draw(Element element) {
+ ElementRenderer.draw(elements, element);
+ }
+
+ @Override
+ protected void init() {
+ elements.clear();
+ width *= 2;
+ height *= 2;
+ ui(); // abstraction to render the saturn ui and also render extra stuff here
+ }
+
+ public abstract void ui();
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (start == null) {
+ start = Instant.now();
+ }
+ long elapsed = Duration.between(start, Instant.now()).toMillis();
+
+ mouseX *= 2;
+ mouseY *= 2;
+
+ if (client.world == null && client.getCurrentServerEntry() == null) {
+ ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, backgroundOpacity, delta);
+ }
+
+ PostEffectProcessor postEffectProcessor = this.client.getShaderLoader().loadPostEffect(
+ Identifier.ofVanilla("blur"),
+ DefaultFramebufferSet.MAIN_ONLY);
+
+ if (postEffectProcessor != null) {
+ postEffectProcessor.setUniforms("Radius", backgroundBlur * Math.min((float) elapsed / 700, 1.0f));
+ postEffectProcessor.render(this.client.getFramebuffer(), this.pool);
+ }
+
+ // We are using a Abstracted RenderScope because older minecraft versions don't
+ // use DrawContext
+ RenderScope renderScope = new RenderScope(context.getMatrices(),
+ ((DrawContextAccessor) context).getVertexConsumers());
+
+ renderScope.matrices.push();
+
+ renderScope.matrices.scale(0.5f, 0.5f, 0.5f);
+
+ ElementRenderer.render(new ArrayList<>(elements), elapsed, renderScope, mouseX, mouseY);
+
+ renderScope.matrices.pop();
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ mouseX *= 2;
+ mouseY *= 2;
+
+ ElementRenderer.mouseClicked(elements, mouseX, mouseY, button);
+
+ return false;
+ }
+
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ mouseX *= 2;
+ mouseY *= 2;
+
+ ElementRenderer.mouseDragged(elements, mouseX, mouseY, button, deltaX, deltaY);
+
+ return super.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ mouseX *= 2;
+ mouseY *= 2;
+
+ ElementRenderer.mouseReleased(elements, mouseX, mouseY, button);
+
+ return super.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseScrolled(
+ double mouseX,
+ double mouseY,
+ double horizontalAmount,
+ double verticalAmount) {
+ mouseX *= 2;
+ mouseY *= 2;
+
+ ElementRenderer.mouseScrolled(elements, mouseX, mouseY, horizontalAmount, verticalAmount);
+
+ return super.mouseScrolled(
+ mouseX,
+ mouseY,
+ horizontalAmount,
+ verticalAmount);
+ }
+
+ @Override
+ public void resize(MinecraftClient client, int width, int height) {
+ elements.clear();
+ super.resize(client, width, height);
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ ElementRenderer.keyPressed(elements, keyCode, scanCode, modifiers);
+
+ return super.keyPressed(keyCode, scanCode, modifiers);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/Utils.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/Utils.java
new file mode 100644
index 0000000..6e3efb2
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/Utils.java
@@ -0,0 +1,65 @@
+package org.saturnclient.ui2;
+
+import java.util.List;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.ui2.elements.Notification;
+import org.saturnclient.ui2.elements.Notification.NotificationKind;
+
+public class Utils {
+ public static boolean isHovering(int mouseX, int mouseY, int elementWidth, int elementHeight, float elementScale) {
+ return mouseX >= 0 &&
+ mouseX <= (elementWidth * elementScale) &&
+ mouseY >= 0 &&
+ mouseY <= (elementHeight * elementScale);
+ }
+
+ public static char getCharFromKey(int keyCode, int modifiers) {
+ boolean shift = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0;
+
+ // A-Z
+ if (keyCode >= GLFW.GLFW_KEY_A && keyCode <= GLFW.GLFW_KEY_Z) {
+ return (char) (shift
+ ? 'A' + (keyCode - GLFW.GLFW_KEY_A)
+ : 'a' + (keyCode - GLFW.GLFW_KEY_A));
+ }
+
+ // 0-9
+ if (keyCode >= GLFW.GLFW_KEY_0 && keyCode <= GLFW.GLFW_KEY_9) {
+ return shift
+ ? ")!@#$%^&*(".charAt(keyCode - GLFW.GLFW_KEY_0)
+ : (char) ('0' + (keyCode - GLFW.GLFW_KEY_0));
+ }
+
+ // Special characters
+ return switch (keyCode) {
+ case GLFW.GLFW_KEY_SPACE -> ' ';
+ case GLFW.GLFW_KEY_PERIOD -> shift ? '>' : '.';
+ case GLFW.GLFW_KEY_COMMA -> shift ? '<' : ',';
+ case GLFW.GLFW_KEY_MINUS -> shift ? '_' : '-';
+ case GLFW.GLFW_KEY_EQUAL -> shift ? '+' : '=';
+ case GLFW.GLFW_KEY_SEMICOLON -> shift ? ':' : ';';
+ case GLFW.GLFW_KEY_APOSTROPHE -> shift ? '"' : '\'';
+ case GLFW.GLFW_KEY_SLASH -> shift ? '?' : '/';
+ case GLFW.GLFW_KEY_BACKSLASH -> shift ? '|' : '\\';
+ case GLFW.GLFW_KEY_LEFT_BRACKET -> shift ? '{' : '[';
+ case GLFW.GLFW_KEY_RIGHT_BRACKET -> shift ? '}' : ']';
+ case GLFW.GLFW_KEY_GRAVE_ACCENT -> shift ? '~' : '`';
+ default -> '\0';
+ };
+ }
+
+ public static T getOrNull(List list, int index) {
+ return (index >= 0 && index < list.size()) ? list.get(index) : null;
+ }
+
+ public static void notify(NotificationKind kind, String title, String toast) {
+ if (SaturnClient.client.currentScreen instanceof SaturnScreen) {
+ ((SaturnScreen) SaturnClient.client.currentScreen)
+ .draw(new Notification(SaturnClient.client.currentScreen.width,
+ SaturnClient.client.currentScreen.height, kind,
+ title, toast));
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Animation.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Animation.java
new file mode 100644
index 0000000..142ff78
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Animation.java
@@ -0,0 +1,37 @@
+package org.saturnclient.ui2.anim;
+
+import java.util.function.Function;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.Element;
+
+public abstract class Animation {
+ public int duration;
+ public int delay = 0;
+ public Function curve = Curve::easeInOutCubic;
+
+ Animation(AnimationConfig config) {
+ this.duration = config.duration.value;
+ switch (config.curve.value) {
+ case 0:
+ this.curve = Curve::easeInOutCubic;
+ break;
+ case 1:
+ this.curve = Curve::easeOutCubic;
+ break;
+ case 2:
+ this.curve = Curve::easeInOutBack;
+ break;
+ default:
+ break;
+ }
+ }
+
+ Animation(int duration) {
+ this.duration = duration;
+ }
+
+ public abstract void tick(double progress, Element element);
+
+ public abstract void init(Element element);
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Curve.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Curve.java
new file mode 100644
index 0000000..ab9b811
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Curve.java
@@ -0,0 +1,22 @@
+package org.saturnclient.ui2.anim;
+
+public class Curve {
+ public static double easeOutCubic(double x) {
+ return 1 - Math.pow(1 - x, 3);
+ }
+
+ public static double easeInOutCubic(double x) {
+ return (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);
+ }
+
+ public static double easeInOutBack(double x) {
+ double c1 = 2.5;
+ double c2 = c1 * 1.525;
+
+ if (x < 0.5) {
+ return (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2;
+ } else {
+ return (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (2 * x - 2) + c2) + 2) / 2;
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Fade.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Fade.java
new file mode 100644
index 0000000..68a0329
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/Fade.java
@@ -0,0 +1,27 @@
+package org.saturnclient.ui2.anim;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.Element;
+
+public class Fade extends Animation {
+ float targetOpacity;
+
+ public Fade(int duration) {
+ super(duration);
+ }
+
+ public Fade(AnimationConfig config) {
+ super(config);
+ }
+
+ @Override
+ public void tick(double progress, Element element) {
+ element.opacity = Math.max(0.1f, targetOpacity * (float) progress);
+ }
+
+ @Override
+ public void init(Element element) {
+ targetOpacity = element.opacity;
+ element.opacity = 0.1f;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/FadeOut.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/FadeOut.java
new file mode 100644
index 0000000..76f39c9
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/FadeOut.java
@@ -0,0 +1,27 @@
+package org.saturnclient.ui2.anim;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.Element;
+
+public class FadeOut extends Animation {
+ float targetOpacity;
+
+ public FadeOut(int duration) {
+ super(duration);
+ }
+
+ public FadeOut(AnimationConfig config) {
+ super(config);
+ }
+
+ @Override
+ public void tick(double progress, Element element) {
+ element.opacity = targetOpacity - (float) progress;
+ }
+
+ @Override
+ public void init(Element element) {
+ targetOpacity = element.opacity;
+ // element.opacity = 1.0f;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideFade.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideFade.java
new file mode 100644
index 0000000..e0a24b4
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideFade.java
@@ -0,0 +1,39 @@
+package org.saturnclient.ui2.anim;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.Element;
+
+public class SlideFade extends Animation {
+ public Fade fade;
+ public SlideY slideY;
+
+ public SlideFade(int duration, int offset) {
+ super(duration);
+ this.fade = new Fade(duration);
+ this.slideY = new SlideY(duration, offset);
+ }
+
+ public SlideFade(AnimationConfig config, int offset) {
+ super(config);
+ this.fade = new Fade(duration);
+ this.slideY = new SlideY(duration, offset);
+ }
+
+ @Override
+ public void tick(double progress, Element element) {
+ fade.delay = this.delay;
+ fade.duration = this.duration;
+
+ slideY.delay = this.delay;
+ slideY.duration = this.duration;
+
+ fade.tick(progress, element);
+ slideY.tick(progress, element);
+ }
+
+ @Override
+ public void init(Element element) {
+ fade.init(element);
+ slideY.init(element);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideX.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideX.java
new file mode 100644
index 0000000..b759865
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideX.java
@@ -0,0 +1,39 @@
+package org.saturnclient.ui2.anim;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.Element;
+
+public class SlideX extends Animation {
+ int offset = 20;
+ double startX;
+
+ public SlideX(AnimationConfig config) {
+ super(config);
+ }
+
+ public SlideX(AnimationConfig config, int offset) {
+ super(config);
+ this.offset = offset;
+ }
+
+ public SlideX(int duration) {
+ super(duration);
+ }
+
+ public SlideX(int duration, int offset) {
+ super(duration);
+ this.offset = offset;
+ }
+
+ @Override
+ public void init(Element element) {
+ startX = element.x - offset;
+ element.x = (int) startX;
+ }
+
+ @Override
+ public void tick(double progress, Element element) {
+ double currentX = startX + offset * progress;
+ element.x = (int) currentX;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideY.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideY.java
new file mode 100644
index 0000000..5c4ce09
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/anim/SlideY.java
@@ -0,0 +1,39 @@
+package org.saturnclient.ui2.anim;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.Element;
+
+public class SlideY extends Animation {
+ int offset = 20;
+ double startY;
+
+ public SlideY(AnimationConfig config) {
+ super(config);
+ }
+
+ public SlideY(AnimationConfig config, int offset) {
+ super(config);
+ this.offset = offset;
+ }
+
+ public SlideY(int duration) {
+ super(duration);
+ }
+
+ public SlideY(int duration, int offset) {
+ super(duration);
+ this.offset = offset;
+ }
+
+ @Override
+ public void init(Element element) {
+ startY = element.y - offset;
+ element.y = (int) startY;
+ }
+
+ @Override
+ public void tick(double progress, Element element) {
+ double currentY = startY + offset * progress;
+ element.y = (int) currentY;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/CosmeticPreview.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/CosmeticPreview.java
new file mode 100644
index 0000000..49ace16
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/CosmeticPreview.java
@@ -0,0 +1,47 @@
+package org.saturnclient.ui2.components;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.util.Identifier;
+
+public class CosmeticPreview extends Element {
+ static int padding = 14;
+
+ Identifier sprite;
+ private Runnable onClick;
+ private boolean isSelected;
+
+ public CosmeticPreview(boolean isSelected, Identifier sprite, Runnable onClick) {
+ this.sprite = sprite;
+ this.onClick = onClick;
+ this.isSelected = isSelected;
+
+ this.width = 50;
+ this.height = 111;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
+ isSelected ? Theme.ACCENT.value : Theme.PRIMARY.value);
+
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ int texWidth = width - padding;
+ int texHeight = height - padding;
+
+ renderScope.drawTexture(sprite, padding / 2, padding / 2, 0, 0, texWidth, texHeight,
+ Theme.FOREGROUND.value);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ if (onClick != null) {
+ onClick.run();
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/ElementRenderer.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/ElementRenderer.java
new file mode 100644
index 0000000..30842a1
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/ElementRenderer.java
@@ -0,0 +1,98 @@
+package org.saturnclient.ui2.components;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.Utils;
+
+public class ElementRenderer {
+ public static void draw(List elements, Element element) {
+ synchronized (elements) {
+ elements.add(element);
+ }
+
+ if (element.animation != null) {
+ element.animation.init(element);
+ }
+ }
+
+ public static void render(List elements, long elapsed, RenderScope renderScope, int mouseX, int mouseY) {
+ for (Element element : elements) {
+ element.playAnimationFrame(elapsed);
+
+ renderScope.matrices.push();
+ renderScope.setOpacity(element.opacity);
+ renderScope.matrices.translate(element.x, element.y, 0);
+ renderScope.matrices.scale(element.scale, element.scale, 1.0f);
+ element.render(renderScope, new ElementContext(elapsed, mouseX, mouseY, element));
+ renderScope.matrices.pop();
+ renderScope.setRenderLayer(null);
+ }
+ }
+
+ public static void mouseClicked(List elements, double mouseX, double mouseY, int button) {
+ if (button == 0) {
+ for (Element element : new ArrayList<>(elements)) {
+ element.focused = false;
+
+ int adjustedMouseX = (int) mouseX - element.x;
+ int adjustedMouseY = (int) mouseY - element.y;
+
+ element.mouseClicked(adjustedMouseX * element.scale, adjustedMouseY * element.scale, button);
+
+ if (element.opacity > 0 && Utils.isHovering(adjustedMouseX, adjustedMouseY, element.width,
+ element.height, element.scale)) {
+ element.focused = true;
+ element.click((int) (adjustedMouseX * element.scale), (int) (adjustedMouseY * element.scale));
+ }
+ }
+ }
+ }
+
+ public static void mouseScrolled(
+ List elements,
+ double mouseX,
+ double mouseY,
+ double horizontalAmount,
+ double verticalAmount) {
+ for (Element element : new ArrayList<>(elements)) {
+ if (Utils.isHovering((int) mouseX - element.x, (int) mouseY - element.y, element.width, element.height,
+ element.scale)) {
+ element.scroll(
+ (int) mouseX - element.x,
+ (int) mouseY - element.y,
+ horizontalAmount,
+ verticalAmount);
+ }
+ }
+ }
+
+ public static void keyPressed(List elements, int keyCode, int scanCode, int modifiers) {
+ for (Element element : new ArrayList<>(elements)) {
+ if (element.focused && element.opacity > 0) {
+ element.keyPressed(keyCode, scanCode, modifiers);
+
+ char typedChar = Utils.getCharFromKey(keyCode, modifiers);
+ if (typedChar != '\0') {
+ element.charTyped(typedChar);
+ }
+ }
+ }
+ }
+
+ public static void mouseDragged(List elements, double mouseX, double mouseY, int button, double deltaX,
+ double deltaY) {
+ for (Element element : new ArrayList<>(elements)) {
+ element.mouseDragged(mouseX - element.x, mouseY - element.y, button, deltaX, deltaY);
+ }
+ }
+
+ public static void mouseReleased(List elements, double mouseX, double mouseY, int button) {
+ for (Element element : new ArrayList<>(elements)) {
+ element.mouseReleased(mouseX - element.x, mouseY - element.y, button);
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/SaturnModule.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/SaturnModule.java
new file mode 100644
index 0000000..21c8b1c
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/SaturnModule.java
@@ -0,0 +1,165 @@
+package org.saturnclient.ui2.components;
+
+import org.saturnclient.modules.Module;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.resources.Fonts;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.Utils;
+import org.saturnclient.ui2.screens.ConfigEditor;
+
+import net.minecraft.client.render.RenderLayer;
+
+public class SaturnModule extends Element {
+
+ private final Module mod;
+
+ static int p = 10; // padding
+ static int s = 14; // settings icon size
+ static int expand = 5; // settings icon click area
+
+ public SaturnModule(Module mod) {
+ this.mod = mod;
+ this.width = 160;
+ this.height = 50;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ boolean hovered = ctx.isHovering();
+ boolean enabled = mod.isEnabled();
+
+ int iconX = width - s - p;
+ int iconY = height - s - p;
+
+ boolean settingsHover = ctx.isHovering(
+ getSettingsX(),
+ getSettingsY(),
+ getSettingsHitBox(),
+ getSettingsHitBox());
+
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ // Resolve theme colors
+ int bgColor = Theme.PRIMARY.value;
+ int fgColor = hovered && !settingsHover ? Theme.ACCENT.value : Theme.PRIMARY_FG.value;
+
+ int iconBg = enabled ? Theme.ACCENT.value : Theme.PRIMARY.value;
+ int iconFg = enabled ? Theme.ACCENT_FG.value : fgColor;
+
+ int settingsColor = settingsHover
+ ? Theme.ACCENT.value
+ : Theme.withAlpha(160, fgColor);
+
+ int h = height - (p * 2);
+
+ // Main background
+ renderScope.drawRoundedRectangle(
+ 0, 0,
+ width, height,
+ Theme.WIDGET_RADIUS.value,
+ bgColor);
+
+ // Icon background
+ renderScope.drawRoundedRectangle(
+ p, p,
+ h, h,
+ Theme.WIDGET_RADIUS.value,
+ iconBg);
+
+ // Icon
+ renderScope.drawTexture(
+ mod.getIconTexture(),
+ p + (p / 2),
+ p + (p / 2),
+ 0, 0,
+ h - p,
+ h - p,
+ iconFg);
+
+ // Module name
+ renderScope.drawText(
+ 0.6f,
+ mod.getName(),
+ p + h + 4,
+ p + 1,
+ Theme.FONT.value,
+ fgColor);
+
+ // Version (right aligned)
+ String version = mod.getVersion();
+ int versionWidth = (int) (Fonts.getWidth(version, Theme.FONT.value) * 0.35f);
+
+ renderScope.drawText(
+ 0.35f,
+ version,
+ width - p - versionWidth,
+ p + 1,
+ Theme.FONT.value,
+ fgColor);
+
+ // Tags
+ int xt = p + h + 4;
+ for (String tag : mod.getTags()) {
+ int tagWidth = (int) (Fonts.getWidth(tag, Theme.FONT.value) * 0.4f);
+
+ renderScope.drawRoundedRectangle(
+ xt,
+ p + (h / 2),
+ tagWidth + 6,
+ 12,
+ 5,
+ Theme.ACCENT.value);
+
+ renderScope.drawText(
+ 0.4f,
+ tag,
+ xt + 2,
+ p + (h / 2) + 3,
+ Theme.FONT.value,
+ Theme.ACCENT_FG.value);
+
+ xt += tagWidth + 9;
+ }
+
+ // Settings icon
+ renderScope.drawTexture(
+ Textures.SETTINGS,
+ iconX,
+ iconY,
+ 0, 0,
+ s, s,
+ settingsColor);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ boolean settingsHover = Utils.isHovering(
+ mouseX - getSettingsX(),
+ mouseY - getSettingsY(),
+ getSettingsHitBox(),
+ getSettingsHitBox(),
+ 1.0f);
+
+ if (settingsHover) {
+ SaturnClient.client.setScreen(new ConfigEditor(mod.getConfig()));
+ } else {
+ mod.setEnabled(!mod.isEnabled());
+ }
+ }
+
+ private int getSettingsX() {
+ return width - s - p - expand;
+ }
+
+ private int getSettingsY() {
+ return height - s - p - expand;
+ }
+
+ private int getSettingsHitBox() {
+ return s + expand * 2;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/Sidebar.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/Sidebar.java
new file mode 100644
index 0000000..bbb7ef7
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/Sidebar.java
@@ -0,0 +1,125 @@
+package org.saturnclient.ui2.components;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.Utils;
+import org.saturnclient.ui2.screens.ConfigEditor;
+import org.saturnclient.ui2.screens.ModMenu;
+import org.saturnclient.ui2.screens.cosmetics.CloakMenu;
+import org.saturnclient.ui2.screens.store.CloakStore;
+
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.util.Identifier;
+
+public class Sidebar extends Element {
+ public static class SidebarComponent {
+ Runnable onClick;
+ boolean end;
+ Identifier sprite;
+
+ public SidebarComponent(Identifier sprite, Runnable onClick, boolean end) {
+ this.onClick = onClick;
+ this.end = end;
+ this.sprite = sprite;
+ }
+ }
+
+ Runnable onClose;
+ SidebarComponent[] components = {
+ new Sidebar.SidebarComponent(Textures.MODS_TAB, () -> {
+ SaturnClient.client.setScreen(new ModMenu());
+ }, false),
+ new Sidebar.SidebarComponent(Textures.SETTINGS, () -> {
+ SaturnClient.client.setScreen(new ConfigEditor(Config.config));
+ }, false),
+
+ new Sidebar.SidebarComponent(Textures.SHIRT, () -> {
+ SaturnClient.client.setScreen(new CloakMenu());
+ }, false),
+
+ new Sidebar.SidebarComponent(Textures.CLOSE, () -> {
+ onClose.run();
+ }, true),
+
+ new Sidebar.SidebarComponent(Textures.STORE, () -> {
+ SaturnClient.client.setScreen(new CloakStore());
+ }, true),
+ };
+ int active = 0;
+
+ public Sidebar(int active, Runnable onClose) {
+ this.width = 30;
+ this.height = 350;
+ this.active = active;
+ this.onClose = onClose;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ int topRow = 0;
+ int bottomRow = 0;
+
+ int gap = 5; // 5px gap between icons
+ int padding = 10;
+ int sp = 30 - padding + gap;
+ int s = 30 - (padding * 2);
+ int s2 = s + padding;
+
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ renderScope.drawRoundedRectangle(0, 0, width, height, Theme.BG_RADIUS.value, Theme.BACKGROUND.value);
+
+ int i = 0;
+
+ for (SidebarComponent component : components) {
+ int y = component.end ? ((height - (sp * bottomRow)) - (s * 2)) : (padding + (sp * topRow));
+ boolean selected = ctx.isHovering(0, y - (padding / 2), s2, s2) || active == i;
+
+ renderScope.drawRoundedRectangle(padding / 2, y - (padding / 2), s + padding,
+ s + padding, Theme.WIDGET_RADIUS.value, Theme.getBg(selected));
+
+ renderScope.drawTexture(component.sprite, padding, y, 0, 0, s, s, Theme.getFg(selected));
+
+ if (component.end) {
+ bottomRow += 1;
+ } else {
+ topRow += 1;
+ }
+
+ i++;
+ }
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ int topRow = 0;
+ int bottomRow = 0;
+
+ int gap = 5;
+ int padding = 10;
+ int sp = 30 - padding + gap;
+ int s = 30 - (padding * 2);
+ int s2 = s + padding;
+
+ for (SidebarComponent component : components) {
+ int y = component.end ? ((height - (sp * bottomRow)) - (s * 2)) : (padding + (sp * topRow));
+ boolean hovering = Utils.isHovering(mouseX - (padding / 2), mouseY - (y - (padding / 2)), s2, s2, scale);
+
+ if (hovering) {
+ component.onClick.run();
+ break;
+ }
+
+ if (component.end) {
+ bottomRow += 1;
+ } else {
+ topRow += 1;
+ }
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/SkinPreview.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/SkinPreview.java
new file mode 100644
index 0000000..88407c0
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/SkinPreview.java
@@ -0,0 +1,87 @@
+package org.saturnclient.ui2.components;
+
+import org.jetbrains.annotations.Nullable;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.client.render.DiffuseLighting;
+import net.minecraft.client.render.entity.EntityRenderDispatcher;
+import net.minecraft.entity.LivingEntity;
+
+public class SkinPreview extends Element {
+ private boolean negativeAngle = false;
+ private float angle = 0.0f;
+
+ public SkinPreview(float angle, boolean negativeAngle) {
+ this.angle = angle;
+ this.negativeAngle = negativeAngle;
+ }
+
+ public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float f, float mouseX,
+ float mouseY, LivingEntity entity, float angle, boolean negativeAngle) {
+ float g = (float) (x1 + x2) / 2.0F;
+ float h = (float) (y1 + y2) / 2.0F;
+ renderScope.enableScissor(0, 6, x2, y2);
+ float i = (float) Math.atan((double) ((g - mouseX) / 40.0F));
+ float j = (float) Math.atan((double) ((h - mouseY) / 40.0F));
+ Quaternionf quaternionf = (new Quaternionf()).rotateZ(3.1415927F);
+ float k = entity.bodyYaw;
+ float l = entity.getYaw();
+ float m = entity.getPitch();
+ float n = entity.prevHeadYaw;
+ float o = entity.headYaw;
+ entity.setYaw(angle);
+ entity.bodyYaw = angle;
+ entity.setPitch(-(j * 20.0F));
+ entity.headYaw = negativeAngle ? (i * 40.0F) + 180f : -(i * 40.0F);
+ entity.prevHeadYaw = entity.getHeadYaw();
+ entity.setCustomNameVisible(false);
+
+ float p = entity.getScale();
+ Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + f * p, 0.0F);
+ float q = (float) size / p;
+ drawEntity(renderScope, g, h, q, vector3f, quaternionf, null, entity);
+
+ entity.setCustomNameVisible(true);
+ entity.bodyYaw = k;
+ entity.setYaw(l);
+ entity.setPitch(m);
+ entity.prevHeadYaw = n;
+ entity.headYaw = o;
+ renderScope.disableScissor();
+ }
+
+ public static void drawEntity(RenderScope renderScope, float x, float y, float size, Vector3f vector3f,
+ Quaternionf quaternionf, @Nullable Quaternionf quaternionf2, LivingEntity entity) {
+ renderScope.matrices.push();
+ renderScope.matrices.translate((double) x, (double) y, 50.0);
+ renderScope.matrices.scale(size, size, -size);
+ renderScope.matrices.translate(vector3f.x, vector3f.y, vector3f.z);
+ renderScope.matrices.multiply(quaternionf);
+ renderScope.draw();
+ DiffuseLighting.method_34742();
+ EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
+
+ entityRenderDispatcher.setRenderShadows(false);
+ renderScope.draw((vertexConsumers) -> {
+ entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F, renderScope.matrices, vertexConsumers,
+ 15728880);
+ });
+ renderScope.draw();
+ entityRenderDispatcher.setRenderShadows(true);
+ renderScope.matrices.pop();
+ DiffuseLighting.enableGuiDepthLighting();
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ LivingEntity entity = SaturnClient.client.player;
+ if (entity != null) {
+ drawEntity(renderScope, 0, 0, 75, 78, 30, 0.0625F, ctx.mouseX / scale, ctx.mouseY / scale, entity, angle, negativeAngle);
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/FloatInput.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/FloatInput.java
new file mode 100644
index 0000000..d1d3b37
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/FloatInput.java
@@ -0,0 +1,59 @@
+package org.saturnclient.ui2.components.inputs;
+
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class FloatInput extends Input {
+ public Property prop;
+
+ public FloatInput(Property prop) {
+ this.width = 120;
+ this.height = Fonts.getHeight();
+ this.prop = prop;
+ this.text = String.valueOf(prop.value);
+ }
+
+ @Override
+ public void charTyped(char chr) {
+ if ((Character.isDigit(chr) || chr == '.' || (chr == '-' && cursorPosition == 0)) && text.length() < 10) {
+ if (text.equals("0") && cursorPosition == 1) {
+ text = "" + chr;
+ } else {
+ text = text.substring(0, cursorPosition) +
+ chr +
+ text.substring(cursorPosition);
+
+ try {
+ prop.value = text.isEmpty() ? 0 : Float.parseFloat(text);
+ cursorPosition++;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void backspace() {
+ try {
+ if (text.length() == 0) {
+ text = "0";
+ cursorPosition = 1;
+ }
+ prop.value = Float.parseFloat(text);
+ ConfigManager.save();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void checkReset() {
+ if (prop.isReset) {
+ this.text = String.valueOf(prop.value);
+ this.cursorPosition = 0;
+ prop.isReset = false;
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/HexInput.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/HexInput.java
new file mode 100644
index 0000000..92830a1
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/HexInput.java
@@ -0,0 +1,183 @@
+package org.saturnclient.ui2.components.inputs;
+
+import java.awt.Color;
+
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+
+public class HexInput extends Element {
+ public Property prop;
+
+ // Picker state
+ private boolean open = false;
+ private float hue = 0f;
+ private float opacity = 1f;
+ private int selectedX = 0;
+ private int selectedY = 0;
+
+ private boolean draggingOpacity = false;
+
+ // Dimensions
+ private static final int PICKER_WIDTH = 100;
+ private static final int PICKER_HEIGHT = 100;
+ private static final int SLIDER_WIDTH = 100;
+ private static final int SLIDER_HEIGHT = 10;
+ private static final int HUE_SLIDER_HEIGHT = 10;
+
+ public HexInput(Property prop) {
+ this.prop = prop;
+ this.width = 58;
+ this.height = 20;
+ syncFromColor();
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ open = !open;
+ }
+
+ @Override
+ public void mouseClicked(double mouseX, double mouseY, int button) {
+ if (open) {
+ handlePickerClick(mouseX - width - 8, mouseY);
+ }
+ }
+
+ @Override
+ public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ if (open) {
+ handlePickerDrag(mouseX - width - 8, mouseY);
+ }
+ }
+
+ @Override
+ public void mouseReleased(double mouseX, double mouseY, int button) {
+ draggingOpacity = false;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ // Draw the main color rectangle
+ renderScope.drawRoundedRectangle(0, 0, width, height, 6, prop.value);
+
+ if (!open)
+ return;
+
+ // Draw the picker relative to (width + 8, 0)
+ int pickerX = width + 8;
+ int pickerY = 0;
+
+ renderColorSquare(renderScope, pickerX, pickerY);
+ renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_HEIGHT - 10);
+ renderHueSlider(renderScope, pickerX, pickerY + PICKER_HEIGHT + 10);
+ }
+
+ private void renderColorSquare(RenderScope renderScope, int xStart, int yStart) {
+ for (int x = 0; x < PICKER_WIDTH; x++) {
+ for (int y = 0; y < PICKER_HEIGHT; y++) {
+ float saturation = x / (float) PICKER_WIDTH;
+ float value = 1f - y / (float) PICKER_HEIGHT;
+ int color = Color.HSBtoRGB(hue, saturation, value);
+ int alpha = (int) (opacity * 255) & 0xFF;
+ color = (alpha << 24) | (color & 0xFFFFFF);
+ renderScope.fill(xStart + x, yStart + y, xStart + x + 1, yStart + y + 1, color);
+ }
+ }
+
+ // Draw selection square
+ int selX = xStart + selectedX - 4;
+ int selY = yStart + selectedY - 4;
+ renderScope.fill(selX, selY, selX + 8, selY + 8, 0xFF000000 | prop.value);
+ renderScope.drawBorder(selX, selY, 8, 8, 0xFFFFFFFF);
+ }
+
+ private void renderOpacitySlider(RenderScope renderScope, int xStart, int yStart) {
+ for (int x = 0; x < SLIDER_WIDTH; x++) {
+ float localOpacity = x / (float) SLIDER_WIDTH;
+ int alpha = (int) (localOpacity * 255) & 0xFF;
+ int color = (alpha << 24) | (prop.value & 0xFFFFFF);
+ renderScope.fill(xStart + x, yStart, xStart + x + 1, yStart + SLIDER_HEIGHT, color);
+ }
+
+ int knobX = xStart + (int) (opacity * SLIDER_WIDTH);
+ renderScope.fill(knobX - 1, yStart - 2, knobX + 1, yStart + SLIDER_HEIGHT + 2, 0xFF000000);
+ }
+
+ private void renderHueSlider(RenderScope renderScope, int xStart, int yStart) {
+ for (int x = 0; x < SLIDER_WIDTH; x++) {
+ float localHue = x / (float) SLIDER_WIDTH;
+ int color = Color.HSBtoRGB(localHue, 1f, 1f);
+ renderScope.fill(xStart + x, yStart, xStart + x + 1, yStart + HUE_SLIDER_HEIGHT, 0xFF000000 | color);
+ }
+
+ int knobX = xStart + (int) (hue * SLIDER_WIDTH);
+ renderScope.fill(knobX - 1, yStart - 2, knobX + 1, yStart + HUE_SLIDER_HEIGHT + 2, 0xFF000000);
+ }
+
+ private void handlePickerClick(double mouseX, double mouseY) {
+ // Color square
+ if (mouseX >= 0 && mouseX < PICKER_WIDTH && mouseY >= 0 && mouseY < PICKER_HEIGHT) {
+ selectedX = (int) mouseX;
+ selectedY = (int) mouseY;
+ updateColorFromSelection();
+ }
+
+ // Opacity slider
+ int opY = -SLIDER_HEIGHT - 10;
+ if (mouseX >= 0 && mouseX < SLIDER_WIDTH && mouseY >= opY && mouseY <= opY + SLIDER_HEIGHT) {
+ updateOpacity(mouseX);
+ draggingOpacity = true;
+ }
+
+ // Hue slider
+ int hueY = PICKER_HEIGHT + 10;
+ if (mouseX >= 0 && mouseX < SLIDER_WIDTH && mouseY >= hueY && mouseY <= hueY + HUE_SLIDER_HEIGHT) {
+ updateHue(mouseX);
+ }
+ }
+
+ private void handlePickerDrag(double mouseX, double mouseY) {
+ // Opacity
+ if (draggingOpacity)
+ updateOpacity(mouseX);
+
+ // Color square
+ if (mouseX >= 0 && mouseX < PICKER_WIDTH && mouseY >= 0 && mouseY < PICKER_HEIGHT) {
+ selectedX = (int) mouseX;
+ selectedY = (int) mouseY;
+ updateColorFromSelection();
+ }
+ }
+
+ private void updateColorFromSelection() {
+ float saturation = selectedX / (float) PICKER_WIDTH;
+ float value = 1f - selectedY / (float) PICKER_HEIGHT;
+ int color = Color.HSBtoRGB(hue, saturation, value);
+ int alpha = (int) (opacity * 255) & 0xFF;
+ prop.value = (alpha << 24) | (color & 0xFFFFFF);
+ }
+
+ private void updateHue(double mouseX) {
+ hue = (float) Math.max(0, Math.min(1, mouseX / SLIDER_WIDTH));
+ updateColorFromSelection();
+ }
+
+ private void updateOpacity(double mouseX) {
+ opacity = (float) Math.max(0, Math.min(1, mouseX / SLIDER_WIDTH));
+ updateColorFromSelection();
+ }
+
+ private void syncFromColor() {
+ int rgb = prop.value & 0xFFFFFF;
+ float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);
+ hue = hsb[0];
+ selectedX = (int) (hsb[1] * PICKER_WIDTH);
+ selectedY = (int) ((1f - hsb[2]) * PICKER_HEIGHT);
+ int alpha = (prop.value >> 24) & 0xFF;
+ if (alpha == 0)
+ alpha = 255;
+ opacity = alpha / 255f;
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Input.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Input.java
new file mode 100644
index 0000000..fbd48d9
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Input.java
@@ -0,0 +1,113 @@
+package org.saturnclient.ui2.components.inputs;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public abstract class Input extends Element {
+ String text = "";
+ public int cursorPosition = 0;
+
+ public abstract void checkReset();
+
+ public abstract void backspace();
+
+ public boolean validateBackspace() {
+ return true;
+ }
+
+ @Override
+ public void keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
+ if (cursorPosition > 0 && validateBackspace()) {
+ text = text.substring(0, cursorPosition - 1) +
+ text.substring(cursorPosition);
+ cursorPosition--;
+ backspace();
+ }
+ } else if (keyCode == GLFW.GLFW_KEY_LEFT) {
+ if (cursorPosition > 0) {
+ cursorPosition--;
+ }
+ } else if (keyCode == GLFW.GLFW_KEY_RIGHT) {
+ if (cursorPosition < text.length()) {
+ cursorPosition++;
+ }
+ }
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ int scrollOffset = getScrollOffset();
+ String visibleText = text.substring(scrollOffset);
+
+ int newCursorPos = scrollOffset;
+ float textWidth = 0;
+
+ for (int i = 0; i < visibleText.length(); i++) {
+ textWidth = SaturnClient.client.textRenderer.getWidth(visibleText.substring(0, i + 1)) * 0.6f;
+
+ if (textWidth > mouseX) {
+ break;
+ }
+
+ newCursorPos++;
+ }
+
+ cursorPosition = newCursorPos;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ checkReset();
+
+ renderScope.drawRoundedRectangle(0, 0, width, height, 10, 0xFF000000);
+
+ int scrollOffset = getScrollOffset();
+ String visibleText = getVisibleText(scrollOffset);
+ int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
+ renderScope.drawText(0.6f, visibleText, 4, 4, Theme.FONT.value, textColor);
+
+ if (focused) {
+ int cursorX = 4 +
+ (int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset), Theme.FONT.value)
+ * 0.6f);
+
+ renderScope.drawRect(cursorX, 2, 1, height - 4, 0xFFFFFFFF);
+ }
+ }
+
+ protected int getScrollOffset() {
+ if (text.isEmpty()) {
+ return 0;
+ }
+
+ int offset = 0;
+ int maxWidth = width - 4; // Available width for text
+ while (offset < cursorPosition) { // Ensure cursor remains visible
+ String visibleText = text.substring(offset, cursorPosition);
+ if (Fonts.getWidth(visibleText, Theme.FONT.value) > maxWidth) {
+ offset++;
+ } else {
+ break;
+ }
+ }
+ return offset;
+ }
+
+ protected String getVisibleText(int scrollOffset) {
+ String visibleText = text.substring(scrollOffset);
+
+ // Ensure text does not overflow past the available width
+ for (int i = 1; i <= visibleText.length(); i++) {
+ if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > width - 4) {
+ return visibleText.substring(0, i - 1); // Trim overflow
+ }
+ }
+ return visibleText;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/IntInput.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/IntInput.java
new file mode 100644
index 0000000..af43dc8
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/IntInput.java
@@ -0,0 +1,59 @@
+package org.saturnclient.ui2.components.inputs;
+
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class IntInput extends Input {
+ public Property prop;
+
+ public IntInput(Property prop) {
+ this.width = 120;
+ this.height = Fonts.getHeight();
+ this.prop = prop;
+ this.text = String.valueOf(prop.value);
+ }
+
+ @Override
+ public void charTyped(char chr) {
+ if ((Character.isDigit(chr) || chr == '.' || (chr == '-' && cursorPosition == 0)) && text.length() < 10) {
+ if (text.equals("0") && cursorPosition == 1) {
+ text = "" + chr;
+ } else {
+ text = text.substring(0, cursorPosition) +
+ chr +
+ text.substring(cursorPosition);
+
+ try {
+ prop.value = text.isEmpty() ? 0 : Integer.parseInt(text);
+ cursorPosition++;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void backspace() {
+ try {
+ if (text.length() == 0) {
+ text = "0";
+ cursorPosition = 1;
+ }
+ prop.value = Integer.parseInt(text);
+ ConfigManager.save();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void checkReset() {
+ if (prop.isReset) {
+ this.text = String.valueOf(prop.value);
+ this.cursorPosition = 0;
+ prop.isReset = false;
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/KeybindingSelector.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/KeybindingSelector.java
new file mode 100644
index 0000000..e4ebafc
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/KeybindingSelector.java
@@ -0,0 +1,41 @@
+package org.saturnclient.ui2.components.inputs;
+
+import org.lwjgl.glfw.GLFW;
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class KeybindingSelector extends Element {
+ Property prop;
+
+ public KeybindingSelector(Property prop) {
+ this.prop = prop;
+ this.width = 120;
+ this.height = Fonts.getHeight();
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
+ renderScope.drawRoundedRectangle(0, 0, width, height, 10, 0xFF000000);
+ String name = GLFW.glfwGetKeyName(prop.value, GLFW.glfwGetKeyScancode(prop.value));
+
+ renderScope.drawText(0.6f,
+ prop.value == -1 ? ""
+ : name != null ? name.toUpperCase() : "",
+ 4, 4, Theme.FONT.value, textColor);
+ }
+
+ @Override
+ public void keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
+ prop.value = -1;
+ } else {
+ prop.value = keyCode;
+ }
+ focused = false;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Select.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Select.java
new file mode 100644
index 0000000..59c2c65
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Select.java
@@ -0,0 +1,43 @@
+package org.saturnclient.ui2.components.inputs;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.Utils;
+import org.saturnclient.ui2.resources.Fonts;
+import net.minecraft.client.render.RenderLayer;
+
+public class Select extends Element {
+ private Property prop;
+
+ public Select(Property prop) {
+ this.prop = prop;
+ this.width = 150;
+ this.height = 15;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ renderScope.drawText(0.8f, "<", 0, 0, Theme.FONT.value, Theme.FOREGROUND.value);
+
+ String text = prop.getSelection();
+
+ renderScope.drawText(0.6f, text, (int) (width - ((float) Fonts.getWidth(text, Theme.FONT.value) * 0.6f)) / 2, 1,
+ Theme.FONT.value, Theme.FOREGROUND.value);
+
+ renderScope.drawText(0.8f, ">", width - 10, 0, Theme.FONT.value, Theme.FOREGROUND.value);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ if (Utils.isHovering(mouseX - (width - 10), mouseY, 10, height, 1.0f)) {
+ prop.next();
+ } else if (Utils.isHovering(mouseX, mouseY, 10, height, 1.0f)) {
+ prop.prev();
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Toggle.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Toggle.java
new file mode 100644
index 0000000..0ff8705
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/components/inputs/Toggle.java
@@ -0,0 +1,30 @@
+package org.saturnclient.ui2.components.inputs;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+
+public class Toggle extends Element {
+ private Property prop;
+
+ public Toggle(Property prop) {
+ this.prop = prop;
+ this.width = 60;
+ this.height = 30;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawRoundedRectangle(0, 0, width, height, height,
+ Theme.withAlpha(0.5f, prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value));
+ renderScope.drawRoundedRectangle(prop.value ? 30 : 0, 0, 30, 30, 30,
+ prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ prop.value = !prop.value;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/AnimationStagger.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/AnimationStagger.java
new file mode 100644
index 0000000..f2b8778
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/AnimationStagger.java
@@ -0,0 +1,66 @@
+package org.saturnclient.ui2.elements;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.components.ElementRenderer;
+
+public class AnimationStagger extends Element {
+ protected List children = new ArrayList<>();
+ public int delay;
+
+ public AnimationStagger(int delay) {
+ this.delay = delay;
+ }
+
+ public AnimationStagger(AnimationConfig config) {
+ this.delay = config.stagger.value;
+ }
+
+ public void draw(Element element) {
+ if (!Config.stagger.value) {
+ element.animation = null;
+ }
+
+ if (element.animation != null) {
+ element.animation.delay = delay * children.size();
+ }
+
+ ElementRenderer.draw(children, element);
+
+ this.height = Math.max(this.height, element.y + element.height);
+ this.width = Math.max(this.width, element.x + element.width);
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ ElementRenderer.render(children, ctx.elapsed, renderScope,
+ ctx.mouseX,
+ ctx.mouseY);
+ }
+
+ @Override
+ public void mouseClicked(double mouseX, double mouseY, int button) {
+ ElementRenderer.mouseClicked(children, mouseX, mouseY, button);
+ }
+
+ @Override
+ public void keyPressed(int keyCode, int scanCode, int modifiers) {
+ ElementRenderer.keyPressed(children, keyCode, scanCode, modifiers);
+ }
+
+ @Override
+ public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ ElementRenderer.mouseDragged(children, mouseX, mouseY, button, deltaX, deltaY);
+ }
+
+ @Override
+ public void mouseReleased(double mouseX, double mouseY, int button) {
+ ElementRenderer.mouseReleased(children, mouseX, mouseY, button);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Button.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Button.java
new file mode 100644
index 0000000..4774f11
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Button.java
@@ -0,0 +1,35 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class Button extends Element {
+ private String text;
+ private Runnable onClick;
+
+ public Button(String text, Runnable onClick) {
+ this.text = text;
+ this.onClick = onClick;
+ this.dimensions(Fonts.getWidth(text, Theme.FONT.value) + 50, 38);
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
+ ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value);
+
+ renderScope.drawText(text, Fonts.centerX(width, text, Theme.FONT.value), Fonts.centerY(height),
+ Theme.FONT.value,
+ ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ if (onClick != null) {
+ onClick.run();
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/ClickableTexture.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/ClickableTexture.java
new file mode 100644
index 0000000..877eeea
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/ClickableTexture.java
@@ -0,0 +1,33 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.util.Identifier;
+
+public class ClickableTexture extends Element {
+ Identifier sprite;
+ private Runnable onClick;
+
+ public ClickableTexture(Identifier sprite, Runnable onClick) {
+ this.sprite = sprite;
+ this.onClick = onClick;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+ renderScope.drawTexture(sprite, 0, 0, 0, 0, width, height,
+ ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ if (onClick != null) {
+ onClick.run();
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/ImageTexture.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/ImageTexture.java
new file mode 100644
index 0000000..037ef31
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/ImageTexture.java
@@ -0,0 +1,32 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.SvgTexture;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.Identifier;
+
+public class ImageTexture extends Element {
+ Identifier sprite;
+
+ public ImageTexture(Identifier sprite) {
+ this.sprite = sprite;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ if (sprite.toString().endsWith(".svg")) {
+ MinecraftClient client = MinecraftClient.getInstance();
+
+ // Get the actual window pixel dimensions for the image
+ int renderWidth = (int) (width * client.getWindow().getFramebufferWidth() / client.getWindow().getWidth());
+ int renderHeight = (int) (height * client.getWindow().getFramebufferHeight() / client.getWindow().getHeight());
+
+ sprite = SvgTexture.getSvg(client, sprite, renderWidth * 2, renderHeight * 2);
+ }
+
+ renderScope.drawTexture(sprite, 0, 0, 0, 0, width, height);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Notification.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Notification.java
new file mode 100644
index 0000000..72fae8d
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Notification.java
@@ -0,0 +1,58 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.anim.SlideX;
+import org.saturnclient.ui2.resources.Fonts;
+import org.saturnclient.ui2.resources.Textures;
+
+public class Notification extends Element {
+ private static Property error = Property.color(0xFFbf212f);
+ private static Property info = Property.color(0xFF264b96);
+ private static Property success = Property.color(0xFF27b376);
+
+ public static enum NotificationKind {
+ Error,
+ Info,
+ Success,
+ }
+
+ NotificationKind kind;
+ String text;
+ String title;
+
+ public Notification(int width, int height, NotificationKind kind, String title, String text) {
+ this.kind = kind;
+ this.text = text;
+ this.title = title;
+ this.width = (int) Math.max(Fonts.getWidth(text, 1) * 0.6f, Fonts.getWidth(title, 1)) + 20;
+ this.height = 50;
+ this.x = width - this.width - 15;
+ this.y = height - this.height - 15;
+ this.animation = new SlideX(700, -width);
+ this.duration = 3000;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawRoundedRectangle(0, 0, width, height, 10, Theme.PRIMARY.value);
+ switch (kind) {
+ case Error:
+ renderScope.drawTexture(Textures.CIRCLE_X, 9, 9, 0, 0, 15, 15, error.value);
+ break;
+
+ case Info:
+ renderScope.drawTexture(Textures.INFO, 9, 9, 0, 0, 15, 15, info.value);
+ break;
+
+ case Success:
+ renderScope.drawTexture(Textures.CHECK, 9, 9, 0, 0, 15, 15, success.value);
+ break;
+ }
+ renderScope.drawText(0.7f, title, 32, 10, 1, -1);
+ renderScope.drawText(0.5f, text, 32, 28, 1, -1);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Scroll.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Scroll.java
new file mode 100644
index 0000000..780e9b8
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Scroll.java
@@ -0,0 +1,130 @@
+package org.saturnclient.ui2.elements;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.components.ElementRenderer;
+
+public class Scroll extends Element {
+ private static Property scrollBarRadius = Property.integer(10);
+ private static Property scrollBarWidth = Property.integer(5);
+ private static Property scrollBarPadding = Property.integer(5);
+
+ int padding = 0;
+
+ protected List children = new ArrayList<>();
+ int scroll = 0;
+ int maxScroll = 0;
+
+ public Scroll(int padding) {
+ this.padding = padding;
+ }
+
+ public void draw(Element element) {
+ ElementRenderer.draw(children, element);
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ calculateMaxScroll();
+
+ renderScope.drawRoundedRectangle(0, 0, width, height,
+ Theme.BG_RADIUS.value, Theme.BACKGROUND.value);
+
+ renderScope.enableScissor(padding, padding, width - padding, height - padding);
+ renderScope.matrices.push();
+ renderScope.matrices.translate(padding, -scroll + padding, 0);
+
+ ElementRenderer.render(children, ctx.elapsed, renderScope,
+ ctx.mouseX - padding,
+ ctx.mouseY - padding + scroll);
+
+ renderScope.matrices.pop();
+ renderScope.disableScissor();
+
+ if (maxScroll > 0) {
+ renderScope.drawRoundedRectangle(
+ width - scrollBarWidth.value - scrollBarPadding.value,
+ calculateScrollBarY(),
+ scrollBarWidth.value,
+ calculateScrollBarHeight(),
+ scrollBarRadius.value,
+ Theme.SCROLL.value);
+ }
+ }
+
+ @Override
+ public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) {
+ scroll -= verticalAmount;
+ if (scroll < 0) {
+ scroll = 0;
+ } else if (scroll > maxScroll) {
+ scroll = maxScroll;
+ }
+ }
+
+ @Override
+ public Element dimensions(int width, int height) {
+ super.dimensions(width, height);
+
+ maxScroll = 0;
+ for (Element element : children) {
+ maxScroll = Math.max(maxScroll, (element.y + element.height + padding) - (height - padding * 2));
+ }
+
+ return this;
+ }
+
+ int calculateScrollBarHeight() {
+ if (maxScroll <= 0)
+ return height - (scrollBarPadding.value * 2);
+ return Math.max(20, (height * height) / (height + maxScroll + 10)) - (scrollBarPadding.value * 2);
+ }
+
+ int calculateScrollBarY() {
+ int scrollBarHeight = calculateScrollBarHeight();
+ if (maxScroll <= 0) return scrollBarPadding.value;
+ int y = (scroll * (height - scrollBarHeight - scrollBarPadding.value * 2)) / maxScroll + scrollBarPadding.value;
+ return Math.min(y, height - scrollBarHeight - scrollBarPadding.value);
+ }
+
+ private void calculateMaxScroll() {
+ maxScroll = 0;
+
+ for (Element element : children) {
+ int bottom = element.y + element.height;
+ maxScroll = Math.max(maxScroll, bottom - height + padding);
+ }
+
+ if (maxScroll > 0) {
+ maxScroll = maxScroll + 10;
+ } else {
+ maxScroll = 0;
+ }
+ }
+
+ @Override
+ public void keyPressed(int keyCode, int scanCode, int modifiers) {
+ ElementRenderer.keyPressed(children, keyCode, scanCode, modifiers);
+ }
+
+ @Override
+ public void mouseClicked(double mouseX, double mouseY, int button) {
+ ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + scroll, button);
+ }
+
+ @Override
+ public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ ElementRenderer.mouseDragged(children, mouseX - padding, mouseY - padding + scroll, button, deltaX, deltaY);
+ }
+
+ @Override
+ public void mouseReleased(double mouseX, double mouseY, int button) {
+ ElementRenderer.mouseReleased(children, mouseX - padding, mouseY - padding + scroll, button);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/TabMenu.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/TabMenu.java
new file mode 100644
index 0000000..b41757b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/TabMenu.java
@@ -0,0 +1,64 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.Utils;
+
+import net.minecraft.util.Identifier;
+
+public class TabMenu extends Element {
+ public static class TabMenuComponent {
+ Runnable onClick;
+ Identifier sprite;
+
+ public TabMenuComponent(Identifier sprite, Runnable onClick) {
+ this.onClick = onClick;
+ this.sprite = sprite;
+ }
+ }
+
+ TabMenuComponent[] components;
+ public int selectedScreen = 0;
+
+ public TabMenu(int active, TabMenuComponent... components) {
+ this.components = components;
+ width = 25 * components.length + 6;
+ height = 30;
+ this.selectedScreen = active;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Theme.PRIMARY.value);
+
+ int col = 0;
+ for (TabMenuComponent component : components) {
+ int x = 25 * col + 5;
+
+ boolean hovering = ctx.isHovering(x, 5, 20, 20) || selectedScreen == col;
+
+ if (hovering) {
+ renderScope.drawRoundedRectangle(x, 5, 20, 20, Theme.WIDGET_RADIUS.value, Theme.ACCENT.value);
+ }
+
+ renderScope.drawTexture(component.sprite, x + 5, 10, 0, 0, 10, 10,
+ hovering ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
+
+ col++;
+ }
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ int col = 0;
+ for (TabMenuComponent component : components) {
+ int x = 25 * col + 5;
+ if (Utils.isHovering(mouseX - x, mouseY - 5, 20, 20, 1.0f)) {
+ component.onClick.run();
+ }
+ col++;
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Text.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Text.java
new file mode 100644
index 0000000..285433d
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/Text.java
@@ -0,0 +1,22 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class Text extends Element {
+ private String text;
+
+ public Text(String text) {
+ this.text = text;
+ width = Fonts.getWidth(text, Theme.FONT.value);
+ height = Fonts.getHeight();
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawText(this.text, 0, 0, Theme.FONT.value, Theme.FOREGROUND.value);
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/TextureButton.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/TextureButton.java
new file mode 100644
index 0000000..ccbf1ba
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/elements/TextureButton.java
@@ -0,0 +1,41 @@
+package org.saturnclient.ui2.elements;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.Element;
+import org.saturnclient.ui2.ElementContext;
+import org.saturnclient.ui2.RenderScope;
+
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.util.Identifier;
+
+public class TextureButton extends Element {
+ Identifier sprite;
+ private Runnable onClick;
+ static int padding = 14;
+
+ public TextureButton(Identifier sprite, Runnable onClick) {
+ this.sprite = sprite;
+ this.onClick = onClick;
+ }
+
+ @Override
+ public void render(RenderScope renderScope, ElementContext ctx) {
+ renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
+ ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value);
+
+ renderScope.setRenderLayer(RenderLayer::getGuiTextured);
+
+ int texWidth = width - padding;
+ int texHeight = height - padding;
+
+ renderScope.drawTexture(sprite, padding / 2, padding / 2, 0, 0, texWidth, texHeight,
+ ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
+ }
+
+ @Override
+ public void click(int mouseX, int mouseY) {
+ if (onClick != null) {
+ onClick.run();
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/Fonts.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/Fonts.java
new file mode 100644
index 0000000..5233f45
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/Fonts.java
@@ -0,0 +1,56 @@
+package org.saturnclient.ui2.resources;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public class Fonts {
+ public static final Identifier INTER = Identifier.of("saturnclient", "inter");
+ public static final Identifier INTER_BOLD = Identifier.of("saturnclient", "inter_bold");
+ public static final Identifier DEFAULT = Identifier.ofVanilla("default");
+
+ public static Identifier getFont(int font) {
+ switch (font) {
+ case 0:
+ return DEFAULT;
+ case 1:
+ return INTER;
+ default:
+ return INTER_BOLD;
+ }
+ }
+
+ public static Text setFont(String text, Identifier font) {
+ return Text.literal(text).setStyle(
+ Style.EMPTY.withFont(font));
+ }
+
+ public static int getWidth(String text, int font) {
+ int w = 0;
+ for (String line : text.split("\n")) {
+ w = Math.max(w, getWidth(line, getFont(font)));
+ }
+ if (font == 0) {
+ return w * 2;
+ } else {
+ return w;
+ }
+ }
+
+ private static int getWidth(String text, Identifier font) {
+ return SaturnClient.client.textRenderer.getWidth(setFont(text, font));
+ }
+
+ public static int getHeight() {
+ return 18;
+ }
+
+ public static int centerX(int w, String text, int font) {
+ return (w - Fonts.getWidth(text, font)) / 2;
+ }
+
+ public static int centerY(int h) {
+ return (h - 18) / 2;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/SvgTexture.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/SvgTexture.java
new file mode 100644
index 0000000..6cc79d3
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/SvgTexture.java
@@ -0,0 +1,80 @@
+package org.saturnclient.ui2.resources;
+
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.kitfox.svg.SVGDiagram;
+import com.kitfox.svg.SVGUniverse;
+
+import org.jetbrains.annotations.Nullable;
+import org.saturnclient.cosmetics.cloak.utils.IdentifierUtils;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.Identifier;
+
+public class SvgTexture {
+
+ private static List svgCache = new ArrayList<>();
+
+ private static BufferedImage renderSvg(InputStream svgStream, int width, int height) throws Exception {
+ // Load SVG
+ SVGUniverse universe = new SVGUniverse();
+ java.net.URI uri = universe.loadSVG(svgStream, "tempSvg");
+ SVGDiagram diagram = universe.getDiagram(uri);
+
+ // Get intrinsic size of SVG
+ float svgWidth = (float) diagram.getWidth();
+ float svgHeight = (float) diagram.getHeight();
+
+ // Compute scale factors
+ float scaleX = width / svgWidth;
+ float scaleY = height / svgHeight;
+
+ // Create output image
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = image.createGraphics();
+
+ // Enable anti-aliasing for smooth scaling
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+
+ // Apply scaling transform
+ g.scale(scaleX, scaleY);
+
+ // Render SVG
+ diagram.render(g);
+
+ g.dispose();
+ return image;
+ }
+
+ @Nullable
+ public static Identifier getSvg(MinecraftClient client, Identifier svgImage, int width, int height) {
+ Identifier id = Identifier
+ .of(svgImage.toString().replaceAll("\\.svg$", (width + "_" + height).toString() + ".png"));
+
+ if (svgCache.contains(id)) {
+ return id;
+ }
+
+ try (InputStream svgStream = client
+ .getResourceManager()
+ .getResource(svgImage).get()
+ .getInputStream()) {
+
+ BufferedImage image = renderSvg(svgStream, width, height);
+
+ IdentifierUtils.registerBufferedImageTexture(id, image);
+
+ return id;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/Textures.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/Textures.java
new file mode 100644
index 0000000..35a238c
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/resources/Textures.java
@@ -0,0 +1,59 @@
+package org.saturnclient.ui2.resources;
+
+import net.minecraft.util.Identifier;
+
+public class Textures {
+ public static final Identifier CROSSHAIR_RANGE = Identifier.of("saturnclient", "textures/gui/hud/crosshair_range.png");
+ public static final Identifier COSMETICS = Identifier.of("saturnclient", "textures/gui/components/cosmetics.png");
+ public static final Identifier HUD_ICON = Identifier.of("saturnclient", "textures/gui/components/hud.png");
+ public static final Identifier SEARCH = Identifier.of("saturnclient", "textures/gui/components/search.png");
+ public static final Identifier CLOSE = Identifier.of("saturnclient", "textures/gui/components/close.png");
+ public static final Identifier MODS_TAB = Identifier.of("saturnclient", "textures/gui/components/mods_tab.png");
+ public static final Identifier SETTINGS = Identifier.of("saturnclient", "textures/gui/components/settings.png");
+ public static final Identifier CLOAK = Identifier.of("saturnclient", "textures/gui/components/cloak.png");
+ public static final Identifier HAT = Identifier.of("saturnclient", "textures/gui/components/hat.png");
+ public static final Identifier RESET = Identifier.of("saturnclient", "textures/gui/components/reset.png");
+ public static final Identifier TOGGLE_INDICATOR = Identifier.of("saturnclient",
+ "textures/gui/components/toggle_indicator.png");
+ public static final Identifier LEFT = Identifier.of("saturnclient", "textures/gui/components/left.png");
+ public static final Identifier RIGHT = Identifier.of("saturnclient", "textures/gui/components/right.png");
+ public static final Identifier STORE = Identifier.of("saturnclient", "textures/gui/components/store.png");
+ public static final Identifier COINS = Identifier.of("saturnclient", "textures/gui/components/coins.png");
+ public static final Identifier SHIRT = Identifier.of("saturnclient", "textures/gui/components/shirt.png");
+ public static final Identifier CIRCLE_X = Identifier.of("saturnclient", "textures/gui/components/circle_x.png");
+ public static final Identifier INFO = Identifier.of("saturnclient", "textures/gui/components/info.png");
+ public static final Identifier CHECK = Identifier.of("saturnclient", "textures/gui/components/check.png");
+
+ public static final Identifier LOGO = Identifier.of("saturnclient", "textures/logo/logo.png");
+ public static final Identifier REALISTIC_LOGO = Identifier.of("saturnclient", "textures/logo/realistic.png");
+ public static final Identifier LOGO_TEXT = Identifier.of("saturnclient", "textures/logo/text.png");
+ public static final Identifier LOGO_TEXT_BIG = Identifier.of("saturnclient", "textures/logo/text_big.png");
+ public static final Identifier SPLASH = Identifier.of("saturnclient", "textures/gui/splash.png");
+
+ // Widgets
+ public static final Identifier BUTTON_BORDER = Identifier.of("saturnclient", "button_border");
+ public static final Identifier BUTTON = Identifier.of("saturnclient", "button");
+ public static final Identifier BUTTON_OP = Identifier.of("saturnclient", "button_op");
+ public static final Identifier SETTINGS_BG = Identifier.of("saturnclient", "settings_bg");
+ public static final Identifier RECT = Identifier.of("saturnclient", "rect");
+ public static final Identifier RECT_BORDER = Identifier.of("saturnclient", "rect_border");
+ public static final Identifier MOD = Identifier.of("saturnclient", "textures/gui/components/mod.png");
+ public static final Identifier MOD_BG = Identifier.of("saturnclient", "textures/gui/components/mod_bg.png");
+
+ public static Identifier getCloakPreview(String cloak) {
+ return Identifier.of("saturnclient", "textures/gui/cloak/" + (cloak.isEmpty() ? "none" : cloak) + ".png");
+ }
+
+ public static Identifier getHatPreview(String hat) {
+ return Identifier.of("saturnclient", "textures/gui/hat/" + (hat.isEmpty() ? "none" : hat) + ".png");
+ }
+
+ public static Identifier getEmotePreview(String emote) {
+ return Identifier.of("saturnclient", "textures/gui/emote/" + (emote == null ? "none" : emote) + ".png");
+ }
+
+ public static Identifier getModIcon(String modId) {
+ return Identifier.of("saturnclient", "textures/gui/mod/" + modId + ".svg");
+ }
+
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ConfigEditor.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ConfigEditor.java
new file mode 100644
index 0000000..52d2f55
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ConfigEditor.java
@@ -0,0 +1,131 @@
+package org.saturnclient.ui2.screens;
+
+import java.util.Map;
+
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.saturnclient.config.manager.ConfigManager;
+import org.saturnclient.saturnclient.config.manager.Property;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.components.Sidebar;
+import org.saturnclient.ui2.components.inputs.FloatInput;
+import org.saturnclient.ui2.components.inputs.HexInput;
+import org.saturnclient.ui2.components.inputs.IntInput;
+import org.saturnclient.ui2.components.inputs.KeybindingSelector;
+import org.saturnclient.ui2.components.inputs.Select;
+import org.saturnclient.ui2.components.inputs.Toggle;
+import org.saturnclient.ui2.elements.Scroll;
+import org.saturnclient.ui2.elements.Text;
+import org.saturnclient.ui2.resources.Fonts;
+
+public class ConfigEditor extends SaturnScreen {
+ private ConfigManager config;
+
+ public ConfigEditor(ConfigManager config) {
+ super("Config Editor");
+
+ this.config = config;
+ }
+
+ @Override
+ public void ui() {
+ int p = 10;
+ int g = 10;
+
+ Scroll scroll = new Scroll(p);
+
+ drawProperties(scroll, config.getProperties(), 0, 0, 480 + 10 + (g * 2));
+
+ int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
+
+ draw(scroll.dimensions(scrollWidth, 350).center(width, height));
+
+ draw(new Sidebar(1, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
+ }
+
+ @SuppressWarnings("unchecked")
+ private int drawProperties(Scroll configScroll, Map> properties, int row, int col, int w) {
+ for (Map.Entry> propEntry : properties.entrySet()) {
+ Property> prop = propEntry.getValue();
+ String propName = propEntry.getKey();
+ boolean full = isFull(prop);
+
+ // Always reset to col 0 if full-width component needs to be drawn mid-row
+ if (full && col != 0) {
+ col = 0;
+ row++;
+ }
+
+ int modX = (w / 2) * col; // two-column layout
+ int modY = 25 * row;
+
+ if (prop.getType() == Property.PropertyType.NAMESPACE) {
+ configScroll.draw(
+ new Text(
+ propName).position(Fonts.centerX(520, propName, Theme.FONT.value), modY + 1)
+ .scale(0.7f));
+ } else {
+ configScroll.draw(
+ new Text(
+ propName).position(modX, modY + 1).scale(0.7f));
+ }
+
+ switch (prop.getType()) {
+ case BOOLEAN:
+ configScroll.draw(
+ new Toggle((Property) prop)
+ .position(modX + (w / 2) - 40, modY)
+ .scale(0.5f));
+ break;
+
+ case HEX:
+ configScroll.draw(new HexInput((Property) prop).position(w / 2, modY));
+ break;
+
+ case INTEGER:
+ configScroll.draw(new IntInput((Property) prop).position(w / 2, modY));
+ break;
+
+ case FLOAT:
+ configScroll.draw(new FloatInput((Property) prop).position(w / 2, modY));
+ break;
+
+ case NAMESPACE:
+ Map> nestedProperties = (Map>) prop.value;
+ row = drawProperties(configScroll, nestedProperties, row + 1, col, w);
+ break;
+
+ case SELECT:
+ configScroll.draw(new Select((Property) prop).position(w / 2, modY));
+ break;
+
+ case KEYBINDING:
+ configScroll.draw(new KeybindingSelector((Property) prop).position(w / 2, modY));
+ break;
+
+ case STRING:
+ }
+
+ if (full) {
+ row++; // full-width takes a whole row
+ col = 0;
+ } else {
+ col++;
+ if (col > 1) { // max 2 columns
+ col = 0;
+ row++;
+ }
+ }
+ }
+
+ return row;
+ }
+
+ public static boolean isFull(Property> prop) {
+ switch (prop.getType()) {
+ case BOOLEAN:
+ return false;
+ default:
+ return true;
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/EmoteWheel.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/EmoteWheel.java
new file mode 100644
index 0000000..96fa7f6
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/EmoteWheel.java
@@ -0,0 +1,133 @@
+package org.saturnclient.ui2.screens;
+
+import org.saturnclient.cosmetics.Emotes;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.Utils;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideY;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.elements.TextureButton;
+import org.saturnclient.ui2.resources.Textures;
+
+import dev.kosmx.playerAnim.api.layered.AnimationStack;
+import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
+import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
+import net.minecraft.util.Identifier;
+
+public class EmoteWheel extends SaturnScreen {
+ int page = 1;
+
+ public EmoteWheel() {
+ super("Emote Wheel");
+ }
+
+ @Override
+ public void ui() {
+ int row = 0;
+ int col = 0;
+
+ // =============================
+ // Logo
+ // =============================
+
+ draw(new ImageTexture(Textures.LOGO_TEXT)
+ .dimensions(98, 10)
+ .centerOffset(width, height, 0, -105)
+ .animation(new Fade(AnimationConfig.logo.duration.value)));
+
+ draw(new ImageTexture(Config.getLogo())
+ .dimensions(98, 98)
+ .centerOffset(width, height, 0, -149)
+ .animation(new SlideY(AnimationConfig.logo, -20)));
+
+ // =============================
+ // Navigation Buttons
+ // =============================
+
+ draw(new TextureButton(Textures.LEFT, () -> {
+ if (page > 1) {
+ page--;
+ elements.clear();
+ ui();
+ }
+ }).dimensions(30, 30)
+ .centerOffset(width, height, -50, 105)
+ .animation(new Fade(500)));
+
+ draw(new TextureButton(Textures.RIGHT, () -> {
+ if ((page * 8) < Emotes.availableEmotes.size()) {
+ page++;
+ elements.clear();
+ ui();
+ }
+ }).dimensions(30, 30)
+ .centerOffset(width, height, 50, 105)
+ .animation(new Fade(500)));
+
+ // =============================
+ // Emote Grid (Staggered)
+ // =============================
+
+ AnimationStagger emoteStagger = new AnimationStagger(AnimationConfig.emoteWheel);
+
+ while (row < 3) {
+
+ int idx = (row * 3 + col) + ((page - 1) * 8);
+
+ if (row == 1 && col == 1) {
+ col++;
+ }
+
+ String emote = Utils.getOrNull(Emotes.availableEmotes, idx);
+
+ emoteStagger.draw(
+ new TextureButton(Textures.getEmotePreview(emote), () -> {
+
+ if (emote == null)
+ return;
+
+ AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(
+ SaturnClient.client.player);
+
+ if (animationStack.isActive()
+ && animationStack.getPriority() == 1000) {
+ animationStack.removeLayer(1000);
+ }
+
+ animationStack.addAnimLayer(
+ 1000,
+ PlayerAnimationRegistry
+ .getAnimation(
+ Identifier.of("saturnclient", emote))
+ .playAnimation());
+
+ ServiceClient.emote(emote);
+ close();
+ })
+ .dimensions(70, 70)
+ .centerOffset(width, height,
+ -80 + (col * 80),
+ -35 + (row * 80))
+ .animation(new SlideY(AnimationConfig.emoteWheel, 12)));
+
+ if (col < 2) {
+ col++;
+ } else {
+ col = 0;
+ row++;
+ }
+ }
+
+ draw(emoteStagger);
+ }
+
+ @Override
+ public boolean shouldPause() {
+ return false;
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ModMenu.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ModMenu.java
new file mode 100644
index 0000000..44b1aca
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ModMenu.java
@@ -0,0 +1,56 @@
+package org.saturnclient.ui2.screens;
+
+import org.saturnclient.modules.ModManager;
+import org.saturnclient.modules.Module;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.anim.SlideFade;
+import org.saturnclient.ui2.components.SaturnModule;
+import org.saturnclient.ui2.components.Sidebar;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.elements.Scroll;
+import org.saturnclient.ui2.resources.Textures;
+
+public class ModMenu extends SaturnScreen {
+ public ModMenu() {
+ super("Mod Menu");
+ }
+
+ @Override
+ public void ui() {
+ int p = 10;
+ int g = 10;
+
+ Scroll scroll = new Scroll(p);
+
+ int row = 0;
+ int col = 0;
+
+ AnimationStagger moduleStagger = new AnimationStagger(AnimationConfig.modMenu);
+
+ for (Module mod : ModManager.ALL_MODS) {
+ moduleStagger.draw(
+ new SaturnModule(mod)
+ .position((160 + g) * col, (50 + g) * row)
+ .animation(new SlideFade(AnimationConfig.modMenu, -12)));
+
+ if (col == 2) {
+ col = 0;
+ row++;
+ } else {
+ col++;
+ }
+ }
+
+ scroll.draw(moduleStagger);
+
+ int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
+
+ draw(scroll.dimensions(scrollWidth, 350).center(width, height));
+
+ draw(new Sidebar(0, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
+
+ draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20));
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ShiftMenu.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ShiftMenu.java
new file mode 100644
index 0000000..185120b
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/ShiftMenu.java
@@ -0,0 +1,63 @@
+package org.saturnclient.ui2.screens;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.saturnclient.menus.HudEditor;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.screens.cosmetics.CloakMenu;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.Button;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.elements.TextureButton;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideY;
+
+public class ShiftMenu extends SaturnScreen {
+ public ShiftMenu() {
+ super("Shift Menu");
+ }
+
+ @Override
+ public void ui() {
+ // Logo animations (unchanged)
+ draw(new ImageTexture(Textures.LOGO_TEXT)
+ .dimensions(98, 10)
+ .centerOffset(width, height, 0, -36)
+ .animation(new Fade(AnimationConfig.logo.duration.value)));
+
+ draw(new ImageTexture(Config.getLogo())
+ .dimensions(98, 98)
+ .centerOffset(width, height, 0, -80)
+ .animation(new SlideY(AnimationConfig.logo, -20)));
+
+ // Spacing and sizes
+ int btnWidth = 120;
+ int btnHeight = 40;
+ int spacing = 4;
+
+ // Create stagger
+
+ AnimationStagger stagger = new AnimationStagger(AnimationConfig.shiftMenu.stagger.value);
+
+ stagger.draw(new TextureButton(Textures.HUD_ICON, () -> {
+ client.setScreen(new HudEditor());
+ }).dimensions(btnHeight, btnHeight).position(0, 0)
+ .animation(new Fade(AnimationConfig.shiftMenu)));
+
+ stagger.draw(new Button("Settings", () -> {
+ SaturnClient.client.setScreen(new ModMenu());
+ }).dimensions(btnWidth, btnHeight).position(btnHeight + spacing, 0)
+ .animation(new Fade(AnimationConfig.shiftMenu)));
+
+ stagger.draw(new TextureButton(Textures.COSMETICS, () -> {
+ client.setScreen(new CloakMenu());
+ }).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0)
+ .animation(new Fade(AnimationConfig.shiftMenu)));
+
+ // Draw the stagger at the absolute center of the screen
+ draw(stagger.dimensions(btnWidth + (btnHeight * 2) + (spacing * 2), btnHeight).centerOffset(width, height, 0,
+ 0));
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/TitleMenu.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/TitleMenu.java
new file mode 100644
index 0000000..aa7101e
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/TitleMenu.java
@@ -0,0 +1,81 @@
+package org.saturnclient.ui2.screens;
+
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.saturnclient.config.Config;
+import org.saturnclient.saturnclient.menus.HudEditor;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.screens.cosmetics.CloakMenu;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideFade;
+import org.saturnclient.ui2.anim.SlideY;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.Button;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.elements.TextureButton;
+import org.saturnclient.ui2.resources.Fonts;
+
+import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
+import net.minecraft.client.gui.screen.option.OptionsScreen;
+import net.minecraft.client.gui.screen.world.SelectWorldScreen;
+
+public class TitleMenu extends SaturnScreen {
+ public TitleMenu() {
+ super("Title Menu");
+ }
+
+ @Override
+ public void ui() {
+ backgroundBlur = 0;
+
+ draw(new ImageTexture(Textures.LOGO_TEXT).dimensions(98, 10).centerOffset(width, height, 0, -40)
+ .animation(new Fade(AnimationConfig.logo.duration.value)));
+ draw(new ImageTexture(Config.getLogo()).dimensions(98, 98).centerOffset(width, height, 0, -84)
+ .animation(new SlideY(AnimationConfig.logo, -20)));
+
+ AnimationStagger mainButtonStagger = new AnimationStagger(AnimationConfig.mainMenu);
+
+ mainButtonStagger.draw(new Button("SINGLEPLAYER", () -> {
+ client.setScreen(new SelectWorldScreen(new TitleMenu()));
+ }).scale(0.69f).dimensions(420, 52).position(0, 0)
+ .animation(new SlideFade(AnimationConfig.mainMenu, -10)));
+
+ mainButtonStagger.draw(new Button("MULTIPLAYER", () -> {
+ client.setScreen(new MultiplayerScreen(new TitleMenu()));
+ }).scale(0.69f).dimensions(420, 52).position(0, 40)
+ .animation(new SlideFade(AnimationConfig.mainMenu, -10)));
+
+ mainButtonStagger.draw(new Button("OPTIONS", () -> {
+ client.setScreen(new OptionsScreen(new TitleMenu(), SaturnClient.client.options));
+ }).scale(0.69f).dimensions(206, 52).position(0, 80)
+ .animation(new SlideFade(AnimationConfig.mainMenu, -10)));
+
+ mainButtonStagger.draw(new Button("QUIT", () -> {
+ client.scheduleStop();
+ }).scale(0.69f).dimensions(206, 52).position(146, 80)
+ .animation(new SlideFade(AnimationConfig.mainMenu, -10)));
+
+ draw(mainButtonStagger.dimensions(289, 143).centerOffset(width, height, 0, 50));
+
+ int s = Fonts.getHeight() + 20;
+
+ AnimationStagger saturnStagger = new AnimationStagger(AnimationConfig.mainMenu.stagger.value);
+
+ saturnStagger.draw(new TextureButton(Textures.HUD_ICON, () -> {
+ client.setScreen(new HudEditor());
+ }).dimensions(s, s).position(0, 0).animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
+
+ saturnStagger.draw(new TextureButton(Textures.SETTINGS, () -> {
+ SaturnClient.client.setScreen(new ModMenu());
+ }).dimensions(s, s).position(s + 2, 0)
+ .animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
+
+ saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> {
+ client.setScreen(new CloakMenu());
+ }).dimensions(s, s).position((s + 2) * 2, 0)
+ .animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
+
+ draw(saturnStagger.dimensions(((s + 2) * 2) + s, s).centerHorizontal(width, height, 0, 8));
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/cosmetics/CloakMenu.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/cosmetics/CloakMenu.java
new file mode 100644
index 0000000..724407a
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/cosmetics/CloakMenu.java
@@ -0,0 +1,83 @@
+package org.saturnclient.ui2.screens.cosmetics;
+
+import org.saturnclient.cosmetics.cloak.Cloaks;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideY;
+import org.saturnclient.ui2.components.CosmeticPreview;
+import org.saturnclient.ui2.components.Sidebar;
+import org.saturnclient.ui2.components.SkinPreview;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.Scroll;
+import org.saturnclient.ui2.elements.TabMenu;
+import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
+
+public class CloakMenu extends SaturnScreen {
+ public CloakMenu() {
+ super("Cloaks Menu");
+ }
+
+ @Override
+ public void ui() {
+ int p = 10;
+ int g = 10;
+
+ Scroll scroll = new Scroll(p);
+
+ int row = 0;
+ int col = 0;
+
+ SaturnPlayer player = SaturnPlayer.get();
+
+ AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
+
+ if (player != null) {
+ for (String cloak : Cloaks.availableCloaks) {
+
+ stagger.draw(
+ new CosmeticPreview(
+ cloak == player.cloak,
+ Textures.getCloakPreview(cloak),
+ () -> Cloaks.setCloak(cloak))
+ .position((50 + g) * col, (111 + g) * row)
+ .animation(new SlideY(AnimationConfig.cosmeticsMenu, 14)));
+
+ if (col == 5) {
+ col = 0;
+ row++;
+ } else {
+ col++;
+ }
+ }
+ }
+
+ scroll.draw(stagger);
+
+ int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
+
+ draw(scroll
+ .dimensions(scrollWidth, 350)
+ .center(width, height));
+
+ draw(new SkinPreview(-30.0f, false)
+ .scale(3.5f)
+ .position(scroll.x + (scrollWidth - 220), scroll.y + 40)
+ .animation(new Fade(500)));
+
+ draw(new Sidebar(2, this::close)
+ .centerOffset(width, height, -(scrollWidth / 2 + 20), 0)
+ .animation(new Fade(400)));
+
+ draw(new TabMenu(0,
+ new TabMenuComponent(Textures.CLOAK, () -> {
+ }),
+ new TabMenuComponent(Textures.HAT, () -> {
+ SaturnClient.client.setScreen(new HatMenu());
+ }))
+ .centerOffset(width, height, 0, -195));
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/cosmetics/HatMenu.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/cosmetics/HatMenu.java
new file mode 100644
index 0000000..f21723a
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/cosmetics/HatMenu.java
@@ -0,0 +1,84 @@
+package org.saturnclient.ui2.screens.cosmetics;
+
+import org.saturnclient.cosmetics.hat.Hats;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.ui2.resources.Textures;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideY;
+import org.saturnclient.ui2.components.CosmeticPreview;
+import org.saturnclient.ui2.components.Sidebar;
+import org.saturnclient.ui2.components.SkinPreview;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.Scroll;
+import org.saturnclient.ui2.elements.TabMenu;
+import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
+
+public class HatMenu extends SaturnScreen {
+ public HatMenu() {
+ super("Hats Menu");
+ }
+
+ @Override
+ public void ui() {
+ int p = 10;
+ int g = 10;
+
+ Scroll scroll = new Scroll(p);
+
+ int row = 0;
+ int col = 0;
+
+ SaturnPlayer player = SaturnPlayer.get();
+
+ AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
+
+ if (player != null) {
+ for (String hat : Hats.availableHats) {
+
+ stagger.draw(
+ new CosmeticPreview(
+ hat == player.hat,
+ Textures.getHatPreview(hat),
+ () -> Hats.setHat(hat))
+ .dimensions(50, 50)
+ .position((50 + g) * col, (50 + g) * row)
+ .animation(new SlideY(AnimationConfig.cosmeticsMenu, 14)));
+
+ if (col == 5) {
+ col = 0;
+ row++;
+ } else {
+ col++;
+ }
+ }
+ }
+
+ scroll.draw(stagger);
+
+ int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
+
+ draw(scroll
+ .dimensions(scrollWidth, 350)
+ .center(width, height));
+
+ draw(new SkinPreview(170f, true)
+ .scale(3.5f)
+ .position(scroll.x + (scrollWidth - 220), scroll.y + 40)
+ .animation(new Fade(500)));
+
+ draw(new Sidebar(2, this::close)
+ .centerOffset(width, height, -(scrollWidth / 2 + 20), 0)
+ .animation(new Fade(400)));
+
+ draw(new TabMenu(1,
+ new TabMenuComponent(Textures.CLOAK, () -> {
+ SaturnClient.client.setScreen(new CloakMenu());
+ }),
+ new TabMenuComponent(Textures.HAT, () -> {
+ }))
+ .centerOffset(width, height, 0, -195));
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java
new file mode 100644
index 0000000..2978972
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java
@@ -0,0 +1,144 @@
+package org.saturnclient.ui2.screens.store;
+
+import org.saturnclient.cosmetics.cloak.Cloaks;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.Utils;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideY;
+import org.saturnclient.ui2.components.CosmeticPreview;
+import org.saturnclient.ui2.components.Sidebar;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.elements.Notification.NotificationKind;
+import org.saturnclient.ui2.elements.Scroll;
+import org.saturnclient.ui2.elements.TabMenu;
+import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
+import org.saturnclient.ui2.elements.Text;
+import org.saturnclient.ui2.resources.Fonts;
+import org.saturnclient.ui2.resources.Textures;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+
+public class CloakStore extends SaturnScreen {
+ private long lastPurchaseTime = 0;
+
+ public CloakStore() {
+ super("Cloak Store");
+ }
+
+ public CloakStore(long lastPurchaseTime) {
+ super("Cloak Store");
+ this.lastPurchaseTime = lastPurchaseTime;
+ }
+
+ @Override
+ public void ui() {
+ int p = 10;
+ int gx = 10;
+ int gy = 20;
+
+ Scroll scroll = new Scroll(p);
+
+ int row = 0;
+ int col = 0;
+
+ SaturnPlayer player = SaturnPlayer.get();
+
+ AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
+
+ if (player != null) {
+ for (String cloak : Cloaks.ALL_CLOAKS) {
+ if (!Cloaks.availableCloaks.contains(cloak)) {
+
+ int x = (50 + gx) * col;
+ int y = (111 + gy) * row;
+
+ stagger.draw(
+ new CosmeticPreview(
+ cloak == player.cloak,
+ Textures.getCloakPreview(cloak),
+ () -> handlePurchase(cloak))
+ .position(x, y)
+ .animation(new SlideY(AnimationConfig.cosmeticsMenu, 16)));
+
+ String t = "100";
+
+ stagger.draw(
+ new Text(t)
+ .position(x + Fonts.centerX(50, t, Theme.FONT.value), y + 113)
+ .scale(0.5f)
+ .animation(new Fade(400)));
+
+ stagger.draw(
+ new ImageTexture(Textures.COINS)
+ .dimensions(16, 16)
+ .position(x + Fonts.getWidth(t, Theme.FONT.value) - 1, y + 112)
+ .scale(0.5f)
+ .animation(new Fade(400)));
+
+ if (col == 8) {
+ col = 0;
+ row++;
+ } else {
+ col++;
+ }
+ }
+ }
+ }
+
+ scroll.draw(stagger);
+
+ int scrollWidth = 480 + 40 + (gx * 2) + (p * 2);
+
+ draw(scroll
+ .dimensions(scrollWidth, 350)
+ .centerOffset(width, height, 15, 0));
+
+ draw(new Sidebar(5, this::close)
+ .centerOffset(width, height, -((scrollWidth - 30) / 2 + 20), 0)
+ .animation(new Fade(400)));
+
+ draw(new TabMenu(0,
+ new TabMenuComponent(Textures.CLOAK, () -> {
+ }),
+ new TabMenuComponent(Textures.HAT, () -> {
+ SaturnClient.client.setScreen(new HatStore());
+ }))
+ .centerOffset(width, height, 0, -195));
+ }
+
+ private void handlePurchase(String cloak) {
+ long now = System.currentTimeMillis();
+
+ if (now - lastPurchaseTime >= 3000) {
+ Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
+
+ ServiceClient.buyCloak(cloak);
+ lastPurchaseTime = now;
+
+ new Thread(() -> {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException ignored) {
+ }
+
+ RenderSystem.recordRenderCall(() -> {
+ SaturnClient.client.setScreen(new CloakStore(now));
+ Utils.notify(NotificationKind.Success,
+ "Purchase complete",
+ "Congrats, enjoy your new cloak!");
+ });
+ }).start();
+
+ } else {
+ Utils.notify(NotificationKind.Error,
+ "Timeout error",
+ "Please wait 3 seconds");
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java
new file mode 100644
index 0000000..328f701
--- /dev/null
+++ b/versions/1.21.4/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java
@@ -0,0 +1,145 @@
+package org.saturnclient.ui2.screens.store;
+
+import org.saturnclient.cosmetics.hat.Hats;
+import org.saturnclient.saturnclient.SaturnClient;
+import org.saturnclient.saturnclient.client.ServiceClient;
+import org.saturnclient.saturnclient.client.player.SaturnPlayer;
+import org.saturnclient.saturnclient.config.AnimationConfig;
+import org.saturnclient.saturnclient.config.Theme;
+import org.saturnclient.ui2.SaturnScreen;
+import org.saturnclient.ui2.Utils;
+import org.saturnclient.ui2.anim.Fade;
+import org.saturnclient.ui2.anim.SlideY;
+import org.saturnclient.ui2.components.CosmeticPreview;
+import org.saturnclient.ui2.components.Sidebar;
+import org.saturnclient.ui2.elements.AnimationStagger;
+import org.saturnclient.ui2.elements.ImageTexture;
+import org.saturnclient.ui2.elements.Notification.NotificationKind;
+import org.saturnclient.ui2.elements.Scroll;
+import org.saturnclient.ui2.elements.TabMenu;
+import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
+import org.saturnclient.ui2.elements.Text;
+import org.saturnclient.ui2.resources.Fonts;
+import org.saturnclient.ui2.resources.Textures;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+
+public class HatStore extends SaturnScreen {
+ private long lastPurchaseTime = 0;
+
+ public HatStore() {
+ super("Hat Store");
+ }
+
+ public HatStore(long lastPurchaseTime) {
+ super("Hat Store");
+ this.lastPurchaseTime = lastPurchaseTime;
+ }
+
+ @Override
+ public void ui() {
+ int p = 10;
+ int gx = 10;
+ int gy = 20;
+
+ Scroll scroll = new Scroll(p);
+
+ int row = 0;
+ int col = 0;
+
+ SaturnPlayer player = SaturnPlayer.get();
+
+ AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
+
+ if (player != null) {
+ for (String hat : Hats.ALL_HATS) {
+ if (!Hats.availableHats.contains(hat)) {
+
+ int x = (50 + gx) * col;
+ int y = (50 + gy) * row;
+
+ stagger.draw(
+ new CosmeticPreview(
+ hat == player.hat,
+ Textures.getHatPreview(hat),
+ () -> handlePurchase(hat))
+ .dimensions(50, 50)
+ .position(x, y)
+ .animation(new SlideY(AnimationConfig.cosmeticsMenu, 16)));
+
+ String t = "50";
+
+ stagger.draw(
+ new Text(t)
+ .position(x + Fonts.centerX(50, t, Theme.FONT.value), y + 53)
+ .scale(0.5f)
+ .animation(new Fade(400)));
+
+ stagger.draw(
+ new ImageTexture(Textures.COINS)
+ .dimensions(16, 16)
+ .position(x + Fonts.getWidth(t, Theme.FONT.value) + 4, y + 52)
+ .scale(0.5f)
+ .animation(new Fade(400)));
+
+ if (col == 8) {
+ col = 0;
+ row++;
+ } else {
+ col++;
+ }
+ }
+ }
+ }
+
+ scroll.draw(stagger);
+
+ int scrollWidth = 480 + 40 + (gx * 2) + (p * 2);
+
+ draw(scroll
+ .dimensions(scrollWidth, 350)
+ .centerOffset(width, height, 15, 0));
+
+ draw(new Sidebar(5, this::close)
+ .centerOffset(width, height, -((scrollWidth - 30) / 2 + 20), 0)
+ .animation(new Fade(400)));
+
+ draw(new TabMenu(1,
+ new TabMenuComponent(Textures.CLOAK, () -> {
+ SaturnClient.client.setScreen(new CloakStore());
+ }),
+ new TabMenuComponent(Textures.HAT, () -> {
+ }))
+ .centerOffset(width, height, 0, -195));
+ }
+
+ private void handlePurchase(String hat) {
+ long now = System.currentTimeMillis();
+
+ if (now - lastPurchaseTime >= 3000) {
+ Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
+
+ ServiceClient.buyHat(hat);
+ lastPurchaseTime = now;
+
+ new Thread(() -> {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException ignored) {
+ }
+
+ RenderSystem.recordRenderCall(() -> {
+ SaturnClient.client.setScreen(new HatStore(now));
+ Utils.notify(NotificationKind.Success,
+ "Purchase complete",
+ "Congrats, enjoy your new hat!");
+ });
+ }).start();
+
+ } else {
+ Utils.notify(NotificationKind.Error,
+ "Timeout error",
+ "Please wait 3 seconds");
+ }
+ }
+}
diff --git a/versions/1.21.4/src/main/resources/assets/minecraft/font/fonts.json b/versions/1.21.4/src/main/resources/assets/minecraft/font/fonts.json
new file mode 100644
index 0000000..7606077
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/minecraft/font/fonts.json
@@ -0,0 +1,12 @@
+{
+ "providers": [
+ {
+ "type": "reference",
+ "id": "saturnclient:inter"
+ },
+ {
+ "type": "reference",
+ "id": "saturnclient:inter_bold"
+ }
+ ]
+}
diff --git a/versions/1.21.4/src/main/resources/assets/minecraft/font/include/default.json b/versions/1.21.4/src/main/resources/assets/minecraft/font/include/default.json
new file mode 100644
index 0000000..e3abe77
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/minecraft/font/include/default.json
@@ -0,0 +1,18 @@
+{
+ "providers": [
+ {
+ "type": "bitmap",
+ "file": "minecraft:font/custom/saturn.png",
+ "ascent": 7,
+ "height": 8,
+ "chars": ["\uE002"]
+ },
+ {
+ "type": "bitmap",
+ "file": "minecraft:font/custom/saturn_realistic.png",
+ "ascent": 7,
+ "height": 8,
+ "chars": ["\uE003"]
+ }
+ ]
+}
diff --git a/versions/1.21.4/src/main/resources/assets/minecraft/textures/font/custom/saturn.png b/versions/1.21.4/src/main/resources/assets/minecraft/textures/font/custom/saturn.png
new file mode 100644
index 0000000..f464196
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/minecraft/textures/font/custom/saturn.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/minecraft/textures/font/custom/saturn_realistic.png b/versions/1.21.4/src/main/resources/assets/minecraft/textures/font/custom/saturn_realistic.png
new file mode 100644
index 0000000..271937a
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/minecraft/textures/font/custom/saturn_realistic.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturn-client/icon.png b/versions/1.21.4/src/main/resources/assets/saturn-client/icon.png
deleted file mode 100644
index 13c6714..0000000
Binary files a/versions/1.21.4/src/main/resources/assets/saturn-client/icon.png and /dev/null differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/font/README.md b/versions/1.21.4/src/main/resources/assets/saturnclient/font/README.md
new file mode 100644
index 0000000..4af02e8
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/font/README.md
@@ -0,0 +1 @@
+Here is the font download: https://fonts.google.com/specimen/Inter
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter.json b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter.json
new file mode 100644
index 0000000..8ffca49
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter.json
@@ -0,0 +1,12 @@
+{
+ "providers": [
+ {
+ "type": "ttf",
+ "file": "saturnclient:inter.ttf",
+ "shift": [0, 0],
+ "size": 18,
+ "oversample": 12.0,
+ "skip_missing": true
+ }
+ ]
+}
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter.ttf b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter.ttf
new file mode 100644
index 0000000..acae361
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter.ttf differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter_bold.json b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter_bold.json
new file mode 100644
index 0000000..ff7a168
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter_bold.json
@@ -0,0 +1,12 @@
+{
+ "providers": [
+ {
+ "type": "ttf",
+ "file": "saturnclient:inter_bold.ttf",
+ "shift": [0, 0],
+ "size": 18,
+ "oversample": 8.0,
+ "skip_missing": true
+ }
+ ]
+}
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter_bold.ttf b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter_bold.ttf
new file mode 100644
index 0000000..cd13f60
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/font/inter_bold.ttf differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/icon.png b/versions/1.21.4/src/main/resources/assets/saturnclient/icon.png
new file mode 100644
index 0000000..39d03ec
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/icon.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/halo/model.json b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/halo/model.json
new file mode 100644
index 0000000..e1f2835
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/halo/model.json
@@ -0,0 +1,4 @@
+{
+ "translate": [0.0, 0.62, 0.0],
+ "scale": [0.2, 0.1, 0.2]
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/halo/model.obj b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/halo/model.obj
new file mode 100644
index 0000000..0289ac5
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/halo/model.obj
@@ -0,0 +1,2083 @@
+# Blender 4.0.2 OBJ File: ''
+# www.blender.org
+mtllib ../white.mtl
+o Torus
+v 1.250000 0.000000 0.000000
+v 1.216506 0.125000 0.000000
+v 1.125000 0.216506 0.000000
+v 1.000000 0.250000 0.000000
+v 0.875000 0.216506 0.000000
+v 0.783494 0.125000 0.000000
+v 0.750000 0.000000 0.000000
+v 0.783494 -0.125000 0.000000
+v 0.875000 -0.216506 0.000000
+v 1.000000 -0.250000 0.000000
+v 1.125000 -0.216506 0.000000
+v 1.216506 -0.125000 0.000000
+v 1.239306 0.000000 -0.163158
+v 1.206099 0.125000 -0.158786
+v 1.115376 0.216506 -0.146842
+v 0.991445 0.250000 -0.130526
+v 0.867514 0.216506 -0.114210
+v 0.776791 0.125000 -0.102266
+v 0.743584 0.000000 -0.097895
+v 0.776791 -0.125000 -0.102266
+v 0.867514 -0.216506 -0.114210
+v 0.991445 -0.250000 -0.130526
+v 1.115376 -0.216506 -0.146842
+v 1.206099 -0.125000 -0.158786
+v 1.207407 0.000000 -0.323524
+v 1.175055 0.125000 -0.314855
+v 1.086667 0.216506 -0.291171
+v 0.965926 0.250000 -0.258819
+v 0.845185 0.216506 -0.226467
+v 0.756797 0.125000 -0.202783
+v 0.724444 0.000000 -0.194114
+v 0.756797 -0.125000 -0.202783
+v 0.845185 -0.216506 -0.226467
+v 0.965926 -0.250000 -0.258819
+v 1.086667 -0.216506 -0.291171
+v 1.175055 -0.125000 -0.314855
+v 1.154849 0.000000 -0.478354
+v 1.123905 0.125000 -0.465537
+v 1.039364 0.216506 -0.430519
+v 0.923880 0.250000 -0.382683
+v 0.808395 0.216506 -0.334848
+v 0.723854 0.125000 -0.299830
+v 0.692910 0.000000 -0.287013
+v 0.723854 -0.125000 -0.299830
+v 0.808395 -0.216506 -0.334848
+v 0.923880 -0.250000 -0.382683
+v 1.039364 -0.216506 -0.430519
+v 1.123905 -0.125000 -0.465537
+v 1.082532 0.000000 -0.625000
+v 1.053525 0.125000 -0.608253
+v 0.974279 0.216506 -0.562500
+v 0.866025 0.250000 -0.500000
+v 0.757772 0.216506 -0.437500
+v 0.678525 0.125000 -0.391747
+v 0.649519 0.000000 -0.375000
+v 0.678525 -0.125000 -0.391747
+v 0.757772 -0.216506 -0.437500
+v 0.866025 -0.250000 -0.500000
+v 0.974279 -0.216506 -0.562500
+v 1.053525 -0.125000 -0.608253
+v 0.991692 0.000000 -0.760952
+v 0.965119 0.125000 -0.740562
+v 0.892523 0.216506 -0.684856
+v 0.793353 0.250000 -0.608761
+v 0.694184 0.216506 -0.532666
+v 0.621587 0.125000 -0.476961
+v 0.595015 0.000000 -0.456571
+v 0.621587 -0.125000 -0.476961
+v 0.694184 -0.216506 -0.532666
+v 0.793353 -0.250000 -0.608761
+v 0.892523 -0.216506 -0.684856
+v 0.965119 -0.125000 -0.740562
+v 0.883883 0.000000 -0.883884
+v 0.860200 0.125000 -0.860200
+v 0.795495 0.216506 -0.795495
+v 0.707107 0.250000 -0.707107
+v 0.618718 0.216506 -0.618719
+v 0.554014 0.125000 -0.554014
+v 0.530330 0.000000 -0.530330
+v 0.554014 -0.125000 -0.554014
+v 0.618718 -0.216506 -0.618719
+v 0.707107 -0.250000 -0.707107
+v 0.795495 -0.216506 -0.795495
+v 0.860200 -0.125000 -0.860200
+v 0.760952 0.000000 -0.991691
+v 0.740562 0.125000 -0.965119
+v 0.684857 0.216506 -0.892522
+v 0.608762 0.250000 -0.793353
+v 0.532666 0.216506 -0.694184
+v 0.476961 0.125000 -0.621587
+v 0.456571 0.000000 -0.595015
+v 0.476961 -0.125000 -0.621587
+v 0.532666 -0.216506 -0.694184
+v 0.608762 -0.250000 -0.793353
+v 0.684857 -0.216506 -0.892522
+v 0.740562 -0.125000 -0.965119
+v 0.625000 0.000000 -1.082532
+v 0.608253 0.125000 -1.053525
+v 0.562500 0.216506 -0.974279
+v 0.500000 0.250000 -0.866025
+v 0.437500 0.216506 -0.757772
+v 0.391747 0.125000 -0.678525
+v 0.375000 0.000000 -0.649519
+v 0.391747 -0.125000 -0.678525
+v 0.437500 -0.216506 -0.757772
+v 0.500000 -0.250000 -0.866025
+v 0.562500 -0.216506 -0.974279
+v 0.608253 -0.125000 -1.053525
+v 0.478355 0.000000 -1.154849
+v 0.465537 0.125000 -1.123905
+v 0.430519 0.216506 -1.039364
+v 0.382684 0.250000 -0.923879
+v 0.334848 0.216506 -0.808394
+v 0.299830 0.125000 -0.723854
+v 0.287013 0.000000 -0.692910
+v 0.299830 -0.125000 -0.723854
+v 0.334848 -0.216506 -0.808394
+v 0.382684 -0.250000 -0.923879
+v 0.430519 -0.216506 -1.039364
+v 0.465537 -0.125000 -1.123905
+v 0.323524 0.000000 -1.207407
+v 0.314855 0.125000 -1.175055
+v 0.291171 0.216506 -1.086667
+v 0.258819 0.250000 -0.965926
+v 0.226467 0.216506 -0.845185
+v 0.202783 0.125000 -0.756797
+v 0.194114 0.000000 -0.724444
+v 0.202783 -0.125000 -0.756797
+v 0.226467 -0.216506 -0.845185
+v 0.258819 -0.250000 -0.965926
+v 0.291171 -0.216506 -1.086667
+v 0.314855 -0.125000 -1.175055
+v 0.163158 0.000000 -1.239306
+v 0.158786 0.125000 -1.206099
+v 0.146842 0.216506 -1.115376
+v 0.130526 0.250000 -0.991445
+v 0.114210 0.216506 -0.867514
+v 0.102266 0.125000 -0.776791
+v 0.097895 0.000000 -0.743584
+v 0.102266 -0.125000 -0.776791
+v 0.114210 -0.216506 -0.867514
+v 0.130526 -0.250000 -0.991445
+v 0.146842 -0.216506 -1.115376
+v 0.158786 -0.125000 -1.206099
+v 0.000000 0.000000 -1.250000
+v 0.000000 0.125000 -1.216506
+v 0.000000 0.216506 -1.125000
+v 0.000000 0.250000 -1.000000
+v 0.000000 0.216506 -0.875000
+v 0.000000 0.125000 -0.783494
+v 0.000000 0.000000 -0.750000
+v 0.000000 -0.125000 -0.783494
+v 0.000000 -0.216506 -0.875000
+v 0.000000 -0.250000 -1.000000
+v 0.000000 -0.216506 -1.125000
+v 0.000000 -0.125000 -1.216506
+v -0.163158 0.000000 -1.239306
+v -0.158786 0.125000 -1.206099
+v -0.146842 0.216506 -1.115375
+v -0.130526 0.250000 -0.991445
+v -0.114211 0.216506 -0.867514
+v -0.102267 0.125000 -0.776791
+v -0.097895 0.000000 -0.743584
+v -0.102267 -0.125000 -0.776791
+v -0.114211 -0.216506 -0.867514
+v -0.130526 -0.250000 -0.991445
+v -0.146842 -0.216506 -1.115375
+v -0.158786 -0.125000 -1.206099
+v -0.323524 0.000000 -1.207407
+v -0.314855 0.125000 -1.175055
+v -0.291171 0.216506 -1.086667
+v -0.258819 0.250000 -0.965926
+v -0.226467 0.216506 -0.845185
+v -0.202783 0.125000 -0.756797
+v -0.194114 0.000000 -0.724444
+v -0.202783 -0.125000 -0.756797
+v -0.226467 -0.216506 -0.845185
+v -0.258819 -0.250000 -0.965926
+v -0.291171 -0.216506 -1.086667
+v -0.314855 -0.125000 -1.175055
+v -0.478354 0.000000 -1.154849
+v -0.465537 0.125000 -1.123905
+v -0.430519 0.216506 -1.039364
+v -0.382684 0.250000 -0.923880
+v -0.334848 0.216506 -0.808395
+v -0.299830 0.125000 -0.723854
+v -0.287013 0.000000 -0.692910
+v -0.299830 -0.125000 -0.723854
+v -0.334848 -0.216506 -0.808395
+v -0.382684 -0.250000 -0.923880
+v -0.430519 -0.216506 -1.039364
+v -0.465537 -0.125000 -1.123905
+v -0.625000 0.000000 -1.082532
+v -0.608253 0.125000 -1.053526
+v -0.562500 0.216506 -0.974279
+v -0.500000 0.250000 -0.866026
+v -0.437500 0.216506 -0.757772
+v -0.391747 0.125000 -0.678525
+v -0.375000 0.000000 -0.649519
+v -0.391747 -0.125000 -0.678525
+v -0.437500 -0.216506 -0.757772
+v -0.500000 -0.250000 -0.866026
+v -0.562500 -0.216506 -0.974279
+v -0.608253 -0.125000 -1.053526
+v -0.760952 0.000000 -0.991692
+v -0.740562 0.125000 -0.965119
+v -0.684857 0.216506 -0.892522
+v -0.608761 0.250000 -0.793353
+v -0.532666 0.216506 -0.694184
+v -0.476961 0.125000 -0.621587
+v -0.456571 0.000000 -0.595015
+v -0.476961 -0.125000 -0.621587
+v -0.532666 -0.216506 -0.694184
+v -0.608761 -0.250000 -0.793353
+v -0.684857 -0.216506 -0.892522
+v -0.740562 -0.125000 -0.965119
+v -0.883884 0.000000 -0.883883
+v -0.860200 0.125000 -0.860200
+v -0.795495 0.216506 -0.795495
+v -0.707107 0.250000 -0.707107
+v -0.618719 0.216506 -0.618718
+v -0.554014 0.125000 -0.554013
+v -0.530330 0.000000 -0.530330
+v -0.554014 -0.125000 -0.554013
+v -0.618719 -0.216506 -0.618718
+v -0.707107 -0.250000 -0.707107
+v -0.795495 -0.216506 -0.795495
+v -0.860200 -0.125000 -0.860200
+v -0.991692 0.000000 -0.760952
+v -0.965119 0.125000 -0.740562
+v -0.892522 0.216506 -0.684857
+v -0.793353 0.250000 -0.608761
+v -0.694184 0.216506 -0.532666
+v -0.621587 0.125000 -0.476961
+v -0.595015 0.000000 -0.456571
+v -0.621587 -0.125000 -0.476961
+v -0.694184 -0.216506 -0.532666
+v -0.793353 -0.250000 -0.608761
+v -0.892522 -0.216506 -0.684857
+v -0.965119 -0.125000 -0.740562
+v -1.082532 0.000000 -0.625000
+v -1.053525 0.125000 -0.608253
+v -0.974278 0.216506 -0.562500
+v -0.866025 0.250000 -0.500000
+v -0.757772 0.216506 -0.437500
+v -0.678525 0.125000 -0.391747
+v -0.649519 0.000000 -0.375000
+v -0.678525 -0.125000 -0.391747
+v -0.757772 -0.216506 -0.437500
+v -0.866025 -0.250000 -0.500000
+v -0.974278 -0.216506 -0.562500
+v -1.053525 -0.125000 -0.608253
+v -1.154849 0.000000 -0.478354
+v -1.123905 0.125000 -0.465537
+v -1.039364 0.216506 -0.430519
+v -0.923880 0.250000 -0.382683
+v -0.808395 0.216506 -0.334848
+v -0.723854 0.125000 -0.299830
+v -0.692910 0.000000 -0.287013
+v -0.723854 -0.125000 -0.299830
+v -0.808395 -0.216506 -0.334848
+v -0.923880 -0.250000 -0.382683
+v -1.039364 -0.216506 -0.430519
+v -1.123905 -0.125000 -0.465537
+v -1.207407 0.000000 -0.323524
+v -1.175055 0.125000 -0.314855
+v -1.086667 0.216506 -0.291171
+v -0.965926 0.250000 -0.258819
+v -0.845185 0.216506 -0.226467
+v -0.756797 0.125000 -0.202783
+v -0.724444 0.000000 -0.194114
+v -0.756797 -0.125000 -0.202783
+v -0.845185 -0.216506 -0.226467
+v -0.965926 -0.250000 -0.258819
+v -1.086667 -0.216506 -0.291171
+v -1.175055 -0.125000 -0.314855
+v -1.239306 0.000000 -0.163158
+v -1.206099 0.125000 -0.158786
+v -1.115375 0.216506 -0.146842
+v -0.991445 0.250000 -0.130526
+v -0.867514 0.216506 -0.114211
+v -0.776791 0.125000 -0.102267
+v -0.743584 0.000000 -0.097895
+v -0.776791 -0.125000 -0.102267
+v -0.867514 -0.216506 -0.114211
+v -0.991445 -0.250000 -0.130526
+v -1.115375 -0.216506 -0.146842
+v -1.206099 -0.125000 -0.158786
+v -1.250000 0.000000 -0.000000
+v -1.216506 0.125000 -0.000000
+v -1.125000 0.216506 -0.000000
+v -1.000000 0.250000 -0.000000
+v -0.875000 0.216506 -0.000000
+v -0.783494 0.125000 -0.000000
+v -0.750000 0.000000 -0.000000
+v -0.783494 -0.125000 -0.000000
+v -0.875000 -0.216506 -0.000000
+v -1.000000 -0.250000 -0.000000
+v -1.125000 -0.216506 -0.000000
+v -1.216506 -0.125000 -0.000000
+v -1.239306 0.000000 0.163158
+v -1.206099 0.125000 0.158786
+v -1.115375 0.216506 0.146842
+v -0.991445 0.250000 0.130526
+v -0.867514 0.216506 0.114211
+v -0.776791 0.125000 0.102267
+v -0.743584 0.000000 0.097895
+v -0.776791 -0.125000 0.102267
+v -0.867514 -0.216506 0.114211
+v -0.991445 -0.250000 0.130526
+v -1.115375 -0.216506 0.146842
+v -1.206099 -0.125000 0.158786
+v -1.207407 0.000000 0.323524
+v -1.175055 0.125000 0.314855
+v -1.086667 0.216506 0.291171
+v -0.965926 0.250000 0.258819
+v -0.845185 0.216506 0.226467
+v -0.756797 0.125000 0.202783
+v -0.724444 0.000000 0.194114
+v -0.756797 -0.125000 0.202783
+v -0.845185 -0.216506 0.226467
+v -0.965926 -0.250000 0.258819
+v -1.086667 -0.216506 0.291171
+v -1.175055 -0.125000 0.314855
+v -1.154850 0.000000 0.478354
+v -1.123906 0.125000 0.465536
+v -1.039365 0.216506 0.430518
+v -0.923880 0.250000 0.382683
+v -0.808395 0.216506 0.334848
+v -0.723854 0.125000 0.299830
+v -0.692910 0.000000 0.287012
+v -0.723854 -0.125000 0.299830
+v -0.808395 -0.216506 0.334848
+v -0.923880 -0.250000 0.382683
+v -1.039365 -0.216506 0.430518
+v -1.123906 -0.125000 0.465536
+v -1.082532 0.000000 0.625000
+v -1.053526 0.125000 0.608253
+v -0.974279 0.216506 0.562500
+v -0.866026 0.250000 0.500000
+v -0.757772 0.216506 0.437500
+v -0.678525 0.125000 0.391747
+v -0.649519 0.000000 0.375000
+v -0.678525 -0.125000 0.391747
+v -0.757772 -0.216506 0.437500
+v -0.866026 -0.250000 0.500000
+v -0.974279 -0.216506 0.562500
+v -1.053526 -0.125000 0.608253
+v -0.991692 0.000000 0.760952
+v -0.965119 0.125000 0.740562
+v -0.892522 0.216506 0.684857
+v -0.793353 0.250000 0.608761
+v -0.694184 0.216506 0.532666
+v -0.621587 0.125000 0.476961
+v -0.595015 0.000000 0.456571
+v -0.621587 -0.125000 0.476961
+v -0.694184 -0.216506 0.532666
+v -0.793353 -0.250000 0.608761
+v -0.892522 -0.216506 0.684857
+v -0.965119 -0.125000 0.740562
+v -0.883884 0.000000 0.883883
+v -0.860200 0.125000 0.860200
+v -0.795495 0.216506 0.795495
+v -0.707107 0.250000 0.707107
+v -0.618719 0.216506 0.618718
+v -0.554014 0.125000 0.554013
+v -0.530330 0.000000 0.530330
+v -0.554014 -0.125000 0.554013
+v -0.618719 -0.216506 0.618718
+v -0.707107 -0.250000 0.707107
+v -0.795495 -0.216506 0.795495
+v -0.860200 -0.125000 0.860200
+v -0.760952 0.000000 0.991691
+v -0.740563 0.125000 0.965119
+v -0.684857 0.216506 0.892522
+v -0.608762 0.250000 0.793353
+v -0.532667 0.216506 0.694184
+v -0.476961 0.125000 0.621587
+v -0.456571 0.000000 0.595015
+v -0.476961 -0.125000 0.621587
+v -0.532667 -0.216506 0.694184
+v -0.608762 -0.250000 0.793353
+v -0.684857 -0.216506 0.892522
+v -0.740563 -0.125000 0.965119
+v -0.625000 0.000000 1.082532
+v -0.608253 0.125000 1.053526
+v -0.562500 0.216506 0.974279
+v -0.500000 0.250000 0.866026
+v -0.437500 0.216506 0.757772
+v -0.391747 0.125000 0.678525
+v -0.375000 0.000000 0.649519
+v -0.391747 -0.125000 0.678525
+v -0.437500 -0.216506 0.757772
+v -0.500000 -0.250000 0.866026
+v -0.562500 -0.216506 0.974279
+v -0.608253 -0.125000 1.053526
+v -0.478354 0.000000 1.154849
+v -0.465537 0.125000 1.123905
+v -0.430519 0.216506 1.039364
+v -0.382684 0.250000 0.923880
+v -0.334848 0.216506 0.808395
+v -0.299830 0.125000 0.723854
+v -0.287013 0.000000 0.692910
+v -0.299830 -0.125000 0.723854
+v -0.334848 -0.216506 0.808395
+v -0.382684 -0.250000 0.923880
+v -0.430519 -0.216506 1.039364
+v -0.465537 -0.125000 1.123905
+v -0.323524 0.000000 1.207407
+v -0.314855 0.125000 1.175055
+v -0.291172 0.216506 1.086666
+v -0.258819 0.250000 0.965926
+v -0.226467 0.216506 0.845185
+v -0.202783 0.125000 0.756797
+v -0.194115 0.000000 0.724444
+v -0.202783 -0.125000 0.756797
+v -0.226467 -0.216506 0.845185
+v -0.258819 -0.250000 0.965926
+v -0.291172 -0.216506 1.086666
+v -0.314855 -0.125000 1.175055
+v -0.163158 0.000000 1.239306
+v -0.158787 0.125000 1.206099
+v -0.146843 0.216506 1.115375
+v -0.130527 0.250000 0.991445
+v -0.114211 0.216506 0.867514
+v -0.102267 0.125000 0.776791
+v -0.097895 0.000000 0.743584
+v -0.102267 -0.125000 0.776791
+v -0.114211 -0.216506 0.867514
+v -0.130527 -0.250000 0.991445
+v -0.146843 -0.216506 1.115375
+v -0.158787 -0.125000 1.206099
+v 0.000000 0.000000 1.250000
+v 0.000000 0.125000 1.216506
+v 0.000000 0.216506 1.125000
+v 0.000000 0.250000 1.000000
+v 0.000000 0.216506 0.875000
+v 0.000000 0.125000 0.783494
+v 0.000000 0.000000 0.750000
+v 0.000000 -0.125000 0.783494
+v 0.000000 -0.216506 0.875000
+v 0.000000 -0.250000 1.000000
+v 0.000000 -0.216506 1.125000
+v 0.000000 -0.125000 1.216506
+v 0.163158 0.000000 1.239306
+v 0.158786 0.125000 1.206099
+v 0.146842 0.216506 1.115376
+v 0.130526 0.250000 0.991445
+v 0.114210 0.216506 0.867514
+v 0.102266 0.125000 0.776791
+v 0.097895 0.000000 0.743584
+v 0.102266 -0.125000 0.776791
+v 0.114210 -0.216506 0.867514
+v 0.130526 -0.250000 0.991445
+v 0.146842 -0.216506 1.115376
+v 0.158786 -0.125000 1.206099
+v 0.323523 0.000000 1.207407
+v 0.314854 0.125000 1.175055
+v 0.291171 0.216506 1.086667
+v 0.258819 0.250000 0.965926
+v 0.226466 0.216506 0.845185
+v 0.202783 0.125000 0.756797
+v 0.194114 0.000000 0.724444
+v 0.202783 -0.125000 0.756797
+v 0.226466 -0.216506 0.845185
+v 0.258819 -0.250000 0.965926
+v 0.291171 -0.216506 1.086667
+v 0.314854 -0.125000 1.175055
+v 0.478355 0.000000 1.154849
+v 0.465537 0.125000 1.123905
+v 0.430519 0.216506 1.039364
+v 0.382684 0.250000 0.923879
+v 0.334848 0.216506 0.808394
+v 0.299830 0.125000 0.723854
+v 0.287013 0.000000 0.692910
+v 0.299830 -0.125000 0.723854
+v 0.334848 -0.216506 0.808394
+v 0.382684 -0.250000 0.923879
+v 0.430519 -0.216506 1.039364
+v 0.465537 -0.125000 1.123905
+v 0.625000 0.000000 1.082532
+v 0.608253 0.125000 1.053525
+v 0.562500 0.216506 0.974279
+v 0.500000 0.250000 0.866025
+v 0.437500 0.216506 0.757772
+v 0.391747 0.125000 0.678525
+v 0.375000 0.000000 0.649519
+v 0.391747 -0.125000 0.678525
+v 0.437500 -0.216506 0.757772
+v 0.500000 -0.250000 0.866025
+v 0.562500 -0.216506 0.974279
+v 0.608253 -0.125000 1.053525
+v 0.760952 0.000000 0.991692
+v 0.740562 0.125000 0.965120
+v 0.684856 0.216506 0.892523
+v 0.608761 0.250000 0.793353
+v 0.532666 0.216506 0.694184
+v 0.476961 0.125000 0.621587
+v 0.456571 0.000000 0.595015
+v 0.476961 -0.125000 0.621587
+v 0.532666 -0.216506 0.694184
+v 0.608761 -0.250000 0.793353
+v 0.684856 -0.216506 0.892523
+v 0.740562 -0.125000 0.965120
+v 0.883883 0.000000 0.883884
+v 0.860199 0.125000 0.860200
+v 0.795495 0.216506 0.795496
+v 0.707106 0.250000 0.707107
+v 0.618718 0.216506 0.618719
+v 0.554013 0.125000 0.554014
+v 0.530330 0.000000 0.530330
+v 0.554013 -0.125000 0.554014
+v 0.618718 -0.216506 0.618719
+v 0.707106 -0.250000 0.707107
+v 0.795495 -0.216506 0.795496
+v 0.860199 -0.125000 0.860200
+v 0.991692 0.000000 0.760952
+v 0.965119 0.125000 0.740562
+v 0.892523 0.216506 0.684856
+v 0.793353 0.250000 0.608761
+v 0.694184 0.216506 0.532666
+v 0.621587 0.125000 0.476961
+v 0.595015 0.000000 0.456571
+v 0.621587 -0.125000 0.476961
+v 0.694184 -0.216506 0.532666
+v 0.793353 -0.250000 0.608761
+v 0.892523 -0.216506 0.684856
+v 0.965119 -0.125000 0.740562
+v 1.082532 0.000000 0.625000
+v 1.053525 0.125000 0.608253
+v 0.974279 0.216506 0.562500
+v 0.866025 0.250000 0.500000
+v 0.757772 0.216506 0.437500
+v 0.678525 0.125000 0.391747
+v 0.649519 0.000000 0.375000
+v 0.678525 -0.125000 0.391747
+v 0.757772 -0.216506 0.437500
+v 0.866025 -0.250000 0.500000
+v 0.974279 -0.216506 0.562500
+v 1.053525 -0.125000 0.608253
+v 1.154849 0.000000 0.478355
+v 1.123905 0.125000 0.465537
+v 1.039364 0.216506 0.430519
+v 0.923879 0.250000 0.382684
+v 0.808394 0.216506 0.334848
+v 0.723854 0.125000 0.299830
+v 0.692910 0.000000 0.287013
+v 0.723854 -0.125000 0.299830
+v 0.808394 -0.216506 0.334848
+v 0.923879 -0.250000 0.382684
+v 1.039364 -0.216506 0.430519
+v 1.123905 -0.125000 0.465537
+v 1.207407 0.000000 0.323523
+v 1.175055 0.125000 0.314855
+v 1.086667 0.216506 0.291171
+v 0.965926 0.250000 0.258819
+v 0.845185 0.216506 0.226466
+v 0.756797 0.125000 0.202783
+v 0.724444 0.000000 0.194114
+v 0.756797 -0.125000 0.202783
+v 0.845185 -0.216506 0.226466
+v 0.965926 -0.250000 0.258819
+v 1.086667 -0.216506 0.291171
+v 1.175055 -0.125000 0.314855
+v 1.239306 0.000000 0.163158
+v 1.206099 0.125000 0.158786
+v 1.115376 0.216506 0.146842
+v 0.991445 0.250000 0.130526
+v 0.867514 0.216506 0.114210
+v 0.776791 0.125000 0.102266
+v 0.743584 0.000000 0.097895
+v 0.776791 -0.125000 0.102266
+v 0.867514 -0.216506 0.114210
+v 0.991445 -0.250000 0.130526
+v 1.115376 -0.216506 0.146842
+v 1.206099 -0.125000 0.158786
+vn 0.9640 0.2583 -0.0632
+vn 0.7063 0.7063 -0.0463
+vn 0.2588 0.9658 -0.0170
+vn -0.2588 0.9658 0.0170
+vn -0.7063 0.7063 0.0463
+vn -0.9640 0.2583 0.0632
+vn -0.9640 -0.2583 0.0632
+vn -0.7063 -0.7063 0.0463
+vn -0.2588 -0.9658 0.0170
+vn 0.2588 -0.9658 -0.0170
+vn 0.7063 -0.7063 -0.0463
+vn 0.9640 -0.2583 -0.0632
+vn 0.9475 0.2583 -0.1885
+vn 0.6943 0.7063 -0.1381
+vn 0.2544 0.9658 -0.0506
+vn -0.2544 0.9658 0.0506
+vn -0.6943 0.7063 0.1381
+vn -0.9475 0.2583 0.1885
+vn -0.9475 -0.2583 0.1885
+vn -0.6943 -0.7063 0.1381
+vn -0.2544 -0.9658 0.0506
+vn 0.2544 -0.9658 -0.0506
+vn 0.6943 -0.7063 -0.1381
+vn 0.9475 -0.2583 -0.1885
+vn 0.9148 0.2583 -0.3105
+vn 0.6703 0.7063 -0.2275
+vn 0.2456 0.9658 -0.0834
+vn -0.2456 0.9658 0.0834
+vn -0.6703 0.7063 0.2275
+vn -0.9148 0.2583 0.3105
+vn -0.9148 -0.2583 0.3105
+vn -0.6703 -0.7063 0.2275
+vn -0.2456 -0.9658 0.0834
+vn 0.2456 -0.9658 -0.0834
+vn 0.6703 -0.7063 -0.2275
+vn 0.9148 -0.2583 -0.3105
+vn 0.8664 0.2583 -0.4273
+vn 0.6349 0.7063 -0.3131
+vn 0.2326 0.9658 -0.1147
+vn -0.2326 0.9658 0.1147
+vn -0.6349 0.7063 0.3131
+vn -0.8664 0.2583 0.4273
+vn -0.8664 -0.2583 0.4273
+vn -0.6349 -0.7063 0.3131
+vn -0.2326 -0.9658 0.1147
+vn 0.2326 -0.9658 -0.1147
+vn 0.6349 -0.7063 -0.3131
+vn 0.8664 -0.2583 -0.4273
+vn 0.8033 0.2583 -0.5367
+vn 0.5886 0.7063 -0.3933
+vn 0.2156 0.9658 -0.1441
+vn -0.2156 0.9658 0.1441
+vn -0.5886 0.7063 0.3933
+vn -0.8033 0.2583 0.5367
+vn -0.8033 -0.2583 0.5367
+vn -0.5886 -0.7063 0.3933
+vn -0.2156 -0.9658 0.1441
+vn 0.2156 -0.9658 -0.1441
+vn 0.5886 -0.7063 -0.3933
+vn 0.8033 -0.2583 -0.5367
+vn 0.7263 0.2583 -0.6370
+vn 0.5322 0.7063 -0.4667
+vn 0.1950 0.9658 -0.1710
+vn -0.1950 0.9658 0.1710
+vn -0.5322 0.7063 0.4667
+vn -0.7263 0.2583 0.6370
+vn -0.7263 -0.2583 0.6370
+vn -0.5322 -0.7063 0.4667
+vn -0.1950 -0.9658 0.1710
+vn 0.1950 -0.9658 -0.1710
+vn 0.5322 -0.7063 -0.4667
+vn 0.7263 -0.2583 -0.6370
+vn 0.6370 0.2583 -0.7263
+vn 0.4667 0.7063 -0.5322
+vn 0.1710 0.9658 -0.1950
+vn -0.1710 0.9658 0.1950
+vn -0.4667 0.7063 0.5322
+vn -0.6370 0.2583 0.7263
+vn -0.6370 -0.2583 0.7263
+vn -0.4667 -0.7063 0.5322
+vn -0.1710 -0.9658 0.1950
+vn 0.1710 -0.9658 -0.1950
+vn 0.4667 -0.7063 -0.5322
+vn 0.6370 -0.2583 -0.7263
+vn 0.5367 0.2583 -0.8033
+vn 0.3933 0.7063 -0.5886
+vn 0.1441 0.9658 -0.2156
+vn -0.1441 0.9658 0.2156
+vn -0.3933 0.7063 0.5886
+vn -0.5367 0.2583 0.8033
+vn -0.5367 -0.2583 0.8033
+vn -0.3933 -0.7063 0.5886
+vn -0.1441 -0.9658 0.2156
+vn 0.1441 -0.9658 -0.2156
+vn 0.3933 -0.7063 -0.5886
+vn 0.5367 -0.2583 -0.8033
+vn 0.4273 0.2583 -0.8664
+vn 0.3131 0.7063 -0.6349
+vn 0.1147 0.9658 -0.2326
+vn -0.1147 0.9658 0.2326
+vn -0.3131 0.7063 0.6349
+vn -0.4273 0.2583 0.8664
+vn -0.4273 -0.2583 0.8664
+vn -0.3131 -0.7063 0.6349
+vn -0.1147 -0.9658 0.2326
+vn 0.1147 -0.9658 -0.2326
+vn 0.3131 -0.7063 -0.6349
+vn 0.4273 -0.2583 -0.8664
+vn 0.3105 0.2583 -0.9148
+vn 0.2275 0.7063 -0.6703
+vn 0.0834 0.9658 -0.2456
+vn -0.0834 0.9658 0.2456
+vn -0.2275 0.7063 0.6703
+vn -0.3105 0.2583 0.9148
+vn -0.3105 -0.2583 0.9148
+vn -0.2275 -0.7063 0.6703
+vn -0.0834 -0.9658 0.2456
+vn 0.0834 -0.9658 -0.2456
+vn 0.2275 -0.7063 -0.6703
+vn 0.3105 -0.2583 -0.9148
+vn 0.1885 0.2583 -0.9475
+vn 0.1381 0.7063 -0.6943
+vn 0.0506 0.9658 -0.2544
+vn -0.0506 0.9658 0.2544
+vn -0.1381 0.7063 0.6943
+vn -0.1885 0.2583 0.9475
+vn -0.1885 -0.2583 0.9475
+vn -0.1381 -0.7063 0.6943
+vn -0.0506 -0.9658 0.2544
+vn 0.0506 -0.9658 -0.2544
+vn 0.1381 -0.7063 -0.6943
+vn 0.1885 -0.2583 -0.9475
+vn 0.0632 0.2583 -0.9640
+vn 0.0463 0.7063 -0.7063
+vn 0.0170 0.9658 -0.2588
+vn -0.0170 0.9658 0.2588
+vn -0.0463 0.7063 0.7063
+vn -0.0632 0.2583 0.9640
+vn -0.0632 -0.2583 0.9640
+vn -0.0463 -0.7063 0.7063
+vn -0.0170 -0.9658 0.2588
+vn 0.0170 -0.9658 -0.2588
+vn 0.0463 -0.7063 -0.7063
+vn 0.0632 -0.2583 -0.9640
+vn -0.0632 0.2583 -0.9640
+vn -0.0463 0.7063 -0.7063
+vn -0.0170 0.9658 -0.2588
+vn 0.0170 0.9658 0.2588
+vn 0.0463 0.7063 0.7063
+vn 0.0632 0.2583 0.9640
+vn 0.0632 -0.2583 0.9640
+vn 0.0463 -0.7063 0.7063
+vn 0.0170 -0.9658 0.2588
+vn -0.0170 -0.9658 -0.2588
+vn -0.0463 -0.7063 -0.7063
+vn -0.0632 -0.2583 -0.9640
+vn -0.1885 0.2583 -0.9475
+vn -0.1381 0.7063 -0.6943
+vn -0.0506 0.9658 -0.2544
+vn 0.0506 0.9658 0.2544
+vn 0.1381 0.7063 0.6943
+vn 0.1885 0.2583 0.9475
+vn 0.1885 -0.2583 0.9475
+vn 0.1381 -0.7063 0.6943
+vn 0.0506 -0.9658 0.2544
+vn -0.0506 -0.9658 -0.2544
+vn -0.1381 -0.7063 -0.6943
+vn -0.1885 -0.2583 -0.9475
+vn -0.3105 0.2583 -0.9148
+vn -0.2275 0.7063 -0.6703
+vn -0.0834 0.9658 -0.2456
+vn 0.0834 0.9658 0.2456
+vn 0.2275 0.7063 0.6703
+vn 0.3105 0.2583 0.9148
+vn 0.3105 -0.2583 0.9148
+vn 0.2275 -0.7063 0.6703
+vn 0.0834 -0.9658 0.2456
+vn -0.0834 -0.9658 -0.2456
+vn -0.2275 -0.7063 -0.6703
+vn -0.3105 -0.2583 -0.9148
+vn -0.4273 0.2583 -0.8664
+vn -0.3131 0.7063 -0.6349
+vn -0.1147 0.9658 -0.2326
+vn 0.1147 0.9658 0.2326
+vn 0.3131 0.7063 0.6349
+vn 0.4273 0.2583 0.8664
+vn 0.4273 -0.2583 0.8664
+vn 0.3131 -0.7063 0.6349
+vn 0.1147 -0.9658 0.2326
+vn -0.1147 -0.9658 -0.2326
+vn -0.3131 -0.7063 -0.6349
+vn -0.4273 -0.2583 -0.8664
+vn -0.5367 0.2583 -0.8033
+vn -0.3933 0.7063 -0.5886
+vn -0.1441 0.9658 -0.2156
+vn 0.1441 0.9658 0.2156
+vn 0.3933 0.7063 0.5886
+vn 0.5367 0.2583 0.8033
+vn 0.5367 -0.2583 0.8033
+vn 0.3933 -0.7063 0.5886
+vn 0.1441 -0.9658 0.2156
+vn -0.1441 -0.9658 -0.2156
+vn -0.3933 -0.7063 -0.5886
+vn -0.5367 -0.2583 -0.8033
+vn -0.6370 0.2583 -0.7263
+vn -0.4667 0.7063 -0.5322
+vn -0.1710 0.9658 -0.1950
+vn 0.1710 0.9658 0.1950
+vn 0.4667 0.7063 0.5322
+vn 0.6370 0.2583 0.7263
+vn 0.6370 -0.2583 0.7263
+vn 0.4667 -0.7063 0.5322
+vn 0.1710 -0.9658 0.1950
+vn -0.1710 -0.9658 -0.1950
+vn -0.4667 -0.7063 -0.5322
+vn -0.6370 -0.2583 -0.7263
+vn -0.7263 0.2583 -0.6370
+vn -0.5322 0.7063 -0.4667
+vn -0.1950 0.9658 -0.1710
+vn 0.1950 0.9658 0.1710
+vn 0.5322 0.7063 0.4667
+vn 0.7263 0.2583 0.6370
+vn 0.7263 -0.2583 0.6370
+vn 0.5322 -0.7063 0.4667
+vn 0.1950 -0.9658 0.1710
+vn -0.1950 -0.9658 -0.1710
+vn -0.5322 -0.7063 -0.4667
+vn -0.7263 -0.2583 -0.6370
+vn -0.8033 0.2583 -0.5367
+vn -0.5886 0.7063 -0.3933
+vn -0.2156 0.9658 -0.1441
+vn 0.2156 0.9658 0.1441
+vn 0.5886 0.7063 0.3933
+vn 0.8033 0.2583 0.5367
+vn 0.8033 -0.2583 0.5367
+vn 0.5886 -0.7063 0.3933
+vn 0.2156 -0.9658 0.1441
+vn -0.2156 -0.9658 -0.1441
+vn -0.5886 -0.7063 -0.3933
+vn -0.8033 -0.2583 -0.5367
+vn -0.8664 0.2583 -0.4273
+vn -0.6349 0.7063 -0.3131
+vn -0.2326 0.9658 -0.1147
+vn 0.2326 0.9658 0.1147
+vn 0.6349 0.7063 0.3131
+vn 0.8664 0.2583 0.4273
+vn 0.8664 -0.2583 0.4273
+vn 0.6349 -0.7063 0.3131
+vn 0.2326 -0.9658 0.1147
+vn -0.2326 -0.9658 -0.1147
+vn -0.6349 -0.7063 -0.3131
+vn -0.8664 -0.2583 -0.4273
+vn -0.9148 0.2583 -0.3105
+vn -0.6703 0.7063 -0.2275
+vn -0.2456 0.9658 -0.0834
+vn 0.2456 0.9658 0.0834
+vn 0.6703 0.7063 0.2275
+vn 0.9148 0.2583 0.3105
+vn 0.9148 -0.2583 0.3105
+vn 0.6703 -0.7063 0.2275
+vn 0.2456 -0.9658 0.0834
+vn -0.2456 -0.9658 -0.0834
+vn -0.6703 -0.7063 -0.2275
+vn -0.9148 -0.2583 -0.3105
+vn -0.9475 0.2583 -0.1885
+vn -0.6943 0.7063 -0.1381
+vn -0.2544 0.9658 -0.0506
+vn 0.2544 0.9658 0.0506
+vn 0.6943 0.7063 0.1381
+vn 0.9475 0.2583 0.1885
+vn 0.9475 -0.2583 0.1885
+vn 0.6943 -0.7063 0.1381
+vn 0.2544 -0.9658 0.0506
+vn -0.2544 -0.9658 -0.0506
+vn -0.6943 -0.7063 -0.1381
+vn -0.9475 -0.2583 -0.1885
+vn -0.9640 0.2583 -0.0632
+vn -0.7063 0.7063 -0.0463
+vn -0.2588 0.9658 -0.0170
+vn 0.2588 0.9658 0.0170
+vn 0.7063 0.7063 0.0463
+vn 0.9640 0.2583 0.0632
+vn 0.9640 -0.2583 0.0632
+vn 0.7063 -0.7063 0.0463
+vn 0.2588 -0.9658 0.0170
+vn -0.2588 -0.9658 -0.0170
+vn -0.7063 -0.7063 -0.0463
+vn -0.9640 -0.2583 -0.0632
+vt 0.500000 0.500000
+vt 0.520833 0.500000
+vt 0.520833 0.583333
+vt 0.500000 0.583333
+vt 0.520833 0.666667
+vt 0.500000 0.666667
+vt 0.520833 0.750000
+vt 0.500000 0.750000
+vt 0.520833 0.833333
+vt 0.500000 0.833333
+vt 0.520833 0.916667
+vt 0.500000 0.916667
+vt 0.520833 1.000000
+vt 0.500000 1.000000
+vt 0.500000 0.000000
+vt 0.520833 0.000000
+vt 0.520833 0.083333
+vt 0.500000 0.083333
+vt 0.520833 0.166667
+vt 0.500000 0.166667
+vt 0.520833 0.250000
+vt 0.500000 0.250000
+vt 0.520833 0.333333
+vt 0.500000 0.333333
+vt 0.520833 0.416667
+vt 0.500000 0.416667
+vt 0.541667 0.500000
+vt 0.541667 0.583333
+vt 0.541667 0.666667
+vt 0.541667 0.750000
+vt 0.541667 0.833333
+vt 0.541667 0.916667
+vt 0.541667 1.000000
+vt 0.541667 0.000000
+vt 0.541667 0.083333
+vt 0.541667 0.166667
+vt 0.541667 0.250000
+vt 0.541667 0.333333
+vt 0.541667 0.416667
+vt 0.562500 0.500000
+vt 0.562500 0.583333
+vt 0.562500 0.666667
+vt 0.562500 0.750000
+vt 0.562500 0.833333
+vt 0.562500 0.916667
+vt 0.562500 1.000000
+vt 0.562500 0.000000
+vt 0.562500 0.083333
+vt 0.562500 0.166667
+vt 0.562500 0.250000
+vt 0.562500 0.333333
+vt 0.562500 0.416667
+vt 0.583333 0.500000
+vt 0.583333 0.583333
+vt 0.583333 0.666667
+vt 0.583333 0.750000
+vt 0.583333 0.833333
+vt 0.583333 0.916667
+vt 0.583333 1.000000
+vt 0.583333 0.000000
+vt 0.583333 0.083333
+vt 0.583333 0.166667
+vt 0.583333 0.250000
+vt 0.583333 0.333333
+vt 0.583333 0.416667
+vt 0.604167 0.500000
+vt 0.604167 0.583333
+vt 0.604167 0.666667
+vt 0.604167 0.750000
+vt 0.604167 0.833333
+vt 0.604167 0.916667
+vt 0.604167 1.000000
+vt 0.604167 0.000000
+vt 0.604167 0.083333
+vt 0.604167 0.166667
+vt 0.604167 0.250000
+vt 0.604167 0.333333
+vt 0.604167 0.416667
+vt 0.625000 0.500000
+vt 0.625000 0.583333
+vt 0.625000 0.666667
+vt 0.625000 0.750000
+vt 0.625000 0.833333
+vt 0.625000 0.916667
+vt 0.625000 1.000000
+vt 0.625000 0.000000
+vt 0.625000 0.083333
+vt 0.625000 0.166667
+vt 0.625000 0.250000
+vt 0.625000 0.333333
+vt 0.625000 0.416667
+vt 0.645833 0.500000
+vt 0.645833 0.583333
+vt 0.645833 0.666667
+vt 0.645833 0.750000
+vt 0.645833 0.833333
+vt 0.645833 0.916667
+vt 0.645833 1.000000
+vt 0.645833 0.000000
+vt 0.645833 0.083333
+vt 0.645833 0.166667
+vt 0.645833 0.250000
+vt 0.645833 0.333333
+vt 0.645833 0.416667
+vt 0.666667 0.500000
+vt 0.666667 0.583333
+vt 0.666667 0.666667
+vt 0.666667 0.750000
+vt 0.666667 0.833333
+vt 0.666667 0.916667
+vt 0.666667 1.000000
+vt 0.666667 0.000000
+vt 0.666667 0.083333
+vt 0.666667 0.166667
+vt 0.666667 0.250000
+vt 0.666667 0.333333
+vt 0.666667 0.416667
+vt 0.687500 0.500000
+vt 0.687500 0.583333
+vt 0.687500 0.666667
+vt 0.687500 0.750000
+vt 0.687500 0.833333
+vt 0.687500 0.916667
+vt 0.687500 1.000000
+vt 0.687500 0.000000
+vt 0.687500 0.083333
+vt 0.687500 0.166667
+vt 0.687500 0.250000
+vt 0.687500 0.333333
+vt 0.687500 0.416667
+vt 0.708333 0.500000
+vt 0.708333 0.583333
+vt 0.708333 0.666667
+vt 0.708333 0.750000
+vt 0.708333 0.833333
+vt 0.708333 0.916667
+vt 0.708333 1.000000
+vt 0.708333 0.000000
+vt 0.708333 0.083333
+vt 0.708333 0.166667
+vt 0.708333 0.250000
+vt 0.708333 0.333333
+vt 0.708333 0.416667
+vt 0.729167 0.500000
+vt 0.729167 0.583333
+vt 0.729167 0.666667
+vt 0.729167 0.750000
+vt 0.729167 0.833333
+vt 0.729167 0.916667
+vt 0.729167 1.000000
+vt 0.729167 0.000000
+vt 0.729167 0.083333
+vt 0.729167 0.166667
+vt 0.729167 0.250000
+vt 0.729167 0.333333
+vt 0.729167 0.416667
+vt 0.750000 0.500000
+vt 0.750000 0.583333
+vt 0.750000 0.666667
+vt 0.750000 0.750000
+vt 0.750000 0.833333
+vt 0.750000 0.916667
+vt 0.750000 1.000000
+vt 0.750000 0.000000
+vt 0.750000 0.083333
+vt 0.750000 0.166667
+vt 0.750000 0.250000
+vt 0.750000 0.333333
+vt 0.750000 0.416667
+vt 0.770833 0.500000
+vt 0.770833 0.583333
+vt 0.770833 0.666667
+vt 0.770833 0.750000
+vt 0.770833 0.833333
+vt 0.770833 0.916667
+vt 0.770833 1.000000
+vt 0.770833 0.000000
+vt 0.770833 0.083333
+vt 0.770833 0.166667
+vt 0.770833 0.250000
+vt 0.770833 0.333333
+vt 0.770833 0.416667
+vt 0.791667 0.500000
+vt 0.791667 0.583333
+vt 0.791667 0.666667
+vt 0.791667 0.750000
+vt 0.791667 0.833333
+vt 0.791667 0.916667
+vt 0.791667 1.000000
+vt 0.791667 0.000000
+vt 0.791667 0.083333
+vt 0.791667 0.166667
+vt 0.791667 0.250000
+vt 0.791667 0.333333
+vt 0.791667 0.416667
+vt 0.812500 0.500000
+vt 0.812500 0.583333
+vt 0.812500 0.666667
+vt 0.812500 0.750000
+vt 0.812500 0.833333
+vt 0.812500 0.916667
+vt 0.812500 1.000000
+vt 0.812500 0.000000
+vt 0.812500 0.083333
+vt 0.812500 0.166667
+vt 0.812500 0.250000
+vt 0.812500 0.333333
+vt 0.812500 0.416667
+vt 0.833333 0.500000
+vt 0.833333 0.583333
+vt 0.833333 0.666667
+vt 0.833333 0.750000
+vt 0.833333 0.833333
+vt 0.833333 0.916667
+vt 0.833333 1.000000
+vt 0.833333 0.000000
+vt 0.833333 0.083333
+vt 0.833333 0.166667
+vt 0.833333 0.250000
+vt 0.833333 0.333333
+vt 0.833333 0.416667
+vt 0.854167 0.500000
+vt 0.854167 0.583333
+vt 0.854167 0.666667
+vt 0.854167 0.750000
+vt 0.854167 0.833333
+vt 0.854167 0.916667
+vt 0.854167 1.000000
+vt 0.854167 0.000000
+vt 0.854167 0.083333
+vt 0.854167 0.166667
+vt 0.854167 0.250000
+vt 0.854167 0.333333
+vt 0.854167 0.416667
+vt 0.875000 0.500000
+vt 0.875000 0.583333
+vt 0.875000 0.666667
+vt 0.875000 0.750000
+vt 0.875000 0.833333
+vt 0.875000 0.916667
+vt 0.875000 1.000000
+vt 0.875000 0.000000
+vt 0.875000 0.083333
+vt 0.875000 0.166667
+vt 0.875000 0.250000
+vt 0.875000 0.333333
+vt 0.875000 0.416667
+vt 0.895833 0.500000
+vt 0.895833 0.583333
+vt 0.895833 0.666667
+vt 0.895833 0.750000
+vt 0.895833 0.833333
+vt 0.895833 0.916667
+vt 0.895833 1.000000
+vt 0.895833 0.000000
+vt 0.895833 0.083333
+vt 0.895833 0.166667
+vt 0.895833 0.250000
+vt 0.895833 0.333333
+vt 0.895833 0.416667
+vt 0.916667 0.500000
+vt 0.916667 0.583333
+vt 0.916667 0.666667
+vt 0.916667 0.750000
+vt 0.916667 0.833333
+vt 0.916667 0.916667
+vt 0.916667 1.000000
+vt 0.916667 0.000000
+vt 0.916667 0.083333
+vt 0.916667 0.166667
+vt 0.916667 0.250000
+vt 0.916667 0.333333
+vt 0.916667 0.416667
+vt 0.937500 0.500000
+vt 0.937500 0.583333
+vt 0.937500 0.666667
+vt 0.937500 0.750000
+vt 0.937500 0.833333
+vt 0.937500 0.916667
+vt 0.937500 1.000000
+vt 0.937500 0.000000
+vt 0.937500 0.083333
+vt 0.937500 0.166667
+vt 0.937500 0.250000
+vt 0.937500 0.333333
+vt 0.937500 0.416667
+vt 0.958333 0.500000
+vt 0.958333 0.583333
+vt 0.958333 0.666667
+vt 0.958333 0.750000
+vt 0.958333 0.833333
+vt 0.958333 0.916667
+vt 0.958333 1.000000
+vt 0.958333 0.000000
+vt 0.958333 0.083333
+vt 0.958333 0.166667
+vt 0.958333 0.250000
+vt 0.958333 0.333333
+vt 0.958333 0.416667
+vt 0.979167 0.500000
+vt 0.979167 0.583333
+vt 0.979167 0.666667
+vt 0.979167 0.750000
+vt 0.979167 0.833333
+vt 0.979167 0.916667
+vt 0.979167 1.000000
+vt 0.979167 0.000000
+vt 0.979167 0.083333
+vt 0.979167 0.166667
+vt 0.979167 0.250000
+vt 0.979167 0.333333
+vt 0.979167 0.416667
+vt 1.000000 0.500000
+vt 1.000000 0.583333
+vt 1.000000 0.666667
+vt 1.000000 0.750000
+vt 1.000000 0.833333
+vt 1.000000 0.916667
+vt 1.000000 1.000000
+vt 1.000000 0.000000
+vt 1.000000 0.083333
+vt 1.000000 0.166667
+vt 1.000000 0.250000
+vt 1.000000 0.333333
+vt 1.000000 0.416667
+vt 0.000000 0.500000
+vt 0.020833 0.500000
+vt 0.020833 0.583333
+vt 0.000000 0.583333
+vt 0.020833 0.666667
+vt 0.000000 0.666667
+vt 0.020833 0.750000
+vt 0.000000 0.750000
+vt 0.020833 0.833333
+vt 0.000000 0.833333
+vt 0.020833 0.916667
+vt 0.000000 0.916667
+vt 0.020833 1.000000
+vt 0.000000 1.000000
+vt 0.000000 0.000000
+vt 0.020833 0.000000
+vt 0.020833 0.083333
+vt 0.000000 0.083333
+vt 0.020833 0.166667
+vt 0.000000 0.166667
+vt 0.020833 0.250000
+vt 0.000000 0.250000
+vt 0.020833 0.333333
+vt 0.000000 0.333333
+vt 0.020833 0.416667
+vt 0.000000 0.416667
+vt 0.041667 0.500000
+vt 0.041667 0.583333
+vt 0.041667 0.666667
+vt 0.041667 0.750000
+vt 0.041667 0.833333
+vt 0.041667 0.916667
+vt 0.041667 1.000000
+vt 0.041667 0.000000
+vt 0.041667 0.083333
+vt 0.041667 0.166667
+vt 0.041667 0.250000
+vt 0.041667 0.333333
+vt 0.041667 0.416667
+vt 0.062500 0.500000
+vt 0.062500 0.583333
+vt 0.062500 0.666667
+vt 0.062500 0.750000
+vt 0.062500 0.833333
+vt 0.062500 0.916667
+vt 0.062500 1.000000
+vt 0.062500 0.000000
+vt 0.062500 0.083333
+vt 0.062500 0.166667
+vt 0.062500 0.250000
+vt 0.062500 0.333333
+vt 0.062500 0.416667
+vt 0.083333 0.500000
+vt 0.083333 0.583333
+vt 0.083333 0.666667
+vt 0.083333 0.750000
+vt 0.083333 0.833333
+vt 0.083333 0.916667
+vt 0.083333 1.000000
+vt 0.083333 0.000000
+vt 0.083333 0.083333
+vt 0.083333 0.166667
+vt 0.083333 0.250000
+vt 0.083333 0.333333
+vt 0.083333 0.416667
+vt 0.104167 0.500000
+vt 0.104167 0.583333
+vt 0.104167 0.666667
+vt 0.104167 0.750000
+vt 0.104167 0.833333
+vt 0.104167 0.916667
+vt 0.104167 1.000000
+vt 0.104167 0.000000
+vt 0.104167 0.083333
+vt 0.104167 0.166667
+vt 0.104167 0.250000
+vt 0.104167 0.333333
+vt 0.104167 0.416667
+vt 0.125000 0.500000
+vt 0.125000 0.583333
+vt 0.125000 0.666667
+vt 0.125000 0.750000
+vt 0.125000 0.833333
+vt 0.125000 0.916667
+vt 0.125000 1.000000
+vt 0.125000 0.000000
+vt 0.125000 0.083333
+vt 0.125000 0.166667
+vt 0.125000 0.250000
+vt 0.125000 0.333333
+vt 0.125000 0.416667
+vt 0.145833 0.500000
+vt 0.145833 0.583333
+vt 0.145833 0.666667
+vt 0.145833 0.750000
+vt 0.145833 0.833333
+vt 0.145833 0.916667
+vt 0.145833 1.000000
+vt 0.145833 0.000000
+vt 0.145833 0.083333
+vt 0.145833 0.166667
+vt 0.145833 0.250000
+vt 0.145833 0.333333
+vt 0.145833 0.416667
+vt 0.166667 0.500000
+vt 0.166667 0.583333
+vt 0.166667 0.666667
+vt 0.166667 0.750000
+vt 0.166667 0.833333
+vt 0.166667 0.916667
+vt 0.166667 1.000000
+vt 0.166667 0.000000
+vt 0.166667 0.083333
+vt 0.166667 0.166667
+vt 0.166667 0.250000
+vt 0.166667 0.333333
+vt 0.166667 0.416667
+vt 0.187500 0.500000
+vt 0.187500 0.583333
+vt 0.187500 0.666667
+vt 0.187500 0.750000
+vt 0.187500 0.833333
+vt 0.187500 0.916667
+vt 0.187500 1.000000
+vt 0.187500 0.000000
+vt 0.187500 0.083333
+vt 0.187500 0.166667
+vt 0.187500 0.250000
+vt 0.187500 0.333333
+vt 0.187500 0.416667
+vt 0.208333 0.500000
+vt 0.208333 0.583333
+vt 0.208333 0.666667
+vt 0.208333 0.750000
+vt 0.208333 0.833333
+vt 0.208333 0.916667
+vt 0.208333 1.000000
+vt 0.208333 0.000000
+vt 0.208333 0.083333
+vt 0.208333 0.166667
+vt 0.208333 0.250000
+vt 0.208333 0.333333
+vt 0.208333 0.416667
+vt 0.229167 0.500000
+vt 0.229167 0.583333
+vt 0.229167 0.666667
+vt 0.229167 0.750000
+vt 0.229167 0.833333
+vt 0.229167 0.916667
+vt 0.229167 1.000000
+vt 0.229167 0.000000
+vt 0.229167 0.083333
+vt 0.229167 0.166667
+vt 0.229167 0.250000
+vt 0.229167 0.333333
+vt 0.229167 0.416667
+vt 0.250000 0.500000
+vt 0.250000 0.583333
+vt 0.250000 0.666667
+vt 0.250000 0.750000
+vt 0.250000 0.833333
+vt 0.250000 0.916667
+vt 0.250000 1.000000
+vt 0.250000 0.000000
+vt 0.250000 0.083333
+vt 0.250000 0.166667
+vt 0.250000 0.250000
+vt 0.250000 0.333333
+vt 0.250000 0.416667
+vt 0.270833 0.500000
+vt 0.270833 0.583333
+vt 0.270833 0.666667
+vt 0.270833 0.750000
+vt 0.270833 0.833333
+vt 0.270833 0.916667
+vt 0.270833 1.000000
+vt 0.270833 0.000000
+vt 0.270833 0.083333
+vt 0.270833 0.166667
+vt 0.270833 0.250000
+vt 0.270833 0.333333
+vt 0.270833 0.416667
+vt 0.291667 0.500000
+vt 0.291667 0.583333
+vt 0.291667 0.666667
+vt 0.291667 0.750000
+vt 0.291667 0.833333
+vt 0.291667 0.916667
+vt 0.291667 1.000000
+vt 0.291667 0.000000
+vt 0.291667 0.083333
+vt 0.291667 0.166667
+vt 0.291667 0.250000
+vt 0.291667 0.333333
+vt 0.291667 0.416667
+vt 0.312500 0.500000
+vt 0.312500 0.583333
+vt 0.312500 0.666667
+vt 0.312500 0.750000
+vt 0.312500 0.833333
+vt 0.312500 0.916667
+vt 0.312500 1.000000
+vt 0.312500 0.000000
+vt 0.312500 0.083333
+vt 0.312500 0.166667
+vt 0.312500 0.250000
+vt 0.312500 0.333333
+vt 0.312500 0.416667
+vt 0.333333 0.500000
+vt 0.333333 0.583333
+vt 0.333333 0.666667
+vt 0.333333 0.750000
+vt 0.333333 0.833333
+vt 0.333333 0.916667
+vt 0.333333 1.000000
+vt 0.333333 0.000000
+vt 0.333333 0.083333
+vt 0.333333 0.166667
+vt 0.333333 0.250000
+vt 0.333333 0.333333
+vt 0.333333 0.416667
+vt 0.354167 0.500000
+vt 0.354167 0.583333
+vt 0.354167 0.666667
+vt 0.354167 0.750000
+vt 0.354167 0.833333
+vt 0.354167 0.916667
+vt 0.354167 1.000000
+vt 0.354167 0.000000
+vt 0.354167 0.083333
+vt 0.354167 0.166667
+vt 0.354167 0.250000
+vt 0.354167 0.333333
+vt 0.354167 0.416667
+vt 0.375000 0.500000
+vt 0.375000 0.583333
+vt 0.375000 0.666667
+vt 0.375000 0.750000
+vt 0.375000 0.833333
+vt 0.375000 0.916667
+vt 0.375000 1.000000
+vt 0.375000 0.000000
+vt 0.375000 0.083333
+vt 0.375000 0.166667
+vt 0.375000 0.250000
+vt 0.375000 0.333333
+vt 0.375000 0.416667
+vt 0.395833 0.500000
+vt 0.395833 0.583333
+vt 0.395833 0.666667
+vt 0.395833 0.750000
+vt 0.395833 0.833333
+vt 0.395833 0.916667
+vt 0.395833 1.000000
+vt 0.395833 0.000000
+vt 0.395833 0.083333
+vt 0.395833 0.166667
+vt 0.395833 0.250000
+vt 0.395833 0.333333
+vt 0.395833 0.416667
+vt 0.416667 0.500000
+vt 0.416667 0.583333
+vt 0.416667 0.666667
+vt 0.416667 0.750000
+vt 0.416667 0.833333
+vt 0.416667 0.916667
+vt 0.416667 1.000000
+vt 0.416667 0.000000
+vt 0.416667 0.083333
+vt 0.416667 0.166667
+vt 0.416667 0.250000
+vt 0.416667 0.333333
+vt 0.416667 0.416667
+vt 0.437500 0.500000
+vt 0.437500 0.583333
+vt 0.437500 0.666667
+vt 0.437500 0.750000
+vt 0.437500 0.833333
+vt 0.437500 0.916667
+vt 0.437500 1.000000
+vt 0.437500 0.000000
+vt 0.437500 0.083333
+vt 0.437500 0.166667
+vt 0.437500 0.250000
+vt 0.437500 0.333333
+vt 0.437500 0.416667
+vt 0.458333 0.500000
+vt 0.458333 0.583333
+vt 0.458333 0.666667
+vt 0.458333 0.750000
+vt 0.458333 0.833333
+vt 0.458333 0.916667
+vt 0.458333 1.000000
+vt 0.458333 0.000000
+vt 0.458333 0.083333
+vt 0.458333 0.166667
+vt 0.458333 0.250000
+vt 0.458333 0.333333
+vt 0.458333 0.416667
+vt 0.479167 0.500000
+vt 0.479167 0.583333
+vt 0.479167 0.666667
+vt 0.479167 0.750000
+vt 0.479167 0.833333
+vt 0.479167 0.916667
+vt 0.479167 1.000000
+vt 0.479167 0.000000
+vt 0.479167 0.083333
+vt 0.479167 0.166667
+vt 0.479167 0.250000
+vt 0.479167 0.333333
+vt 0.479167 0.416667
+usemtl tex
+s off
+f 1/1/1 13/2/1 14/3/1 2/4/1
+f 2/4/2 14/3/2 15/5/2 3/6/2
+f 3/6/3 15/5/3 16/7/3 4/8/3
+f 4/8/4 16/7/4 17/9/4 5/10/4
+f 5/10/5 17/9/5 18/11/5 6/12/5
+f 6/12/6 18/11/6 19/13/6 7/14/6
+f 7/15/7 19/16/7 20/17/7 8/18/7
+f 8/18/8 20/17/8 21/19/8 9/20/8
+f 9/20/9 21/19/9 22/21/9 10/22/9
+f 10/22/10 22/21/10 23/23/10 11/24/10
+f 11/24/11 23/23/11 24/25/11 12/26/11
+f 12/26/12 24/25/12 13/2/12 1/1/12
+f 13/2/13 25/27/13 26/28/13 14/3/13
+f 14/3/14 26/28/14 27/29/14 15/5/14
+f 15/5/15 27/29/15 28/30/15 16/7/15
+f 16/7/16 28/30/16 29/31/16 17/9/16
+f 17/9/17 29/31/17 30/32/17 18/11/17
+f 18/11/18 30/32/18 31/33/18 19/13/18
+f 19/16/19 31/34/19 32/35/19 20/17/19
+f 20/17/20 32/35/20 33/36/20 21/19/20
+f 21/19/21 33/36/21 34/37/21 22/21/21
+f 22/21/22 34/37/22 35/38/22 23/23/22
+f 23/23/23 35/38/23 36/39/23 24/25/23
+f 24/25/24 36/39/24 25/27/24 13/2/24
+f 25/27/25 37/40/25 38/41/25 26/28/25
+f 26/28/26 38/41/26 39/42/26 27/29/26
+f 27/29/27 39/42/27 40/43/27 28/30/27
+f 28/30/28 40/43/28 41/44/28 29/31/28
+f 29/31/29 41/44/29 42/45/29 30/32/29
+f 30/32/30 42/45/30 43/46/30 31/33/30
+f 31/34/31 43/47/31 44/48/31 32/35/31
+f 32/35/32 44/48/32 45/49/32 33/36/32
+f 33/36/33 45/49/33 46/50/33 34/37/33
+f 34/37/34 46/50/34 47/51/34 35/38/34
+f 35/38/35 47/51/35 48/52/35 36/39/35
+f 36/39/36 48/52/36 37/40/36 25/27/36
+f 37/40/37 49/53/37 50/54/37 38/41/37
+f 38/41/38 50/54/38 51/55/38 39/42/38
+f 39/42/39 51/55/39 52/56/39 40/43/39
+f 40/43/40 52/56/40 53/57/40 41/44/40
+f 41/44/41 53/57/41 54/58/41 42/45/41
+f 42/45/42 54/58/42 55/59/42 43/46/42
+f 43/47/43 55/60/43 56/61/43 44/48/43
+f 44/48/44 56/61/44 57/62/44 45/49/44
+f 45/49/45 57/62/45 58/63/45 46/50/45
+f 46/50/46 58/63/46 59/64/46 47/51/46
+f 47/51/47 59/64/47 60/65/47 48/52/47
+f 48/52/48 60/65/48 49/53/48 37/40/48
+f 49/53/49 61/66/49 62/67/49 50/54/49
+f 50/54/50 62/67/50 63/68/50 51/55/50
+f 51/55/51 63/68/51 64/69/51 52/56/51
+f 52/56/52 64/69/52 65/70/52 53/57/52
+f 53/57/53 65/70/53 66/71/53 54/58/53
+f 54/58/54 66/71/54 67/72/54 55/59/54
+f 55/60/55 67/73/55 68/74/55 56/61/55
+f 56/61/56 68/74/56 69/75/56 57/62/56
+f 57/62/57 69/75/57 70/76/57 58/63/57
+f 58/63/58 70/76/58 71/77/58 59/64/58
+f 59/64/59 71/77/59 72/78/59 60/65/59
+f 60/65/60 72/78/60 61/66/60 49/53/60
+f 61/66/61 73/79/61 74/80/61 62/67/61
+f 62/67/62 74/80/62 75/81/62 63/68/62
+f 63/68/63 75/81/63 76/82/63 64/69/63
+f 64/69/64 76/82/64 77/83/64 65/70/64
+f 65/70/65 77/83/65 78/84/65 66/71/65
+f 66/71/66 78/84/66 79/85/66 67/72/66
+f 67/73/67 79/86/67 80/87/67 68/74/67
+f 68/74/68 80/87/68 81/88/68 69/75/68
+f 69/75/69 81/88/69 82/89/69 70/76/69
+f 70/76/70 82/89/70 83/90/70 71/77/70
+f 71/77/71 83/90/71 84/91/71 72/78/71
+f 72/78/72 84/91/72 73/79/72 61/66/72
+f 73/79/73 85/92/73 86/93/73 74/80/73
+f 74/80/74 86/93/74 87/94/74 75/81/74
+f 75/81/75 87/94/75 88/95/75 76/82/75
+f 76/82/76 88/95/76 89/96/76 77/83/76
+f 77/83/77 89/96/77 90/97/77 78/84/77
+f 78/84/78 90/97/78 91/98/78 79/85/78
+f 79/86/79 91/99/79 92/100/79 80/87/79
+f 80/87/80 92/100/80 93/101/80 81/88/80
+f 81/88/81 93/101/81 94/102/81 82/89/81
+f 82/89/82 94/102/82 95/103/82 83/90/82
+f 83/90/83 95/103/83 96/104/83 84/91/83
+f 84/91/84 96/104/84 85/92/84 73/79/84
+f 85/92/85 97/105/85 98/106/85 86/93/85
+f 86/93/86 98/106/86 99/107/86 87/94/86
+f 87/94/87 99/107/87 100/108/87 88/95/87
+f 88/95/88 100/108/88 101/109/88 89/96/88
+f 89/96/89 101/109/89 102/110/89 90/97/89
+f 90/97/90 102/110/90 103/111/90 91/98/90
+f 91/99/91 103/112/91 104/113/91 92/100/91
+f 92/100/92 104/113/92 105/114/92 93/101/92
+f 93/101/93 105/114/93 106/115/93 94/102/93
+f 94/102/94 106/115/94 107/116/94 95/103/94
+f 95/103/95 107/116/95 108/117/95 96/104/95
+f 96/104/96 108/117/96 97/105/96 85/92/96
+f 97/105/97 109/118/97 110/119/97 98/106/97
+f 98/106/98 110/119/98 111/120/98 99/107/98
+f 99/107/99 111/120/99 112/121/99 100/108/99
+f 100/108/100 112/121/100 113/122/100 101/109/100
+f 101/109/101 113/122/101 114/123/101 102/110/101
+f 102/110/102 114/123/102 115/124/102 103/111/102
+f 103/112/103 115/125/103 116/126/103 104/113/103
+f 104/113/104 116/126/104 117/127/104 105/114/104
+f 105/114/105 117/127/105 118/128/105 106/115/105
+f 106/115/106 118/128/106 119/129/106 107/116/106
+f 107/116/107 119/129/107 120/130/107 108/117/107
+f 108/117/108 120/130/108 109/118/108 97/105/108
+f 109/118/109 121/131/109 122/132/109 110/119/109
+f 110/119/110 122/132/110 123/133/110 111/120/110
+f 111/120/111 123/133/111 124/134/111 112/121/111
+f 112/121/112 124/134/112 125/135/112 113/122/112
+f 113/122/113 125/135/113 126/136/113 114/123/113
+f 114/123/114 126/136/114 127/137/114 115/124/114
+f 115/125/115 127/138/115 128/139/115 116/126/115
+f 116/126/116 128/139/116 129/140/116 117/127/116
+f 117/127/117 129/140/117 130/141/117 118/128/117
+f 118/128/118 130/141/118 131/142/118 119/129/118
+f 119/129/119 131/142/119 132/143/119 120/130/119
+f 120/130/120 132/143/120 121/131/120 109/118/120
+f 121/131/121 133/144/121 134/145/121 122/132/121
+f 122/132/122 134/145/122 135/146/122 123/133/122
+f 123/133/123 135/146/123 136/147/123 124/134/123
+f 124/134/124 136/147/124 137/148/124 125/135/124
+f 125/135/125 137/148/125 138/149/125 126/136/125
+f 126/136/126 138/149/126 139/150/126 127/137/126
+f 127/138/127 139/151/127 140/152/127 128/139/127
+f 128/139/128 140/152/128 141/153/128 129/140/128
+f 129/140/129 141/153/129 142/154/129 130/141/129
+f 130/141/130 142/154/130 143/155/130 131/142/130
+f 131/142/131 143/155/131 144/156/131 132/143/131
+f 132/143/132 144/156/132 133/144/132 121/131/132
+f 133/144/133 145/157/133 146/158/133 134/145/133
+f 134/145/134 146/158/134 147/159/134 135/146/134
+f 135/146/135 147/159/135 148/160/135 136/147/135
+f 136/147/136 148/160/136 149/161/136 137/148/136
+f 137/148/137 149/161/137 150/162/137 138/149/137
+f 138/149/138 150/162/138 151/163/138 139/150/138
+f 139/151/139 151/164/139 152/165/139 140/152/139
+f 140/152/140 152/165/140 153/166/140 141/153/140
+f 141/153/141 153/166/141 154/167/141 142/154/141
+f 142/154/142 154/167/142 155/168/142 143/155/142
+f 143/155/143 155/168/143 156/169/143 144/156/143
+f 144/156/144 156/169/144 145/157/144 133/144/144
+f 145/157/145 157/170/145 158/171/145 146/158/145
+f 146/158/146 158/171/146 159/172/146 147/159/146
+f 147/159/147 159/172/147 160/173/147 148/160/147
+f 148/160/148 160/173/148 161/174/148 149/161/148
+f 149/161/149 161/174/149 162/175/149 150/162/149
+f 150/162/150 162/175/150 163/176/150 151/163/150
+f 151/164/151 163/177/151 164/178/151 152/165/151
+f 152/165/152 164/178/152 165/179/152 153/166/152
+f 153/166/153 165/179/153 166/180/153 154/167/153
+f 154/167/154 166/180/154 167/181/154 155/168/154
+f 155/168/155 167/181/155 168/182/155 156/169/155
+f 156/169/156 168/182/156 157/170/156 145/157/156
+f 157/170/157 169/183/157 170/184/157 158/171/157
+f 158/171/158 170/184/158 171/185/158 159/172/158
+f 159/172/159 171/185/159 172/186/159 160/173/159
+f 160/173/160 172/186/160 173/187/160 161/174/160
+f 161/174/161 173/187/161 174/188/161 162/175/161
+f 162/175/162 174/188/162 175/189/162 163/176/162
+f 163/177/163 175/190/163 176/191/163 164/178/163
+f 164/178/164 176/191/164 177/192/164 165/179/164
+f 165/179/165 177/192/165 178/193/165 166/180/165
+f 166/180/166 178/193/166 179/194/166 167/181/166
+f 167/181/167 179/194/167 180/195/167 168/182/167
+f 168/182/168 180/195/168 169/183/168 157/170/168
+f 169/183/169 181/196/169 182/197/169 170/184/169
+f 170/184/170 182/197/170 183/198/170 171/185/170
+f 171/185/171 183/198/171 184/199/171 172/186/171
+f 172/186/172 184/199/172 185/200/172 173/187/172
+f 173/187/173 185/200/173 186/201/173 174/188/173
+f 174/188/174 186/201/174 187/202/174 175/189/174
+f 175/190/175 187/203/175 188/204/175 176/191/175
+f 176/191/176 188/204/176 189/205/176 177/192/176
+f 177/192/177 189/205/177 190/206/177 178/193/177
+f 178/193/178 190/206/178 191/207/178 179/194/178
+f 179/194/179 191/207/179 192/208/179 180/195/179
+f 180/195/180 192/208/180 181/196/180 169/183/180
+f 181/196/181 193/209/181 194/210/181 182/197/181
+f 182/197/182 194/210/182 195/211/182 183/198/182
+f 183/198/183 195/211/183 196/212/183 184/199/183
+f 184/199/184 196/212/184 197/213/184 185/200/184
+f 185/200/185 197/213/185 198/214/185 186/201/185
+f 186/201/186 198/214/186 199/215/186 187/202/186
+f 187/203/187 199/216/187 200/217/187 188/204/187
+f 188/204/188 200/217/188 201/218/188 189/205/188
+f 189/205/189 201/218/189 202/219/189 190/206/189
+f 190/206/190 202/219/190 203/220/190 191/207/190
+f 191/207/191 203/220/191 204/221/191 192/208/191
+f 192/208/192 204/221/192 193/209/192 181/196/192
+f 193/209/193 205/222/193 206/223/193 194/210/193
+f 194/210/194 206/223/194 207/224/194 195/211/194
+f 195/211/195 207/224/195 208/225/195 196/212/195
+f 196/212/196 208/225/196 209/226/196 197/213/196
+f 197/213/197 209/226/197 210/227/197 198/214/197
+f 198/214/198 210/227/198 211/228/198 199/215/198
+f 199/216/199 211/229/199 212/230/199 200/217/199
+f 200/217/200 212/230/200 213/231/200 201/218/200
+f 201/218/201 213/231/201 214/232/201 202/219/201
+f 202/219/202 214/232/202 215/233/202 203/220/202
+f 203/220/203 215/233/203 216/234/203 204/221/203
+f 204/221/204 216/234/204 205/222/204 193/209/204
+f 205/222/205 217/235/205 218/236/205 206/223/205
+f 206/223/206 218/236/206 219/237/206 207/224/206
+f 207/224/207 219/237/207 220/238/207 208/225/207
+f 208/225/208 220/238/208 221/239/208 209/226/208
+f 209/226/209 221/239/209 222/240/209 210/227/209
+f 210/227/210 222/240/210 223/241/210 211/228/210
+f 211/229/211 223/242/211 224/243/211 212/230/211
+f 212/230/212 224/243/212 225/244/212 213/231/212
+f 213/231/213 225/244/213 226/245/213 214/232/213
+f 214/232/214 226/245/214 227/246/214 215/233/214
+f 215/233/215 227/246/215 228/247/215 216/234/215
+f 216/234/216 228/247/216 217/235/216 205/222/216
+f 217/235/217 229/248/217 230/249/217 218/236/217
+f 218/236/218 230/249/218 231/250/218 219/237/218
+f 219/237/219 231/250/219 232/251/219 220/238/219
+f 220/238/220 232/251/220 233/252/220 221/239/220
+f 221/239/221 233/252/221 234/253/221 222/240/221
+f 222/240/222 234/253/222 235/254/222 223/241/222
+f 223/242/223 235/255/223 236/256/223 224/243/223
+f 224/243/224 236/256/224 237/257/224 225/244/224
+f 225/244/225 237/257/225 238/258/225 226/245/225
+f 226/245/226 238/258/226 239/259/226 227/246/226
+f 227/246/227 239/259/227 240/260/227 228/247/227
+f 228/247/228 240/260/228 229/248/228 217/235/228
+f 229/248/229 241/261/229 242/262/229 230/249/229
+f 230/249/230 242/262/230 243/263/230 231/250/230
+f 231/250/231 243/263/231 244/264/231 232/251/231
+f 232/251/232 244/264/232 245/265/232 233/252/232
+f 233/252/233 245/265/233 246/266/233 234/253/233
+f 234/253/234 246/266/234 247/267/234 235/254/234
+f 235/255/235 247/268/235 248/269/235 236/256/235
+f 236/256/236 248/269/236 249/270/236 237/257/236
+f 237/257/237 249/270/237 250/271/237 238/258/237
+f 238/258/238 250/271/238 251/272/238 239/259/238
+f 239/259/239 251/272/239 252/273/239 240/260/239
+f 240/260/240 252/273/240 241/261/240 229/248/240
+f 241/261/241 253/274/241 254/275/241 242/262/241
+f 242/262/242 254/275/242 255/276/242 243/263/242
+f 243/263/243 255/276/243 256/277/243 244/264/243
+f 244/264/244 256/277/244 257/278/244 245/265/244
+f 245/265/245 257/278/245 258/279/245 246/266/245
+f 246/266/246 258/279/246 259/280/246 247/267/246
+f 247/268/247 259/281/247 260/282/247 248/269/247
+f 248/269/248 260/282/248 261/283/248 249/270/248
+f 249/270/249 261/283/249 262/284/249 250/271/249
+f 250/271/250 262/284/250 263/285/250 251/272/250
+f 251/272/251 263/285/251 264/286/251 252/273/251
+f 252/273/252 264/286/252 253/274/252 241/261/252
+f 253/274/253 265/287/253 266/288/253 254/275/253
+f 254/275/254 266/288/254 267/289/254 255/276/254
+f 255/276/255 267/289/255 268/290/255 256/277/255
+f 256/277/256 268/290/256 269/291/256 257/278/256
+f 257/278/257 269/291/257 270/292/257 258/279/257
+f 258/279/258 270/292/258 271/293/258 259/280/258
+f 259/281/259 271/294/259 272/295/259 260/282/259
+f 260/282/260 272/295/260 273/296/260 261/283/260
+f 261/283/261 273/296/261 274/297/261 262/284/261
+f 262/284/262 274/297/262 275/298/262 263/285/262
+f 263/285/263 275/298/263 276/299/263 264/286/263
+f 264/286/264 276/299/264 265/287/264 253/274/264
+f 265/287/265 277/300/265 278/301/265 266/288/265
+f 266/288/266 278/301/266 279/302/266 267/289/266
+f 267/289/267 279/302/267 280/303/267 268/290/267
+f 268/290/268 280/303/268 281/304/268 269/291/268
+f 269/291/269 281/304/269 282/305/269 270/292/269
+f 270/292/270 282/305/270 283/306/270 271/293/270
+f 271/294/271 283/307/271 284/308/271 272/295/271
+f 272/295/272 284/308/272 285/309/272 273/296/272
+f 273/296/273 285/309/273 286/310/273 274/297/273
+f 274/297/274 286/310/274 287/311/274 275/298/274
+f 275/298/275 287/311/275 288/312/275 276/299/275
+f 276/299/276 288/312/276 277/300/276 265/287/276
+f 277/300/277 289/313/277 290/314/277 278/301/277
+f 278/301/278 290/314/278 291/315/278 279/302/278
+f 279/302/279 291/315/279 292/316/279 280/303/279
+f 280/303/280 292/316/280 293/317/280 281/304/280
+f 281/304/281 293/317/281 294/318/281 282/305/281
+f 282/305/282 294/318/282 295/319/282 283/306/282
+f 283/307/283 295/320/283 296/321/283 284/308/283
+f 284/308/284 296/321/284 297/322/284 285/309/284
+f 285/309/285 297/322/285 298/323/285 286/310/285
+f 286/310/286 298/323/286 299/324/286 287/311/286
+f 287/311/287 299/324/287 300/325/287 288/312/287
+f 288/312/288 300/325/288 289/313/288 277/300/288
+f 289/326/6 301/327/6 302/328/6 290/329/6
+f 290/329/5 302/328/5 303/330/5 291/331/5
+f 291/331/4 303/330/4 304/332/4 292/333/4
+f 292/333/3 304/332/3 305/334/3 293/335/3
+f 293/335/2 305/334/2 306/336/2 294/337/2
+f 294/337/1 306/336/1 307/338/1 295/339/1
+f 295/340/12 307/341/12 308/342/12 296/343/12
+f 296/343/11 308/342/11 309/344/11 297/345/11
+f 297/345/10 309/344/10 310/346/10 298/347/10
+f 298/347/9 310/346/9 311/348/9 299/349/9
+f 299/349/8 311/348/8 312/350/8 300/351/8
+f 300/351/7 312/350/7 301/327/7 289/326/7
+f 301/327/18 313/352/18 314/353/18 302/328/18
+f 302/328/17 314/353/17 315/354/17 303/330/17
+f 303/330/16 315/354/16 316/355/16 304/332/16
+f 304/332/15 316/355/15 317/356/15 305/334/15
+f 305/334/14 317/356/14 318/357/14 306/336/14
+f 306/336/13 318/357/13 319/358/13 307/338/13
+f 307/341/24 319/359/24 320/360/24 308/342/24
+f 308/342/23 320/360/23 321/361/23 309/344/23
+f 309/344/22 321/361/22 322/362/22 310/346/22
+f 310/346/21 322/362/21 323/363/21 311/348/21
+f 311/348/20 323/363/20 324/364/20 312/350/20
+f 312/350/19 324/364/19 313/352/19 301/327/19
+f 313/352/30 325/365/30 326/366/30 314/353/30
+f 314/353/29 326/366/29 327/367/29 315/354/29
+f 315/354/28 327/367/28 328/368/28 316/355/28
+f 316/355/27 328/368/27 329/369/27 317/356/27
+f 317/356/26 329/369/26 330/370/26 318/357/26
+f 318/357/25 330/370/25 331/371/25 319/358/25
+f 319/359/36 331/372/36 332/373/36 320/360/36
+f 320/360/35 332/373/35 333/374/35 321/361/35
+f 321/361/34 333/374/34 334/375/34 322/362/34
+f 322/362/33 334/375/33 335/376/33 323/363/33
+f 323/363/32 335/376/32 336/377/32 324/364/32
+f 324/364/31 336/377/31 325/365/31 313/352/31
+f 325/365/42 337/378/42 338/379/42 326/366/42
+f 326/366/41 338/379/41 339/380/41 327/367/41
+f 327/367/40 339/380/40 340/381/40 328/368/40
+f 328/368/39 340/381/39 341/382/39 329/369/39
+f 329/369/38 341/382/38 342/383/38 330/370/38
+f 330/370/37 342/383/37 343/384/37 331/371/37
+f 331/372/48 343/385/48 344/386/48 332/373/48
+f 332/373/47 344/386/47 345/387/47 333/374/47
+f 333/374/46 345/387/46 346/388/46 334/375/46
+f 334/375/45 346/388/45 347/389/45 335/376/45
+f 335/376/44 347/389/44 348/390/44 336/377/44
+f 336/377/43 348/390/43 337/378/43 325/365/43
+f 337/378/54 349/391/54 350/392/54 338/379/54
+f 338/379/53 350/392/53 351/393/53 339/380/53
+f 339/380/52 351/393/52 352/394/52 340/381/52
+f 340/381/51 352/394/51 353/395/51 341/382/51
+f 341/382/50 353/395/50 354/396/50 342/383/50
+f 342/383/49 354/396/49 355/397/49 343/384/49
+f 343/385/60 355/398/60 356/399/60 344/386/60
+f 344/386/59 356/399/59 357/400/59 345/387/59
+f 345/387/58 357/400/58 358/401/58 346/388/58
+f 346/388/57 358/401/57 359/402/57 347/389/57
+f 347/389/56 359/402/56 360/403/56 348/390/56
+f 348/390/55 360/403/55 349/391/55 337/378/55
+f 349/391/66 361/404/66 362/405/66 350/392/66
+f 350/392/65 362/405/65 363/406/65 351/393/65
+f 351/393/64 363/406/64 364/407/64 352/394/64
+f 352/394/63 364/407/63 365/408/63 353/395/63
+f 353/395/62 365/408/62 366/409/62 354/396/62
+f 354/396/61 366/409/61 367/410/61 355/397/61
+f 355/398/72 367/411/72 368/412/72 356/399/72
+f 356/399/71 368/412/71 369/413/71 357/400/71
+f 357/400/70 369/413/70 370/414/70 358/401/70
+f 358/401/69 370/414/69 371/415/69 359/402/69
+f 359/402/68 371/415/68 372/416/68 360/403/68
+f 360/403/67 372/416/67 361/404/67 349/391/67
+f 361/404/78 373/417/78 374/418/78 362/405/78
+f 362/405/77 374/418/77 375/419/77 363/406/77
+f 363/406/76 375/419/76 376/420/76 364/407/76
+f 364/407/75 376/420/75 377/421/75 365/408/75
+f 365/408/74 377/421/74 378/422/74 366/409/74
+f 366/409/73 378/422/73 379/423/73 367/410/73
+f 367/411/84 379/424/84 380/425/84 368/412/84
+f 368/412/83 380/425/83 381/426/83 369/413/83
+f 369/413/82 381/426/82 382/427/82 370/414/82
+f 370/414/81 382/427/81 383/428/81 371/415/81
+f 371/415/80 383/428/80 384/429/80 372/416/80
+f 372/416/79 384/429/79 373/417/79 361/404/79
+f 373/417/90 385/430/90 386/431/90 374/418/90
+f 374/418/89 386/431/89 387/432/89 375/419/89
+f 375/419/88 387/432/88 388/433/88 376/420/88
+f 376/420/87 388/433/87 389/434/87 377/421/87
+f 377/421/86 389/434/86 390/435/86 378/422/86
+f 378/422/85 390/435/85 391/436/85 379/423/85
+f 379/424/96 391/437/96 392/438/96 380/425/96
+f 380/425/95 392/438/95 393/439/95 381/426/95
+f 381/426/94 393/439/94 394/440/94 382/427/94
+f 382/427/93 394/440/93 395/441/93 383/428/93
+f 383/428/92 395/441/92 396/442/92 384/429/92
+f 384/429/91 396/442/91 385/430/91 373/417/91
+f 385/430/102 397/443/102 398/444/102 386/431/102
+f 386/431/101 398/444/101 399/445/101 387/432/101
+f 387/432/100 399/445/100 400/446/100 388/433/100
+f 388/433/99 400/446/99 401/447/99 389/434/99
+f 389/434/98 401/447/98 402/448/98 390/435/98
+f 390/435/97 402/448/97 403/449/97 391/436/97
+f 391/437/108 403/450/108 404/451/108 392/438/108
+f 392/438/107 404/451/107 405/452/107 393/439/107
+f 393/439/106 405/452/106 406/453/106 394/440/106
+f 394/440/105 406/453/105 407/454/105 395/441/105
+f 395/441/104 407/454/104 408/455/104 396/442/104
+f 396/442/103 408/455/103 397/443/103 385/430/103
+f 397/443/114 409/456/114 410/457/114 398/444/114
+f 398/444/113 410/457/113 411/458/113 399/445/113
+f 399/445/112 411/458/112 412/459/112 400/446/112
+f 400/446/111 412/459/111 413/460/111 401/447/111
+f 401/447/110 413/460/110 414/461/110 402/448/110
+f 402/448/109 414/461/109 415/462/109 403/449/109
+f 403/450/120 415/463/120 416/464/120 404/451/120
+f 404/451/119 416/464/119 417/465/119 405/452/119
+f 405/452/118 417/465/118 418/466/118 406/453/118
+f 406/453/117 418/466/117 419/467/117 407/454/117
+f 407/454/116 419/467/116 420/468/116 408/455/116
+f 408/455/115 420/468/115 409/456/115 397/443/115
+f 409/456/126 421/469/126 422/470/126 410/457/126
+f 410/457/125 422/470/125 423/471/125 411/458/125
+f 411/458/124 423/471/124 424/472/124 412/459/124
+f 412/459/123 424/472/123 425/473/123 413/460/123
+f 413/460/122 425/473/122 426/474/122 414/461/122
+f 414/461/121 426/474/121 427/475/121 415/462/121
+f 415/463/132 427/476/132 428/477/132 416/464/132
+f 416/464/131 428/477/131 429/478/131 417/465/131
+f 417/465/130 429/478/130 430/479/130 418/466/130
+f 418/466/129 430/479/129 431/480/129 419/467/129
+f 419/467/128 431/480/128 432/481/128 420/468/128
+f 420/468/127 432/481/127 421/469/127 409/456/127
+f 421/469/138 433/482/138 434/483/138 422/470/138
+f 422/470/137 434/483/137 435/484/137 423/471/137
+f 423/471/136 435/484/136 436/485/136 424/472/136
+f 424/472/135 436/485/135 437/486/135 425/473/135
+f 425/473/134 437/486/134 438/487/134 426/474/134
+f 426/474/133 438/487/133 439/488/133 427/475/133
+f 427/476/144 439/489/144 440/490/144 428/477/144
+f 428/477/143 440/490/143 441/491/143 429/478/143
+f 429/478/142 441/491/142 442/492/142 430/479/142
+f 430/479/141 442/492/141 443/493/141 431/480/141
+f 431/480/140 443/493/140 444/494/140 432/481/140
+f 432/481/139 444/494/139 433/482/139 421/469/139
+f 433/482/150 445/495/150 446/496/150 434/483/150
+f 434/483/149 446/496/149 447/497/149 435/484/149
+f 435/484/148 447/497/148 448/498/148 436/485/148
+f 436/485/147 448/498/147 449/499/147 437/486/147
+f 437/486/146 449/499/146 450/500/146 438/487/146
+f 438/487/145 450/500/145 451/501/145 439/488/145
+f 439/489/156 451/502/156 452/503/156 440/490/156
+f 440/490/155 452/503/155 453/504/155 441/491/155
+f 441/491/154 453/504/154 454/505/154 442/492/154
+f 442/492/153 454/505/153 455/506/153 443/493/153
+f 443/493/152 455/506/152 456/507/152 444/494/152
+f 444/494/151 456/507/151 445/495/151 433/482/151
+f 445/495/162 457/508/162 458/509/162 446/496/162
+f 446/496/161 458/509/161 459/510/161 447/497/161
+f 447/497/160 459/510/160 460/511/160 448/498/160
+f 448/498/159 460/511/159 461/512/159 449/499/159
+f 449/499/158 461/512/158 462/513/158 450/500/158
+f 450/500/157 462/513/157 463/514/157 451/501/157
+f 451/502/168 463/515/168 464/516/168 452/503/168
+f 452/503/167 464/516/167 465/517/167 453/504/167
+f 453/504/166 465/517/166 466/518/166 454/505/166
+f 454/505/165 466/518/165 467/519/165 455/506/165
+f 455/506/164 467/519/164 468/520/164 456/507/164
+f 456/507/163 468/520/163 457/508/163 445/495/163
+f 457/508/174 469/521/174 470/522/174 458/509/174
+f 458/509/173 470/522/173 471/523/173 459/510/173
+f 459/510/172 471/523/172 472/524/172 460/511/172
+f 460/511/171 472/524/171 473/525/171 461/512/171
+f 461/512/170 473/525/170 474/526/170 462/513/170
+f 462/513/169 474/526/169 475/527/169 463/514/169
+f 463/515/180 475/528/180 476/529/180 464/516/180
+f 464/516/179 476/529/179 477/530/179 465/517/179
+f 465/517/178 477/530/178 478/531/178 466/518/178
+f 466/518/177 478/531/177 479/532/177 467/519/177
+f 467/519/176 479/532/176 480/533/176 468/520/176
+f 468/520/175 480/533/175 469/521/175 457/508/175
+f 469/521/186 481/534/186 482/535/186 470/522/186
+f 470/522/185 482/535/185 483/536/185 471/523/185
+f 471/523/184 483/536/184 484/537/184 472/524/184
+f 472/524/183 484/537/183 485/538/183 473/525/183
+f 473/525/182 485/538/182 486/539/182 474/526/182
+f 474/526/181 486/539/181 487/540/181 475/527/181
+f 475/528/192 487/541/192 488/542/192 476/529/192
+f 476/529/191 488/542/191 489/543/191 477/530/191
+f 477/530/190 489/543/190 490/544/190 478/531/190
+f 478/531/189 490/544/189 491/545/189 479/532/189
+f 479/532/188 491/545/188 492/546/188 480/533/188
+f 480/533/187 492/546/187 481/534/187 469/521/187
+f 481/534/198 493/547/198 494/548/198 482/535/198
+f 482/535/197 494/548/197 495/549/197 483/536/197
+f 483/536/196 495/549/196 496/550/196 484/537/196
+f 484/537/195 496/550/195 497/551/195 485/538/195
+f 485/538/194 497/551/194 498/552/194 486/539/194
+f 486/539/193 498/552/193 499/553/193 487/540/193
+f 487/541/204 499/554/204 500/555/204 488/542/204
+f 488/542/203 500/555/203 501/556/203 489/543/203
+f 489/543/202 501/556/202 502/557/202 490/544/202
+f 490/544/201 502/557/201 503/558/201 491/545/201
+f 491/545/200 503/558/200 504/559/200 492/546/200
+f 492/546/199 504/559/199 493/547/199 481/534/199
+f 493/547/210 505/560/210 506/561/210 494/548/210
+f 494/548/209 506/561/209 507/562/209 495/549/209
+f 495/549/208 507/562/208 508/563/208 496/550/208
+f 496/550/207 508/563/207 509/564/207 497/551/207
+f 497/551/206 509/564/206 510/565/206 498/552/206
+f 498/552/205 510/565/205 511/566/205 499/553/205
+f 499/554/216 511/567/216 512/568/216 500/555/216
+f 500/555/215 512/568/215 513/569/215 501/556/215
+f 501/556/214 513/569/214 514/570/214 502/557/214
+f 502/557/213 514/570/213 515/571/213 503/558/213
+f 503/558/212 515/571/212 516/572/212 504/559/212
+f 504/559/211 516/572/211 505/560/211 493/547/211
+f 505/560/222 517/573/222 518/574/222 506/561/222
+f 506/561/221 518/574/221 519/575/221 507/562/221
+f 507/562/220 519/575/220 520/576/220 508/563/220
+f 508/563/219 520/576/219 521/577/219 509/564/219
+f 509/564/218 521/577/218 522/578/218 510/565/218
+f 510/565/217 522/578/217 523/579/217 511/566/217
+f 511/567/228 523/580/228 524/581/228 512/568/228
+f 512/568/227 524/581/227 525/582/227 513/569/227
+f 513/569/226 525/582/226 526/583/226 514/570/226
+f 514/570/225 526/583/225 527/584/225 515/571/225
+f 515/571/224 527/584/224 528/585/224 516/572/224
+f 516/572/223 528/585/223 517/573/223 505/560/223
+f 517/573/234 529/586/234 530/587/234 518/574/234
+f 518/574/233 530/587/233 531/588/233 519/575/233
+f 519/575/232 531/588/232 532/589/232 520/576/232
+f 520/576/231 532/589/231 533/590/231 521/577/231
+f 521/577/230 533/590/230 534/591/230 522/578/230
+f 522/578/229 534/591/229 535/592/229 523/579/229
+f 523/580/240 535/593/240 536/594/240 524/581/240
+f 524/581/239 536/594/239 537/595/239 525/582/239
+f 525/582/238 537/595/238 538/596/238 526/583/238
+f 526/583/237 538/596/237 539/597/237 527/584/237
+f 527/584/236 539/597/236 540/598/236 528/585/236
+f 528/585/235 540/598/235 529/586/235 517/573/235
+f 529/586/246 541/599/246 542/600/246 530/587/246
+f 530/587/245 542/600/245 543/601/245 531/588/245
+f 531/588/244 543/601/244 544/602/244 532/589/244
+f 532/589/243 544/602/243 545/603/243 533/590/243
+f 533/590/242 545/603/242 546/604/242 534/591/242
+f 534/591/241 546/604/241 547/605/241 535/592/241
+f 535/593/252 547/606/252 548/607/252 536/594/252
+f 536/594/251 548/607/251 549/608/251 537/595/251
+f 537/595/250 549/608/250 550/609/250 538/596/250
+f 538/596/249 550/609/249 551/610/249 539/597/249
+f 539/597/248 551/610/248 552/611/248 540/598/248
+f 540/598/247 552/611/247 541/599/247 529/586/247
+f 541/599/258 553/612/258 554/613/258 542/600/258
+f 542/600/257 554/613/257 555/614/257 543/601/257
+f 543/601/256 555/614/256 556/615/256 544/602/256
+f 544/602/255 556/615/255 557/616/255 545/603/255
+f 545/603/254 557/616/254 558/617/254 546/604/254
+f 546/604/253 558/617/253 559/618/253 547/605/253
+f 547/606/264 559/619/264 560/620/264 548/607/264
+f 548/607/263 560/620/263 561/621/263 549/608/263
+f 549/608/262 561/621/262 562/622/262 550/609/262
+f 550/609/261 562/622/261 563/623/261 551/610/261
+f 551/610/260 563/623/260 564/624/260 552/611/260
+f 552/611/259 564/624/259 553/612/259 541/599/259
+f 553/612/270 565/625/270 566/626/270 554/613/270
+f 554/613/269 566/626/269 567/627/269 555/614/269
+f 555/614/268 567/627/268 568/628/268 556/615/268
+f 556/615/267 568/628/267 569/629/267 557/616/267
+f 557/616/266 569/629/266 570/630/266 558/617/266
+f 558/617/265 570/630/265 571/631/265 559/618/265
+f 559/619/276 571/632/276 572/633/276 560/620/276
+f 560/620/275 572/633/275 573/634/275 561/621/275
+f 561/621/274 573/634/274 574/635/274 562/622/274
+f 562/622/273 574/635/273 575/636/273 563/623/273
+f 563/623/272 575/636/272 576/637/272 564/624/272
+f 564/624/271 576/637/271 565/625/271 553/612/271
+f 565/625/282 1/1/282 2/4/282 566/626/282
+f 566/626/281 2/4/281 3/6/281 567/627/281
+f 567/627/280 3/6/280 4/8/280 568/628/280
+f 568/628/279 4/8/279 5/10/279 569/629/279
+f 569/629/278 5/10/278 6/12/278 570/630/278
+f 570/630/277 6/12/277 7/14/277 571/631/277
+f 571/632/288 7/15/288 8/18/288 572/633/288
+f 572/633/287 8/18/287 9/20/287 573/634/287
+f 573/634/286 9/20/286 10/22/286 574/635/286
+f 574/635/285 10/22/285 11/24/285 575/636/285
+f 575/636/284 11/24/284 12/26/284 576/637/284
+f 576/637/283 12/26/283 1/1/283 565/625/283
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/horns/model.json b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/horns/model.json
new file mode 100644
index 0000000..1387e00
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/horns/model.json
@@ -0,0 +1,4 @@
+{
+ "translate": [0.0, 0.25, 0.0],
+ "scale": [0.25, 0.25, 0.25]
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/horns/model.obj b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/horns/model.obj
new file mode 100644
index 0000000..ae6f75c
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/hat/horns/model.obj
@@ -0,0 +1,1109 @@
+# Blender 4.4.0
+# www.blender.org
+mtllib horns.mtl
+o Cylinder.002
+v -1.242236 1.125547 0.100000
+v -0.992896 1.414159 0.021275
+v -1.261081 1.130596 0.098079
+v -0.996905 1.415234 0.020866
+v -1.279201 1.135451 0.092388
+v -1.000761 1.416267 0.019656
+v -1.295900 1.139926 0.083147
+v -1.004313 1.417219 0.017690
+v -1.310538 1.143848 0.070711
+v -1.007428 1.418053 0.015044
+v -1.322550 1.147067 0.055557
+v -1.009983 1.418738 0.011820
+v -1.331476 1.149459 0.038268
+v -1.011882 1.419247 0.008142
+v -1.336973 1.150931 0.019509
+v -1.013052 1.419560 0.004151
+v -1.338829 1.151429 0.000000
+v -1.013447 1.419666 -0.000000
+v -1.336973 1.150931 -0.019509
+v -1.013052 1.419560 -0.004151
+v -1.331476 1.149459 -0.038268
+v -1.011882 1.419247 -0.008142
+v -1.322550 1.147067 -0.055557
+v -1.009983 1.418738 -0.011820
+v -1.310538 1.143848 -0.070711
+v -1.007428 1.418053 -0.015044
+v -1.295900 1.139926 -0.083147
+v -1.004313 1.417219 -0.017690
+v -1.279201 1.135451 -0.092388
+v -1.000761 1.416267 -0.019656
+v -1.261081 1.130596 -0.098079
+v -0.996905 1.415234 -0.020866
+v -1.242236 1.125547 -0.100000
+v -0.992896 1.414159 -0.021275
+v -1.223392 1.120497 -0.098079
+v -0.988887 1.413085 -0.020866
+v -1.205272 1.115642 -0.092388
+v -0.985032 1.412052 -0.019656
+v -1.188572 1.111168 -0.083147
+v -0.981479 1.411100 -0.017690
+v -1.173935 1.107245 -0.070711
+v -0.978365 1.410266 -0.015044
+v -1.161923 1.104027 -0.055557
+v -0.975809 1.409581 -0.011820
+v -1.152996 1.101635 -0.038268
+v -0.973910 1.409072 -0.008142
+v -1.147500 1.100162 -0.019509
+v -0.972741 1.408759 -0.004151
+v -1.145644 1.099665 -0.000000
+v -0.972346 1.408653 -0.000000
+v -1.147500 1.100162 0.019509
+v -0.972741 1.408759 0.004151
+v -1.152996 1.101635 0.038268
+v -0.973910 1.409072 0.008142
+v -1.161923 1.104027 0.055557
+v -0.975809 1.409581 0.011820
+v -1.173935 1.107245 0.070711
+v -0.978365 1.410266 0.015044
+v -1.188572 1.111168 0.083147
+v -0.981479 1.411100 0.017690
+v -1.205272 1.115642 0.092388
+v -0.985032 1.412052 0.019656
+v -1.223392 1.120497 0.098079
+v -0.988887 1.413085 0.020866
+v -1.211112 0.788704 0.150000
+v -1.235372 0.772340 0.147118
+v -1.258701 0.756605 0.138582
+v -1.280200 0.742103 0.124720
+v -1.299044 0.729392 0.106066
+v -1.314510 0.718961 0.083336
+v -1.326001 0.711210 0.057403
+v -1.333078 0.706437 0.029264
+v -1.335467 0.704825 0.000000
+v -1.333078 0.706437 -0.029264
+v -1.326001 0.711210 -0.057403
+v -1.314510 0.718961 -0.083336
+v -1.299044 0.729392 -0.106066
+v -1.280200 0.742103 -0.124720
+v -1.258701 0.756605 -0.138582
+v -1.235372 0.772340 -0.147118
+v -1.211112 0.788704 -0.150000
+v -1.186851 0.805068 -0.147118
+v -1.163523 0.820803 -0.138582
+v -1.142023 0.835304 -0.124720
+v -1.123179 0.848015 -0.106066
+v -1.107714 0.858447 -0.083336
+v -1.096222 0.866198 -0.057403
+v -1.089146 0.870971 -0.029264
+v -1.086756 0.872583 -0.000000
+v -1.089146 0.870971 0.029264
+v -1.096222 0.866198 0.057403
+v -1.107714 0.858447 0.083336
+v -1.123179 0.848015 0.106066
+v -1.142023 0.835304 0.124720
+v -1.163523 0.820803 0.138582
+v -1.186851 0.805068 0.147118
+v -0.880525 0.487694 0.190000
+v -1.067972 0.630938 0.190000
+v -0.901786 0.457330 0.186349
+v -1.089233 0.600574 0.186349
+v -0.922230 0.428133 0.175537
+v -1.109677 0.571377 0.175537
+v -0.941071 0.401225 0.157979
+v -1.128518 0.544469 0.157979
+v -0.957585 0.377640 0.134350
+v -1.145033 0.520884 0.134350
+v -0.971138 0.358285 0.105558
+v -1.158586 0.501529 0.105558
+v -0.981209 0.343902 0.072710
+v -1.168656 0.487146 0.072710
+v -0.987411 0.335045 0.037067
+v -1.174858 0.478289 0.037067
+v -0.989505 0.332055 -0.000000
+v -1.176952 0.475299 -0.000000
+v -0.987411 0.335045 -0.037067
+v -1.174858 0.478289 -0.037067
+v -0.981209 0.343902 -0.072710
+v -1.168656 0.487146 -0.072710
+v -0.971138 0.358285 -0.105558
+v -1.158586 0.501529 -0.105558
+v -0.957585 0.377640 -0.134350
+v -1.145033 0.520884 -0.134350
+v -0.941071 0.401225 -0.157979
+v -1.128518 0.544469 -0.157979
+v -0.922230 0.428133 -0.175537
+v -1.109677 0.571377 -0.175537
+v -0.901786 0.457330 -0.186349
+v -1.089233 0.600574 -0.186349
+v -0.880525 0.487694 -0.190000
+v -1.067972 0.630938 -0.190000
+v -0.859264 0.518057 -0.186349
+v -1.046712 0.661301 -0.186349
+v -0.838820 0.547254 -0.175537
+v -1.026268 0.690498 -0.175537
+v -0.819979 0.574162 -0.157979
+v -1.007427 0.717406 -0.157979
+v -0.803465 0.597747 -0.134350
+v -0.990912 0.740991 -0.134350
+v -0.789912 0.617103 -0.105558
+v -0.977359 0.760347 -0.105558
+v -0.779841 0.631485 -0.072710
+v -0.967288 0.774729 -0.072710
+v -0.773640 0.640342 -0.037067
+v -0.961087 0.783586 -0.037067
+v -0.771546 0.643333 -0.000000
+v -0.958993 0.786576 -0.000000
+v -0.773640 0.640342 0.037067
+v -0.961087 0.783586 0.037067
+v -0.779841 0.631485 0.072710
+v -0.967288 0.774729 0.072710
+v -0.789912 0.617103 0.105558
+v -0.977359 0.760347 0.105558
+v -0.803465 0.597747 0.134350
+v -0.990912 0.740991 0.134350
+v -0.819979 0.574162 0.157979
+v -1.007427 0.717406 0.157979
+v -0.838820 0.547254 0.175537
+v -1.026268 0.690498 0.175537
+v -0.859264 0.518057 0.186349
+v -1.046712 0.661301 0.186349
+vn -0.0225 0.2811 0.9594
+vn -0.1736 0.4046 0.8979
+vn -0.3068 0.5120 0.8023
+vn -0.4181 0.6006 0.6815
+vn -0.5061 0.6697 0.5434
+vn -0.5708 0.7202 0.3943
+vn -0.6132 0.7530 0.2388
+vn -0.6341 0.7691 0.0799
+vn -0.6341 0.7691 -0.0799
+vn -0.6132 0.7530 -0.2388
+vn -0.5708 0.7202 -0.3943
+vn -0.5061 0.6697 -0.5434
+vn -0.4181 0.6006 -0.6815
+vn -0.3068 0.5120 -0.8023
+vn -0.1736 0.4046 -0.8979
+vn -0.0225 0.2811 -0.9594
+vn 0.1392 0.1469 -0.9793
+vn 0.3019 0.0096 -0.9533
+vn 0.4554 -0.1219 -0.8819
+vn 0.5905 -0.2396 -0.7706
+vn 0.7015 -0.3374 -0.6277
+vn 0.7851 -0.4120 -0.4624
+vn 0.8407 -0.4620 -0.2826
+vn 0.8682 -0.4870 -0.0950
+vn 0.8682 -0.4870 0.0950
+vn 0.8407 -0.4620 0.2826
+vn 0.7851 -0.4120 0.4624
+vn 0.7015 -0.3374 0.6277
+vn 0.5905 -0.2396 0.7706
+vn 0.4554 -0.1219 0.8819
+vn 0.2588 0.9659 -0.0000
+vn 0.3019 0.0096 0.9533
+vn 0.1392 0.1469 0.9793
+vn 0.0687 0.1567 0.9853
+vn 0.0722 0.3343 0.9397
+vn -0.0596 -0.0779 0.9952
+vn -0.1764 -0.2308 0.9569
+vn -0.2864 -0.3748 0.8818
+vn -0.3854 -0.5043 0.7727
+vn -0.4695 -0.6144 0.6341
+vn -0.5356 -0.7009 0.4711
+vn -0.5811 -0.7604 0.2901
+vn -0.6043 -0.7907 0.0979
+vn -0.6043 -0.7907 -0.0979
+vn -0.5811 -0.7604 -0.2901
+vn -0.5356 -0.7009 -0.4711
+vn -0.4695 -0.6144 -0.6341
+vn -0.3854 -0.5043 -0.7727
+vn -0.2864 -0.3748 -0.8818
+vn -0.1764 -0.2308 -0.9569
+vn -0.0596 -0.0779 -0.9952
+vn 0.0596 0.0779 -0.9952
+vn 0.1764 0.2308 -0.9569
+vn 0.2864 0.3748 -0.8818
+vn 0.3854 0.5043 -0.7727
+vn 0.4695 0.6144 -0.6341
+vn 0.5356 0.7009 -0.4711
+vn 0.5811 0.7604 -0.2901
+vn 0.6043 0.7907 -0.0979
+vn 0.6043 0.7907 0.0979
+vn 0.5811 0.7604 0.2901
+vn 0.5356 0.7009 0.4711
+vn 0.4695 0.6144 0.6341
+vn 0.3854 0.5043 0.7727
+vn 0.2864 0.3748 0.8818
+vn -0.0628 0.1980 0.9782
+vn 0.1764 0.2308 0.9569
+vn 0.0596 0.0779 0.9952
+vn 0.8192 -0.5736 -0.0000
+vn 0.1987 0.4621 0.8642
+vn 0.3116 0.5764 0.7554
+vn 0.4067 0.6728 0.6180
+vn 0.4805 0.7479 0.4579
+vn 0.5310 0.7993 0.2814
+vn 0.5565 0.8254 0.0949
+vn 0.5565 0.8254 -0.0949
+vn 0.5310 0.7993 -0.2814
+vn 0.4805 0.7479 -0.4579
+vn 0.4067 0.6728 -0.6180
+vn 0.3116 0.5764 -0.7554
+vn 0.1987 0.4621 -0.8642
+vn 0.0722 0.3343 -0.9397
+vn -0.0628 0.1980 -0.9782
+vn -0.2008 0.0589 -0.9779
+vn -0.3357 -0.0772 -0.9388
+vn -0.4620 -0.2046 -0.8629
+vn -0.5747 -0.3184 -0.7539
+vn -0.6696 -0.4141 -0.6166
+vn -0.7434 -0.4887 -0.4567
+vn -0.7938 -0.5396 -0.2806
+vn -0.8194 -0.5654 -0.0946
+vn -0.8194 -0.5654 0.0946
+vn -0.7938 -0.5396 0.2806
+vn -0.7434 -0.4887 0.4567
+vn -0.6696 -0.4141 0.6166
+vn -0.5747 -0.3184 0.7539
+vn -0.4620 -0.2046 0.8629
+vn -0.3357 -0.0772 0.9388
+vn -0.2008 0.0589 0.9779
+vn 0.2759 0.1827 0.9437
+vn 0.4667 0.2045 0.8604
+vn 0.6317 0.2217 0.7429
+vn 0.7653 0.2341 0.5996
+vn 0.8653 0.2424 0.4388
+vn 0.9314 0.2474 0.2671
+vn 0.9642 0.2497 0.0896
+vn 0.9642 0.2497 -0.0896
+vn 0.9314 0.2474 -0.2671
+vn 0.8653 0.2424 -0.4388
+vn 0.7653 0.2341 -0.5996
+vn 0.6317 0.2217 -0.7429
+vn 0.4667 0.2045 -0.8604
+vn 0.2759 0.1827 -0.9437
+vn 0.0687 0.1567 -0.9853
+vn -0.1430 0.1281 -0.9814
+vn -0.3462 0.0988 -0.9330
+vn -0.5298 0.0708 -0.8452
+vn -0.6862 0.0458 -0.7260
+vn -0.8115 0.0249 -0.5838
+vn -0.9046 0.0090 -0.4262
+vn -0.9659 -0.0018 -0.2591
+vn -0.9962 -0.0072 -0.0869
+vn -0.9962 -0.0072 0.0869
+vn -0.9659 -0.0018 0.2591
+vn -0.9046 0.0090 0.4262
+vn -0.8115 0.0249 0.5838
+vn -0.6862 0.0458 0.7260
+vn -0.5298 0.0708 0.8452
+vn -0.3462 0.0988 0.9330
+vn -0.1430 0.1281 0.9814
+vt 1.000000 0.500000
+vt 1.000000 1.000000
+vt 0.968750 1.000000
+vt 0.968750 0.500000
+vt 0.937500 1.000000
+vt 0.937500 0.500000
+vt 0.906250 1.000000
+vt 0.906250 0.500000
+vt 0.875000 1.000000
+vt 0.875000 0.500000
+vt 0.843750 1.000000
+vt 0.843750 0.500000
+vt 0.812500 1.000000
+vt 0.812500 0.500000
+vt 0.781250 1.000000
+vt 0.781250 0.500000
+vt 0.750000 1.000000
+vt 0.750000 0.500000
+vt 0.718750 1.000000
+vt 0.718750 0.500000
+vt 0.687500 1.000000
+vt 0.687500 0.500000
+vt 0.656250 1.000000
+vt 0.656250 0.500000
+vt 0.625000 1.000000
+vt 0.625000 0.500000
+vt 0.593750 1.000000
+vt 0.593750 0.500000
+vt 0.562500 1.000000
+vt 0.562500 0.500000
+vt 0.531250 1.000000
+vt 0.531250 0.500000
+vt 0.500000 1.000000
+vt 0.500000 0.500000
+vt 0.468750 1.000000
+vt 0.468750 0.500000
+vt 0.437500 1.000000
+vt 0.437500 0.500000
+vt 0.406250 1.000000
+vt 0.406250 0.500000
+vt 0.375000 1.000000
+vt 0.375000 0.500000
+vt 0.343750 1.000000
+vt 0.343750 0.500000
+vt 0.312500 1.000000
+vt 0.312500 0.500000
+vt 0.281250 1.000000
+vt 0.281250 0.500000
+vt 0.250000 1.000000
+vt 0.250000 0.500000
+vt 0.218750 1.000000
+vt 0.218750 0.500000
+vt 0.187500 1.000000
+vt 0.187500 0.500000
+vt 0.156250 1.000000
+vt 0.156250 0.500000
+vt 0.125000 1.000000
+vt 0.125000 0.500000
+vt 0.093750 1.000000
+vt 0.093750 0.500000
+vt 0.062500 1.000000
+vt 0.062500 0.500000
+vt 0.296822 0.485388
+vt 0.250000 0.490000
+vt 0.203178 0.485388
+vt 0.158156 0.471731
+vt 0.116663 0.449553
+vt 0.080294 0.419706
+vt 0.050447 0.383337
+vt 0.028269 0.341844
+vt 0.014612 0.296822
+vt 0.010000 0.250000
+vt 0.014612 0.203178
+vt 0.028269 0.158156
+vt 0.050447 0.116663
+vt 0.080294 0.080294
+vt 0.116663 0.050447
+vt 0.158156 0.028269
+vt 0.203178 0.014612
+vt 0.250000 0.010000
+vt 0.296822 0.014612
+vt 0.341844 0.028269
+vt 0.383337 0.050447
+vt 0.419706 0.080294
+vt 0.449553 0.116663
+vt 0.471731 0.158156
+vt 0.485388 0.203178
+vt 0.490000 0.250000
+vt 0.485388 0.296822
+vt 0.471731 0.341844
+vt 0.449553 0.383337
+vt 0.419706 0.419706
+vt 0.383337 0.449553
+vt 0.341844 0.471731
+vt 0.031250 1.000000
+vt 0.031250 0.500000
+vt 0.000000 1.000000
+vt 0.000000 0.500000
+vt 0.750000 0.490000
+vt 0.796822 0.485388
+vt 0.841844 0.471731
+vt 0.883337 0.449553
+vt 0.919706 0.419706
+vt 0.949553 0.383337
+vt 0.971731 0.341844
+vt 0.985388 0.296822
+vt 0.990000 0.250000
+vt 0.985388 0.203178
+vt 0.971731 0.158156
+vt 0.949553 0.116663
+vt 0.919706 0.080294
+vt 0.883337 0.050447
+vt 0.841844 0.028269
+vt 0.796822 0.014612
+vt 0.750000 0.010000
+vt 0.703178 0.014612
+vt 0.658156 0.028269
+vt 0.616663 0.050447
+vt 0.580294 0.080294
+vt 0.550447 0.116663
+vt 0.528269 0.158156
+vt 0.514612 0.203178
+vt 0.510000 0.250000
+vt 0.514612 0.296822
+vt 0.528269 0.341844
+vt 0.550447 0.383337
+vt 0.580294 0.419706
+vt 0.616663 0.449553
+vt 0.658156 0.471731
+vt 0.703178 0.485388
+usemtl tex
+s off
+f 1/1/1 2/2/1 4/3/1 3/4/1
+f 3/4/2 4/3/2 6/5/2 5/6/2
+f 5/6/3 6/5/3 8/7/3 7/8/3
+f 7/8/4 8/7/4 10/9/4 9/10/4
+f 9/10/5 10/9/5 12/11/5 11/12/5
+f 11/12/6 12/11/6 14/13/6 13/14/6
+f 13/14/7 14/13/7 16/15/7 15/16/7
+f 15/16/8 16/15/8 18/17/8 17/18/8
+f 17/18/9 18/17/9 20/19/9 19/20/9
+f 19/20/10 20/19/10 22/21/10 21/22/10
+f 21/22/11 22/21/11 24/23/11 23/24/11
+f 23/24/12 24/23/12 26/25/12 25/26/12
+f 25/26/13 26/25/13 28/27/13 27/28/13
+f 27/28/14 28/27/14 30/29/14 29/30/14
+f 29/30/15 30/29/15 32/31/15 31/32/15
+f 31/32/16 32/31/16 34/33/16 33/34/16
+f 33/34/17 34/33/17 36/35/17 35/36/17
+f 35/36/18 36/35/18 38/37/18 37/38/18
+f 37/38/19 38/37/19 40/39/19 39/40/19
+f 39/40/20 40/39/20 42/41/20 41/42/20
+f 41/42/21 42/41/21 44/43/21 43/44/21
+f 43/44/22 44/43/22 46/45/22 45/46/22
+f 45/46/23 46/45/23 48/47/23 47/48/23
+f 47/48/24 48/47/24 50/49/24 49/50/24
+f 49/50/25 50/49/25 52/51/25 51/52/25
+f 51/52/26 52/51/26 54/53/26 53/54/26
+f 53/54/27 54/53/27 56/55/27 55/56/27
+f 55/56/28 56/55/28 58/57/28 57/58/28
+f 57/58/29 58/57/29 60/59/29 59/60/29
+f 59/60/30 60/59/30 62/61/30 61/62/30
+f 4/63/31 2/64/31 64/65/31 62/66/31 60/67/31 58/68/31 56/69/31 54/70/31 52/71/31 50/72/31 48/73/31 46/74/31 44/75/31 42/76/31 40/77/31 38/78/31 36/79/31 34/80/31 32/81/31 30/82/31 28/83/31 26/84/31 24/85/31 22/86/31 20/87/31 18/88/31 16/89/31 14/90/31 12/91/31 10/92/31 8/93/31 6/94/31
+f 61/62/32 62/61/32 64/95/32 63/96/32
+f 63/96/33 64/95/33 2/97/33 1/98/33
+f 65/98/34 96/96/34 63/96/34 1/98/34
+f 160/95/35 158/61/35 95/62/35 96/96/35
+f 97/1/36 98/2/36 100/3/36 99/4/36
+f 99/4/37 100/3/37 102/5/37 101/6/37
+f 101/6/38 102/5/38 104/7/38 103/8/38
+f 103/8/39 104/7/39 106/9/39 105/10/39
+f 105/10/40 106/9/40 108/11/40 107/12/40
+f 107/12/41 108/11/41 110/13/41 109/14/41
+f 109/14/42 110/13/42 112/15/42 111/16/42
+f 111/16/43 112/15/43 114/17/43 113/18/43
+f 113/18/44 114/17/44 116/19/44 115/20/44
+f 115/20/45 116/19/45 118/21/45 117/22/45
+f 117/22/46 118/21/46 120/23/46 119/24/46
+f 119/24/47 120/23/47 122/25/47 121/26/47
+f 121/26/48 122/25/48 124/27/48 123/28/48
+f 123/28/49 124/27/49 126/29/49 125/30/49
+f 125/30/50 126/29/50 128/31/50 127/32/50
+f 127/32/51 128/31/51 130/33/51 129/34/51
+f 129/34/52 130/33/52 132/35/52 131/36/52
+f 131/36/53 132/35/53 134/37/53 133/38/53
+f 133/38/54 134/37/54 136/39/54 135/40/54
+f 135/40/55 136/39/55 138/41/55 137/42/55
+f 137/42/56 138/41/56 140/43/56 139/44/56
+f 139/44/57 140/43/57 142/45/57 141/46/57
+f 141/46/58 142/45/58 144/47/58 143/48/58
+f 143/48/59 144/47/59 146/49/59 145/50/59
+f 145/50/60 146/49/60 148/51/60 147/52/60
+f 147/52/61 148/51/61 150/53/61 149/54/61
+f 149/54/62 150/53/62 152/55/62 151/56/62
+f 151/56/63 152/55/63 154/57/63 153/58/63
+f 153/58/64 154/57/64 156/59/64 155/60/64
+f 155/60/65 156/59/65 158/61/65 157/62/65
+f 98/97/66 160/95/66 96/96/66 65/98/66
+f 157/62/67 158/61/67 160/95/67 159/96/67
+f 159/96/68 160/95/68 98/97/68 97/98/68
+f 97/99/69 99/100/69 101/101/69 103/102/69 105/103/69 107/104/69 109/105/69 111/106/69 113/107/69 115/108/69 117/109/69 119/110/69 121/111/69 123/112/69 125/113/69 127/114/69 129/115/69 131/116/69 133/117/69 135/118/69 137/119/69 139/120/69 141/121/69 143/122/69 145/123/69 147/124/69 149/125/69 151/126/69 153/127/69 155/128/69 157/129/69 159/130/69
+f 158/61/70 156/59/70 94/60/70 95/62/70
+f 156/59/71 154/57/71 93/58/71 94/60/71
+f 154/57/72 152/55/72 92/56/72 93/58/72
+f 152/55/73 150/53/73 91/54/73 92/56/73
+f 150/53/74 148/51/74 90/52/74 91/54/74
+f 148/51/75 146/49/75 89/50/75 90/52/75
+f 146/49/76 144/47/76 88/48/76 89/50/76
+f 144/47/77 142/45/77 87/46/77 88/48/77
+f 142/45/78 140/43/78 86/44/78 87/46/78
+f 140/43/79 138/41/79 85/42/79 86/44/79
+f 138/41/80 136/39/80 84/40/80 85/42/80
+f 136/39/81 134/37/81 83/38/81 84/40/81
+f 134/37/82 132/35/82 82/36/82 83/38/82
+f 132/35/83 130/33/83 81/34/83 82/36/83
+f 130/33/84 128/31/84 80/32/84 81/34/84
+f 128/31/85 126/29/85 79/30/85 80/32/85
+f 126/29/86 124/27/86 78/28/86 79/30/86
+f 124/27/87 122/25/87 77/26/87 78/28/87
+f 122/25/88 120/23/88 76/24/88 77/26/88
+f 120/23/89 118/21/89 75/22/89 76/24/89
+f 118/21/90 116/19/90 74/20/90 75/22/90
+f 116/19/91 114/17/91 73/18/91 74/20/91
+f 114/17/92 112/15/92 72/16/92 73/18/92
+f 112/15/93 110/13/93 71/14/93 72/16/93
+f 110/13/94 108/11/94 70/12/94 71/14/94
+f 108/11/95 106/9/95 69/10/95 70/12/95
+f 106/9/96 104/7/96 68/8/96 69/10/96
+f 104/7/97 102/5/97 67/6/97 68/8/97
+f 102/5/98 100/3/98 66/4/98 67/6/98
+f 100/3/99 98/2/99 65/1/99 66/4/99
+f 96/96/100 95/62/100 61/62/100 63/96/100
+f 95/62/101 94/60/101 59/60/101 61/62/101
+f 94/60/102 93/58/102 57/58/102 59/60/102
+f 93/58/103 92/56/103 55/56/103 57/58/103
+f 92/56/104 91/54/104 53/54/104 55/56/104
+f 91/54/105 90/52/105 51/52/105 53/54/105
+f 90/52/106 89/50/106 49/50/106 51/52/106
+f 89/50/107 88/48/107 47/48/107 49/50/107
+f 88/48/108 87/46/108 45/46/108 47/48/108
+f 87/46/109 86/44/109 43/44/109 45/46/109
+f 86/44/110 85/42/110 41/42/110 43/44/110
+f 85/42/111 84/40/111 39/40/111 41/42/111
+f 84/40/112 83/38/112 37/38/112 39/40/112
+f 83/38/113 82/36/113 35/36/113 37/38/113
+f 82/36/114 81/34/114 33/34/114 35/36/114
+f 81/34/115 80/32/115 31/32/115 33/34/115
+f 80/32/116 79/30/116 29/30/116 31/32/116
+f 79/30/117 78/28/117 27/28/117 29/30/117
+f 78/28/118 77/26/118 25/26/118 27/28/118
+f 77/26/119 76/24/119 23/24/119 25/26/119
+f 76/24/120 75/22/120 21/22/120 23/24/120
+f 75/22/121 74/20/121 19/20/121 21/22/121
+f 74/20/122 73/18/122 17/18/122 19/20/122
+f 73/18/123 72/16/123 15/16/123 17/18/123
+f 72/16/124 71/14/124 13/14/124 15/16/124
+f 71/14/125 70/12/125 11/12/125 13/14/125
+f 70/12/126 69/10/126 9/10/126 11/12/126
+f 69/10/127 68/8/127 7/8/127 9/10/127
+f 68/8/128 67/6/128 5/6/128 7/8/128
+f 67/6/129 66/4/129 3/4/129 5/6/129
+f 66/4/130 65/1/130 1/1/130 3/4/130
+o Cylinder.003
+v 1.242236 1.125547 -0.100000
+v 0.992896 1.414159 -0.021275
+v 1.261081 1.130596 -0.098079
+v 0.996905 1.415234 -0.020866
+v 1.279201 1.135451 -0.092388
+v 1.000761 1.416267 -0.019656
+v 1.295900 1.139926 -0.083147
+v 1.004313 1.417219 -0.017690
+v 1.310538 1.143848 -0.070711
+v 1.007428 1.418053 -0.015044
+v 1.322550 1.147067 -0.055557
+v 1.009983 1.418738 -0.011820
+v 1.331476 1.149459 -0.038268
+v 1.011882 1.419247 -0.008142
+v 1.336973 1.150931 -0.019509
+v 1.013052 1.419560 -0.004151
+v 1.338829 1.151429 0.000000
+v 1.013447 1.419666 0.000000
+v 1.336973 1.150931 0.019509
+v 1.013052 1.419560 0.004151
+v 1.331476 1.149459 0.038268
+v 1.011882 1.419247 0.008142
+v 1.322550 1.147067 0.055557
+v 1.009983 1.418738 0.011820
+v 1.310538 1.143848 0.070711
+v 1.007428 1.418053 0.015044
+v 1.295900 1.139926 0.083147
+v 1.004313 1.417219 0.017690
+v 1.279201 1.135451 0.092388
+v 1.000761 1.416267 0.019656
+v 1.261081 1.130596 0.098079
+v 0.996905 1.415234 0.020866
+v 1.242236 1.125547 0.100000
+v 0.992896 1.414159 0.021275
+v 1.223392 1.120497 0.098079
+v 0.988887 1.413085 0.020866
+v 1.205272 1.115642 0.092388
+v 0.985032 1.412052 0.019656
+v 1.188572 1.111168 0.083147
+v 0.981479 1.411100 0.017690
+v 1.173935 1.107245 0.070711
+v 0.978365 1.410266 0.015044
+v 1.161923 1.104027 0.055557
+v 0.975809 1.409581 0.011820
+v 1.152996 1.101635 0.038268
+v 0.973910 1.409072 0.008142
+v 1.147500 1.100162 0.019509
+v 0.972741 1.408759 0.004151
+v 1.145644 1.099665 0.000000
+v 0.972346 1.408653 0.000000
+v 1.147500 1.100162 -0.019509
+v 0.972741 1.408759 -0.004151
+v 1.152996 1.101635 -0.038268
+v 0.973910 1.409072 -0.008142
+v 1.161923 1.104027 -0.055557
+v 0.975809 1.409581 -0.011820
+v 1.173935 1.107245 -0.070711
+v 0.978365 1.410266 -0.015044
+v 1.188572 1.111168 -0.083147
+v 0.981479 1.411100 -0.017690
+v 1.205272 1.115642 -0.092388
+v 0.985032 1.412052 -0.019656
+v 1.223392 1.120497 -0.098079
+v 0.988887 1.413085 -0.020866
+v 1.211112 0.788704 -0.150000
+v 1.235372 0.772340 -0.147118
+v 1.258701 0.756605 -0.138582
+v 1.280200 0.742103 -0.124720
+v 1.299044 0.729392 -0.106066
+v 1.314510 0.718961 -0.083336
+v 1.326001 0.711210 -0.057403
+v 1.333078 0.706437 -0.029264
+v 1.335467 0.704825 0.000000
+v 1.333078 0.706437 0.029264
+v 1.326001 0.711210 0.057403
+v 1.314510 0.718961 0.083336
+v 1.299044 0.729392 0.106066
+v 1.280200 0.742103 0.124720
+v 1.258701 0.756605 0.138582
+v 1.235372 0.772340 0.147118
+v 1.211112 0.788704 0.150000
+v 1.186851 0.805068 0.147118
+v 1.163523 0.820803 0.138582
+v 1.142023 0.835304 0.124720
+v 1.123179 0.848015 0.106066
+v 1.107714 0.858447 0.083336
+v 1.096222 0.866198 0.057403
+v 1.089146 0.870971 0.029264
+v 1.086756 0.872583 0.000000
+v 1.089146 0.870971 -0.029264
+v 1.096222 0.866198 -0.057403
+v 1.107714 0.858447 -0.083336
+v 1.123179 0.848015 -0.106066
+v 1.142023 0.835304 -0.124720
+v 1.163523 0.820803 -0.138582
+v 1.186851 0.805068 -0.147118
+v 0.880525 0.487694 -0.190000
+v 1.067972 0.630938 -0.190000
+v 0.901786 0.457330 -0.186349
+v 1.089233 0.600574 -0.186349
+v 0.922230 0.428133 -0.175537
+v 1.109677 0.571377 -0.175537
+v 0.941071 0.401225 -0.157979
+v 1.128518 0.544469 -0.157979
+v 0.957585 0.377640 -0.134350
+v 1.145033 0.520884 -0.134350
+v 0.971138 0.358285 -0.105558
+v 1.158586 0.501529 -0.105558
+v 0.981209 0.343902 -0.072710
+v 1.168656 0.487146 -0.072710
+v 0.987411 0.335045 -0.037067
+v 1.174858 0.478289 -0.037067
+v 0.989505 0.332055 -0.000000
+v 1.176952 0.475299 0.000000
+v 0.987411 0.335045 0.037067
+v 1.174858 0.478289 0.037067
+v 0.981209 0.343902 0.072710
+v 1.168656 0.487146 0.072710
+v 0.971138 0.358285 0.105558
+v 1.158586 0.501529 0.105558
+v 0.957585 0.377640 0.134350
+v 1.145033 0.520884 0.134350
+v 0.941071 0.401225 0.157979
+v 1.128518 0.544469 0.157979
+v 0.922230 0.428133 0.175537
+v 1.109677 0.571377 0.175537
+v 0.901786 0.457330 0.186349
+v 1.089233 0.600574 0.186349
+v 0.880525 0.487694 0.190000
+v 1.067972 0.630938 0.190000
+v 0.859264 0.518057 0.186349
+v 1.046712 0.661301 0.186349
+v 0.838820 0.547254 0.175537
+v 1.026268 0.690498 0.175537
+v 0.819979 0.574162 0.157979
+v 1.007427 0.717406 0.157979
+v 0.803465 0.597747 0.134350
+v 0.990912 0.740991 0.134350
+v 0.789912 0.617103 0.105558
+v 0.977359 0.760347 0.105558
+v 0.779841 0.631485 0.072710
+v 0.967288 0.774729 0.072710
+v 0.773640 0.640342 0.037067
+v 0.961087 0.783586 0.037067
+v 0.771546 0.643333 -0.000000
+v 0.958993 0.786576 0.000000
+v 0.773640 0.640342 -0.037067
+v 0.961087 0.783586 -0.037067
+v 0.779841 0.631485 -0.072710
+v 0.967288 0.774729 -0.072710
+v 0.789912 0.617103 -0.105558
+v 0.977359 0.760347 -0.105558
+v 0.803465 0.597747 -0.134350
+v 0.990912 0.740991 -0.134350
+v 0.819979 0.574162 -0.157979
+v 1.007427 0.717406 -0.157979
+v 0.838820 0.547254 -0.175537
+v 1.026268 0.690498 -0.175537
+v 0.859264 0.518057 -0.186349
+v 1.046712 0.661301 -0.186349
+vn 0.0225 0.2811 -0.9594
+vn 0.1736 0.4046 -0.8979
+vn 0.3068 0.5120 -0.8023
+vn 0.4181 0.6006 -0.6815
+vn 0.5061 0.6697 -0.5434
+vn 0.5708 0.7202 -0.3943
+vn 0.6132 0.7530 -0.2388
+vn 0.6341 0.7691 -0.0799
+vn 0.6341 0.7691 0.0799
+vn 0.6132 0.7530 0.2388
+vn 0.5708 0.7202 0.3943
+vn 0.5061 0.6697 0.5434
+vn 0.4181 0.6006 0.6815
+vn 0.3068 0.5120 0.8023
+vn 0.1736 0.4046 0.8979
+vn 0.0225 0.2811 0.9594
+vn -0.1392 0.1469 0.9793
+vn -0.3019 0.0096 0.9533
+vn -0.4554 -0.1219 0.8819
+vn -0.5905 -0.2396 0.7706
+vn -0.7015 -0.3374 0.6277
+vn -0.7851 -0.4120 0.4624
+vn -0.8407 -0.4620 0.2826
+vn -0.8682 -0.4870 0.0950
+vn -0.8682 -0.4870 -0.0950
+vn -0.8407 -0.4620 -0.2826
+vn -0.7851 -0.4120 -0.4624
+vn -0.7015 -0.3374 -0.6277
+vn -0.5905 -0.2396 -0.7706
+vn -0.4554 -0.1219 -0.8819
+vn -0.2588 0.9659 -0.0000
+vn -0.3019 0.0096 -0.9533
+vn -0.1392 0.1469 -0.9793
+vn -0.0687 0.1567 -0.9853
+vn -0.0722 0.3343 -0.9397
+vn 0.0596 -0.0779 -0.9952
+vn 0.1764 -0.2308 -0.9569
+vn 0.2864 -0.3748 -0.8818
+vn 0.3854 -0.5043 -0.7727
+vn 0.4695 -0.6144 -0.6341
+vn 0.5356 -0.7009 -0.4711
+vn 0.5811 -0.7604 -0.2901
+vn 0.6043 -0.7907 -0.0979
+vn 0.6043 -0.7907 0.0979
+vn 0.5811 -0.7604 0.2901
+vn 0.5356 -0.7009 0.4711
+vn 0.4695 -0.6144 0.6341
+vn 0.3854 -0.5043 0.7727
+vn 0.2864 -0.3748 0.8818
+vn 0.1764 -0.2308 0.9569
+vn 0.0596 -0.0779 0.9952
+vn -0.0596 0.0779 0.9952
+vn -0.1764 0.2308 0.9569
+vn -0.2864 0.3748 0.8818
+vn -0.3854 0.5043 0.7727
+vn -0.4695 0.6144 0.6341
+vn -0.5356 0.7009 0.4711
+vn -0.5811 0.7604 0.2901
+vn -0.6043 0.7907 0.0979
+vn -0.6043 0.7907 -0.0979
+vn -0.5811 0.7604 -0.2901
+vn -0.5356 0.7009 -0.4711
+vn -0.4695 0.6144 -0.6341
+vn -0.3854 0.5043 -0.7727
+vn -0.2864 0.3748 -0.8818
+vn 0.0628 0.1980 -0.9782
+vn -0.1764 0.2308 -0.9569
+vn -0.0596 0.0779 -0.9952
+vn -0.8192 -0.5736 -0.0000
+vn -0.1987 0.4621 -0.8642
+vn -0.3116 0.5764 -0.7554
+vn -0.4067 0.6728 -0.6180
+vn -0.4805 0.7479 -0.4579
+vn -0.5310 0.7993 -0.2814
+vn -0.5565 0.8254 -0.0949
+vn -0.5565 0.8254 0.0949
+vn -0.5310 0.7993 0.2814
+vn -0.4805 0.7479 0.4579
+vn -0.4067 0.6728 0.6180
+vn -0.3116 0.5764 0.7554
+vn -0.1987 0.4621 0.8642
+vn -0.0722 0.3343 0.9397
+vn 0.0628 0.1980 0.9782
+vn 0.2008 0.0589 0.9779
+vn 0.3357 -0.0772 0.9388
+vn 0.4620 -0.2046 0.8629
+vn 0.5747 -0.3184 0.7539
+vn 0.6696 -0.4141 0.6166
+vn 0.7434 -0.4887 0.4567
+vn 0.7938 -0.5396 0.2806
+vn 0.8194 -0.5654 0.0946
+vn 0.8194 -0.5654 -0.0946
+vn 0.7938 -0.5396 -0.2806
+vn 0.7434 -0.4887 -0.4567
+vn 0.6696 -0.4141 -0.6166
+vn 0.5747 -0.3184 -0.7539
+vn 0.4620 -0.2046 -0.8629
+vn 0.3357 -0.0772 -0.9388
+vn 0.2008 0.0589 -0.9779
+vn -0.2759 0.1827 -0.9437
+vn -0.4667 0.2045 -0.8604
+vn -0.6317 0.2217 -0.7429
+vn -0.7653 0.2341 -0.5996
+vn -0.8653 0.2424 -0.4388
+vn -0.9314 0.2474 -0.2671
+vn -0.9642 0.2497 -0.0896
+vn -0.9642 0.2497 0.0896
+vn -0.9314 0.2474 0.2671
+vn -0.8653 0.2424 0.4388
+vn -0.7653 0.2341 0.5996
+vn -0.6317 0.2217 0.7429
+vn -0.4667 0.2045 0.8604
+vn -0.2759 0.1827 0.9437
+vn -0.0687 0.1567 0.9853
+vn 0.1430 0.1281 0.9814
+vn 0.3462 0.0988 0.9330
+vn 0.5298 0.0708 0.8452
+vn 0.6862 0.0458 0.7260
+vn 0.8115 0.0249 0.5838
+vn 0.9046 0.0090 0.4262
+vn 0.9659 -0.0018 0.2591
+vn 0.9962 -0.0072 0.0869
+vn 0.9962 -0.0072 -0.0869
+vn 0.9659 -0.0018 -0.2591
+vn 0.9046 0.0090 -0.4262
+vn 0.8115 0.0249 -0.5838
+vn 0.6862 0.0458 -0.7260
+vn 0.5298 0.0708 -0.8452
+vn 0.3462 0.0988 -0.9330
+vn 0.1430 0.1281 -0.9814
+vt 1.000000 0.500000
+vt 1.000000 1.000000
+vt 0.968750 1.000000
+vt 0.968750 0.500000
+vt 0.937500 1.000000
+vt 0.937500 0.500000
+vt 0.906250 1.000000
+vt 0.906250 0.500000
+vt 0.875000 1.000000
+vt 0.875000 0.500000
+vt 0.843750 1.000000
+vt 0.843750 0.500000
+vt 0.812500 1.000000
+vt 0.812500 0.500000
+vt 0.781250 1.000000
+vt 0.781250 0.500000
+vt 0.750000 1.000000
+vt 0.750000 0.500000
+vt 0.718750 1.000000
+vt 0.718750 0.500000
+vt 0.687500 1.000000
+vt 0.687500 0.500000
+vt 0.656250 1.000000
+vt 0.656250 0.500000
+vt 0.625000 1.000000
+vt 0.625000 0.500000
+vt 0.593750 1.000000
+vt 0.593750 0.500000
+vt 0.562500 1.000000
+vt 0.562500 0.500000
+vt 0.531250 1.000000
+vt 0.531250 0.500000
+vt 0.500000 1.000000
+vt 0.500000 0.500000
+vt 0.468750 1.000000
+vt 0.468750 0.500000
+vt 0.437500 1.000000
+vt 0.437500 0.500000
+vt 0.406250 1.000000
+vt 0.406250 0.500000
+vt 0.375000 1.000000
+vt 0.375000 0.500000
+vt 0.343750 1.000000
+vt 0.343750 0.500000
+vt 0.312500 1.000000
+vt 0.312500 0.500000
+vt 0.281250 1.000000
+vt 0.281250 0.500000
+vt 0.250000 1.000000
+vt 0.250000 0.500000
+vt 0.218750 1.000000
+vt 0.218750 0.500000
+vt 0.187500 1.000000
+vt 0.187500 0.500000
+vt 0.156250 1.000000
+vt 0.156250 0.500000
+vt 0.125000 1.000000
+vt 0.125000 0.500000
+vt 0.093750 1.000000
+vt 0.093750 0.500000
+vt 0.062500 1.000000
+vt 0.062500 0.500000
+vt 0.296822 0.485388
+vt 0.250000 0.490000
+vt 0.203178 0.485388
+vt 0.158156 0.471731
+vt 0.116663 0.449553
+vt 0.080294 0.419706
+vt 0.050447 0.383337
+vt 0.028269 0.341844
+vt 0.014612 0.296822
+vt 0.010000 0.250000
+vt 0.014612 0.203178
+vt 0.028269 0.158156
+vt 0.050447 0.116663
+vt 0.080294 0.080294
+vt 0.116663 0.050447
+vt 0.158156 0.028269
+vt 0.203178 0.014612
+vt 0.250000 0.010000
+vt 0.296822 0.014612
+vt 0.341844 0.028269
+vt 0.383337 0.050447
+vt 0.419706 0.080294
+vt 0.449553 0.116663
+vt 0.471731 0.158156
+vt 0.485388 0.203178
+vt 0.490000 0.250000
+vt 0.485388 0.296822
+vt 0.471731 0.341844
+vt 0.449553 0.383337
+vt 0.419706 0.419706
+vt 0.383337 0.449553
+vt 0.341844 0.471731
+vt 0.031250 1.000000
+vt 0.031250 0.500000
+vt 0.000000 1.000000
+vt 0.000000 0.500000
+vt 0.750000 0.490000
+vt 0.796822 0.485388
+vt 0.841844 0.471731
+vt 0.883337 0.449553
+vt 0.919706 0.419706
+vt 0.949553 0.383337
+vt 0.971731 0.341844
+vt 0.985388 0.296822
+vt 0.990000 0.250000
+vt 0.985388 0.203178
+vt 0.971731 0.158156
+vt 0.949553 0.116663
+vt 0.919706 0.080294
+vt 0.883337 0.050447
+vt 0.841844 0.028269
+vt 0.796822 0.014612
+vt 0.750000 0.010000
+vt 0.703178 0.014612
+vt 0.658156 0.028269
+vt 0.616663 0.050447
+vt 0.580294 0.080294
+vt 0.550447 0.116663
+vt 0.528269 0.158156
+vt 0.514612 0.203178
+vt 0.510000 0.250000
+vt 0.514612 0.296822
+vt 0.528269 0.341844
+vt 0.550447 0.383337
+vt 0.580294 0.419706
+vt 0.616663 0.449553
+vt 0.658156 0.471731
+vt 0.703178 0.485388
+usemtl tex
+s off
+f 161/131/131 162/132/131 164/133/131 163/134/131
+f 163/134/132 164/133/132 166/135/132 165/136/132
+f 165/136/133 166/135/133 168/137/133 167/138/133
+f 167/138/134 168/137/134 170/139/134 169/140/134
+f 169/140/135 170/139/135 172/141/135 171/142/135
+f 171/142/136 172/141/136 174/143/136 173/144/136
+f 173/144/137 174/143/137 176/145/137 175/146/137
+f 175/146/138 176/145/138 178/147/138 177/148/138
+f 177/148/139 178/147/139 180/149/139 179/150/139
+f 179/150/140 180/149/140 182/151/140 181/152/140
+f 181/152/141 182/151/141 184/153/141 183/154/141
+f 183/154/142 184/153/142 186/155/142 185/156/142
+f 185/156/143 186/155/143 188/157/143 187/158/143
+f 187/158/144 188/157/144 190/159/144 189/160/144
+f 189/160/145 190/159/145 192/161/145 191/162/145
+f 191/162/146 192/161/146 194/163/146 193/164/146
+f 193/164/147 194/163/147 196/165/147 195/166/147
+f 195/166/148 196/165/148 198/167/148 197/168/148
+f 197/168/149 198/167/149 200/169/149 199/170/149
+f 199/170/150 200/169/150 202/171/150 201/172/150
+f 201/172/151 202/171/151 204/173/151 203/174/151
+f 203/174/152 204/173/152 206/175/152 205/176/152
+f 205/176/153 206/175/153 208/177/153 207/178/153
+f 207/178/154 208/177/154 210/179/154 209/180/154
+f 209/180/155 210/179/155 212/181/155 211/182/155
+f 211/182/156 212/181/156 214/183/156 213/184/156
+f 213/184/157 214/183/157 216/185/157 215/186/157
+f 215/186/158 216/185/158 218/187/158 217/188/158
+f 217/188/159 218/187/159 220/189/159 219/190/159
+f 219/190/160 220/189/160 222/191/160 221/192/160
+f 164/193/161 162/194/161 224/195/161 222/196/161 220/197/161 218/198/161 216/199/161 214/200/161 212/201/161 210/202/161 208/203/161 206/204/161 204/205/161 202/206/161 200/207/161 198/208/161 196/209/161 194/210/161 192/211/161 190/212/161 188/213/161 186/214/161 184/215/161 182/216/161 180/217/161 178/218/161 176/219/161 174/220/161 172/221/161 170/222/161 168/223/161 166/224/161
+f 221/192/162 222/191/162 224/225/162 223/226/162
+f 223/226/163 224/225/163 162/227/163 161/228/163
+f 225/228/164 256/226/164 223/226/164 161/228/164
+f 320/225/165 318/191/165 255/192/165 256/226/165
+f 257/131/166 258/132/166 260/133/166 259/134/166
+f 259/134/167 260/133/167 262/135/167 261/136/167
+f 261/136/168 262/135/168 264/137/168 263/138/168
+f 263/138/169 264/137/169 266/139/169 265/140/169
+f 265/140/170 266/139/170 268/141/170 267/142/170
+f 267/142/171 268/141/171 270/143/171 269/144/171
+f 269/144/172 270/143/172 272/145/172 271/146/172
+f 271/146/173 272/145/173 274/147/173 273/148/173
+f 273/148/174 274/147/174 276/149/174 275/150/174
+f 275/150/175 276/149/175 278/151/175 277/152/175
+f 277/152/176 278/151/176 280/153/176 279/154/176
+f 279/154/177 280/153/177 282/155/177 281/156/177
+f 281/156/178 282/155/178 284/157/178 283/158/178
+f 283/158/179 284/157/179 286/159/179 285/160/179
+f 285/160/180 286/159/180 288/161/180 287/162/180
+f 287/162/181 288/161/181 290/163/181 289/164/181
+f 289/164/182 290/163/182 292/165/182 291/166/182
+f 291/166/183 292/165/183 294/167/183 293/168/183
+f 293/168/184 294/167/184 296/169/184 295/170/184
+f 295/170/185 296/169/185 298/171/185 297/172/185
+f 297/172/186 298/171/186 300/173/186 299/174/186
+f 299/174/187 300/173/187 302/175/187 301/176/187
+f 301/176/188 302/175/188 304/177/188 303/178/188
+f 303/178/189 304/177/189 306/179/189 305/180/189
+f 305/180/190 306/179/190 308/181/190 307/182/190
+f 307/182/191 308/181/191 310/183/191 309/184/191
+f 309/184/192 310/183/192 312/185/192 311/186/192
+f 311/186/193 312/185/193 314/187/193 313/188/193
+f 313/188/194 314/187/194 316/189/194 315/190/194
+f 315/190/195 316/189/195 318/191/195 317/192/195
+f 258/227/196 320/225/196 256/226/196 225/228/196
+f 317/192/197 318/191/197 320/225/197 319/226/197
+f 319/226/198 320/225/198 258/227/198 257/228/198
+f 257/229/199 259/230/199 261/231/199 263/232/199 265/233/199 267/234/199 269/235/199 271/236/199 273/237/199 275/238/199 277/239/199 279/240/199 281/241/199 283/242/199 285/243/199 287/244/199 289/245/199 291/246/199 293/247/199 295/248/199 297/249/199 299/250/199 301/251/199 303/252/199 305/253/199 307/254/199 309/255/199 311/256/199 313/257/199 315/258/199 317/259/199 319/260/199
+f 318/191/200 316/189/200 254/190/200 255/192/200
+f 316/189/201 314/187/201 253/188/201 254/190/201
+f 314/187/202 312/185/202 252/186/202 253/188/202
+f 312/185/203 310/183/203 251/184/203 252/186/203
+f 310/183/204 308/181/204 250/182/204 251/184/204
+f 308/181/205 306/179/205 249/180/205 250/182/205
+f 306/179/206 304/177/206 248/178/206 249/180/206
+f 304/177/207 302/175/207 247/176/207 248/178/207
+f 302/175/208 300/173/208 246/174/208 247/176/208
+f 300/173/209 298/171/209 245/172/209 246/174/209
+f 298/171/210 296/169/210 244/170/210 245/172/210
+f 296/169/211 294/167/211 243/168/211 244/170/211
+f 294/167/212 292/165/212 242/166/212 243/168/212
+f 292/165/213 290/163/213 241/164/213 242/166/213
+f 290/163/214 288/161/214 240/162/214 241/164/214
+f 288/161/215 286/159/215 239/160/215 240/162/215
+f 286/159/216 284/157/216 238/158/216 239/160/216
+f 284/157/217 282/155/217 237/156/217 238/158/217
+f 282/155/218 280/153/218 236/154/218 237/156/218
+f 280/153/219 278/151/219 235/152/219 236/154/219
+f 278/151/220 276/149/220 234/150/220 235/152/220
+f 276/149/221 274/147/221 233/148/221 234/150/221
+f 274/147/222 272/145/222 232/146/222 233/148/222
+f 272/145/223 270/143/223 231/144/223 232/146/223
+f 270/143/224 268/141/224 230/142/224 231/144/224
+f 268/141/225 266/139/225 229/140/225 230/142/225
+f 266/139/226 264/137/226 228/138/226 229/140/226
+f 264/137/227 262/135/227 227/136/227 228/138/227
+f 262/135/228 260/133/228 226/134/228 227/136/228
+f 260/133/229 258/132/229 225/131/229 226/134/229
+f 256/226/230 255/192/230 221/192/230 223/226/230
+f 255/192/231 254/190/231 219/190/231 221/192/231
+f 254/190/232 253/188/232 217/188/232 219/190/232
+f 253/188/233 252/186/233 215/186/233 217/188/233
+f 252/186/234 251/184/234 213/184/234 215/186/234
+f 251/184/235 250/182/235 211/182/235 213/184/235
+f 250/182/236 249/180/236 209/180/236 211/182/236
+f 249/180/237 248/178/237 207/178/237 209/180/237
+f 248/178/238 247/176/238 205/176/238 207/178/238
+f 247/176/239 246/174/239 203/174/239 205/176/239
+f 246/174/240 245/172/240 201/172/240 203/174/240
+f 245/172/241 244/170/241 199/170/241 201/172/241
+f 244/170/242 243/168/242 197/168/242 199/170/242
+f 243/168/243 242/166/243 195/166/243 197/168/243
+f 242/166/244 241/164/244 193/164/244 195/166/244
+f 241/164/245 240/162/245 191/162/245 193/164/245
+f 240/162/246 239/160/246 189/160/246 191/162/246
+f 239/160/247 238/158/247 187/158/247 189/160/247
+f 238/158/248 237/156/248 185/156/248 187/158/248
+f 237/156/249 236/154/249 183/154/249 185/156/249
+f 236/154/250 235/152/250 181/152/250 183/154/250
+f 235/152/251 234/150/251 179/150/251 181/152/251
+f 234/150/252 233/148/252 177/148/252 179/150/252
+f 233/148/253 232/146/253 175/146/253 177/148/253
+f 232/146/254 231/144/254 173/144/254 175/146/254
+f 231/144/255 230/142/255 171/142/255 173/144/255
+f 230/142/256 229/140/256 169/140/256 171/142/256
+f 229/140/257 228/138/257 167/138/257 169/140/257
+f 228/138/258 227/136/258 165/136/258 167/138/258
+f 227/136/259 226/134/259 163/134/259 165/136/259
+f 226/134/260 225/131/260 161/131/260 163/134/260
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/materials/black.mtl b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/materials/black.mtl
new file mode 100644
index 0000000..3661766
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/materials/black.mtl
@@ -0,0 +1,13 @@
+# Blender MTL File: 'None'
+# Material Count: 1
+
+newmtl tex
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.800000 0.800000 0.800000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.450000
+d 1.000000
+illum 2
+map_Kd saturnclient:textures/item/halo_black.png
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/materials/white.mtl b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/materials/white.mtl
new file mode 100644
index 0000000..9c0dc53
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/models/cosmetic/materials/white.mtl
@@ -0,0 +1,13 @@
+# Blender MTL File: 'None'
+# Material Count: 1
+
+newmtl tex
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.800000 0.800000 0.800000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.450000
+d 1.000000
+illum 2
+map_Kd saturnclient:textures/item/halo.png
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/back_flip.json b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/back_flip.json
new file mode 100644
index 0000000..3a15c12
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/back_flip.json
@@ -0,0 +1,1385 @@
+{
+ "name": "back_flip",
+ "author": "Kale Ko",
+ "description": "Makes You Do A Back Flip",
+ "uuid": "ebfb1e69-330a-4970-8bca-f5625c90681a",
+ "emote":{
+ "isLoop": "false",
+ "returnTick": 2,
+ "beginTick":0,
+ "endTick":80,
+ "stopTick":81,
+ "degrees":false,
+ "moves":[
+
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "y":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "y":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "x":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "x":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "pitch":-0.2994975745677948
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "head":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":10,
+ "easing": "EASEOUTQUAD",
+ "turn": 0,
+ "torso":{
+ "yaw":0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "torso":{
+ "yaw":0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "yaw":0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEOUTSINE",
+ "turn": 0,
+ "torso":{
+ "yaw":0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "CONSTANT",
+ "turn": 0,
+ "torso":{
+ "yaw":0.0
+ }
+ },
+ {
+ "tick":71,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "yaw":0.0
+ }
+ },
+ {
+ "tick":10,
+ "easing": "EASEOUTQUAD",
+ "turn": 0,
+ "torso":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "torso":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEOUTSINE",
+ "turn": 0,
+ "torso":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "CONSTANT",
+ "turn": 0,
+ "torso":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":71,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":10,
+ "easing": "EASEOUTQUAD",
+ "turn": 0,
+ "torso":{
+ "pitch":0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "torso":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "pitch":0.9649674892425537
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEOUTSINE",
+ "turn": 0,
+ "torso":{
+ "pitch":5.110422611236572
+ }
+ },
+ {
+ "tick":70,
+ "easing": "CONSTANT",
+ "turn": 0,
+ "torso":{
+ "pitch":6.2831854820251465
+ }
+ },
+ {
+ "tick":71,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "pitch":0.0
+ }
+ },
+ {
+ "tick":10,
+ "easing": "EASEOUTQUAD",
+ "turn": 0,
+ "torso":{
+ "y":-0.05682346969842911
+ }
+ },
+ {
+ "tick":40,
+ "easing": "EASEINQUAD",
+ "turn": 0,
+ "torso":{
+ "y":1.46522855758667
+ }
+ },
+ {
+ "tick":71,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "y":0.0
+ }
+ },
+ {
+ "tick":10,
+ "easing": "EASEOUTQUAD",
+ "turn": 0,
+ "torso":{
+ "x":0.0
+ }
+ },
+ {
+ "tick":40,
+ "easing": "EASEINQUAD",
+ "turn": 0,
+ "torso":{
+ "x":0.0
+ }
+ },
+ {
+ "tick":71,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "x":0.0
+ }
+ },
+ {
+ "tick":10,
+ "easing": "EASEOUTQUAD",
+ "turn": 0,
+ "torso":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":40,
+ "easing": "EASEINQUAD",
+ "turn": 0,
+ "torso":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":71,
+ "easing": "LINEAR",
+ "turn": 0,
+ "torso":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "rightArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "rightArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "rightArm":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "rightArm":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightArm":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "rightArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "rightArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "rightArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "rightArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "rightArm":{
+ "x":-5.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "rightArm":{
+ "x":-5.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "x":-5.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightArm":{
+ "x":-5.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "x":-5.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "rightArm":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "rightArm":{
+ "pitch":-3.4906585216522217
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "pitch":-3.665191411972046
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightArm":{
+ "pitch":-2.7925267219543457
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightArm":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "leftArm":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "leftArm":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftArm":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "leftArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "leftArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "leftArm":{
+ "pitch":0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "leftArm":{
+ "pitch":-3.4906585216522217
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "pitch":-3.665191411972046
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftArm":{
+ "pitch":-2.7925267219543457
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "leftArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "leftArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "y":2.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "leftArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "leftArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "z":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINSINE",
+ "turn": 0,
+ "leftArm":{
+ "x":5.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEOUTQUART",
+ "turn": 0,
+ "leftArm":{
+ "x":5.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "x":5.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftArm":{
+ "x":5.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftArm":{
+ "x":5.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "rightLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "rightLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "rightLeg":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "pitch":0.2617993950843811
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "pitch":0.45378559827804565
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "pitch":-0.1745329201221466
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "rightLeg":{
+ "x":-1.9
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "x":-1.9
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "x":-1.9
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "x":-1.9
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "x":-1.9
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "x":-1.9
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "rightLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "rightLeg":{
+ "z":0.1
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "z":0.1
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "z":0.1
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "rightLeg":{
+ "z":0.1
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "z":0.1
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "rightLeg":{
+ "z":0.1
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "leftLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "yaw":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "leftLeg":{
+ "roll":0.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "roll":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "leftLeg":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "pitch":0.2617993950843811
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "pitch":0.45378559827804565
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "pitch":-0.1745329201221466
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "pitch":-0.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "leftLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "y":12.0
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "leftLeg":{
+ "z":-0.1
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "z":-0.1
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "z":-0.1
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "z":-0.1
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "z":-0.1
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "z":-0.1
+ }
+ },
+ {
+ "tick":1,
+ "easing": "EASEINOUTQUAD",
+ "turn": 0,
+ "leftLeg":{
+ "x":1.9
+ }
+ },
+ {
+ "tick":5,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "x":1.9
+ }
+ },
+ {
+ "tick":15,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "x":1.9
+ }
+ },
+ {
+ "tick":24,
+ "easing": "EASEINOUTSINE",
+ "turn": 0,
+ "leftLeg":{
+ "x":1.9
+ }
+ },
+ {
+ "tick":55,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "x":1.9
+ }
+ },
+ {
+ "tick":70,
+ "easing": "LINEAR",
+ "turn": 0,
+ "leftLeg":{
+ "x":1.9
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/cry.json b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/cry.json
new file mode 100644
index 0000000..d6a1817
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/cry.json
@@ -0,0 +1,305 @@
+{
+ "version": 1,
+ "valid": true,
+ "name": "cry",
+ "description": "T_T",
+ "author": "dsty",
+ "emote": {
+ "beginTick": 0,
+ "endTick": 60,
+ "stopTick": 61,
+ "isLoop": true,
+ "returnTick": 5,
+ "nsfw": false,
+ "degrees": false,
+ "moves": [
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.34134695
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.22808763
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.21269174
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.32248768
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.0874397
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.17582592
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.21476103
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -6.320644E-4
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.029434478
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.039604172
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.037607383
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.050604735
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.00930274
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0010032838
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.009209918
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.012478679
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.031160546
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -1.8700758
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.4349194
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.4970688
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.9622451
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.9203311
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.6307226
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.2971223
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "bend": -0.8864816
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/facepalm.json b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/facepalm.json
new file mode 100644
index 0000000..cf79018
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/facepalm.json
@@ -0,0 +1,389 @@
+{
+ "version": 1,
+ "valid": true,
+ "name": "facepalm",
+ "description": "just facepalm",
+ "author": "dsty",
+ "emote": {
+ "beginTick": 0,
+ "endTick": 60,
+ "stopTick": 61,
+ "isLoop": false,
+ "returnTick": 2,
+ "nsfw": false,
+ "degrees": false,
+ "moves": [
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.067995355
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.14453278
+ }
+ },
+ {
+ "tick": 30,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.11871816
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.19018666
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.20064461
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.060454715
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.086299464
+ }
+ },
+ {
+ "tick": 30,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.087619215
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.083825104
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.036273066
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0041144555
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.12942825
+ }
+ },
+ {
+ "tick": 30,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.050149817
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.08701207
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.08676299
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -3.8523226
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -3.8523226
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -2.0664942
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -2.074402
+ }
+ },
+ {
+ "tick": 30,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -2.074402
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -2.02932
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -2.02932
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.47555572
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.5093512
+ }
+ },
+ {
+ "tick": 30,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.5093512
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.38563538
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.38563538
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.20147677
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.20521602
+ }
+ },
+ {
+ "tick": 30,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.3274244
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.30787703
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.40478125
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.49199954
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.54730254
+ }
+ },
+ {
+ "tick": 40,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.52019864
+ }
+ },
+ {
+ "tick": 60,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.0
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/front_flip.json b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/front_flip.json
new file mode 100644
index 0000000..c97d782
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/front_flip.json
@@ -0,0 +1,697 @@
+{
+ "version": 1,
+ "valid": true,
+ "name": "front_flip",
+ "description": "Parkour!!!!1",
+ "author": "DKinchik",
+ "emote": {
+ "beginTick": 0,
+ "endTick": 17,
+ "stopTick": 18,
+ "isLoop": false,
+ "returnTick": 1,
+ "nsfw": false,
+ "degrees": false,
+ "moves": [
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.7020477
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.7020477
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.9
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.9
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 11.169379
+ }
+ },
+ {
+ "tick": 3,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -0.39214998
+ }
+ },
+ {
+ "tick": 3,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": 0.1
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": 0.1
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 3,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -1.9707682
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -1.9707682
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "bend": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -1.3665518
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -1.3665518
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.0793308
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.0793308
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "bend": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 1.9
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 1.9
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 10.861001
+ }
+ },
+ {
+ "tick": 3,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -0.5082285
+ }
+ },
+ {
+ "tick": 3,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -0.1
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -0.1
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 3,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -1.9850811
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -1.9850811
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "bend": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 16,
+ "easing": "INQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INQUAD",
+ "body": {
+ "y": -0.061333913
+ }
+ },
+ {
+ "tick": 7,
+ "easing": "OUTQUAD",
+ "body": {
+ "y": 0.48852682
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "CONSTANT",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 16,
+ "easing": "INQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 16,
+ "easing": "INQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INQUAD",
+ "body": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 7,
+ "easing": "OUTQUAD",
+ "body": {
+ "pitch": -2.8362873
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "CONSTANT",
+ "body": {
+ "pitch": -6.2831855
+ }
+ },
+ {
+ "tick": 16,
+ "easing": "INQUAD",
+ "body": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 16,
+ "easing": "INQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 16,
+ "easing": "INQUAD",
+ "body": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 2,
+ "easing": "INOUTQUAD",
+ "body": {
+ "bend": -0.0
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/giga_chad.json b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/giga_chad.json
new file mode 100644
index 0000000..da2ae51
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/giga_chad.json
@@ -0,0 +1,5135 @@
+{
+ "version": 1,
+ "valid": true,
+ "name": "giga_chad",
+ "description": "The Chaddest of them all",
+ "author": "Wah0o",
+ "emote": {
+ "beginTick": 0,
+ "endTick": 165,
+ "stopTick": 166,
+ "isLoop": true,
+ "returnTick": 5,
+ "nsfw": false,
+ "degrees": false,
+ "moves": [
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "head": {
+ "x": -0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "head": {
+ "y": -0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "head": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.13989598
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.13989598
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.032506067
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.032506067
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.04490119
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.04490119
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.13459775
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.13459775
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -1.5314963
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -1.5314963
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -1.1120538
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -1.1120538
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.14775816
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.14775816
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.037381746
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.037381746
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.037799735
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.037799735
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.060097966
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.060097966
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -1.286084
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -1.286084
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "head": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.4168441
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.4168441
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -1.00269
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -1.00269
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.11216698
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.11216698
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.24195862
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.24195862
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.002758297
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.002758297
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.0013488329
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.0013488329
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0029103598
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0029103598
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -1.4095863
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -1.4095863
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 1.0407902E-4
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 1.0407902E-4
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.15502043
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.15502043
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.20529102
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": 0.20529102
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.013530715
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.013530715
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "head": {
+ "yaw": -0.4168441
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.21926047
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.21926047
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.025936412
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.025936412
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.015539514
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.015539514
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.054117903
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.054117903
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 9.091206E-5
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 9.091206E-5
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0027981726
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0027981726
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.78003484
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.78003484
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0014062009
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0014062009
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0016081737
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0016081737
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.017228942
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.017228942
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.018058004
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.018058004
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "head": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.9
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.252326
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.252326
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.9982228
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.9982228
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.5480254
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.5480254
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.9
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.9
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.7722428
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -1.7722428
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -3.169594
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -3.169594
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "x": -2.252326
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.195459
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.195459
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.277176
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.277176
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.025742
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.025742
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 11.914537
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 11.914537
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "y": 10.195459
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": 0.1
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -2.9733403
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -2.9733403
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -3.4964054
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -3.4964054
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": 0.1
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": 0.1
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -1.5458566
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -1.5458566
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "z": -2.9733403
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.33713692
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.33713692
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.3196764
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.3196764
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.027517445
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.027517445
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.047809538
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.047809538
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0026976853
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0026976853
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.03274877
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.03274877
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.007906961
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.007906961
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.07262061
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.07262061
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 5.0244353E-10
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 5.0244353E-10
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.22003074
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.22003074
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -1.5668823
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -1.5668823
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.9465621
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.9465621
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": -0.33713692
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.37796307
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.37796307
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.76882756
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.76882756
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.27188113
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.27188113
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.26609528
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.26609528
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.2948
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.2948
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.26220575
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.26220575
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.039796446
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.039796446
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.39853075
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.39853075
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 2.5175512E-5
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 2.5175512E-5
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.38755667
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.38755667
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.10965421
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.10965421
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.660008
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.660008
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": -0.018245801
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": -0.018245801
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 1.5213028
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 1.5213028
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.6063431
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.6063431
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "yaw": 0.37796307
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.025020538
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.025020538
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.048341107
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.048341107
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.069259174
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.069259174
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.1402302
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.1402302
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.20118771
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.20118771
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.095185064
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.095185064
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -7.341183E-7
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -7.341183E-7
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.21809976
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.21809976
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 6.93035E-4
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 6.93035E-4
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.061397005
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.061397005
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.22244482
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.22244482
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.08971669
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 0.08971669
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 2.2946439
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": 2.2946439
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "roll": -0.025020538
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.9686313
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.9686313
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -6.0498323
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -6.0498323
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.9686313
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 3.9937258
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 3.9937258
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -1.961665
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -1.961665
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -1.9947729E-8
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -1.9947729E-8
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -1.961665
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.87733024
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.87733024
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.8773303
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.8773303
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.037967823
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.037967823
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.67362005
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.67362005
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.67564476
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.67564476
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.142549
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.142549
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 2.9807994
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 2.9807994
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 1.693267
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 1.693267
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0014576487
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0014576487
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.65763783
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.65763783
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.4441948
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.4441948
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.149893
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.149893
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.65401036
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.65401036
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.1320117
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -3.1320117
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.7990288
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.7990288
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.8078986
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.8078986
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.8078987
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.8078987
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.31803387
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.31803387
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.14138679
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.14138679
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.9297993
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.9297993
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 0.2030788
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 0.2030788
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 1.0315573
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 1.0315573
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 0.010718631
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 0.010718631
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.049475487
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.049475487
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.21283935
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.21283935
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 0.88883036
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 0.88883036
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0376238
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0376238
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 1.4946102
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": 1.4946102
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -1.3037015
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -1.3037015
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.7990288
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.30187678
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.30187678
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.2721525
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.2721525
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.18416819
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.18416819
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.09789227
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.09789227
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.2198998
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.2198998
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.48504347
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.48504347
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.67907065
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.67907065
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -6.110932E-5
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -6.110932E-5
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 4.3807907
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 4.3807907
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.009480219
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.009480219
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.029465547
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.029465547
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.6154332
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.6154332
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.8863503
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.8863503
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.5616314E-4
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.5616314E-4
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.8965981
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.8965981
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.2532393
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.2532393
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": -0.30187678
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 2.9568586
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 2.9568586
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 4.0875206
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 4.0875206
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 4.0875206
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -2.203474
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -2.203474
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.51976997
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.51976997
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.6709974
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.6709974
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.6709973
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.6709973
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.30776563
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.30776563
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.65677315
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.65677315
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.6417192
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.6417192
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 3.1355157
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 3.1355157
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.0010496362
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.0010496362
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 1.6392988
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 1.6392988
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0025698922
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0025698922
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.5024246
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.5024246
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -2.8041947
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -2.8041947
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -3.1532238
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -3.1532238
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.1308259
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.1308259
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -0.51976997
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 1.172347
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 1.172347
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.037808705
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.037808705
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.037808713
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.037808713
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.62366265
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.62366265
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.7531556
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.7531556
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 1.2664338
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 1.2664338
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.13954905
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.13954905
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.013174082
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.013174082
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.010205563
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.010205563
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.06644766
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.06644766
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.010088136
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.010088136
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.6029994
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.6029994
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.03762318
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.03762318
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.44563633
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.44563633
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 1.172347
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.48641014
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.48641014
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.10834044
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.10834044
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.5676788
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.5676788
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.07504642
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.07504642
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.006862871
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.006862871
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.5692944
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.5692944
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -2.8592016E-5
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -2.8592016E-5
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -1.7287096
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -1.7287096
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.009529364
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.009529364
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.038684685
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.038684685
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.31971967
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.31971967
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.88968116
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.88968116
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 2.188246E-4
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 2.188246E-4
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -2.1538641
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -2.1538641
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": 0.48641014
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 1.9
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 2.338033
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 2.338033
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 2.4034452
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 2.4034452
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 1.9
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 1.9
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "x": 2.338033
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 9.917855
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 9.917855
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 10.922021
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 10.922021
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 10.576937
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 10.576937
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 12.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "y": 9.917855
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -0.1
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -3.7886016
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -3.7886016
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -3.7174206
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -3.7174206
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -0.1
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -0.1
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "z": -3.7886016
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.28943428
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.28943428
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.29512814
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.29512814
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.01708789
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.01708789
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.04704775
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.04704775
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.0030718336
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.0030718336
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.043501973
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.043501973
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -1.5781744
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -1.5781744
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 3.203295E-12
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 3.203295E-12
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.13094698
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.13094698
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.109108955
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.109108955
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -1.5687531
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -1.5687531
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": -0.28943428
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.24234551
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.24234551
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.04647371
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.04647371
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.046473704
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.046473704
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.14872508
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.14872508
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.27384213
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.27384213
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.22910896
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.22910896
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.29659358
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.29659358
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.09799422
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.09799422
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": 0.009622238
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": 0.009622238
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.37212652
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.37212652
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": 0.22992273
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": 0.22992273
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.297723
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.297723
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.01908363
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.01908363
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.16646482
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.16646482
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "yaw": -0.24234551
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.07622462
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.07622462
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.066452764
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.066452764
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.18356355
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.18356355
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.04682022
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.04682022
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.0011612747
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.0011612747
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.16643818
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.16643818
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.09162157
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.09162157
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -1.1308775E-5
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -1.1308775E-5
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": 0.19563156
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": 0.19563156
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": 0.009551507
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": 0.009551507
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.08669772
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.08669772
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.02894436
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.02894436
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.1509232
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.1509232
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.1525886
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.1525886
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.73833025
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.73833025
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "roll": -0.07622462
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.04586473
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.019460544
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.14749607
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.055940144
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.04586473
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.019460544
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.16599017
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.15493058
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.1035458
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.025333587
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.11034301
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.21000633
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.26974648
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.22764045
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.15241866
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.16667084
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.21193272
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.29072618
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.2166983
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.14110439
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "body": {
+ "x": -0.04586473
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": -0.1720419
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": -0.1720419
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": -0.1720419
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": -0.1720419
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.5178939
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.5178939
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.28458548
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.28458548
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.4809237
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.4809237
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.4706339
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.4706339
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 164,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "body": {
+ "y": -0.1720419
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": 0.011486796
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": 0.011486796
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "body": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": 0.23216064
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": 0.23216064
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "body": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -1.3986131
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -1.3986131
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 34,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 35,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -0.07036926
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -0.07036926
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -0.07036925
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -0.07036925
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 1.5803477
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 1.5803477
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 114,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 115,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -0.11275339
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -0.11275339
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.43098667
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.43098667
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -1.5896086
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": -1.5896086
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "body": {
+ "yaw": 0.0
+ }
+ },
+ {
+ "tick": 4,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 5,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 14,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 15,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.10164733
+ }
+ },
+ {
+ "tick": 24,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.10164733
+ }
+ },
+ {
+ "tick": 25,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 44,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 45,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.061503474
+ }
+ },
+ {
+ "tick": 54,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.061503474
+ }
+ },
+ {
+ "tick": 55,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.037977725
+ }
+ },
+ {
+ "tick": 64,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.037977725
+ }
+ },
+ {
+ "tick": 65,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.010051205
+ }
+ },
+ {
+ "tick": 74,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.010051205
+ }
+ },
+ {
+ "tick": 75,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -3.1445053
+ }
+ },
+ {
+ "tick": 84,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -3.1445053
+ }
+ },
+ {
+ "tick": 85,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 1.7155838
+ }
+ },
+ {
+ "tick": 94,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 1.7155838
+ }
+ },
+ {
+ "tick": 95,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -1.6908722
+ }
+ },
+ {
+ "tick": 104,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -1.6908722
+ }
+ },
+ {
+ "tick": 105,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 124,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 0.0
+ }
+ },
+ {
+ "tick": 125,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 0.030365204
+ }
+ },
+ {
+ "tick": 134,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 0.030365204
+ }
+ },
+ {
+ "tick": 135,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -3.145151
+ }
+ },
+ {
+ "tick": 144,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -3.145151
+ }
+ },
+ {
+ "tick": 145,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 2.160377
+ }
+ },
+ {
+ "tick": 154,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": 2.160377
+ }
+ },
+ {
+ "tick": 155,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ },
+ {
+ "tick": 165,
+ "easing": "INOUTQUAD",
+ "body": {
+ "roll": -0.0
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/tpose.json b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/tpose.json
new file mode 100644
index 0000000..abd421f
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/player_animations/tpose.json
@@ -0,0 +1,354 @@
+{
+ "version": 1,
+ "valid": true,
+ "name": "tpose",
+ "description": "MENACING",
+ "author": "stephanieisdumb",
+ "emote": {
+ "beginTick": 0,
+ "endTick": 80,
+ "stopTick": 81,
+ "isLoop": true,
+ "returnTick": 20,
+ "nsfw": false,
+ "degrees": false,
+ "moves": [
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -5.0
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -6.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "x": -6.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 3.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "y": 3.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -1.0704756
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": 0.059108086
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "pitch": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.61077195
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.008776078
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 0.6060502
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.5707964
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.5765934
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "roll": 1.5707964
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -1.3806202
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "rightArm": {
+ "bend": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 5.0
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 6.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "x": 6.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 2.0
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 3.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "y": 3.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "z": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": -1.0045553
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.010133168
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.44322997
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": 0.038214765
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "yaw": -0.0
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -0.5571526
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -1.5707964
+ }
+ },
+ {
+ "tick": 50,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -1.5692185
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "roll": -1.5707964
+ }
+ },
+ {
+ "tick": 10,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "bend": -1.3810014
+ }
+ },
+ {
+ "tick": 20,
+ "easing": "INOUTQUAD",
+ "leftArm": {
+ "bend": -0.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "leftLeg": {
+ "pitch": 0.0
+ }
+ },
+ {
+ "tick": 80,
+ "easing": "INOUTQUAD",
+ "rightLeg": {
+ "pitch": 0.0
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/amg.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/amg.png
new file mode 100644
index 0000000..ee63ca0
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/amg.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/amg_petronas.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/amg_petronas.png
new file mode 100644
index 0000000..ba0169d
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/amg_petronas.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.gif b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.gif
new file mode 100644
index 0000000..4efb183
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.gif differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.png
new file mode 100644
index 0000000..4b4c8c9
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.gif b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.gif
new file mode 100644
index 0000000..fa833b5
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.gif differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.png
new file mode 100644
index 0000000..0d30ca3
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.gif b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.gif
new file mode 100644
index 0000000..f534763
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.gif differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.png
new file mode 100644
index 0000000..17fa98e
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/bmw.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/bmw.png
new file mode 100644
index 0000000..2f3a171
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/bmw.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/crimson_mark.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/crimson_mark.png
new file mode 100644
index 0000000..ed56e71
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/crimson_mark.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/ferrari.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/ferrari.png
new file mode 100644
index 0000000..f7b2db6
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/ferrari.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/glitch.gif b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/glitch.gif
new file mode 100644
index 0000000..e7cdfbd
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/glitch.gif differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/glitch.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/glitch.png
new file mode 100644
index 0000000..beb3cd3
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/glitch.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/mercedes_flow.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/mercedes_flow.png
new file mode 100644
index 0000000..2f7ad61
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/mercedes_flow.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/redbull.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/redbull.png
new file mode 100644
index 0000000..7f1fa5c
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/cloaks/redbull.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/amg.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/amg.png
new file mode 100644
index 0000000..55c0cd4
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/amg.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/amg_petronas.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/amg_petronas.png
new file mode 100644
index 0000000..0013b33
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/amg_petronas.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_amethyst.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_amethyst.png
new file mode 100644
index 0000000..d68de7d
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_amethyst.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_flame.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_flame.png
new file mode 100644
index 0000000..714a073
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_flame.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_white.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_white.png
new file mode 100644
index 0000000..1fd6c4e
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_white.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/bmw.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/bmw.png
new file mode 100644
index 0000000..db981dd
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/bmw.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/crimson_mark.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/crimson_mark.png
new file mode 100644
index 0000000..c946473
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/crimson_mark.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/ferrari.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/ferrari.png
new file mode 100644
index 0000000..210c9e2
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/ferrari.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/glitch.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/glitch.png
new file mode 100644
index 0000000..b0ea453
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/glitch.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/mercedes_flow.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/mercedes_flow.png
new file mode 100644
index 0000000..0e40488
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/mercedes_flow.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/none.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/none.png
new file mode 100644
index 0000000..57b0853
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/none.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/redbull.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/redbull.png
new file mode 100644
index 0000000..23e5add
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/cloak/redbull.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/check.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/check.png
new file mode 100644
index 0000000..039befb
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/check.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/circle_x.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/circle_x.png
new file mode 100644
index 0000000..11401de
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/circle_x.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/cloak.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/cloak.png
new file mode 100644
index 0000000..ab853dd
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/cloak.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/close.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/close.png
new file mode 100644
index 0000000..cf7f967
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/close.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/coins.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/coins.png
new file mode 100644
index 0000000..0946546
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/coins.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.png
new file mode 100644
index 0000000..725110f
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/hat.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/hat.png
new file mode 100644
index 0000000..b84414f
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/hat.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/hud.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/hud.png
new file mode 100644
index 0000000..a71c971
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/hud.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/info.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/info.png
new file mode 100644
index 0000000..81686b1
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/info.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/left.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/left.png
new file mode 100644
index 0000000..073a748
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/left.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/mods_tab.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/mods_tab.png
new file mode 100644
index 0000000..3c46cd0
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/mods_tab.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/reset.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/reset.png
new file mode 100644
index 0000000..fa62098
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/reset.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/right.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/right.png
new file mode 100644
index 0000000..fa6ee7c
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/right.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/search.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/search.png
new file mode 100644
index 0000000..ac03290
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/search.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/settings.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/settings.png
new file mode 100644
index 0000000..93074e0
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/settings.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/shirt.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/shirt.png
new file mode 100644
index 0000000..96a85f1
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/shirt.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/store.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/store.png
new file mode 100644
index 0000000..b067b97
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/components/store.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/back_flip.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/back_flip.png
new file mode 100644
index 0000000..7d8cdad
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/back_flip.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/cry.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/cry.png
new file mode 100644
index 0000000..ac33bfe
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/cry.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/facepalm.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/facepalm.png
new file mode 100644
index 0000000..b67f0b1
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/facepalm.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/giga_chad.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/giga_chad.png
new file mode 100644
index 0000000..c0a166b
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/giga_chad.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/none.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/none.png
new file mode 100644
index 0000000..515541c
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/none.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/tpose.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/tpose.png
new file mode 100644
index 0000000..8a64524
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/tpose.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/back_flip.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/back_flip.png
new file mode 100644
index 0000000..f2edac8
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/back_flip.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/cry.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/cry.png
new file mode 100644
index 0000000..2b743f6
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/cry.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/facepalm.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/facepalm.png
new file mode 100644
index 0000000..5afc49b
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/facepalm.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/giga_chad.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/giga_chad.png
new file mode 100644
index 0000000..9802c3f
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/giga_chad.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/tpose.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/tpose.png
new file mode 100644
index 0000000..c49dd68
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/emote/with_bg/tpose.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/halo.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/halo.png
new file mode 100644
index 0000000..cbdeafa
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/halo.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/halo_black.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/halo_black.png
new file mode 100644
index 0000000..244aeca
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/halo_black.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/horns_black.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/horns_black.png
new file mode 100644
index 0000000..446bf41
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/horns_black.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/horns_white.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/horns_white.png
new file mode 100644
index 0000000..d411bb7
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/horns_white.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/none.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/none.png
new file mode 100644
index 0000000..16c2bbe
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hat/none.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hud/crosshair_range.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hud/crosshair_range.png
new file mode 100644
index 0000000..fe1e8cf
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/hud/crosshair_range.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/armor.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/armor.svg
new file mode 100644
index 0000000..c5995e0
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/armor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg
new file mode 100644
index 0000000..c928bd6
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/coords.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/coords.svg
new file mode 100644
index 0000000..317f49b
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/coords.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg
new file mode 100644
index 0000000..bb0cb50
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/day.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/day.svg
new file mode 100644
index 0000000..9432885
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/day.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/effect.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/effect.svg
new file mode 100644
index 0000000..0e61d06
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/effect.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/fps.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/fps.svg
new file mode 100644
index 0000000..6e76d84
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/fps.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/freelook.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/freelook.svg
new file mode 100644
index 0000000..199e319
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/freelook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/fullbright.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/fullbright.svg
new file mode 100644
index 0000000..3e8360d
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/fullbright.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/health.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/health.svg
new file mode 100644
index 0000000..491bbd7
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/health.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/keystrokes.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/keystrokes.svg
new file mode 100644
index 0000000..f7e19c9
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/keystrokes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg
new file mode 100644
index 0000000..876a0f0
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/nofog.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/nofog.svg
new file mode 100644
index 0000000..63a48cc
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/nofog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/particles.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/particles.svg
new file mode 100644
index 0000000..f4addd6
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/particles.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg
new file mode 100644
index 0000000..6eaafc9
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/speed.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/speed.svg
new file mode 100644
index 0000000..e8ff01c
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/speed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg
new file mode 100644
index 0000000..35e91e7
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg
@@ -0,0 +1,35 @@
+
+
+
+
+Created by potrace 1.15, written by Peter Selinger 2001-2017
+
+
+
+
+
+
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg
new file mode 100644
index 0000000..f2ad6c9
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
performance_line
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg
new file mode 100644
index 0000000..320a59d
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/splash.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/splash.png
new file mode 100644
index 0000000..84f2988
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/splash.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_0.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_0.png
new file mode 100644
index 0000000..6653bad
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_0.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_1.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_1.png
new file mode 100644
index 0000000..523b7a4
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_1.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_2.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_2.png
new file mode 100644
index 0000000..354c1b1
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_2.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_3.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_3.png
new file mode 100644
index 0000000..da74054
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_3.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_4.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_4.png
new file mode 100644
index 0000000..f0c51f7
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_4.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_5.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_5.png
new file mode 100644
index 0000000..93663f5
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/gui/title/background/panorama_5.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/item/halo.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/item/halo.png
new file mode 100644
index 0000000..341eb3a
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/item/halo.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/item/halo_black.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/item/halo_black.png
new file mode 100644
index 0000000..c0d7493
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/item/halo_black.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/logo.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/logo.png
new file mode 100644
index 0000000..cff8085
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/logo.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/realistic.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/realistic.png
new file mode 100644
index 0000000..ee0a6ea
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/realistic.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/text.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/text.png
new file mode 100644
index 0000000..6fd0bf1
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/text.png differ
diff --git a/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/text_big.png b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/text_big.png
new file mode 100644
index 0000000..62eb09c
Binary files /dev/null and b/versions/1.21.4/src/main/resources/assets/saturnclient/textures/logo/text_big.png differ
diff --git a/versions/1.21.4/src/main/resources/fabric.mod.json b/versions/1.21.4/src/main/resources/fabric.mod.json
index 0313c15..fc7fb33 100644
--- a/versions/1.21.4/src/main/resources/fabric.mod.json
+++ b/versions/1.21.4/src/main/resources/fabric.mod.json
@@ -1,31 +1,27 @@
{
- "schemaVersion": 1,
- "id": "saturn-client",
- "version": "${version}",
- "name": "Saturn Client",
- "description": "This is an example description! Tell everyone what your mod is about!",
- "authors": [
- "Me!"
- ],
- "contact": {
- "homepage": "https://fabricmc.net/",
- "sources": "https://github.com/FabricMC/fabric-example-mod"
- },
- "license": "CC0-1.0",
- "icon": "assets/saturn-client/icon.png",
- "environment": "*",
- "entrypoints": {
- "main": [
- "org.saturnclient.SaturnClient"
- ]
- },
- "mixins": [
- "saturn-client.mixins.json"
- ],
- "depends": {
- "fabricloader": ">=0.18.4",
- "minecraft": "~1.21.4",
- "java": ">=21",
- "fabric-api": "*"
- }
-}
\ No newline at end of file
+ "schemaVersion": 1,
+ "id": "saturnclient",
+ "version": "${version}",
+ "name": "saturnclient",
+ "description": "The ultimate open source client",
+ "authors": ["Saturn Mc", "ORUS"],
+ "contact": {
+ "website": "https://kleo-dev.netlify.app",
+ "sources": "https://github.com/saturnclientmc/saturnclient"
+ },
+ "license": "Apache-2.0",
+ "icon": "assets/saturnclient/icon.png",
+ "environment": "client",
+ "entrypoints": {
+ "main": ["org.saturnclient.saturnclient.SaturnClient"]
+ },
+ "mixins": [
+ "saturnclient.mixins.json",
+ "mods.mixins.json"
+ ],
+ "depends": {
+ "fabricloader": ">=${loader_version}",
+ "fabric": "*",
+ "minecraft": "${minecraft_version}"
+ }
+}
diff --git a/versions/1.21.4/src/main/resources/mods.mixins.json b/versions/1.21.4/src/main/resources/mods.mixins.json
new file mode 100644
index 0000000..6cd236b
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/mods.mixins.json
@@ -0,0 +1,21 @@
+{
+ "required": true,
+ "minVersion": "0.8",
+ "package": "org.saturnclient.modules.mods.mixins",
+ "compatibilityLevel": "JAVA_21",
+ "client": [
+ "CameraMixin",
+ "EntityMixin",
+ "RenderMixin",
+ "LightmapTextureManagerMixin",
+ "BackgroundRendererMixin",
+ "GameRendererMixin",
+ "NametagsMixin",
+ "LivingEntityRenderStateMixin",
+ "LivingEntityRendererMixin",
+ "TpsMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/versions/1.21.4/src/main/resources/saturn-client.mixins.json b/versions/1.21.4/src/main/resources/saturn-client.mixins.json
deleted file mode 100644
index a8e0653..0000000
--- a/versions/1.21.4/src/main/resources/saturn-client.mixins.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "required": true,
- "package": "org.saturnclient.mixin",
- "compatibilityLevel": "JAVA_21",
- "mixins": [
- "ExampleMixin"
- ],
- "injectors": {
- "defaultRequire": 1
- },
- "overwrites": {
- "requireAnnotations": true
- }
-}
\ No newline at end of file
diff --git a/versions/1.21.4/src/main/resources/saturnclient.mixins.json b/versions/1.21.4/src/main/resources/saturnclient.mixins.json
new file mode 100644
index 0000000..3bc9ccc
--- /dev/null
+++ b/versions/1.21.4/src/main/resources/saturnclient.mixins.json
@@ -0,0 +1,18 @@
+{
+ "required": true,
+ "minVersion": "0.8",
+ "package": "org.saturnclient.saturnclient.mixin",
+ "compatibilityLevel": "JAVA_21",
+ "client": [
+ "TabListMixin",
+ "NameTagMixin",
+ "PlayerEntityRendererMixin",
+ "WhoAmI",
+ "DrawContextAccessor",
+ "SplashOverlayMixin",
+ "SetScreenMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}