Merge pull request #37 from saturnclientmc/final-architecture

Final architecture
This commit is contained in:
2026-03-08 17:34:50 -07:00
committed by GitHub
79 changed files with 1415 additions and 2426 deletions
+1 -9
View File
@@ -2,15 +2,14 @@ subprojects {
apply plugin: "java"
repositories {
mavenCentral()
maven { url "https://maven.fabricmc.net/" }
maven { url "https://jitpack.io" }
mavenCentral()
}
dependencies {
implementation "de.javagl:obj:0.4.0"
implementation "com.formdev:svgSalamander:1.1.4"
implementation "com.github.selimaj-dev:session-java:0.1.6"
}
}
@@ -28,10 +27,3 @@ configure(subprojects.findAll { it.name != "common" }) {
}
}
configure(subprojects.findAll { it.name == "common" }) {
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0'
}
}
+1 -1
Submodule common updated: 799d914dba...efb7a8bbe4
@@ -1,265 +0,0 @@
package org.saturnclient.cosmetics.cloak;
import net.minecraft.util.Identifier;
import org.saturnclient.client.ServiceClient;
import org.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");
}
}
}
@@ -1,791 +0,0 @@
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;
}
}
@@ -1,36 +0,0 @@
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;
}
}
@@ -1,98 +0,0 @@
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));
}
}
@@ -1,48 +0,0 @@
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;
}
}
@@ -1,37 +0,0 @@
package org.saturnclient.impl;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModManager.ModuleProvider;
import org.saturnclient.modules.mods.*;
import org.saturnclient.impl.modules.*;
public class FabricModuleProvider implements ModuleProvider {
public static final Module[] MODS = {
new ArmorDisplay(new ArmorDisplayFabric()),
new AutoSprint(new AutoSprintFabric()),
new Coordinates(new CoordinatesFabric()),
new Crosshair(new CrosshairFabric()),
new DayCounter(new DayCounterFabric()),
new Fps(new FpsFabric()),
new Freelook(new FreelookFabric()),
new HealthDisplay(new HealthDisplayFabric()),
new Keystrokes(new KeystrokesFabric()),
new Clock(),
new Fullbright(),
new Nametags(new NametagsFabric()),
new NoFog(),
new Ping(new PingFabric()),
new Speedometer(new SpeedometerFabric()),
new StatusEffects(new StatusEffectsFabric()),
new Tps(),
new Zoom()
};
@Override
public Module[] getMods() {
return MODS;
}
}
@@ -1,57 +0,0 @@
package org.saturnclient.impl;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public class Fonts {
public static final Identifier INTER = Identifier.of("saturnclient", "inter");
public static final Identifier INTER_BOLD = Identifier.of("saturnclient", "inter_bold");
public static final Identifier DEFAULT = Identifier.ofVanilla("default");
public static Identifier getFont(int font) {
switch (font) {
case 0:
return DEFAULT;
case 1:
return INTER;
default:
return INTER_BOLD;
}
}
public static Text setFont(String text, Identifier font) {
return Text.literal(text).setStyle(
Style.EMPTY.withFont(font));
}
public static Text setFont(String text, int font) {
return Text.literal(text).setStyle(
Style.EMPTY.withFont(getFont(font)));
}
public static int getWidth(String text, int font) {
int w = 0;
for (String line : text.split("\n")) {
w = Math.max(w, getWidth(line, getFont(font)));
}
return w;
}
private static int getWidth(String text, Identifier font) {
return SaturnClient.client.textRenderer.getWidth(setFont(text, font));
}
public static int getHeight() {
return 18;
}
public static int centerX(int w, String text, int font) {
return (w - Fonts.getWidth(text, font)) / 2;
}
public static int centerY(int h) {
return (h - 18) / 2;
}
}
@@ -1,80 +0,0 @@
package org.saturnclient.impl;
import java.awt.image.BufferedImage;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.common.minecraft.IMinecraftClient;
import org.saturnclient.common.minecraft.MinecraftProvider;
import org.saturnclient.common.minecraft.bindings.SaturnIdentifier;
import org.saturnclient.cosmetics.cloak.utils.IdentifierUtils;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.SaturnScreenFabric;
import org.saturnclient.ui.screens.TitleMenu;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.option.OptionsScreen;
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
import net.minecraft.util.Identifier;
public class SaturnClientProvider extends MinecraftProvider {
@Override
public IMinecraftClient getClient() {
return (IMinecraftClient) SaturnClient.client;
}
@Override
public Object createIdentifier(String namespace, String path) {
return Identifier.of(namespace, path);
}
@Override
public boolean isKeyPressed(int key) {
return GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), key) == GLFW.GLFW_PRESS
&& SaturnClient.client.currentScreen == null;
}
@Override
public int getWidth(String text, int font) {
return Fonts.getWidth(text, font);
}
@Override
public void registerBufferedImageTexture(SaturnIdentifier i, BufferedImage bi) {
IdentifierUtils.registerBufferedImageTextureFast((Identifier) i.inner, bi);
}
@Override
public String getKeyName(int key) {
return GLFW.glfwGetKeyName(key, GLFW.glfwGetKeyScancode(key));
}
@Override
public void setScreen(SaturnScreen screen) {
SaturnClient.client.setScreen(new SaturnScreenFabric(screen));
}
@Override
public void setScreen(MinecraftScreen screen) {
SaturnScreenFabric exitTarget = new SaturnScreenFabric(new TitleMenu());
switch (screen) {
case SelectWorld:
SaturnClient.client.setScreen(new SelectWorldScreen(exitTarget));
break;
case Multiplayer:
SaturnClient.client.setScreen(new MultiplayerScreen(exitTarget));
break;
case Options:
SaturnClient.client.setScreen(new OptionsScreen(exitTarget, SaturnClient.client.options));
break;
}
}
@Override
public void stop() {
SaturnClient.client.scheduleStop();
}
}
@@ -1,9 +1,10 @@
package org.saturnclient.cosmetics.cloak;
package org.saturnclient.impl.cosmetics;
import java.util.Arrays;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.config.Config;
import org.saturnclient.cosmetics.Cloaks;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.RenderLayer;
@@ -282,7 +283,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
return;
}
Identifier customCape = Cloaks.getCurrentCloakTexture(player.uuid);
Identifier customCape = (Identifier) (Object) Cloaks.getCurrentCloakTexture(player.uuid);
if (customCape == null
|| this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
return;
@@ -1,8 +1,8 @@
package org.saturnclient.cosmetics.hat;
package org.saturnclient.impl.cosmetics;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.cosmetics.obj.MtlLoader;
import org.saturnclient.cosmetics.obj.ObjModel;
import org.saturnclient.impl.cosmetics.obj.MtlLoader;
import org.saturnclient.impl.cosmetics.obj.ObjModel;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
@@ -1,4 +1,4 @@
package org.saturnclient.cosmetics.obj;
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
@@ -27,7 +27,6 @@ public class MtlLoader {
}
public static Map<String, Mtl> of(Identifier mtlId) {
System.out.println(mtlId);
if (loadedMtls.containsKey(mtlId))
return loadedMtls.get(mtlId);
@@ -1,4 +1,4 @@
package org.saturnclient.cosmetics.obj;
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
@@ -1,4 +1,4 @@
package org.saturnclient.cosmetics.obj;
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
@@ -0,0 +1,70 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
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;
import java.util.Map;
public class IdentifierUtils {
public static void registerBufferedImageTextureFast(IdentifierRef identifierRef, BufferedImage bufferedImage) {
try {
Identifier identifier = (Identifier) (Object) identifierRef;
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, pixels[index++]);
}
}
} else {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, bufferedImage.getRGB(x, y));
}
}
}
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
SaturnClient.client.execute(() ->
SaturnClient.client.getTextureManager().registerTexture(identifier, texture)
);
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register texture: {}", identifierRef, e);
}
}
public static void registerBufferedImageTexturesBulk(Map<IdentifierRef, BufferedImage> textures) {
long start = System.currentTimeMillis();
textures.forEach(IdentifierUtils::registerBufferedImageTextureFast);
long end = System.currentTimeMillis();
SaturnClient.LOGGER.info(
"Registered {} textures in {}ms",
textures.size(),
(end - start)
);
}
}
@@ -0,0 +1,13 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.util.Identifier;
@Mixin(Identifier.class)
public abstract class IdentifierMixin implements IdentifierRef {
@Shadow
public abstract String toString();
}
@@ -0,0 +1,19 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.item.ItemStack;
@Mixin(ItemStack.class)
public abstract class ItemStackMixin implements ItemStackRef {
@Shadow
public abstract boolean isEmpty();
@Shadow
public abstract int getMaxDamage();
@Shadow
public abstract int getDamage();
}
@@ -1,8 +1,8 @@
package org.saturnclient.impl.mixins;
import org.joml.Quaternionf;
import org.saturnclient.common.minecraft.bindings.SaturnQuaternionf;
import org.saturnclient.common.minecraft.render.IMatrixStack;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.saturnclient.common.ref.render.QuaternionfRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.gen.Invoker;
@@ -10,7 +10,7 @@ import org.spongepowered.asm.mixin.gen.Invoker;
import net.minecraft.client.util.math.MatrixStack;
@Mixin(MatrixStack.class)
public abstract class MatrixStackMixin implements IMatrixStack {
public abstract class MatrixStackMixin implements MatrixStackRef {
@Shadow
public abstract void translate(double x, double y, double z);
@@ -43,13 +43,13 @@ public abstract class MatrixStackMixin implements IMatrixStack {
public abstract void loadIdentity();
@Override
public void multiply(SaturnQuaternionf quaternion) {
this.multiply((Quaternionf) quaternion.inner);
public void multiply(QuaternionfRef quaternion) {
this.multiply((Quaternionf) quaternion);
}
@Override
public void multiply(SaturnQuaternionf quaternion, float originX, float originY, float originZ) {
this.multiply((Quaternionf) quaternion.inner, originX, originY, originZ);
public void multiply(QuaternionfRef quaternion, float originX, float originY, float originZ) {
this.multiply((Quaternionf) quaternion, originX, originY, originZ);
}
@Override
@@ -2,20 +2,32 @@ package org.saturnclient.impl.mixins;
import java.io.File;
import java.io.InputStream;
import java.util.UUID;
import org.saturnclient.common.minecraft.IMinecraftClient;
import org.saturnclient.common.minecraft.bindings.SaturnIdentifier;
import org.saturnclient.common.minecraft.render.IWindow;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.game.MinecraftClientRef;
import org.saturnclient.common.ref.render.WindowRef;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.screens.TitleMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.option.OptionsScreen;
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
import net.minecraft.client.option.GameOptions;
import net.minecraft.client.session.Session;
import net.minecraft.client.util.Window;
import net.minecraft.resource.ReloadableResourceManagerImpl;
import net.minecraft.util.Identifier;
@Mixin(MinecraftClient.class)
public class MinecraftClientMixin implements IMinecraftClient {
public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Shadow
public File runDirectory;
@@ -23,7 +35,20 @@ public class MinecraftClientMixin implements IMinecraftClient {
private Window window;
@Shadow
public ReloadableResourceManagerImpl resourceManager;
private ReloadableResourceManagerImpl resourceManager;
@Shadow
public GameOptions options;
@Shadow
private Session session;
@Shadow
public abstract void setScreen(@Nullable Screen screen);
@Shadow
@Override
public abstract void scheduleStop();
@Override
public File getRunDirectory() {
@@ -31,16 +56,75 @@ public class MinecraftClientMixin implements IMinecraftClient {
}
@Override
public InputStream getResource(SaturnIdentifier identifier) {
public InputStream getResource(IdentifierRef identifier) {
try {
return resourceManager.getResource((Identifier) identifier.inner).get().getInputStream();
return resourceManager.getResource((Identifier) (Object) identifier).get().getInputStream();
} catch (Exception e) {
return null;
}
}
@Override
public IWindow getWindow() {
return (IWindow) (Object) this.window;
public WindowRef getWindow() {
return (WindowRef) (Object) this.window;
}
@Override
public void setScreen(SaturnScreen screen) {
this.setScreen(new SaturnScreenFabric(screen));
}
@Override
public void setScreen(MinecraftScreen screen) {
SaturnScreenFabric exitTarget = new SaturnScreenFabric(new TitleMenu());
switch (screen) {
case SelectWorld:
this.setScreen(new SelectWorldScreen(exitTarget));
break;
case Multiplayer:
this.setScreen(new MultiplayerScreen(exitTarget));
break;
case Options:
this.setScreen(new OptionsScreen(exitTarget, this.options));
break;
}
}
@Override
public String getAccessToken() {
if (session == null) {
return null;
}
return this.session.getAccessToken();
}
@Override
public UUID getUuid() {
if (session == null) {
return null;
}
return this.session.getUuidOrNull();
}
@Override
public String getUsername() {
if (session == null) {
return null;
}
return this.session.getUsername();
}
@Override
public void onClientStopping(Runnable handler) {
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> handler.run());
}
@Override
public void executeOnThread(Runnable runnable) {
((java.util.concurrent.Executor) (Object) this).execute(runnable);
}
}
@@ -0,0 +1,9 @@
package org.saturnclient.impl.mixins;
import org.joml.Quaternionf;
import org.saturnclient.common.ref.render.QuaternionfRef;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(Quaternionf.class)
public abstract class QuaternionfMixin implements QuaternionfRef {
}
@@ -0,0 +1,9 @@
package org.saturnclient.impl.mixins;
import net.minecraft.client.texture.Sprite;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(Sprite.class)
public abstract class SpriteMixin implements SpriteRef {
}
@@ -0,0 +1,24 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.render.TextRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
@Mixin(Text.class)
public interface TextMixin extends TextRef {
@Override
public default TextRef withFont(IdentifierRef font) {
return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
}
@Override
public default int getWidth() {
return SaturnClient.client.textRenderer.getWidth((Text) this);
}
}
@@ -1,13 +1,13 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.minecraft.render.IWindow;
import org.saturnclient.common.ref.render.WindowRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.util.Window;
@Mixin(Window.class)
public abstract class WindowMixin implements IWindow {
public abstract class WindowMixin implements WindowRef {
@Shadow
public abstract int getWidth();
@@ -1,83 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.common.minecraft.bindings.SaturnItemStack;
import org.saturnclient.modules.interfaces.ArmorDisplayInterface;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
public class ArmorDisplayFabric implements ArmorDisplayInterface {
private ItemStack get(EquipmentSlot slot) {
return MinecraftClient.getInstance().player.getEquippedStack(slot);
}
@Override
public SaturnItemStack getMainHand() {
return new SaturnItemStack(get(EquipmentSlot.MAINHAND));
}
@Override
public SaturnItemStack getHelmet() {
return new SaturnItemStack(get(EquipmentSlot.HEAD));
}
@Override
public SaturnItemStack getChestplate() {
return new SaturnItemStack(get(EquipmentSlot.CHEST));
}
@Override
public SaturnItemStack getLeggings() {
return new SaturnItemStack(get(EquipmentSlot.LEGS));
}
@Override
public SaturnItemStack getBoots() {
return new SaturnItemStack(get(EquipmentSlot.FEET));
}
@Override
public boolean isEmpty(SaturnItemStack stack) {
return ((ItemStack) stack.get()).isEmpty();
}
@Override
public int getMaxDamage(SaturnItemStack stack) {
return ((ItemStack) stack.get()).getMaxDamage();
}
@Override
public int getDamage(SaturnItemStack stack) {
return ((ItemStack) stack.get()).getDamage();
}
// Dummy items
@Override
public SaturnItemStack getDummyMainHand() {
return new SaturnItemStack(new ItemStack(Items.DIAMOND_SWORD));
}
@Override
public SaturnItemStack getDummyHelmet() {
return new SaturnItemStack(new ItemStack(Items.DIAMOND_HELMET));
}
@Override
public SaturnItemStack getDummyChestplate() {
return new SaturnItemStack(new ItemStack(Items.DIAMOND_CHESTPLATE));
}
@Override
public SaturnItemStack getDummyLeggings() {
return new SaturnItemStack(new ItemStack(Items.DIAMOND_LEGGINGS));
}
@Override
public SaturnItemStack getDummyBoots() {
return new SaturnItemStack(new ItemStack(Items.DIAMOND_BOOTS));
}
}
@@ -1,50 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.AutoSprintInterface;
import net.minecraft.client.MinecraftClient;
public class AutoSprintFabric implements AutoSprintInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public boolean hasPlayer() {
return mc.player != null;
}
@Override
public boolean hasNetwork() {
return mc.getNetworkHandler() != null;
}
@Override
public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed();
}
@Override
public boolean isBackPressed() {
return mc.options.backKey.isPressed();
}
@Override
public boolean isSneaking() {
return mc.player.isSneaking();
}
@Override
public boolean hasHorizontalCollision() {
return mc.player.horizontalCollision;
}
@Override
public boolean isUsingItem() {
return mc.player.isUsingItem();
}
@Override
public void setSprinting(boolean sprinting) {
mc.player.setSprinting(sprinting);
}
}
@@ -1,29 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.CoordinatesInterface;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.math.Vec3d;
public class CoordinatesFabric implements CoordinatesInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public int getPlayerX() {
Vec3d pos = mc.player.getPos();
return (int) pos.x;
}
@Override
public int getPlayerY() {
Vec3d pos = mc.player.getPos();
return (int) pos.y;
}
@Override
public int getPlayerZ() {
Vec3d pos = mc.player.getPos();
return (int) pos.z;
}
}
@@ -1,19 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.CrosshairInterface;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
public class CrosshairFabric implements CrosshairInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public boolean isTargetingLivingEntity() {
Entity entity = mc.targetedEntity;
return entity != null && entity.isAlive();
}
}
@@ -1,19 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.DayCounterInterface;
import net.minecraft.client.MinecraftClient;
public class DayCounterFabric implements DayCounterInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public long getDay() {
if (mc.world == null) {
return 0;
}
return mc.world.getTimeOfDay() / 24000L;
}
}
@@ -0,0 +1,80 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.entity.LivingEntity;
import org.saturnclient.common.module.EntityModule;
/**
* Fabric implementation of {@link EntityModule}.
*
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* Entity state for nametag rendering is supplied at render time by
* {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which
* writes into the render state; {@link NametagsMixin} then constructs
* a {@link RenderStateEntityState} from the already-extracted snapshot.
*/
public class EntityModuleFabric implements EntityModule {
private final MinecraftClient mc;
public EntityModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean isTargetingLivingEntity() {
HitResult target = mc.crosshairTarget;
if (target == null || target.getType() != HitResult.Type.ENTITY)
return false;
Entity hit = ((EntityHitResult) target).getEntity();
return hit instanceof LivingEntity;
}
// ---------------------------------------------------------------
// Render-state adapter
// ---------------------------------------------------------------
/**
* Wraps a {@link HealthRenderState} (already injected into the
* Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}.
*
* This is constructed inside {@link NametagsMixin} where both the
* render-state mixin interface and the entity name are available.
*/
public static final class RenderStateEntityState implements EntityState {
private final String customName;
private final HealthRenderState hrs;
public RenderStateEntityState(String customName, HealthRenderState hrs) {
this.customName = customName;
this.hrs = hrs;
}
@Override
public String getCustomName() {
return customName;
}
@Override
public float getHealth() {
return hrs.saturn$getHealth();
}
@Override
public float getMaxHealth() {
return hrs.saturn$getMaxHealth();
}
@Override
public EntityType getEntityType() {
return hrs.saturn$getEntityType();
}
}
}
@@ -1,15 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.FpsInterface;
import net.minecraft.client.MinecraftClient;
public class FpsFabric implements FpsInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public int getFps() {
return mc.getCurrentFps();
}
}
@@ -1,27 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.FreelookInterface;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.Perspective;
public class FreelookFabric implements FreelookInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public boolean isFirstPerson() {
return mc.options.getPerspective() == Perspective.FIRST_PERSON;
}
@Override
public void setFirstPerson() {
mc.options.setPerspective(Perspective.FIRST_PERSON);
}
@Override
public void setThirdPersonBack() {
mc.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
}
@@ -1,20 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.HealthDisplayInterface;
import net.minecraft.client.MinecraftClient;
public class HealthDisplayFabric implements HealthDisplayInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public float getPlayerHealth() {
if (mc.player == null) {
return 0f;
}
return mc.player.getHealth();
}
}
@@ -0,0 +1,20 @@
package org.saturnclient.impl.modules;
import org.saturnclient.common.module.EntityModule;
/**
* Injected interface on {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
*
* Carries the entity's health snapshot and classification into the render
* pipeline so that {@link NametagsMixin} can read them without touching
* the live entity again.
*/
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityModule.EntityType saturn$getEntityType();
void saturn$setEntityType(EntityModule.EntityType type);
}
@@ -1,45 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.KeystrokesInterface;
import net.minecraft.client.MinecraftClient;
public class KeystrokesFabric implements KeystrokesInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed();
}
@Override
public boolean isBackPressed() {
return mc.options.backKey.isPressed();
}
@Override
public boolean isLeftPressed() {
return mc.options.leftKey.isPressed();
}
@Override
public boolean isRightPressed() {
return mc.options.rightKey.isPressed();
}
@Override
public boolean isJumpPressed() {
return mc.options.jumpKey.isPressed();
}
@Override
public boolean isAttackPressed() {
return mc.options.attackKey.isPressed();
}
@Override
public boolean isUsePressed() {
return mc.options.useKey.isPressed();
}
}
@@ -1,49 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.HealthRenderState;
import org.saturnclient.modules.interfaces.NametagsInterface;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.text.Text;
public class NametagsFabric implements NametagsInterface {
public static class EntityStateImpl implements EntityState {
private final LivingEntityRenderState state;
public EntityStateImpl(LivingEntityRenderState state) {
this.state = state;
}
@Override
public String getCustomName() {
Text name = state.customName;
return name != null ? name.getString() : null;
}
@Override
public float getHealth() {
if (!(state instanceof HealthRenderState hrs))
return 0.0f;
return hrs.saturn$getHealth();
}
@Override
public float getMaxHealth() {
if (!(state instanceof HealthRenderState hrs))
return 0.0f;
return hrs.saturn$getMaxHealth();
}
@Override
public EntityType getEntityType() {
if (!(state instanceof HealthRenderState hrs))
return null;
return hrs.saturn$getEntityType();
}
}
}
@@ -0,0 +1,37 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.PlayerListEntry;
import org.saturnclient.common.module.NetworkModule;
/**
* Fabric implementation of {@link NetworkModule}.
*
* Connection presence is checked via the network handler.
* Ping is read from the player's {@link PlayerListEntry} which the
* server populates; returns {@code -1} when unavailable.
*/
public class NetworkModuleFabric implements NetworkModule {
private final MinecraftClient mc;
public NetworkModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasNetwork() {
return mc.getNetworkHandler() != null;
}
@Override
public int getPing() {
if (mc.player == null || mc.getNetworkHandler() == null)
return -1;
PlayerListEntry entry = mc.getNetworkHandler().getPlayerListEntry(mc.player.getUuid());
return entry != null ? entry.getLatency() : -1;
}
}
@@ -1,19 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.PingInterface;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayNetworkHandler;
public class PingFabric implements PingInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public int getPing() {
if (mc.player == null || mc.getNetworkHandler() == null) return 0;
ClientPlayNetworkHandler handler = mc.getNetworkHandler();
if (handler.getPlayerListEntry(mc.player.getUuid()) == null) return 0;
return handler.getPlayerListEntry(mc.player.getUuid()).getLatency();
}
}
@@ -0,0 +1,302 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.feature.features.StatusEffectsFeature;
import org.saturnclient.saturnclient.SaturnClient;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Fabric implementation of {@link PlayerModule}.
*
* All access to {@link MinecraftClient} and {@link ClientPlayerEntity}
* is encapsulated here. Features only ever see the {@link PlayerModule}
* interface; they never import Minecraft classes directly.
*
* Every method guards against a missing player and returns a safe
* default so features are free of null checks.
*/
public class PlayerModuleFabric implements PlayerModule {
private final MinecraftClient mc;
public PlayerModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Internal helper — never returns null
// ---------------------------------------------------------------
/** Returns the local player, or {@code null} if one is not loaded. */
private ClientPlayerEntity player() {
return mc.player;
}
// ---------------------------------------------------------------
// Presence
// ---------------------------------------------------------------
@Override
public boolean hasPlayer() {
return mc.player != null;
}
// ---------------------------------------------------------------
// Position
// ---------------------------------------------------------------
@Override
public int getX() {
return hasPlayer() ? (int) player().getX() : 0;
}
@Override
public int getY() {
return hasPlayer() ? (int) player().getY() : 0;
}
@Override
public int getZ() {
return hasPlayer() ? (int) player().getZ() : 0;
}
// ---------------------------------------------------------------
// Input / movement
// ---------------------------------------------------------------
@Override
public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed();
}
@Override
public boolean isBackPressed() {
return mc.options.backKey.isPressed();
}
@Override
public boolean isLeftPressed() {
return mc.options.leftKey.isPressed();
}
@Override
public boolean isRightPressed() {
return mc.options.rightKey.isPressed();
}
@Override
public boolean isJumpPressed() {
return mc.options.jumpKey.isPressed();
}
@Override
public boolean isAttackPressed() {
return mc.options.attackKey.isPressed();
}
@Override
public boolean isUsePressed() {
return mc.options.useKey.isPressed();
}
@Override
public boolean isSneaking() {
return hasPlayer() && player().isSneaking();
}
@Override
public boolean isUsingItem() {
return hasPlayer() && player().isUsingItem();
}
@Override
public boolean hasHorizontalCollision() {
return hasPlayer() && player().horizontalCollision;
}
@Override
public boolean isOnGround() {
return hasPlayer() && player().isOnGround();
}
@Override
public void setSprinting(boolean sprinting) {
if (hasPlayer())
player().setSprinting(sprinting);
}
// ---------------------------------------------------------------
// Velocity
// ---------------------------------------------------------------
@Override
public Velocity getVelocity() {
if (!hasPlayer())
return Velocity.ZERO;
var v = player().getVelocity();
return new Velocity(v.x, v.y, v.z);
}
// ---------------------------------------------------------------
// Stats
// ---------------------------------------------------------------
@Override
public float getHealth() {
return hasPlayer() ? player().getHealth() : 0f;
}
// ---------------------------------------------------------------
// Equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getMainHand() {
return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY);
}
@Override
public ItemStackRef getHelmet() {
return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getChestplate() {
return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getLeggings() {
return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getBoots() {
return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
}
// ---------------------------------------------------------------
// Dummy equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getDummyMainHand() {
return wrap(new ItemStack(Items.DIAMOND_SWORD));
}
@Override
public ItemStackRef getDummyHelmet() {
return wrap(new ItemStack(Items.DIAMOND_HELMET));
}
@Override
public ItemStackRef getDummyChestplate() {
return wrap(new ItemStack(Items.DIAMOND_CHESTPLATE));
}
@Override
public ItemStackRef getDummyLeggings() {
return wrap(new ItemStack(Items.DIAMOND_LEGGINGS));
}
@Override
public ItemStackRef getDummyBoots() {
return wrap(new ItemStack(Items.DIAMOND_BOOTS));
}
// ---------------------------------------------------------------
// Status effects
// ---------------------------------------------------------------
@Override
public List<? extends StatusEffectsFeature.EffectView> getActiveEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<FabricEffectView> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add(new FabricEffectView(instance));
}
return result;
}
@Override
public List<? extends StatusEffectsFeature.EffectView> getDummyEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<FabricEffectView> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add(new FabricEffectView(instance));
}
return result;
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static ItemStackRef wrap(ItemStack stack) {
return (ItemStackRef) (Object) stack;
}
// ---------------------------------------------------------------
// Inner — EffectView adapter
// ---------------------------------------------------------------
/**
* Bridges a Fabric {@link StatusEffectInstance} to the
* platform-neutral {@link StatusEffectsFeature.EffectView}.
*/
public static final class FabricEffectView implements StatusEffectsFeature.EffectView {
private final StatusEffectInstance instance;
public FabricEffectView(StatusEffectInstance instance) {
this.instance = instance;
}
@Override
public boolean shouldShowIcon() {
return instance.shouldShowIcon();
}
/**
* Returns the Minecraft sprite identifier for this effect's icon.
* The platform-specific renderer knows how to draw a
* {@link net.minecraft.util.Identifier}.
*/
@Override
public SpriteRef getIcon() {
return (SpriteRef) SaturnClient.client.getStatusEffectSpriteManager().getSprite(instance.getEffectType());
}
@Override
public boolean isInfinite() {
return instance.isInfinite();
}
@Override
public String getInfiniteText() {
return "∞";
}
@Override
public int getDurationSeconds() {
return instance.getDuration() / 20; // ticks → seconds
}
}
}
@@ -0,0 +1,64 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.Perspective;
import org.saturnclient.common.module.RenderModule;
/**
* Fabric implementation of {@link RenderModule}.
*
* Window dimensions and FPS are read directly from
* {@link MinecraftClient}. Camera perspective control delegates to
* {@link net.minecraft.client.option.GameOptions}.
*/
public class RenderModuleFabric implements RenderModule {
private final MinecraftClient mc;
public RenderModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Window
// ---------------------------------------------------------------
@Override
public int getScaledWindowWidth() {
return mc.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return mc.getWindow().getScaledHeight();
}
// ---------------------------------------------------------------
// Performance
// ---------------------------------------------------------------
@Override
public int getFps() {
return mc.getCurrentFps();
}
// ---------------------------------------------------------------
// Camera perspective
// ---------------------------------------------------------------
@Override
public boolean isFirstPerson() {
return mc.options.getPerspective() == Perspective.FIRST_PERSON;
}
@Override
public void setFirstPerson() {
mc.options.setPerspective(Perspective.FIRST_PERSON);
}
@Override
public void setThirdPersonBack() {
mc.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
}
@@ -1,23 +0,0 @@
package org.saturnclient.impl.modules;
import org.saturnclient.modules.interfaces.SpeedometerInterface;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
public class SpeedometerFabric implements SpeedometerInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public Velocity getVelocity() {
Entity entity = mc.player.getVehicle() != null ? mc.player.getVehicle() : mc.player;
return new Velocity(entity.getVelocity().x, entity.getVelocity().y, entity.getVelocity().z);
}
@Override
public boolean isOnGround() {
Entity entity = mc.player.getVehicle() != null ? mc.player.getVehicle() : mc.player;
return entity.isOnGround();
}
}
@@ -1,38 +0,0 @@
package org.saturnclient.impl.modules;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Arrays;
import org.saturnclient.modules.interfaces.StatusEffectsInterface;
import org.saturnclient.common.minecraft.bindings.SaturnSprite;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.texture.StatusEffectSpriteManager;
import net.minecraft.entity.effect.StatusEffectInstance;
public class StatusEffectsFabric implements StatusEffectsInterface {
private final MinecraftClient mc = MinecraftClient.getInstance();
@Override
public List<Effect> getActiveEffects() {
StatusEffectSpriteManager manager = mc.getStatusEffectSpriteManager();
return mc.player.getStatusEffects().stream()
.filter(StatusEffectInstance::shouldShowIcon)
.map(se -> new Effect(
new SaturnSprite(manager.getSprite(se.getEffectType())),
se.getDuration() / 20,
se.isInfinite(),
se.shouldShowIcon(),
"Infinite"))
.collect(Collectors.toList());
}
@Override
public List<Effect> getDummyEffects() {
return Arrays.asList(
new Effect(new SaturnSprite(null), 600, false, true, "Infinite"),
new Effect(new SaturnSprite(null), 600, false, true, "Infinite"));
}
}
@@ -0,0 +1,36 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import org.saturnclient.common.module.WorldModule;
/**
* Fabric implementation of {@link WorldModule}.
*
* Reads the client world's time directly from {@link MinecraftClient}.
* Returns safe defaults when no world is loaded.
*/
public class WorldModuleFabric implements WorldModule {
private final MinecraftClient mc;
public WorldModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasWorld() {
return mc.world != null;
}
/**
* Returns the total world age in ticks, or {@code 0} when no world
* is loaded. Used by {@link TpsFeature#onTimePacket} (which is
* called from the mixin and stores its own copy) and by
* {@link DayCounterFeature} for the day count.
*/
@Override
public long getWorldAge() {
return hasWorld() ? mc.world.getTime() : 0L;
}
}
@@ -0,0 +1,37 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.GameRenderer;
import org.saturnclient.feature.features.ZoomFeature;
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;
/**
* Overrides the game's field-of-view when {@link ZoomFeature} is active.
*
* The old code checked both {@code Zoom.isZooming} and
* {@code Zoom.shouldZoom()} separately. The refactored
* {@link ZoomFeature#shouldZoom()} already ORs both conditions
* ({@code enabled.value && isZooming}), so only one check is needed.
*
* No {@link org.saturnclient.common.module.ModuleProvider} is required
* here because zooming is a pure render-time override with no per-tick
* engine queries.
*/
@Mixin(GameRenderer.class)
public class GameRendererMixin {
/**
* After the vanilla FOV has been computed, divide it by the zoom
* level to produce the narrowed field-of-view.
*/
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (ZoomFeature.shouldZoom()) {
float zoomedFov = cir.getReturnValue() / ZoomFeature.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -0,0 +1,35 @@
package org.saturnclient.impl.modules.mixins;
// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager;
import org.saturnclient.feature.features.FullbrightFeature;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Overrides the lightmap's gamma computation when
* {@link FullbrightFeature} is active, replacing the vanilla
* brightness with a user-configured value.
*
* The old import was {@code org.saturnclient.feature.features.Fullbright};
* it is now {@link FullbrightFeature}. The injection target and
* strategy are unchanged.
*/
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
/**
* If fullbright is active, substitute the feature's brightness
* value for whatever vanilla calculated.
*/
@ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
private float modifyGamma(float original) {
if (FullbrightFeature.shouldOverrideBrightness()) {
return FullbrightFeature.getBrightnessValue();
}
return original;
}
}
@@ -0,0 +1,59 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.impl.modules.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Implements {@link HealthRenderState} on
* {@link LivingEntityRenderState}, injecting health and entity-type
* storage into every living entity's render-state snapshot.
*
* Previously imported {@code NametagsInterface.EntityType}; the enum
* now lives in {@link EntityModule.EntityType} which is the single
* canonical definition shared by the mixin layer and the feature layer.
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by
* {@link NametagsMixin} at render time.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@Unique
private float saturn$health = 0f;
@Unique
private float saturn$maxHealth = 1f;
@Unique
private EntityModule.EntityType saturn$entityType = EntityModule.EntityType.OTHER;
@Override
public float saturn$getHealth() {
return saturn$health;
}
@Override
public float saturn$getMaxHealth() {
return saturn$maxHealth;
}
@Override
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
@Override
public EntityModule.EntityType saturn$getEntityType() {
return saturn$entityType;
}
@Override
public void saturn$setEntityType(EntityModule.EntityType type) {
saturn$entityType = type;
}
}
@@ -0,0 +1,51 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
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.saturnclient.common.module.EntityModule;
import org.saturnclient.impl.modules.HealthRenderState;
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;
/**
* Captures each living entity's health and type into the render state
* during {@code updateRenderState}, so that nametag rendering can read
* them without touching the live entity again.
*
* Previously imported {@code NametagsInterface.EntityType}; now uses
* {@link EntityModule.EntityType} — the canonical enum shared across
* the whole module system.
*
* The injection target and strategy are unchanged from the original.
*/
@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());
// Classify entity type for the nametag feature's filtering logic
EntityModule.EntityType type;
if (entity instanceof PlayerEntity)
type = EntityModule.EntityType.PLAYER;
else if (entity instanceof HostileEntity)
type = EntityModule.EntityType.HOSTILE;
else if (entity instanceof PassiveEntity)
type = EntityModule.EntityType.PASSIVE;
else
type = EntityModule.EntityType.OTHER;
hrs.saturn$setEntityType(type);
}
}
@@ -0,0 +1,94 @@
package org.saturnclient.impl.modules.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.feature.features.NametagsFeature;
import org.saturnclient.impl.modules.EntityModuleFabric;
import org.saturnclient.impl.modules.HealthRenderState;
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;
/**
* Intercepts nametag rendering and replaces the displayed text with
* the string produced by {@link NametagsFeature#getNametagString}.
*
* Key changes from the original:
*
* <ul>
* <li>The old {@code NametagsFabric.EntityStateImpl} bridge class is
* replaced by {@link EntityModuleFabric.RenderStateEntityState},
* which wraps the {@link HealthRenderState} already injected into
* the render state by {@link LivingEntityRenderStateMixin}.</li>
* <li>The feature import changes from {@code Nametags} to
* {@link NametagsFeature}; the two static methods
* ({@code shouldReplaceName} / {@code getNametagString}) are
* unchanged in signature.</li>
* <li>The entity's display name is extracted from the render state's
* {@code nameVisible} / text fields where available, falling back
* to the plain text string passed to this method.</li>
* </ul>
*
* The re-entrancy guard ({@code saturn$rendering}) is kept intact to
* prevent infinite recursion when we call {@code renderLabelIfPresent}
* with the replacement text.
*/
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
/** Guards against the recursive call we make with the replacement text. */
@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) {
// Skip our own recursive call
if (saturn$rendering.get())
return;
if (!NametagsFeature.shouldReplaceName())
return;
// Only living entities carry health data
if (!(state instanceof LivingEntityRenderState living))
return;
if (!(living instanceof HealthRenderState hrs))
return;
// Build the platform-neutral EntityState from the render-state snapshot
// (health/type were written by LivingEntityRendererMixin at extract time).
String customName = text != null ? text.getString() : null;
EntityModuleFabric.RenderStateEntityState entityState = new EntityModuleFabric.RenderStateEntityState(
customName, hrs);
// Ask the feature for the replacement string
String replacement = NametagsFeature.getNametagString(entityState);
if (replacement == null)
return; // feature decided not to replace this entity
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
}
@@ -0,0 +1,85 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.FeatureManager;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.impl.ui.RenderScopeImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.screens.HudEditor;
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;
/**
* Drives per-frame rendering for all registered features.
*
* This mixin is intentionally kept thin: it creates a {@link RenderScope},
* iterates the enabled feature list from {@link FeatureManager}, and
* dispatches either {@link HudFeature#renderHud} (for HUD elements) or
* {@link Feature#render} (for world-overlay features like Crosshair).
*
* No feature-specific logic lives here — all decisions about what to
* draw are made inside each feature class.
*
* Changes from the original:
* - Package imports updated to the refactored feature names (no
* more {@code feature.features.*}).
* - The HUD-editor screen check still uses the existing
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
* unchanged infrastructure.
*/
@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;
// Skip rendering while the HUD editor is open so the editor
// can draw its own preview layout without double-drawing.
if (textRenderer == null)
return;
if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f
&& f.screen instanceof HudEditor)
return;
RenderScope scope = new RenderScopeImpl(
context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (Feature feature : FeatureManager.ENABLED_MODS) {
if (!feature.isEnabled())
continue;
if (feature instanceof HudFeature hud) {
FeatureLayout dim = hud.getDimensions();
scope.getMatrixStack().push();
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value, 0f);
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
scope.drawRoundedRectangle(0, 0, dim.width, dim.height,
dim.radius.value, dim.bgColor.value);
}
hud.renderHud(scope);
scope.getMatrixStack().pop();
} else {
// Non-HUD features (e.g. CrosshairFeature) use the plain render hook.
feature.render(scope);
}
}
}
}
@@ -0,0 +1,30 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
import org.saturnclient.feature.features.TpsFeature;
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;
/**
* Feeds world-time packets into {@link TpsFeature}'s TPS estimator.
*
* This is the only place in the entire codebase that reads a network
* packet and passes data to a feature. The feature's
* {@link TpsFeature#onTimePacket(long)} method handles all averaging
* logic; the mixin is intentionally reduced to a single forwarding call.
*
* The only change from the original is renaming the import from
* {@code feature.features.Tps} to {@link TpsFeature}.
*/
@Mixin(ClientPlayNetworkHandler.class)
public class TpsMixin {
@Inject(method = "onWorldTimeUpdate", at = @At("HEAD"))
private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) {
TpsFeature.onTimePacket(packet.time());
}
}
@@ -0,0 +1,18 @@
package org.saturnclient.impl.provider;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.saturnclient.SaturnClient;
public class GLFWProviderImpl implements GLFWProvider {
@Override
public boolean isKeyPressed(int key) {
return GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), key) == GLFW.GLFW_PRESS
&& SaturnClient.client.currentScreen == null;
}
@Override
public String getKeyName(int key) {
return GLFW.glfwGetKeyName(key, GLFW.glfwGetKeyScancode(key));
}
}
@@ -0,0 +1,68 @@
package org.saturnclient.impl.provider;
import net.minecraft.client.MinecraftClient;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.impl.modules.EntityModuleFabric;
import org.saturnclient.impl.modules.NetworkModuleFabric;
import org.saturnclient.impl.modules.PlayerModuleFabric;
import org.saturnclient.impl.modules.RenderModuleFabric;
import org.saturnclient.impl.modules.WorldModuleFabric;
import org.saturnclient.common.module.*;
/**
* Fabric implementation of {@link ModuleProvider}.
*
* Instantiated once during client startup (e.g. in the mod initialiser)
* and passed to every feature constructor. All five sub-modules share
* the same {@link MinecraftClient} reference so there is no
* inconsistency between modules reading state within the same tick.
*
* <pre>{@code
* ModuleProvider provider = new ModuleProviderFabric(MinecraftClient.getInstance());
* FeatureManager.register(new AutoSprintFeature(provider));
* FeatureManager.register(new CoordinatesFeature(provider));
* // …
* }</pre>
*/
public class ModuleProviderFabric implements ModuleProvider {
private final PlayerModule player;
private final WorldModule world;
private final EntityModule entity;
private final RenderModule render;
private final NetworkModule network;
public ModuleProviderFabric(MinecraftClient mc) {
this.player = new PlayerModuleFabric(mc);
this.world = new WorldModuleFabric(mc);
this.entity = new EntityModuleFabric(mc);
this.render = new RenderModuleFabric(mc);
this.network = new NetworkModuleFabric(mc);
}
@Override
public PlayerModule player() {
return player;
}
@Override
public WorldModule world() {
return world;
}
@Override
public EntityModule entity() {
return entity;
}
@Override
public RenderModule render() {
return render;
}
@Override
public NetworkModule network() {
return network;
}
}
@@ -0,0 +1,25 @@
package org.saturnclient.impl.provider;
import org.saturnclient.common.provider.RefConstructorProvider;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.render.TextRef;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public class RefConstructorImpl implements RefConstructorProvider {
@Override
public IdentifierRef identifier(String fullPath) {
return (IdentifierRef) (Object) Identifier.of(fullPath);
}
@Override
public IdentifierRef identifier(String namespace, String path) {
return (IdentifierRef) (Object) Identifier.of(namespace, path);
}
@Override
public TextRef text(String text) {
return (TextRef) Text.literal(text);
}
}
@@ -1,8 +1,12 @@
package org.saturnclient.saturnclient.bindings;
package org.saturnclient.impl.provider;
import java.awt.image.BufferedImage;
import java.util.UUID;
import org.saturnclient.common.minecraft.bindings.SaturnEmoteBindings;
import org.saturnclient.common.provider.SaturnProvider;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.game.MinecraftClientRef;
import org.saturnclient.impl.cosmetics.utils.IdentifierUtils;
import org.saturnclient.saturnclient.SaturnClient;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
@@ -11,10 +15,14 @@ import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.util.Identifier;
public class SaturnEmoteBindingsImpl implements SaturnEmoteBindings {
public class SaturnProviderImpl implements SaturnProvider {
@Override
public MinecraftClientRef getClient() {
return (MinecraftClientRef) SaturnClient.client;
}
@Override
public void setEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
if (SaturnClient.client == null || SaturnClient.client.world == null) {
return;
}
@@ -41,4 +49,24 @@ public class SaturnEmoteBindingsImpl implements SaturnEmoteBindings {
break;
}
}
@Override
public void logInfo(String message) {
SaturnClient.LOGGER.info(message);
}
@Override
public void logError(String message) {
SaturnClient.LOGGER.error(message);
}
@Override
public void logError(String message, Throwable throwable) {
SaturnClient.LOGGER.error(message, throwable);
}
@Override
public void registerBufferedImageTexture(IdentifierRef i, BufferedImage bi) {
IdentifierUtils.registerBufferedImageTextureFast(i, bi);
}
}
@@ -1,10 +1,12 @@
package org.saturnclient.ui;
package org.saturnclient.impl.ui;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.saturnclient.common.minecraft.bindings.SaturnQuaternionf;
import org.saturnclient.common.ref.render.QuaternionfRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
import net.minecraft.client.render.DiffuseLighting;
@@ -53,18 +55,19 @@ public class EntityDrawerImpl implements EntityDrawer {
renderScope.getMatrixStack().translate((double) x, (double) y, 50.0);
renderScope.getMatrixStack().scale(size, size, -size);
renderScope.getMatrixStack().translate(vector3f.x, vector3f.y, vector3f.z);
renderScope.getMatrixStack().multiply(new SaturnQuaternionf(quaternionf));
renderScope.getMatrixStack().multiply((QuaternionfRef) quaternionf);
renderScope.draw();
DiffuseLighting.method_34742();
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
entityRenderDispatcher.setRenderShadows(false);
// TODO add the method draw with vertex consumers
// renderScope.draw((vertexConsumers) -> {
// entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F,
// renderScope.getMatrixStack(), vertexConsumers,
// 15728880);
// });
if (renderScope instanceof RenderScopeImpl r) {
r.draw((vertexConsumers) -> {
entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F,
r.matrices, vertexConsumers,
15728880);
});
}
renderScope.draw();
entityRenderDispatcher.setRenderShadows(true);
renderScope.getMatrixStack().pop();
@@ -1,4 +1,4 @@
package org.saturnclient.ui;
package org.saturnclient.impl.ui;
import java.util.ArrayDeque;
import java.util.Deque;
@@ -10,13 +10,14 @@ import org.joml.Matrix4f;
import com.mojang.blaze3d.systems.RenderSystem;
import org.saturnclient.common.minecraft.MinecraftProvider;
import org.saturnclient.common.minecraft.bindings.SaturnIdentifier;
import org.saturnclient.common.minecraft.bindings.SaturnItemStack;
import org.saturnclient.common.minecraft.bindings.SaturnSprite;
import org.saturnclient.common.minecraft.render.IMatrixStack;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.SvgTexture;
@@ -36,6 +37,7 @@ 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.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
@@ -62,8 +64,8 @@ public class RenderScopeImpl implements RenderScope {
}
@Override
public IMatrixStack getMatrixStack() {
return (IMatrixStack) matrices;
public MatrixStackRef getMatrixStack() {
return (MatrixStackRef) matrices;
}
@Override
@@ -130,7 +132,7 @@ public class RenderScopeImpl implements RenderScope {
matrices.translate(x, y + (i * Fonts.getHeight()), 0);
matrices.scale(scale, scale, 1.0f);
TextRenderer textRenderer = SaturnClient.client.textRenderer;
textRenderer.draw(org.saturnclient.impl.Fonts.setFont(line, font), 0,
textRenderer.draw((Text) Fonts.setFont(line, font), 0,
font == 0 ? 1 : 7, color, false,
this.matrices.peek().getPositionMatrix(),
this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
@@ -218,27 +220,28 @@ public class RenderScopeImpl implements RenderScope {
}
@Override
public void drawTexture(SaturnIdentifier sprite, int x, int y, float u, float v, int width, int height, int color) {
public void drawTexture(IdentifierRef 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);
}
@Override
public void drawTexture(SaturnIdentifier sprite, int x, int y, float u, float v, int width, int height) {
public void drawTexture(IdentifierRef 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);
}
@Override
public void drawTexture(SaturnIdentifier sprite, int x, int y, float u, float v, int width, int height,
public void drawTexture(IdentifierRef 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);
}
@Override
public void drawTexture(SaturnIdentifier sprite, int x, int y, float u, float v, int width, int height,
public void drawTexture(IdentifierRef 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((Identifier) sprite.inner, x, x + width, y, y + height, (u + 0.0F) / (float) textureWidth,
this.drawTexturedQuad((Identifier) (Object) 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);
}
@@ -269,9 +272,9 @@ public class RenderScopeImpl implements RenderScope {
float v2, int color) {
if (sprite.toString().endsWith(".svg")) {
sprite = (Identifier) SvgTexture.getSvg(MinecraftProvider.PROVIDER.getClient(),
new SaturnIdentifier(sprite),
(x2 - x1) * 2, (y2 - y1) * 2).inner;
sprite = (Identifier) (Object) SvgTexture.getSvg(Providers.saturn.getClient(),
(IdentifierRef) (Object) sprite,
(x2 - x1) * 2, (y2 - y1) * 2);
}
if (color == 0)
@@ -371,33 +374,33 @@ public class RenderScopeImpl implements RenderScope {
}
@Override
public void drawItem(SaturnItemStack item, int x, int y) {
public void drawItem(ItemStackRef item, int x, int y) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0);
}
@Override
public void drawItem(SaturnItemStack stack, int x, int y, int seed) {
public void drawItem(ItemStackRef stack, int x, int y, int seed) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed);
}
@Override
public void drawItem(SaturnItemStack stack, int x, int y, int seed, int z) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, (ItemStack) stack.inner, x, y, seed, z);
public void drawItem(ItemStackRef stack, int x, int y, int seed, int z) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, (ItemStack) (Object) stack, x, y, seed, z);
}
@Override
public void drawItemWithoutEntity(SaturnItemStack stack, int x, int y) {
public void drawItemWithoutEntity(ItemStackRef stack, int x, int y) {
this.drawItemWithoutEntity(stack, x, y, 0);
}
@Override
public void drawItemWithoutEntity(SaturnItemStack stack, int x, int y, int seed) {
public void drawItemWithoutEntity(ItemStackRef stack, int x, int y, int seed) {
this.drawItem((LivingEntity) null, SaturnClient.client.world, stack, x, y, seed);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, SaturnItemStack stack, int x, int y,
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
int seed) {
this.drawItem(entity, world, (ItemStack) stack.inner, x, y, seed, 0);
this.drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
@@ -442,13 +445,13 @@ public class RenderScopeImpl implements RenderScope {
}
@Override
public void drawSpriteStretched(SaturnSprite sprite, int x, int y, int width, int height) {
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
this.drawSpriteStretched(sprite, x, y, width, height, -1);
}
@Override
public void drawSpriteStretched(SaturnSprite saturnSprite, int x, int y, int width, int height, int color) {
Sprite sprite = (Sprite) saturnSprite.inner;
public void drawSpriteStretched(SpriteRef saturnSprite, int x, int y, int width, int height, int color) {
Sprite sprite = (Sprite) saturnSprite;
if (color == 0)
return;
if (width != 0 && height != 0) {
@@ -1,9 +1,11 @@
package org.saturnclient.ui;
package org.saturnclient.impl.ui;
import java.time.Duration;
import java.time.Instant;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.SaturnScreen.ScreenProvider;
import net.minecraft.client.MinecraftClient;
@@ -1,9 +0,0 @@
package org.saturnclient.modules;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -1,15 +0,0 @@
package org.saturnclient.modules;
import org.saturnclient.modules.interfaces.NametagsInterface.EntityType;
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityType saturn$getEntityType();
void saturn$setEntityType(EntityType type);
}
@@ -1,33 +0,0 @@
package org.saturnclient.modules.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();
}
}
@@ -1,44 +0,0 @@
package org.saturnclient.modules.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.CameraOverriddenEntity;
import org.saturnclient.modules.mods.Freelook;
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;
}
}
}
@@ -1,62 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
import org.saturnclient.modules.CameraOverriddenEntity;
import org.saturnclient.modules.mods.Freelook;
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;
}
}
@@ -1,22 +0,0 @@
package org.saturnclient.modules.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);
}
}
}
@@ -1,23 +0,0 @@
// code modified from Gamma utils: https://github.com/Sjouwer/gamma-utils
package org.saturnclient.modules.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;
}
}
@@ -1,26 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.modules.interfaces.NametagsInterface.EntityType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements org.saturnclient.modules.HealthRenderState {
@Unique private float saturn$health = 0f;
@Unique private float saturn$maxHealth = 1f;
@Unique private EntityType saturn$entityType = 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 EntityType saturn$getEntityType() { return saturn$entityType; }
public void saturn$setEntityType(EntityType type) { this.saturn$entityType = type; }
}
@@ -1,38 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.modules.HealthRenderState;
import org.saturnclient.modules.interfaces.NametagsInterface.EntityType;
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(EntityType.PLAYER);
} else if (entity instanceof HostileEntity) {
hrs.saturn$setEntityType(EntityType.HOSTILE);
} else if (entity instanceof PassiveEntity) {
hrs.saturn$setEntityType(EntityType.PASSIVE);
} else {
hrs.saturn$setEntityType(EntityType.OTHER);
}
}
}
@@ -1,50 +0,0 @@
package org.saturnclient.modules.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.impl.modules.NametagsFabric;
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(new NametagsFabric.EntityStateImpl(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);
}
}
}
@@ -1,57 +0,0 @@
package org.saturnclient.modules.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.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.RenderScopeImpl;
import org.saturnclient.ui.SaturnScreenFabric;
import org.saturnclient.ui.screens.HudEditor;
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.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 SaturnScreenFabric f && f.screen instanceof HudEditor)) {
RenderScope renderScope = new RenderScopeImpl(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.getMatrixStack().push();
renderScope.getMatrixStack().translate(dim.x.value, (float) dim.y.value, 0);
renderScope.getMatrixStack().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);
}
((HudMod) m).renderHud(renderScope);
renderScope.getMatrixStack().pop();
} else {
m.render(renderScope);
}
}
}
}
}
@@ -1,18 +0,0 @@
package org.saturnclient.modules.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());
}
}
@@ -8,21 +8,18 @@ import net.minecraft.client.MinecraftClient;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.cosmetics.Emotes;
import org.saturnclient.cosmetics.Hats;
import org.saturnclient.cosmetics.cloak.Cloaks;
import org.saturnclient.common.minecraft.bindings.SaturnClientBindings;
import org.saturnclient.impl.FabricModuleProvider;
import org.saturnclient.impl.SaturnClientProvider;
import org.saturnclient.modules.ModManager;
import org.saturnclient.common.minecraft.MinecraftProvider;
import org.saturnclient.feature.FeatureManager;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.impl.provider.GLFWProviderImpl;
import org.saturnclient.impl.provider.ModuleProviderFabric;
import org.saturnclient.impl.provider.RefConstructorImpl;
import org.saturnclient.impl.provider.SaturnProviderImpl;
import org.saturnclient.impl.ui.EntityDrawerImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.config.Config;
import org.saturnclient.config.manager.ConfigManager;
import org.saturnclient.saturnclient.bindings.SaturnEmoteBindingsImpl;
import org.saturnclient.saturnclient.bindings.SaturnPlatformBindingsImpl;
import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.ui.ElementRenderer;
import org.saturnclient.ui.EntityDrawerImpl;
import org.saturnclient.ui.SaturnScreenFabric;
import org.saturnclient.ui.components.ElementRendererImpl;
import org.saturnclient.ui.components.SkinPreview;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,26 +38,23 @@ public class SaturnClient implements ModInitializer {
LOGGER.info("Initializing " + MOD_ID);
client = MinecraftClient.getInstance();
MinecraftProvider.PROVIDER = new SaturnClientProvider();
ElementRenderer.INSTANCE = new ElementRendererImpl();
Providers.saturn = new SaturnProviderImpl();
Providers.refConstructor = new RefConstructorImpl();
Providers.GLFW = new GLFWProviderImpl();
Providers.module = new ModuleProviderFabric(client);
SkinPreview.DRAWER = new EntityDrawerImpl();
Config.init();
ModManager.init(new FabricModuleProvider());
FeatureManager.init();
client.execute(() -> {
SaturnScreenFabric.preload(client);
});
SaturnClientBindings.setPlatform(new SaturnPlatformBindingsImpl());
SaturnClientBindings.setEmotes(new SaturnEmoteBindingsImpl());
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save());
ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (org.saturnclient.modules.Module m : ModManager.ENABLED_MODS) {
for (org.saturnclient.feature.Feature m : FeatureManager.ENABLED_MODS) {
m.tick();
}
});
@@ -1,59 +0,0 @@
package org.saturnclient.saturnclient.bindings;
import java.util.UUID;
import org.saturnclient.common.minecraft.bindings.SaturnPlatformBindings;
import org.saturnclient.saturnclient.SaturnClient;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.MinecraftClient;
public class SaturnPlatformBindingsImpl implements SaturnPlatformBindings {
@Override
public String getAccessToken() {
var session = MinecraftClient.getInstance().getSession();
if (session == null) {
return null;
}
return session.getAccessToken();
}
@Override
public UUID getUuid() {
var session = MinecraftClient.getInstance().getSession();
if (session == null) {
return null;
}
return session.getUuidOrNull();
}
@Override
public String getUsername() {
var session = MinecraftClient.getInstance().getSession();
if (session == null) {
return null;
}
return session.getUsername();
}
@Override
public void onClientStopping(Runnable handler) {
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> handler.run());
}
@Override
public void logInfo(String message) {
SaturnClient.LOGGER.info(message);
}
@Override
public void logError(String message) {
SaturnClient.LOGGER.error(message);
}
@Override
public void logError(String message, Throwable throwable) {
SaturnClient.LOGGER.error(message, throwable);
}
}
@@ -5,10 +5,10 @@ import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import org.saturnclient.common.minecraft.MinecraftProvider;
import org.saturnclient.config.Config;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.ui.screens.EmoteWheel;
import org.saturnclient.ui.screens.ShiftMenu;
@@ -37,12 +37,12 @@ public class KeyInputHandler {
// Register the event handler for the main menu key
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (mainMenuKeyBinding.wasPressed()) {
MinecraftProvider.PROVIDER.setScreen(new ShiftMenu());
Providers.saturn.getClient().setScreen(new ShiftMenu());
}
if (Config.openEmoteWheel.wasKeyPressed() && client.currentScreen == null
&& !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) {
MinecraftProvider.PROVIDER.setScreen(new EmoteWheel());
Providers.saturn.getClient().setScreen(new EmoteWheel());
}
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
@@ -1,6 +1,5 @@
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;
@@ -16,6 +15,7 @@ import net.minecraft.util.math.Vec3d;
import org.saturnclient.client.player.Roles;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.saturnclient.SaturnClient;
import java.util.UUID;
@@ -7,8 +7,8 @@ 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.saturnclient.impl.cosmetics.CloakFeatureRenderer;
import org.saturnclient.impl.cosmetics.HatFeatureRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -1,7 +1,7 @@
package org.saturnclient.saturnclient.mixin;
import org.saturnclient.config.Config;
import org.saturnclient.ui.SaturnScreenFabric;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.ui.screens.TitleMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -4,8 +4,8 @@ import java.util.Optional;
import java.util.function.Consumer;
import org.saturnclient.config.Theme;
import org.saturnclient.impl.ui.RenderScopeImpl;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.RenderScopeImpl;
import org.saturnclient.ui.resources.Textures;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
@@ -4,9 +4,9 @@
"version": "${version}",
"name": "saturnclient",
"description": "The ultimate open source client",
"authors": ["Saturn Mc", "ORUS"],
"authors": ["Saturn", "ORUS"],
"contact": {
"website": "https://kleo-dev.netlify.app",
"website": "https://selimaj.dev",
"sources": "https://github.com/saturnclientmc/saturnclient"
},
"license": "Apache-2.0",
@@ -4,9 +4,14 @@
"package": "org.saturnclient.impl.mixins",
"compatibilityLevel": "JAVA_21",
"client": [
"MinecraftClientMixin",
"IdentifierMixin",
"ItemStackMixin",
"MatrixStackMixin",
"WindowMixin"
"MinecraftClientMixin",
"QuaternionfMixin",
"WindowMixin",
"TextMixin",
"SpriteMixin"
],
"injectors": {
"defaultRequire": 1
@@ -1,18 +1,15 @@
{
"required": true,
"minVersion": "0.8",
"package": "org.saturnclient.modules.mixins",
"package": "org.saturnclient.impl.modules.mixins",
"compatibilityLevel": "JAVA_21",
"client": [
"CameraMixin",
"EntityMixin",
"RenderMixin",
"LightmapTextureManagerMixin",
"BackgroundRendererMixin",
"GameRendererMixin",
"NametagsMixin",
"LivingEntityRenderStateMixin",
"LightmapTextureManagerMixin",
"LivingEntityRendererMixin",
"LivingEntityRenderStateMixin",
"NametagsMixin",
"RenderMixin",
"TpsMixin"
],
"injectors": {