Added saturnclient 1.21.4 code

This commit is contained in:
2026-03-05 16:07:35 +01:00
parent 55b1814c1e
commit da24cc584b
235 changed files with 21179 additions and 83 deletions
@@ -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!");
}
}
@@ -0,0 +1,17 @@
package org.saturnclient.cosmetics;
import java.util.ArrayList;
import java.util.List;
public class Emotes {
public static List<String> 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");
}
}
@@ -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<PlayerEntityRenderState, PlayerEntityModel> {
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<PlayerEntityRenderState, PlayerEntityModel> 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<EquipmentAsset>) 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();
}
}
@@ -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<String> availableCloaks = new ArrayList<>();
public static Identifier cloakCacheIdentifier = null;
public static final Map<UUID, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>();
private static final Map<UUID, Long> 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<String, List<AnimatedCloakData>> 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<Void> loadAnimatedCloakAsync(UUID uuid, String cloakName) {
return CompletableFuture.runAsync(() -> {
String fileName = cloakName + ".gif";
// Check cache first
List<AnimatedCloakData> 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<AnimatedCloakData> 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<AnimatedCloakData> 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");
}
}
}
@@ -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.
*/
/**
* <p>
* A thread-safe decoder capable of processing a GIF data stream to render the graphics
* contained in it. This implementation follows the official
* <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF
* specification</A>.
* </p>
*
* <p>
* Example usage:
* </p>
*
* <p>
*
* <pre>
* 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);
* }
* </pre>
*
* </p>
*
* @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<DecodingContext> 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<GifFrame> 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 <i>after</i> 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;
}
}
@@ -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;
}
}
@@ -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<Identifier, BufferedImage> textures) {
long startTime = System.currentTimeMillis();
for (java.util.Map.Entry<Identifier, BufferedImage> entry : textures.entrySet()) {
registerBufferedImageTextureFast(entry.getKey(), entry.getValue());
}
long endTime = System.currentTimeMillis();
SaturnClient.LOGGER.info("Registered {} textures in {}ms", textures.size(), (endTime - startTime));
}
}
@@ -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;
}
}
@@ -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<PlayerEntityRenderState, PlayerEntityModel> {
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> 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();
}
}
@@ -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<String> 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;
}
}
}
@@ -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<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>();
static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
.getResource(mtlId).get().getInputStream()) {
List<Mtl> mtlList = MtlReader.read(is);
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
for (Mtl mtl : mtlList) {
mtlMap.put(mtl.getName(), mtl);
}
return mtlMap;
}
}
public static Map<String, Mtl> of(Identifier mtlId) {
System.out.println(mtlId);
if (loadedMtls.containsKey(mtlId))
return loadedMtls.get(mtlId);
try {
Map<String, Mtl> mtl = loadMtl(mtlId);
loadedMtls.put(mtlId, mtl);
return mtl;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static Map<String, Mtl> 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"));
}
}
@@ -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<Identifier, ObjModel> 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> 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<String, Mtl> 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();
}
}
@@ -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<String, Mtl> mtlMap, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light, int overlay) {
MatrixStack.Entry entry = matrices.peek();
Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
for (Map.Entry<String, Obj> 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);
}
}
@@ -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
}
}
@@ -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);
}
@@ -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<Integer> x = Property.integer(0);
public Property<Integer> y = Property.integer(0);
public Property<Float> scale = Property.floatProp(1.0f);
public Property<Integer> bgColor = Property.color(0x00000000);
public Property<Integer> fgColor = Property.color(0xFFffffff);
public Property<Integer> radius = Property.integer(0);
public Property<Integer> font = Property.font(0);
public boolean renderBackground = true;
public int width = 0;
public int height = 0;
public NamedProperty<Map<String, Property<?>>> 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;
}
}
@@ -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<Module> 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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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**
@@ -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<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 75);
private static Property<Boolean> 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;
}
}
@@ -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<Boolean> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Integer> format = Property.select(0, "24 hour", "12 hour");
private static Property<Boolean> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Integer> 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;
}
}
@@ -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<Boolean> 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;
}
}
@@ -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<Boolean> 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;
}
}
@@ -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<Boolean> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(true);
public static Property<Integer> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Integer> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMode = Property.select(0, "Health", "Hearts");
private static Property<Integer> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> showMouse = Property.bool(true);
private static final Property<Boolean> showSpace = Property.bool(false);
private static final Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static final Property<Integer> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Boolean> healthDisplay = Property.bool(false);
private static Property<Boolean> players = Property.bool(true);
private static Property<Boolean> hostile = Property.bool(false);
private static Property<Boolean> passive = Property.bool(false);
private static Property<Boolean> heartEmoji = Property.bool(true);
private static Property<Boolean> obfuscate = Property.bool(false);
private static Property<Integer> unit = Property.select(0, "Health", "Hearts");
private static Property<Integer> format = Property.select(0, "Value", "Value / Total", "%");
private static Property<Integer> 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<Integer> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Boolean> 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;
}
}
@@ -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<Boolean> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Integer> axis = Property.select(1, "Absolute", "X axis", "Y axis");
private static Property<Integer> unitText = Property.select(0, "None", "Blocks/s", "blocks/s", "b/s");
private static Property<Boolean> 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;
}
}
@@ -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<Boolean> 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<StatusEffectInstance> effects) {
int row = 0;
StatusEffectSpriteManager statusEffectSpriteManager = SaturnClient.client.getStatusEffectSpriteManager();
for (StatusEffectInstance effect : effects) {
if (effect.shouldShowIcon()) {
RegistryEntry<StatusEffect> 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;
}
}
@@ -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<Boolean> 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;
}
}
@@ -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<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(false);
public static Property<Integer> zoomKey = Property.keybinding(GLFW.GLFW_KEY_C);
public static Property<Float> 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;
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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<Float> cir) {
if (Zoom.isZooming && Zoom.shouldZoom()) {
float originalFov = cir.getReturnValue();
float zoomedFov = originalFov / Zoom.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -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;
}
}
@@ -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; }
}
@@ -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<T extends LivingEntity, S extends LivingEntityRenderState> {
@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);
}
}
}
@@ -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<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
@Shadow
protected abstract <S extends EntityRenderState> void renderLabelIfPresent(
S state, Text text, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light
);
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
private <S extends EntityRenderState> 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);
}
}
}
@@ -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);
}
}
}
}
}
@@ -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());
}
}
@@ -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);
}
@@ -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
}
}
@@ -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");
}
}
}
@@ -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);
}
}
}
@@ -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<String, Types.Player, String> Authenticate = new Method<>("auth",
String.class,
Types.Player.class,
String.class);
// =========================
// EQUIP
// =========================
public static final Method<String, String, String> SetCloak = new Method<>("set_cloak",
String.class,
String.class,
String.class);
public static final Method<String, String, String> SetHat = new Method<>("set_hat",
String.class,
String.class,
String.class);
// =========================
// BUY
// =========================
public static final Method<String, String, String> BuyCloak = new Method<>("buy_cloak",
String.class,
String.class,
String.class);
public static final Method<String, String, String> BuyHat = new Method<>("buy_hat",
String.class,
String.class,
String.class);
// =========================
// EMOTE
// =========================
public static final Method<Types.EmoteRequest, Void, String> Emote = new Method<>("emote",
Types.EmoteRequest.class,
Void.class,
String.class);
public static final Method<Types.EmoteEvent, Void, Void> EmoteEvent = new Method<>("emote_event",
Types.EmoteEvent.class,
Void.class,
Void.class);
// =========================
// GET PLAYER
// =========================
public static final Method<String, Types.Player, String> GetPlayer = new Method<>("get_player",
String.class,
Types.Player.class,
String.class);
// =========================
// PLAYER (Server Notification)
// =========================
public static final Method<Types.PlayerStream, Void, Void> Player = new Method<>("player",
Types.PlayerStream.class,
Void.class,
Void.class);
// =========================
// SEND PLAYER
// =========================
public static final Method<SendPlayerRequest, Void, String> SendPlayer = new Method<>("send_player",
SendPlayerRequest.class,
Void.class,
String.class);
}
@@ -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<UUID> 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());
}
}
@@ -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<UUID, Role> 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 ? "" : "";
}
}
@@ -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<UUID, SaturnPlayer> PLAYERS = new HashMap<>();
private static Map<String, UUID> 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);
}
}
@@ -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<Integer> duration;
public final Property<Integer> stagger;
public final Property<Integer> 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);
}
}
@@ -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<Boolean> realisticLogo = Property.bool(false);
public static Property<Boolean> saturnTitleScreen = Property.bool(true);
public static Property<Boolean> cloakPhysics = Property.bool(true);
public static Property<Integer> openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B);
public static Property<Boolean> 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);
}
}
@@ -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<Integer> BACKGROUND = Property.color(0xFF12100D);
// Default text color
public static final Property<Integer> FOREGROUND = Property.color(0xFFF2E6D2);
// Widget default background (normal state)
public static final Property<Integer> PRIMARY = Property.color(0xFF1C1915);
// Widget text color
public static final Property<Integer> PRIMARY_FG = Property.color(0xFFEAD9B8);
// Saturn brand color (hovered / enabled / selected)
public static final Property<Integer> ACCENT = Property.color(0xFFE2BF7D);
// Text/icons shown on accent background
public static final Property<Integer> ACCENT_FG = Property.color(0xFF1A140A);
// Misc colors
public static final Property<Integer> SCROLL = Property.color(0xFFE2BF7D);
// UI metrics
public static final Property<Integer> BG_RADIUS = Property.integer(12);
public static final Property<Integer> WIDGET_RADIUS = Property.integer(10);
// Font
public static final Property<Integer> 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);
}
}
@@ -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<String, Property<?>> ROOT = new LinkedHashMap<>();
private static JsonObject cachedJson = null;
private final Map<String, Property<?>> 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 <T> Property<T> property(String name, Property<T> 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<String, Property<?>> 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<String, Property<?>> 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<String, Property<?>> getProperties() {
return currentMap;
}
}
@@ -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<T> {
public static class NamedProperty<T> {
public String name;
public Property<T> prop;
public NamedProperty(String name, Property<T> 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 <T> Property<T> 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<Integer> font(int value) {
return select(value, "Default", "Inter", "Inter bold");
}
public static Property<Integer> select(Integer value, String... availableValues) {
Property<Integer> property = new Property<>(value, PropertyType.SELECT);
property.availableValues = availableValues;
return property;
}
public static Property<Map<String, Property<?>>> namespace(Map<String, Property<?>> value) {
return new Property<>(value, PropertyType.NAMESPACE);
}
public static Property<Boolean> bool(boolean value) {
return new Property<>(value, PropertyType.BOOLEAN);
}
public static Property<Integer> integer(int value) {
return new Property<>(value, PropertyType.INTEGER);
}
public static Property<Float> floatProp(float value) {
return new Property<>(value, PropertyType.FLOAT);
}
public static Property<String> string(String value) {
return new Property<>(value, PropertyType.STRING);
}
public static Property<Integer> color(int value) {
return new Property<>(value, PropertyType.HEX);
}
public static Property<Integer> 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<String, Property<?>> getNamespaceValue() {
if (type == PropertyType.NAMESPACE && value instanceof Map) {
return (Map<String, Property<?>>) 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<T> 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<T> 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<String, Property<?>> getNamespaceMap() {
return isNamespace() ? (Map<String, Property<?>>) (Object) value : null;
}
}
@@ -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("");
}
}
});
}
}
@@ -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<HudMod> 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();
}
}
@@ -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();
}
@@ -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<S extends EntityRenderState> {
@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();
}
}
}
@@ -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<PlayerEntity, PlayerEntityRenderState, PlayerEntityModel> {
protected PlayerEntityRendererMixin() {
super(null, null, 0.0f);
}
@Inject(method = "<init>", 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()));
}
}
@@ -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;
}
}
@@ -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<Optional<Throwable>> 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());
}
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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<Identifier, RenderLayer> 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<Identifier, RenderLayer> 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<VertexConsumerProvider> drawer) {
drawer.accept(this.vertexConsumers);
this.vertexConsumers.draw();
}
static class ScissorStack {
private final Deque<ScreenRect> 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);
}
}
@@ -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<Element> 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);
}
}
@@ -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> T getOrNull(List<T> 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));
}
}
}
@@ -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<Double, Double> 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);
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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<Element> elements, Element element) {
synchronized (elements) {
elements.add(element);
}
if (element.animation != null) {
element.animation.init(element);
}
}
public static void render(List<Element> 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<Element> 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<Element> 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<Element> 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<Element> 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<Element> elements, double mouseX, double mouseY, int button) {
for (Element element : new ArrayList<>(elements)) {
element.mouseReleased(mouseX - element.x, mouseY - element.y, button);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
}
}
@@ -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);
}
}
}
@@ -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<Float> prop;
public FloatInput(Property<Float> 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;
}
}
}
@@ -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<Integer> 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<Integer> 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;
}
}
@@ -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;
}
}
@@ -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<Integer> prop;
public IntInput(Property<Integer> 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;
}
}
}
@@ -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<Integer> prop;
public KeybindingSelector(Property<Integer> 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 ? "<NONE>"
: name != null ? name.toUpperCase() : "<Invalid-Key>",
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;
}
}
@@ -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<Integer> prop;
public Select(Property<Integer> 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();
}
}
}
@@ -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<Boolean> prop;
public Toggle(Property<Boolean> 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;
}
}
@@ -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<Element> 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);
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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<Integer> error = Property.color(0xFFbf212f);
private static Property<Integer> info = Property.color(0xFF264b96);
private static Property<Integer> 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);
}
}
@@ -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<Integer> scrollBarRadius = Property.integer(10);
private static Property<Integer> scrollBarWidth = Property.integer(5);
private static Property<Integer> scrollBarPadding = Property.integer(5);
int padding = 0;
protected List<Element> 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);
}
}

Some files were not shown because too many files have changed in this diff Show More