Merge pull request #4 from saturnclientmc/updated-cloak

Optimised, added wavy cloaks and added new cloaks
This commit is contained in:
Leo Codes
2025-09-04 18:19:49 +02:00
committed by GitHub
31 changed files with 1202 additions and 666 deletions
@@ -1,11 +1,14 @@
package org.saturnclient.modules; package org.saturnclient.modules;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.modules.mods.*; import org.saturnclient.modules.mods.*;
public class ModManager { public class ModManager {
public static Module[] MODS = { public static final List<Module> ENABLED_MODS = new ArrayList<>();
public static final Module[] ALL_MODS = {
new Crosshair(), new Crosshair(),
new AutoSprint(), new AutoSprint(),
new ArmorDisplay(), new ArmorDisplay(),
@@ -17,12 +20,31 @@ public class ModManager {
}; };
public static void init() { public static void init() {
// Pre-filter enabled modules to avoid checking every tick
updateEnabledModules();
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (Module m : MODS) { // Only iterate over enabled modules
if (m.isEnabled()) { for (Module m : ENABLED_MODS) {
m.tick(); m.tick();
} }
}
}); });
} }
public static void updateEnabledModules() {
ENABLED_MODS.clear();
for (Module m : ALL_MODS) {
if (m.isEnabled()) {
ENABLED_MODS.add(m);
}
}
}
public static Module[] getAllMods() {
return ALL_MODS;
}
public static void refreshEnabledModules() {
updateEnabledModules();
}
} }
@@ -14,7 +14,8 @@ import net.minecraft.item.Items;
public class ArmorDisplay extends Module implements HudMod { public class ArmorDisplay extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false); private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 60); private static ModDimensions dimensions = new ModDimensions(40, 75);
private static Property<Boolean> useMainHand = Property.bool(false);
public ArmorDisplay() { public ArmorDisplay() {
super(new ModuleDetails("Armor Display", "armor") super(new ModuleDetails("Armor Display", "armor")
@@ -22,14 +23,22 @@ public class ArmorDisplay extends Module implements HudMod {
.version("v0.1.0") .version("v0.1.0")
.tags("Utility"), .tags("Utility"),
enabled.named("Enabled"), enabled.named("Enabled"),
useMainHand.named("Use Main Hand"),
dimensions.prop()); dimensions.prop());
} }
public void renderArmor(RenderScope scope, ItemStack helmet, ItemStack chestplate, ItemStack leggings, ItemStack boots) { public void renderArmor(RenderScope scope, ItemStack mainHand, ItemStack helmet, ItemStack chestplate,
ItemStack leggings, ItemStack boots) {
int row = 0; int row = 0;
if (useMainHand.value && !mainHand.isEmpty()) {
scope.drawItem(mainHand, 0, 15 * row);
renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
row++;
}
if (!helmet.isEmpty()) { if (!helmet.isEmpty()) {
scope.drawItem(helmet, 0, 0); scope.drawItem(helmet, 0, 15 * row);
renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage()); renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
row++; row++;
} }
@@ -54,13 +63,15 @@ public class ArmorDisplay extends Module implements HudMod {
public void renderHealth(RenderScope scope, int i, int max_damage, int damage) { public void renderHealth(RenderScope scope, int i, int max_damage, int damage) {
if (max_damage > 0) { if (max_damage > 0) {
scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value, dimensions.fgColor.value); scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value,
dimensions.fgColor.value);
} }
} }
@Override @Override
public void renderHud(RenderScope scope) { public void renderHud(RenderScope scope) {
renderArmor(scope, renderArmor(scope,
SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD), SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST), SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS), SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS),
@@ -69,7 +80,9 @@ public class ArmorDisplay extends Module implements HudMod {
@Override @Override
public void renderDummy(RenderScope scope) { public void renderDummy(RenderScope scope) {
renderArmor(scope, new ItemStack(Items.DIAMOND_HELMET), new ItemStack(Items.DIAMOND_CHESTPLATE), new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS)); renderArmor(scope, new ItemStack(Items.DIAMOND_SWORD), new ItemStack(Items.DIAMOND_HELMET),
new ItemStack(Items.DIAMOND_CHESTPLATE),
new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS));
} }
@Override @Override
@@ -13,6 +13,8 @@ public class Keystrokes extends Module implements HudMod {
public static Property<Boolean> showMouse = Property.bool(true); public static Property<Boolean> showMouse = Property.bool(true);
public static Property<Boolean> showSpace = Property.bool(false); public static Property<Boolean> showSpace = Property.bool(false);
public static ModDimensions dimensions = new ModDimensions(78, 46); public static ModDimensions dimensions = new ModDimensions(78, 46);
// Cache key states to avoid repeated calls
private boolean w = false; private boolean w = false;
private boolean a = false; private boolean a = false;
private boolean s = false; private boolean s = false;
@@ -21,6 +23,10 @@ public class Keystrokes extends Module implements HudMod {
private boolean rmb = false; private boolean rmb = false;
private boolean space = false; private boolean space = false;
// Cache dimensions to avoid recalculation
private int cachedHeight = 54;
private boolean dimensionsDirty = true;
private static Property<Integer> clickBg = Property.color(0xFFCCCCCC); private static Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static Property<Integer> clickFg = Property.color(0xFFFFFFFF); private static Property<Integer> clickFg = Property.color(0xFFFFFFFF);
@@ -42,6 +48,7 @@ public class Keystrokes extends Module implements HudMod {
@Override @Override
public void tick() { public void tick() {
// Cache key states to reduce method calls
w = SaturnClient.client.options.forwardKey.isPressed(); w = SaturnClient.client.options.forwardKey.isPressed();
a = SaturnClient.client.options.leftKey.isPressed(); a = SaturnClient.client.options.leftKey.isPressed();
s = SaturnClient.client.options.backKey.isPressed(); s = SaturnClient.client.options.backKey.isPressed();
@@ -49,33 +56,53 @@ public class Keystrokes extends Module implements HudMod {
lmb = SaturnClient.client.options.attackKey.isPressed(); lmb = SaturnClient.client.options.attackKey.isPressed();
rmb = SaturnClient.client.options.useKey.isPressed(); rmb = SaturnClient.client.options.useKey.isPressed();
space = SaturnClient.client.options.jumpKey.isPressed(); space = SaturnClient.client.options.jumpKey.isPressed();
// Mark dimensions as dirty if mouse or space visibility changed
if (dimensionsDirty) {
updateDimensions();
dimensionsDirty = false;
}
}
private void updateDimensions() {
cachedHeight = 54; // base height with padding considered
if (showMouse.value) {
cachedHeight += 27;
}
if (showSpace.value) {
cachedHeight += 22;
}
dimensions.height = cachedHeight;
} }
@Override @Override
public void renderDummy(RenderScope scope) { public void renderDummy(RenderScope scope) {
dimensions.height = 54; // base height with padding considered // Use cached dimensions
dimensions.height = cachedHeight;
// WASD keys (3px padding between each key) // WASD keys (3px padding between each key)
renderKey(scope, true, 'W', 27, 0); // Top center renderKey(scope, false, 'W', 27, 0); // Top center
renderKey(scope, false, 'A', 0, 27); // Bottom left renderKey(scope, false, 'A', 0, 27); // Bottom left
renderKey(scope, false, 'S', 27, 27); // Bottom center renderKey(scope, false, 'S', 27, 27); // Bottom center
renderKey(scope, false, 'D', 54, 27); // Bottom right renderKey(scope, false, 'D', 54, 27); // Bottom right
if (showMouse.value) { if (showMouse.value) {
renderKeyM(scope, true, "LMB", 0, 54); // Below A/S/D renderKeyM(scope, false, "LMB", 0, 54); // Below A/S/D
renderKeyM(scope, false, "RMB", 40, 54); // With 3px between renderKeyM(scope, false, "RMB", 40, 54); // With 3px between
dimensions.height += 27;
} }
if (showSpace.value) { if (showSpace.value) {
renderKeySpace(scope, false, 0, dimensions.height); renderKeySpace(scope, false, 0, cachedHeight - (showSpace.value ? 22 : 0));
dimensions.height += 22;
} }
} }
@Override @Override
public void renderHud(RenderScope scope) { public void renderHud(RenderScope scope) {
dimensions.height = 54; // base height with padding considered // Use cached dimensions
dimensions.height = cachedHeight;
// WASD keys (3px padding between each key) // WASD keys (3px padding between each key)
renderKey(scope, w, 'W', 27, 0); // Top center renderKey(scope, w, 'W', 27, 0); // Top center
@@ -86,12 +113,10 @@ public class Keystrokes extends Module implements HudMod {
if (showMouse.value) { if (showMouse.value) {
renderKeyM(scope, lmb, "LMB", 0, 54); // Below A/S/D renderKeyM(scope, lmb, "LMB", 0, 54); // Below A/S/D
renderKeyM(scope, rmb, "RMB", 40, 54); // With 3px between renderKeyM(scope, rmb, "RMB", 40, 54); // With 3px between
dimensions.height += 27;
} }
if (showSpace.value) { if (showSpace.value) {
renderKeySpace(scope, space, 0, dimensions.height); renderKeySpace(scope, space, 0, cachedHeight - (showSpace.value ? 22 : 0));
dimensions.height += 22;
} }
} }
@@ -99,27 +124,36 @@ public class Keystrokes extends Module implements HudMod {
private void renderKeySpace(RenderScope scope, boolean isPressed, int x, int y) { private void renderKeySpace(RenderScope scope, boolean isPressed, int x, int y) {
int height = 19; int height = 19;
scope.drawRoundedRectangle(x, y, dimensions.width, height, dimensions.radius.value, isPressed ? clickBg.value : dimensions.bgColor.value); int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, dimensions.width, height, dimensions.radius.value, bgColor);
// A 30-pixel line centered vertically with 3px top padding // A 30-pixel line centered vertically with 3px top padding
int lineY = y + (height - 1) / 2; int lineY = y + (height - 1) / 2;
scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, isPressed ? clickFg.value : dimensions.fgColor.value); scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, fgColor);
} }
private void renderKeyM(RenderScope scope, boolean isPressed, String c, int x, int y) { private void renderKeyM(RenderScope scope, boolean isPressed, String c, int x, int y) {
int width = 38, height = 24; int width = 38, height = 24;
scope.drawRoundedRectangle(x, y, width, height, dimensions.radius.value, isPressed ? clickBg.value : dimensions.bgColor.value); int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, width, height, dimensions.radius.value, bgColor);
// Apply padding: 3px top and left -> add slight offset to x/y // Apply padding: 3px top and left -> add slight offset to x/y
scope.drawText(0.6f, c, x + 9, y + 7, dimensions.font.value, isPressed ? clickFg.value : dimensions.fgColor.value); scope.drawText(0.6f, c, x + 9, y + 7, dimensions.font.value, fgColor);
} }
private void renderKey(RenderScope scope, boolean isPressed, char c, int x, int y) { private void renderKey(RenderScope scope, boolean isPressed, char c, int x, int y) {
int size = 24; int size = 24;
scope.drawRoundedRectangle(x, y, size, size, dimensions.radius.value, isPressed ? clickBg.value : dimensions.bgColor.value); int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, size, size, dimensions.radius.value, bgColor);
// Center the character with 3px top padding // Center the character with 3px top padding
scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, isPressed ? clickFg.value : dimensions.fgColor.value); scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, fgColor);
} }
@Override @Override
@@ -135,5 +169,6 @@ public class Keystrokes extends Module implements HudMod {
@Override @Override
public void setEnabled(boolean e) { public void setEnabled(boolean e) {
enabled.value = e; enabled.value = e;
dimensionsDirty = true;
} }
} }
@@ -29,7 +29,7 @@ public class RenderMixin {
RenderScope renderScope = new RenderScope(context.getMatrices(), RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers()); ((DrawContextAccessor) context).getVertexConsumers());
for (org.saturnclient.modules.Module m : ModManager.MODS) { for (org.saturnclient.modules.Module m : ModManager.ENABLED_MODS) {
if (m instanceof HudMod && m.isEnabled()) { if (m instanceof HudMod && m.isEnabled()) {
ModDimensions dim = ((HudMod) m).getDimensions(); ModDimensions dim = ((HudMod) m).getDimensions();
@@ -10,8 +10,7 @@ import net.minecraft.util.Identifier;
import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.ui2.SaturnScreen; import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.elements.Notification;
import org.saturnclient.ui2.elements.Notification.NotificationKind; import org.saturnclient.ui2.elements.Notification.NotificationKind;
import dev.kosmx.playerAnim.api.layered.AnimationStack; import dev.kosmx.playerAnim.api.layered.AnimationStack;
@@ -28,7 +27,6 @@ public class Auth {
private static Thread pingThread; private static Thread pingThread;
private static volatile boolean running = false; private static volatile boolean running = false;
@SuppressWarnings("resource")
public static boolean authenticate() { public static boolean authenticate() {
// Register shutdown hook ONCE // Register shutdown hook ONCE
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close()); ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close());
@@ -166,15 +164,8 @@ public class Auth {
default: default:
if (parser.error != null) { if (parser.error != null) {
SaturnClient.LOGGER.error("Error from the server: " + parser.error);
if (SaturnClient.client.currentScreen instanceof SaturnScreen) {
String[] a = parser.error.replace("!", "").split(": "); String[] a = parser.error.replace("!", "").split(": ");
((SaturnScreen) SaturnClient.client.currentScreen) Utils.notify(NotificationKind.Error, a[0], a[1]);
.draw(new Notification(SaturnClient.client.currentScreen.width,
SaturnClient.client.currentScreen.height, NotificationKind.Error,
a[0], a[1]));
}
} }
break; break;
} }
@@ -1,223 +1,116 @@
package org.saturnclient.saturnclient.config; package org.saturnclient.saturnclient.config;
import com.google.gson.Gson; import com.google.gson.*;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.HashMap; import java.util.*;
import java.util.LinkedHashMap;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
public class ConfigManager { public class ConfigManager {
private static File configFile = new File( private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json");
SaturnClient.client.runDirectory, private static final Map<String, Map<String, Property<?>>> properties = new HashMap<>();
"saturn.json");
private static Map<String, Map<String, Property<?>>> properties = new HashMap<>();
private static JsonObject cachedThemeJson = null; private static JsonObject cachedThemeJson = null;
private Map<String, Property<?>> currentMap; private final Map<String, Property<?>> currentMap;
private String namespace; private final String namespace;
public ConfigManager(String namespace) { public ConfigManager(String namespace) {
currentMap = new LinkedHashMap<>(); this.namespace = namespace;
this.currentMap = new LinkedHashMap<>();
properties.put(namespace, currentMap); properties.put(namespace, currentMap);
System.out.println("Created namespace: " + namespace); System.out.println("Created namespace: " + namespace);
} }
public ConfigManager(ConfigManager config, String namespace) { public ConfigManager(ConfigManager parent, String namespace) {
currentMap = new LinkedHashMap<>(); this.namespace = namespace;
Property<Map<String, Property<?>>> namespaceProperty = Property.namespace(currentMap); this.currentMap = new LinkedHashMap<>();
config.property(namespace, namespaceProperty); Property<Map<String, Property<?>>> nsProp = Property.namespace(currentMap);
// Update the parent's state parent.property(namespace, nsProp);
config.currentMap.put(namespace, namespaceProperty);
System.out.println("Created sub-namespace: " + namespace); System.out.println("Created sub-namespace: " + namespace);
} }
public void sub(String namespace) { public void sub(String name) {
Property<Map<String, Property<?>>> namespaceProperty = Property.namespace(currentMap); property(name, Property.namespace(new LinkedHashMap<>()));
property(namespace, namespaceProperty);
} }
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> value) { public <T> Property<T> property(String name, Property<T> value) {
currentMap.put(name, value); currentMap.put(name, value);
loadProp(name, value); loadProp(name, value);
return value; return value;
} }
@SuppressWarnings("unchecked") private <T> void loadProp(String name, Property<T> prop) {
public <T> void loadProp(String name, Property<T> prop) { JsonObject source = cachedThemeJson != null ? cachedThemeJson : loadAndCache();
if (cachedThemeJson != null) { if (source == null)
JsonElement element = cachedThemeJson.get(namespace); return;
if (element != null && element.isJsonObject()) {
JsonObject theme = element.getAsJsonObject(); JsonElement namespaceJson = source.get(namespace);
JsonElement value = theme.get(name); if (namespaceJson == null || !namespaceJson.isJsonObject())
if (value != null && prop.matchesJson(value)) { return;
switch (prop.getType()) {
case BOOLEAN: JsonElement value = namespaceJson.getAsJsonObject().get(name);
((Property<Boolean>) prop).setValue(value.getAsBoolean()); if (value != null)
break; prop.loadFromJson(value);
case INTEGER:
((Property<Integer>) prop).setValue(value.getAsInt());
break;
case FLOAT:
((Property<Float>) prop).setValue(value.getAsFloat());
break;
case STRING:
((Property<String>) prop).setValue(value.getAsString());
break;
case HEX:
((Property<Integer>) prop).setValue(value.getAsInt());
break;
default:
break;
} }
private static JsonObject loadAndCache() {
try {
if (!configFile.exists()) {
Files.writeString(configFile.toPath(), "{}");
return new JsonObject();
} }
} String json = Files.readString(configFile.toPath());
} else { cachedThemeJson = JsonParser.parseString(json).getAsJsonObject();
load(); return cachedThemeJson;
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading config file", e);
return null;
} }
} }
public static void load() { public static void load() {
try { SaturnClient.LOGGER.info("Loading config...");
SaturnClient.LOGGER.info("Starting to load config..."); JsonObject json = loadAndCache();
if (!configFile.exists()) { if (json == null)
configFile.createNewFile(); return;
Files.write(configFile.toPath(), "{}".getBytes());
properties.forEach((namespace, map) -> {
JsonElement nsElement = json.get(namespace);
if (nsElement != null && nsElement.isJsonObject()) {
loadProperties(nsElement.getAsJsonObject(), map);
}
});
} }
JsonObject jsonObject = JsonParser.parseString( private static void loadProperties(JsonObject json, Map<String, Property<?>> props) {
new String(Files.readAllBytes(configFile.toPath()))).getAsJsonObject(); props.forEach((name, prop) -> {
JsonElement el = json.get(name);
for (String namespace : properties.keySet()) { if (el != null)
JsonElement configElement = jsonObject.get(namespace); prop.loadFromJson(el);
});
if (configElement == null)
continue;
JsonObject config = jsonObject.get(namespace).getAsJsonObject();
if (config == null) {
continue;
}
Map<String, Property<?>> propertyMap = properties.get(namespace);
loadProperties(config, propertyMap);
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the config file", e);
}
}
@SuppressWarnings("unchecked")
private static void loadProperties(JsonObject config, Map<String, Property<?>> propertyMap) {
for (String propertyName : propertyMap.keySet()) {
JsonElement c = config.get(propertyName);
if (c == null) {
continue;
}
Property<?> p = propertyMap.get(propertyName);
if (p.matchesJson(c)) {
if (p.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedConfig = c.getAsJsonObject();
Map<String, Property<?>> nestedProperties = p.getNamespaceValue();
loadProperties(nestedConfig, nestedProperties);
} else {
if (p.value instanceof Integer) {
((Property<Integer>) p).value = c.getAsInt();
} else if (p.value instanceof String) {
((Property<String>) p).value = c.getAsString();
} else if (p.value instanceof Float) {
((Property<Float>) p).value = c.getAsFloat();
} else if (p.value instanceof Boolean) {
((Property<Boolean>) p).value = c.getAsBoolean();
} else {
SaturnClient.LOGGER.warn(
"Unknown property type for: " + propertyName);
}
}
} else {
SaturnClient.LOGGER.warn(
"Property does not match JSON: " + propertyName);
}
}
} }
public static void save() { public static void save() {
SaturnClient.LOGGER.info("Saving config...");
JsonObject root = new JsonObject();
properties.forEach((namespace, map) -> {
JsonObject nsJson = new JsonObject();
saveProperties(nsJson, map);
root.add(namespace, nsJson);
});
try { try {
SaturnClient.LOGGER.info("Starting to save config..."); String formatted = new GsonBuilder().setPrettyPrinting().create().toJson(root);
Files.writeString(configFile.toPath(), formatted);
JsonObject jsonObject = new JsonObject();
// Iterate through all namespaces and their properties
for (String namespace : properties.keySet()) {
JsonObject namespaceConfig = new JsonObject();
Map<String, Property<?>> propertyMap = properties.get(namespace);
// Save properties for this namespace
saveProperties(namespaceConfig, propertyMap);
// Add the namespace config to the main JSON object
jsonObject.add(namespace, namespaceConfig);
}
// Format the json so it can easily be edited
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String formattedJson = gson.toJson(jsonObject);
// Write the JSON object to the config file
Files.write(configFile.toPath(), formattedJson.getBytes());
SaturnClient.LOGGER.info("Config saved successfully."); SaturnClient.LOGGER.info("Config saved successfully.");
} catch (IOException e) { } catch (IOException e) {
SaturnClient.LOGGER.error("Error saving the config file", e); SaturnClient.LOGGER.error("Error saving config file", e);
} }
} }
@SuppressWarnings("unchecked") private static void saveProperties(JsonObject json, Map<String, Property<?>> props) {
private static void saveProperties(JsonObject config, Map<String, Property<?>> propertyMap) { props.forEach((name, prop) -> json.add(name, prop.toJson()));
// Iterate through each property in the namespace
for (String propertyName : propertyMap.keySet()) {
Property<?> property = propertyMap.get(propertyName);
JsonElement propertyValue = null;
// Convert the property value based on its type
switch (property.getType()) {
case NAMESPACE:
// Handle nested namespace
JsonObject nestedConfig = new JsonObject();
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) property.value;
saveProperties(nestedConfig, nestedProperties);
propertyValue = nestedConfig;
break;
default:
if (property.value instanceof Integer) {
propertyValue = new JsonPrimitive((Integer) property.value);
} else if (property.value instanceof String) {
propertyValue = new JsonPrimitive((String) property.value);
} else if (property.value instanceof Float) {
propertyValue = new JsonPrimitive((Float) property.value);
} else if (property.value instanceof Boolean) {
propertyValue = new JsonPrimitive((Boolean) property.value);
} else {
SaturnClient.LOGGER.warn(
"Unknown property type for: " + propertyName);
}
break;
}
if (propertyValue != null) {
config.add(propertyName, propertyValue);
}
}
} }
public Map<String, Property<?>> getProperties() { public Map<String, Property<?>> getProperties() {
@@ -6,6 +6,7 @@ import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
public class Property<T> { public class Property<T> {
@@ -25,7 +26,7 @@ public class Property<T> {
public boolean isReset = false; public boolean isReset = false;
private T defaultValue; private T defaultValue;
private String[] availableValues; private String[] availableValues;
private PropertyType type; private final PropertyType type;
private boolean wasPressedLastTick = false; private boolean wasPressedLastTick = false;
private Property(T value, PropertyType type) { private Property(T value, PropertyType type) {
@@ -34,25 +35,20 @@ public class Property<T> {
this.type = type; this.type = type;
} }
// ---------- Factory Methods ----------
public static <T> Property<T> from(T value) { public static <T> Property<T> from(T value) {
if (value instanceof Boolean) { if (value instanceof Boolean)
return new Property<>(value, PropertyType.BOOLEAN); return new Property<>(value, PropertyType.BOOLEAN);
} else if (value instanceof Integer) { if (value instanceof Integer)
return new Property<>(value, PropertyType.INTEGER); return new Property<>(value, PropertyType.INTEGER);
} else if (value instanceof Float) { if (value instanceof Float)
return new Property<>(value, PropertyType.FLOAT); return new Property<>(value, PropertyType.FLOAT);
} else if (value instanceof String) { if (value instanceof String)
return new Property<>(value, PropertyType.STRING); return new Property<>(value, PropertyType.STRING);
} else if (valueIsNamespace(value)) { if (valueIsNamespace(value))
return new Property<>(value, PropertyType.NAMESPACE); return new Property<>(value, PropertyType.NAMESPACE);
} else {
return null; return null;
} }
}
public Property<T> copy() {
return new Property<>(this.value, this.type);
}
public static Property<Integer> font(int value) { public static Property<Integer> font(int value) {
return select(value, "Default", "Inter", "Inter bold"); return select(value, "Default", "Inter", "Inter bold");
@@ -92,55 +88,37 @@ public class Property<T> {
return new Property<>(value, PropertyType.KEYBINDING); return new Property<>(value, PropertyType.KEYBINDING);
} }
// ---------- Select Helpers ----------
public void next() { public void next() {
if ((Integer) value < availableValues.length - 1) if (type == PropertyType.SELECT) {
setValue((Integer) value + 1); int i = (Integer) value;
else setValue((i < availableValues.length - 1) ? i + 1 : 0);
setValue(0); }
} }
public void prev() { public void prev() {
if ((Integer) value > 0) if (type == PropertyType.SELECT) {
setValue((Integer) value - 1); int i = (Integer) value;
else setValue((i > 0) ? i - 1 : availableValues.length - 1);
setValue(availableValues.length - 1); }
} }
public void setSelection(int selection) { public void setSelection(int selection) {
if (selection >= 0 && selection < availableValues.length && type == PropertyType.SELECT) { if (type == PropertyType.SELECT && selection >= 0 && selection < availableValues.length) {
setValue(selection); setValue(selection);
} }
} }
public String getSelection() { public String getSelection() {
if (type == PropertyType.SELECT) { return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null;
return availableValues[(Integer) value];
} else {
return null;
}
} }
// ---------- Lifecycle ----------
public void reset() { public void reset() {
value = defaultValue; value = defaultValue;
isReset = true; isReset = true;
} }
public int getHexString() {
if (value instanceof String) {
String str = (String) value;
if (str.startsWith("0x") || str.startsWith("0X")) {
try {
return Integer.parseInt(str.substring(2), 16);
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Invalid hexadecimal integer format");
}
}
}
throw new IllegalStateException(
"Property does not contain a valid hex integer");
}
public PropertyType getType() { public PropertyType getType() {
return type; return type;
} }
@@ -153,34 +131,93 @@ public class Property<T> {
throw new IllegalStateException("Property is not a namespace"); throw new IllegalStateException("Property is not a namespace");
} }
public boolean matchesJson(JsonElement element) { // ---------- JSON Serialization ----------
if (element.isJsonPrimitive()) { /*
JsonPrimitive primitive = element.getAsJsonPrimitive(); * BOOLEAN,
if (value instanceof Boolean && primitive.isBoolean()) * INTEGER,
return true; * FLOAT,
if (value instanceof Integer && primitive.isNumber()) * STRING,
return true; * HEX,
if (value instanceof Float && primitive.isNumber()) * NAMESPACE,
return true; * SELECT,
if (value instanceof String && primitive.isString()) * KEYBINDING,
return true; */
if (type == PropertyType.HEX && primitive.isString()) public JsonElement toJson() {
return true; switch (type) {
} else if (element.isJsonObject()) { case BOOLEAN:
return isNamespace(value); return new JsonPrimitive((Boolean) value);
case INTEGER:
return new JsonPrimitive((Integer) value);
case FLOAT:
return new JsonPrimitive((Float) value);
case NAMESPACE:
JsonObject nested = new JsonObject();
getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson()));
return nested;
case HEX:
return new JsonPrimitive(String.format("#%08X", (Integer) value));
case KEYBINDING:
return new JsonPrimitive((Integer) value);
case SELECT:
return new JsonPrimitive((Integer) value);
default:
return new JsonPrimitive(String.valueOf(value));
} }
return false;
} }
public void loadFromJson(JsonElement element) {
if (element == null)
return;
switch (type) {
case BOOLEAN:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean())
setValue(element.getAsBoolean());
break;
case INTEGER:
if (element.isJsonPrimitive()) {
JsonPrimitive p = element.getAsJsonPrimitive();
if (p.isNumber())
setValue(p.getAsInt());
else if (p.isString())
setValue(parseHexToInt(p.getAsString()));
}
break;
case FLOAT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsFloat());
break;
case STRING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(element.getAsString());
break;
case HEX:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(parseHexToInt(element.getAsString()));
break;
case NAMESPACE:
if (element.isJsonObject()) {
JsonObject obj = element.getAsJsonObject();
getNamespaceValue().forEach((k, v) -> v.loadFromJson(obj.get(k)));
}
break;
case SELECT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setSelection(element.getAsInt());
break;
case KEYBINDING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsInt());
break;
}
}
// ---------- Utility ----------
@Override @Override
public String toString() { public String toString() {
return String.valueOf(value); return String.valueOf(value);
} }
public boolean isNamespace(Object obj) {
return type == PropertyType.NAMESPACE && value instanceof Map;
}
public static boolean valueIsNamespace(Object obj) { public static boolean valueIsNamespace(Object obj) {
return obj instanceof Map; return obj instanceof Map;
} }
@@ -200,59 +237,44 @@ public class Property<T> {
return new NamedProperty<>(name, this); return new NamedProperty<>(name, this);
} }
// ---------- Keybindings ----------
public boolean isKeyPressed() { public boolean isKeyPressed() {
return (Integer) value == -1 ? false return (Integer) value != -1 &&
: GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS &&
(Integer) (Object) value) == GLFW.GLFW_PRESS && SaturnClient.client.currentScreen == null; SaturnClient.client.currentScreen == null;
} }
public boolean wasKeyPressed() { public boolean wasKeyPressed() {
boolean isPressed = this.isKeyPressed(); boolean pressed = isKeyPressed();
wasPressedLastTick = isPressed && !wasPressedLastTick; boolean result = pressed && !wasPressedLastTick;
return wasPressedLastTick; wasPressedLastTick = pressed;
} return result;
@SuppressWarnings("unchecked")
public void load(String name, JsonElement element) {
if (element != null && element.isJsonObject()) {
JsonElement value = element.getAsJsonObject().get(name);
if (value != null) {
switch (this.getType()) {
case BOOLEAN:
((Property<Boolean>) this).setValue(value.getAsBoolean());
break;
case INTEGER:
JsonPrimitive primitive = value.getAsJsonPrimitive();
if (primitive.isNumber())
((Property<Integer>) this).setValue(value.getAsInt());
else if (primitive.isString())
((Property<Integer>) this).setValue(parseHexToInt(value.getAsString()));
break;
case FLOAT:
((Property<Float>) this).setValue(value.getAsFloat());
break;
case STRING:
((Property<String>) this).setValue(value.getAsString());
break;
case HEX:
((Property<Integer>) this).setValue(parseHexToInt(value.getAsString()));
break;
default:
break;
}
}
}
} }
// ---------- HEX ----------
public static int parseHexToInt(String hex) { public static int parseHexToInt(String hex) {
hex = hex.replaceAll("#", ""); hex = hex.replace("#", "");
if (hex.length() == 6)
if (hex.length() == 6) {
hex = "FF" + hex; hex = "FF" + hex;
} else if (hex.length() != 8) { if (hex.length() != 8)
throw new IllegalArgumentException("Hex string must be 6 or 8 characters long"); throw new IllegalArgumentException("Hex must be 6 or 8 chars long, got \'" + hex + "\'");
}
return (int) Long.parseLong(hex, 16); return (int) Long.parseLong(hex, 16);
} }
// Misc
public Property<T> copy() {
return new Property<>(this.value, this.type);
}
public void load(String name, JsonElement element) {
if (element != null && element.isJsonObject()) {
load(name, element.getAsJsonObject());
}
}
public void load(String name, JsonObject element) {
JsonElement value = element.get(name);
if (value != null)
this.loadFromJson(value);
}
} }
@@ -12,10 +12,6 @@ import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType; import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
import net.minecraft.client.render.entity.feature.FeatureRenderer; import net.minecraft.client.render.entity.feature.FeatureRenderer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext; import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.model.BipedEntityModel;
import net.minecraft.client.render.entity.model.EntityModelLayers;
import net.minecraft.client.render.entity.model.LoadedEntityModels;
import net.minecraft.client.render.entity.model.PlayerCapeModel;
import net.minecraft.client.render.entity.model.PlayerEntityModel; import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState; import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.SkinTextures; import net.minecraft.client.util.SkinTextures;
@@ -26,16 +22,17 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.equipment.EquipmentAsset; import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey; import net.minecraft.registry.RegistryKey;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.Vec3d;
public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> { public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
private final BipedEntityModel<PlayerEntityRenderState> model;
private final EquipmentModelLoader equipmentModelLoader; private final EquipmentModelLoader equipmentModelLoader;
private static final int PARTS = 16;
private float currentVelocity = 0.0f;
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context, public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context,
LoadedEntityModels modelLoader, EquipmentModelLoader equipmentModelLoader) { EquipmentModelLoader equipmentModelLoader) {
super(context); super(context);
this.model = new PlayerCapeModel<PlayerEntityRenderState>(
modelLoader.getModelPart(EntityModelLayers.PLAYER_CAPE));
this.equipmentModelLoader = equipmentModelLoader; this.equipmentModelLoader = equipmentModelLoader;
} }
@@ -50,7 +47,220 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
} }
} }
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, // private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack
// matrixStack,
// float x1, float y1, float z1, float x2, float y2, float z2,
// float u1, float v1, float u2, float v2, int light, int overlay,
// float normalX, float normalY, float normalZ, boolean flipWinding) {
// // Create the four corner vectors from the provided x, y, z coordinates
// Vec3d bottomLeft = new Vec3d(x1, y2, z1);
// Vec3d bottomRight = new Vec3d(x2, y2, z2);
// Vec3d topRight = new Vec3d(x2, y1, z2);
// Vec3d topLeft = new Vec3d(x1, y1, z1);
// // Call the more detailed render method with the newly created vectors
// renderCapeQuad(vertexConsumer, matrixStack,
// bottomLeft, bottomRight, topRight, topLeft,
// u1, v1, u2, v2, light, overlay,
// normalX, normalY, normalZ, flipWinding);
// }
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
float u1, float v1, float u2, float v2, int light, int overlay,
float normalX, float normalY, float normalZ, boolean flipWinding) {
MatrixStack.Entry entry = matrixStack.peek();
if (!flipWinding) {
// Counter-clockwise winding (front face)
// Vertex 1 (bottom-left)
vertexConsumer
.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y, (float) bottomLeft.z)
.color(255, 255, 255, 255)
.texture(u1, v2)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
// Vertex 2 (bottom-right)
vertexConsumer
.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
(float) bottomRight.z)
.color(255, 255, 255, 255)
.texture(u2, v2)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
// Vertex 3 (top-right)
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y, (float) topRight.z)
.color(255, 255, 255, 255)
.texture(u2, v1)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
// Vertex 4 (top-left)
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y, (float) topLeft.z)
.color(255, 255, 255, 255)
.texture(u1, v1)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
} else {
// Clockwise winding (back face) - reverse vertex order
// Vertex 4 (top-left)
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y, (float) topLeft.z)
.color(255, 255, 255, 255)
.texture(u1, v1)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
// Vertex 3 (top-right)
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y, (float) topRight.z)
.color(255, 255, 255, 255)
.texture(u2, v1)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
// Vertex 2 (bottom-right)
vertexConsumer
.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
(float) bottomRight.z)
.color(255, 255, 255, 255)
.texture(u2, v2)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
// Vertex 1 (bottom-left)
vertexConsumer
.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y, (float) bottomLeft.z)
.color(255, 255, 255, 255)
.texture(u1, v2)
.overlay(overlay)
.light(light)
.normal(normalX, normalY, normalZ);
}
}
private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer,
PlayerEntityRenderState playerState, int light, int overlay, float rotation, float curveMagnitude) {
float capeWidth = 10.0f / 16.0f; // 10 pixels wide
float capeHeight = 16.0f / 16.0f; // 16 pixels tall
float capeDepth = 1.0f / 16.0f; // 1 pixel deep
float capePartHeight = capeHeight / PARTS;
matrixStack.push();
matrixStack.translate(0.0f, 1.25, 0.125f);
matrixStack.translate(0, -capeHeight, 0);
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(rotation));
matrixStack.translate(0, capeHeight, 0);
float x1 = -capeWidth / 2.0f;
float x2 = capeWidth / 2.0f;
float y2 = -capeHeight;
float z1 = 0.0f;
float z2 = capeDepth;
float frontU1 = 1.0f / 64.0f; // Start at pixel 1
float frontU2 = 11.0f / 64.0f; // End at pixel 11 (10 pixels wide)
float frontV1 = 1.0f / 32.0f; // Start at pixel 1
float frontV2 = 17.0f / 32.0f; // End at pixel 17 (16 pixels tall)
// Back face UV (inside of cape) - typically offset in cape texture
float backU1 = 12.0f / 64.0f; // Back texture starts after front
float backU2 = 22.0f / 64.0f; // 10 pixels wide
float backV2 = 17.0f / 32.0f;
// Top face UV (edge along shoulders)
float topU1 = 1.0f / 64.0f; // Start at pixel 1
float topU2 = 11.0f / 64.0f; // End at pixel 11
float topV1 = 0.0f / 32.0f; // Start at pixel 0
float topV2 = 1.0f / 32.0f; // End at pixel 1
// Bottom face UV (lower edge of cape)
float bottomU1 = 11.0f / 64.0f; // Start at pixel 11
float bottomU2 = 22.0f / 64.0f; // End at pixel 22
float bottomV1 = 0.0f / 32.0f; // Start at pixel 0
float bottomV2 = 1.0f / 32.0f; // End at pixel 1
for (int i = 0; i < PARTS; i++) {
float y3 = -(capePartHeight * i);
float y4 = -(capePartHeight * (i + 1));
float totalVHeight = frontV2 - frontV1;
float partVHeight = totalVHeight / PARTS;
float curveOffsetTop = (float) Math.pow((double) (PARTS - i) / PARTS, 2) * curveMagnitude;
float curveOffsetBottom = (float) Math.pow((double) (PARTS - i - 1) / PARTS, 2) * curveMagnitude;
// FRONT FACE
this.renderCapeQuad(vertexConsumer, matrixStack,
new Vec3d(x2, y4, z2 + curveOffsetBottom), new Vec3d(x1, y4, z2 +
curveOffsetBottom),
new Vec3d(x1, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z2 +
curveOffsetTop),
frontU1, frontV2 - (partVHeight * i), frontU2, frontV2 - (partVHeight * (i +
1)),
light, overlay,
0.0f, 0.0f, 1.0f, false);
// BACK FACE
this.renderCapeQuad(vertexConsumer, matrixStack,
new Vec3d(x1, y4, z1 + curveOffsetBottom), new Vec3d(x2, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z1 + curveOffsetTop),
backU1, backV2 - (partVHeight * i), backU2, backV2 - (partVHeight * (i + 1)),
light, overlay,
0.0f, 0.0f, 1.0f, false);
// LEFT FACE
this.renderCapeQuad(vertexConsumer, matrixStack,
new Vec3d(x2, y4, z1 + curveOffsetBottom), new Vec3d(x2, y4, z2 + curveOffsetBottom),
new Vec3d(x2, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z1 + curveOffsetTop),
0.0f, frontV2 - (partVHeight * i), 1.0f / 64.0f, frontV2 - (partVHeight * (i + 1)),
light, overlay,
1.0f, 0.0f, 0.0f, false);
// RIGHT FACE
this.renderCapeQuad(vertexConsumer, matrixStack,
new Vec3d(x1, y4, z2 + curveOffsetBottom), new Vec3d(x1, y4, z1 + curveOffsetBottom),
new Vec3d(x1, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z2 + curveOffsetTop),
frontU2, frontV2 - (partVHeight * i), frontU2 + 1.0f / 64.0f, frontV2 - (partVHeight * (i + 1)),
light, overlay,
-1.0f, 0.0f, 0.0f, false);
}
// TOP FACE
matrixStack.push();
matrixStack.translate(0.0f, y2 + capeDepth, 0.0f);
matrixStack.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(90.0f));
matrixStack.translate(0.0f, -y2, 0.0f);
this.renderCapeQuad(vertexConsumer, matrixStack,
new Vec3d(x2, y2 + capeDepth, z2), new Vec3d(x1, y2 + capeDepth, z2), new Vec3d(x1, y2, z2),
new Vec3d(x2, y2, z2),
topU1, topV1, topU2, topV2,
light, overlay,
0.0f, 1.0f, 0.0f, true);
matrixStack.pop();
// BOTTOM FACE
float curveOffsetTop = (float) Math.pow(1.0f, 2) * curveMagnitude;
float curveOffsetBottom = (float) Math.pow(1.0f, 2) * curveMagnitude;
this.renderCapeQuad(vertexConsumer, matrixStack,
new Vec3d(x2, 0.0f, z2 + curveOffsetBottom), new Vec3d(x1, 0.0f, z2 + curveOffsetBottom),
new Vec3d(x1, 0.0f, z1 + curveOffsetTop), new Vec3d(x2, 0.0f, z1 + curveOffsetTop),
bottomU1, bottomV1, bottomU2, bottomV2,
light, overlay,
0.0f, 0.0f, 1.0f, false);
matrixStack.pop();
}
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
PlayerEntityRenderState playerEntityRenderState, float f, float g) { PlayerEntityRenderState playerEntityRenderState, float f, float g) {
if (!playerEntityRenderState.invisible && playerEntityRenderState.capeVisible) { if (!playerEntityRenderState.invisible && playerEntityRenderState.capeVisible) {
SkinTextures skinTextures = playerEntityRenderState.skinTextures; SkinTextures skinTextures = playerEntityRenderState.skinTextures;
@@ -66,16 +276,34 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
if (customCape != null if (customCape != null
&& !this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) { && !this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
float targetVelocity = ((6.0F + playerEntityRenderState.field_53537 / 2.0F
+ playerEntityRenderState.field_53536) * 0.02f);
targetVelocity = Math.max(0.0f, targetVelocity - 0.1f);
float accelerationRate = 0.02f;
this.currentVelocity = this.currentVelocity
+ (targetVelocity - this.currentVelocity) * accelerationRate;
float rotation = this.currentVelocity * 82.0f;
float curve = this.currentVelocity * 0.8f;
int minBrightness = 7;
int blockLight = (light >> 4) & 0xF;
int skyLight = (light >> 20) & 0xF;
blockLight = Math.max(blockLight, minBrightness);
skyLight = Math.max(skyLight, minBrightness);
light = (skyLight << 20) | (blockLight << 4);
matrixStack.push(); matrixStack.push();
matrixStack.translate(0.0f, -0.25f, 0.0f);
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) { if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) {
matrixStack.translate(0.0F, -0.053125F, 0.06875F); matrixStack.translate(0.0F, -0.053125F, 0.06875F);
} }
VertexConsumer vertexConsumer = vertexConsumerProvider VertexConsumer vertexConsumer = vertexConsumerProvider
.getBuffer(RenderLayer.getEntityAlpha(customCape)); .getBuffer(RenderLayer.getEntityAlpha(customCape));
((PlayerEntityModel) this.getContextModel()).copyTransforms(this.model); renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV,
this.model.setAngles(playerEntityRenderState); Math.min(rotation, 70.0f), Math.min(curve, 0.46f));
this.model.render(matrixStack, vertexConsumer, 0xF000F0, OverlayTexture.DEFAULT_UV);
matrixStack.pop(); matrixStack.pop();
} }
} }
@@ -11,7 +11,11 @@ import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
/** /**
@@ -20,8 +24,10 @@ import org.saturnclient.saturnclient.SaturnClient;
* Originally created by IIpho3nix and modified for Saturn Client by leo. * Originally created by IIpho3nix and modified for Saturn Client by leo.
*/ */
public class Cloaks { public class Cloaks {
public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas", "ferrari", "redbull" }; public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas",
private static final String[] ANIMATED_CLOAKS = { "glitch" }; "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/"; private static final String CLOAKS_RESOURCE_PATH = "assets/saturnclient/textures/cloaks/";
public static final List<String> availableCloaks = new ArrayList<>(); public static final List<String> availableCloaks = new ArrayList<>();
@@ -29,6 +35,17 @@ public class Cloaks {
public static final Map<String, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>(); public static final Map<String, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>();
private static final Map<String, Long> lastFrameTime = new ConcurrentHashMap<>(); private static final Map<String, 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. * Initializes the cloak system.
* Loads cloak textures from resources. * Loads cloak textures from resources.
@@ -87,52 +104,13 @@ public class Cloaks {
if (player != null && player.cloak != null) { if (player != null && player.cloak != null) {
if (!player.cloak.isEmpty()) { if (!player.cloak.isEmpty()) {
if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) { if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) {
// Load animated cloak data in background thread loadAnimatedCloakAsync(uuid, player.cloak)
new Thread(() -> { .exceptionally(throwable -> {
try { SaturnClient.LOGGER.error("Failed to load animated cloak for player: " + player.cloak,
String fileName = player.cloak + ".gif"; throwable);
String resourcePath = CLOAKS_RESOURCE_PATH + fileName; return null;
InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath);
if (inputStream != null) {
byte[] data = inputStream.readAllBytes();
GifDecoder.GifImage gif = GifDecoder.read(data);
List<BufferedImage> frames = new ArrayList<>();
List<Integer> delays = new ArrayList<>();
int frameCount = gif.getFrameCount();
for (int i = 0; i < frameCount; i++) {
frames.add(gif.getFrame(i));
delays.add(gif.getDelay(i) * 10);
}
// Register textures on main thread
SaturnClient.client.execute(() -> {
try {
List<AnimatedCloakData> animatedFrames = new ArrayList<>();
for (int i = 0; i < frameCount; i++) {
String frameId = fileName.replace(".gif", "") + "_frame_" + i;
Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID,
"cloaks_" + frameId);
IdentifierUtils.registerBufferedImageTexture(frameIdentifier,
frames.get(i));
animatedFrames.add(new AnimatedCloakData(frameIdentifier, delays.get(i)));
}
animatedCloaks.put(uuid, animatedFrames);
lastFrameTime.put(uuid, System.currentTimeMillis());
SaturnClient.LOGGER.info(
"Loaded " + frames.size() + " frames for animated cloak: " + fileName);
} catch (Exception e) {
SaturnClient.LOGGER
.error("Failed to register animated cloak textures: " + fileName, e);
}
}); });
}
} catch (IOException e) {
SaturnClient.LOGGER
.error("Failed to load animated cloak from resources: " + player.cloak + ".gif", e);
}
}).start();
} else { } else {
SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png")); SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png"));
} }
@@ -168,6 +146,86 @@ public class Cloaks {
} }
} }
public static CompletableFuture<Void> loadAnimatedCloakAsync(String 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(String uuid) { public static Identifier getCurrentCloakTexture(String uuid) {
if (!Auth.players.containsKey(uuid)) { if (!Auth.players.containsKey(uuid)) {
return null; return null;
@@ -204,7 +262,13 @@ public class Cloaks {
currentFrame = 0; currentFrame = 0;
} }
return frames.get(currentFrame).getTextureId(); Identifier identifier = frames.get(currentFrame).getTextureId();
if (identifier == null) {
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
}
return identifier;
} else { } else {
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png"); return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
} }
@@ -6,9 +6,10 @@ import java.awt.image.DataBufferInt;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import static java.lang.System.arraycopy; import java.util.concurrent.locks.ReentrantReadWriteLock;
/* /*
* Copyright 2014 Dhyan Blum * Copyright 2014 Dhyan Blum
@@ -28,7 +29,7 @@ import static java.lang.System.arraycopy;
/** /**
* <p> * <p>
* A decoder capable of processing a GIF data stream to render the graphics * A thread-safe decoder capable of processing a GIF data stream to render the graphics
* contained in it. This implementation follows the official * contained in it. This implementation follows the official
* <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF * <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF
* specification</A>. * specification</A>.
@@ -54,9 +55,51 @@ import static java.lang.System.arraycopy;
* </p> * </p>
* *
* @author Dhyan Blum * @author Dhyan Blum
* @version 1.09 November 2017 * @version 1.09 November 2017 (Thread-safe version)
*/ */
public final class GifDecoder { 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 { static final class BitReader {
private int nextBitToRead; private int nextBitToRead;
private int numberOfBitsToRead; private int numberOfBitsToRead;
@@ -71,14 +114,16 @@ public final class GifDecoder {
private int read() { private int read() {
// Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2 // Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8 final int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8
int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8 final int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8
int byte0, byte1, byte2;
byte0 = bytes[byteIndex++] & 0xFF; // & 0xFF gives us the unsigned values // Use int operations to avoid repeated array access
byte1 = bytes[byteIndex++] & 0xFF; final int byte0 = bytes[byteIndex] & 0xFF;
byte2 = bytes[byteIndex] & 0xFF; final int byte1 = bytes[byteIndex + 1] & 0xFF;
// Glue the bytes together, don't do more shifting than necessary final int byte2 = bytes[byteIndex + 2] & 0xFF;
int buffer = ((byte2 << 8 | byte1) << 8 | byte0) >>> bitsToShiftRight;
// Optimized bit manipulation
final int buffer = ((byte2 << 16) | (byte1 << 8) | byte0) >>> bitsToShiftRight;
nextBitToRead += numberOfBitsToRead; nextBitToRead += numberOfBitsToRead;
return buffer & bitMask; // Kill the unwanted higher bits return buffer & bitMask; // Kill the unwanted higher bits
} }
@@ -100,7 +145,7 @@ public final class GifDecoder {
private BitReader bitReader; // Notify when code sizes increases private BitReader bitReader; // Notify when code sizes increases
public CodeTable() { public CodeTable() {
table = new int[4096][1]; table = new int[4096][];
} }
private int add(final int[] indices) { private int add(final int[] indices) {
@@ -130,14 +175,18 @@ public final class GifDecoder {
initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1 initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1
initTableSize = fr.endOfInfoCode + 1; initTableSize = fr.endOfInfoCode + 1;
nextCode = initTableSize; nextCode = initTableSize;
for (int c = numColors - 1; c >= 0; c--) {
table[c][0] = activeColTbl[c]; // Translated color // Initialize single color entries
} // A gap may follow with no colors assigned if numCols < CLEAR 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.clearCode] = new int[] { fr.clearCode }; // CLEAR
table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
// Locate transparent color in code table and set to 0 // Locate transparent color in code table and set to 0
if (fr.transpColFlag && fr.transpColIndex < numColors) { if (fr.transpColFlag && fr.transpColIndex < numColors) {
table[fr.transpColIndex][0] = 0; table[fr.transpColIndex] = new int[]{0};
} }
} }
} }
@@ -145,73 +194,87 @@ public final class GifDecoder {
final class GifFrame { final class GifFrame {
// Graphic control extension (optional) // Graphic control extension (optional)
// Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV // Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
private int disposalMethod; // 0-3 as above, 4-7 undefined private volatile int disposalMethod; // 0-3 as above, 4-7 undefined
private boolean transpColFlag; // 1 Bit private volatile boolean transpColFlag; // 1 Bit
private int delay; // Unsigned, LSByte first, n * 1/100 * s private volatile int delay; // Unsigned, LSByte first, n * 1/100 * s
private int transpColIndex; // 1 Byte private volatile int transpColIndex; // 1 Byte
// Image descriptor // Image descriptor
private int x; // Position on the canvas from the left private volatile int x; // Position on the canvas from the left
private int y; // Position on the canvas from the top private volatile int y; // Position on the canvas from the top
private int w; // May be smaller than the base image private volatile int w; // May be smaller than the base image
private int h; // May be smaller than the base image private volatile int h; // May be smaller than the base image
private int wh; // width * height private volatile int wh; // width * height
private boolean hasLocColTbl; // Has local color table? 1 Bit private volatile boolean hasLocColTbl; // Has local color table? 1 Bit
private boolean interlaceFlag; // Is an interlace image? 1 Bit private volatile boolean interlaceFlag; // Is an interlace image? 1 Bit
@SuppressWarnings("unused") @SuppressWarnings("unused")
private boolean sortFlag; // True if local colors are sorted, 1 Bit private volatile boolean sortFlag; // True if local colors are sorted, 1 Bit
private int sizeOfLocColTbl; // Size of the local color table, 3 Bits private volatile int sizeOfLocColTbl; // Size of the local color table, 3 Bits
private int[] localColTbl; // Local color table (optional) private volatile int[] localColTbl; // Local color table (optional)
// Image data // Image data
private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI private volatile int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
private int clearCode; private volatile int clearCode;
private int endOfInfoCode; private volatile int endOfInfoCode;
private byte[] data; // Holds LZW encoded data private volatile byte[] data; // Holds LZW encoded data
private BufferedImage img; // Full drawn image, not just the frame area 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 final class GifImage {
public String header; // Bytes 0-5, GIF87a or GIF89a public volatile String header; // Bytes 0-5, GIF87a or GIF89a
private int w; // Unsigned 16 Bit, the least significant byte first private volatile int w; // Unsigned 16 Bit, the least significant byte first
private int h; // Unsigned 16 Bit, the least significant byte first private volatile int h; // Unsigned 16 Bit, the least significant byte first
private int wh; // Image width * image height private volatile int wh; // Image width * image height
public boolean hasGlobColTbl; // 1 Bit public volatile boolean hasGlobColTbl; // 1 Bit
public int colorResolution; // 3 Bits public volatile int colorResolution; // 3 Bits
public boolean sortFlag; // True if global colors are sorted, 1 Bit public volatile boolean sortFlag; // True if global colors are sorted, 1 Bit
public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec public volatile int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
public int bgColIndex; // Background color index, 1 Byte public volatile int bgColIndex; // Background color index, 1 Byte
public int pxAspectRatio; // Pixel aspect ratio, 1 Byte public volatile int pxAspectRatio; // Pixel aspect ratio, 1 Byte
public int[] globalColTbl; // Global color table public volatile int[] globalColTbl; // Global color table
private final List<GifFrame> frames = new ArrayList<GifFrame>(64); private final List<GifFrame> frames = new ArrayList<>(64);
public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE" public volatile String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0" public volatile String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
public int repetitions = 0; // 0: infinite loop, N: number of loops public volatile int repetitions = 0; // 0: infinite loop, N: number of loops
private BufferedImage img = null; // Currently, drawn frame
private final BitReader bits = new BitReader(); // Thread-safe frame rendering state
private final CodeTable codes = new CodeTable(); private volatile BufferedImage img = null; // Currently, drawn frame
private Graphics2D g; 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) { private int[] decode(final GifFrame fr, final int[] activeColTbl) {
codes.init(fr, activeColTbl, bits); final DecodingContext ctx = DECODING_CONTEXT.get();
bits.init(fr.data); // Incoming codes ctx.codeTable.init(fr, activeColTbl, ctx.bitReader);
ctx.bitReader.init(fr.data); // Incoming codes
final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode; final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
final int[] out = new int[wh]; // Target image pixel array
final int[][] tbl = codes.table; // Code table // 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 int outPos = 0; // Next pixel position in the output image array
codes.clear(); // Init code table ctx.codeTable.clear(); // Init code table
bits.read(); // Skip leading clear code ctx.bitReader.read(); // Skip leading clear code
int code = bits.read(); // Read first code int code = ctx.bitReader.read(); // Read first code
int[] pixels = tbl[code]; // Output pixel for first code int[] pixels = tbl[code]; // Output pixel for first code
arraycopy(pixels, 0, out, outPos, pixels.length);
// Use System.arraycopy for better performance
System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length; outPos += pixels.length;
try { try {
while (true) { while (true) {
final int prevCode = code; final int prevCode = code;
code = bits.read(); // Get next code in stream code = ctx.bitReader.read(); // Get next code in stream
if (code == clearCode) { // After a CLEAR table, there is if (code == clearCode) { // After a CLEAR table, there is
codes.clear(); // no previous code, we need to read ctx.codeTable.clear(); // no previous code, we need to read
code = bits.read(); // a new one code = ctx.bitReader.read(); // a new one
pixels = tbl[code]; // Output pixels pixels = tbl[code]; // Output pixels
arraycopy(pixels, 0, out, outPos, pixels.length); System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length; outPos += pixels.length;
continue; // Back to the loop with a valid previous code continue; // Back to the loop with a valid previous code
} else if (code == endCode) { } else if (code == endCode) {
@@ -219,84 +282,96 @@ public final class GifDecoder {
} }
final int[] prevVals = tbl[prevCode]; final int[] prevVals = tbl[prevCode];
final int[] prevValsAndK = new int[prevVals.length + 1]; final int[] prevValsAndK = new int[prevVals.length + 1];
arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length); System.arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
if (code < codes.nextCode) { // Code table contains code if (code < ctx.codeTable.nextCode) { // Code table contains code
pixels = tbl[code]; // Output pixels pixels = tbl[code]; // Output pixels
arraycopy(pixels, 0, out, outPos, pixels.length); System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length; outPos += pixels.length;
prevValsAndK[prevVals.length] = tbl[code][0]; // K prevValsAndK[prevVals.length] = tbl[code][0]; // K
} else { } else {
prevValsAndK[prevVals.length] = prevVals[0]; // K prevValsAndK[prevVals.length] = prevVals[0]; // K
arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length); System.arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
outPos += prevValsAndK.length; outPos += prevValsAndK.length;
} }
codes.add(prevValsAndK); // Previous indices + K ctx.codeTable.add(prevValsAndK); // Previous indices + K
} }
} catch (final ArrayIndexOutOfBoundsException ignored) { } catch (final ArrayIndexOutOfBoundsException ignored) {
} }
return out;
// Return a properly sized array
return Arrays.copyOf(out, outPos);
} }
private int[] deinterlace(final int[] src, final GifFrame fr) { private int[] deinterlace(final int[] src, final GifFrame fr) {
final int w = fr.w, h = fr.h, wh = fr.wh; final int w = fr.w, h = fr.h, wh = fr.wh;
final int[] dest = new int[src.length]; final DecodingContext ctx = DECODING_CONTEXT.get();
final int[] dest = ctx.getTempBuffer(src.length);
// Interlaced images are organized in 4 sets of pixel lines // Interlaced images are organized in 4 sets of pixel lines
final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0) 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 set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0) final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
// Sets' start indices in source array // Sets' start indices in source array
final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y; final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
// Line skips in destination array
// Line skips in destination array - use bit shifts for multiplication
final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1; final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
// Group 1 contains every 8th line starting from 0 // Group 1 contains every 8th line starting from 0
int from = 0, to = 0; int from = 0, to = 0;
for (; from < set2; from += w, to += w8) { for (; from < set2; from += w, to += w8) {
arraycopy(src, from, dest, to, w); 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) {
arraycopy(src, from, dest, to, w);
} // Group 3 contains every 4th line starting from 2
for (to = w2; from < set4; from += w, to += w4) {
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) {
arraycopy(src, from, dest, to, w);
} }
return dest; // All pixel lines have now been rearranged // 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) { private void drawFrame(final GifFrame fr) {
renderLock.writeLock().lock();
try {
// Determine the color table that will be active for this frame // Determine the color table that will be active for this frame
final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl; final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
// Get pixels from data stream // Get pixels from data stream
int[] pixels = decode(fr, activeColTbl); int[] pixels = decode(fr, activeColTbl);
if (fr.interlaceFlag) { if (fr.interlaceFlag) {
pixels = deinterlace(pixels, fr); // Rearrange pixel lines pixels = deinterlace(pixels, fr); // Rearrange pixel lines
} }
// Create image of type 2=ARGB for frame area
final BufferedImage frame = new BufferedImage(fr.w, fr.h, 2); // Create image of type TYPE_INT_ARGB for frame area
arraycopy(pixels, 0, ((DataBufferInt) frame.getRaster().getDataBuffer()).getData(), 0, fr.wh); 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 // Draw frame area on top of working image
g.drawImage(frame, fr.x, fr.y, null); g.drawImage(frame, fr.x, fr.y, null);
// Visualize frame boundaries during testing // Keep a copy of the previous frame's pixels
// if (DEBUG_MODE) { final DecodingContext ctx = DECODING_CONTEXT.get();
// if (prev != null) { final int[] prevPixelBuffer = ctx.getPrevPixelBuffer(wh);
// g.setColor(Color.RED); // Previous frame color final int[] imgData = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
// g.drawRect(prev.x, prev.y, prev.w - 1, prev.h - 1); System.arraycopy(imgData, 0, prevPixelBuffer, 0, wh);
// }
// g.setColor(Color.GREEN); // New frame color
// g.drawRect(fr.x, fr.y, fr.w - 1, fr.h - 1);
// }
// Keep a copy of the previous frame's pixels in case we need to restore the
// frame
int[] prevPx = new int[wh];
arraycopy(((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, prevPx, 0, wh);
// Create another copy for the end user to not expose internal state // Create another copy for the end user to not expose internal state
fr.img = new BufferedImage(w, h, 2); // 2 = ARGB synchronized (fr.imageLock) {
arraycopy(prevPx, 0, ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(), 0, wh); 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 // Handle disposal of current frame
if (fr.disposalMethod == 2) { if (fr.disposalMethod == 2) {
@@ -304,7 +379,10 @@ public final class GifDecoder {
g.clearRect(fr.x, fr.y, fr.w, fr.h); g.clearRect(fr.x, fr.y, fr.w, fr.h);
} else if (fr.disposalMethod == 3) { } else if (fr.disposalMethod == 3) {
// Restore previous frame // Restore previous frame
arraycopy(prevPx, 0, ((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, wh); System.arraycopy(prevPixelBuffer, 0, imgData, 0, wh);
}
} finally {
renderLock.writeLock().unlock();
} }
} }
@@ -317,6 +395,8 @@ public final class GifDecoder {
* @return 32 bit ARGB color in the form 0xAARRGGBB * @return 32 bit ARGB color in the form 0xAARRGGBB
*/ */
public final int getBackgroundColor() { public final int getBackgroundColor() {
renderLock.readLock().lock();
try {
final GifFrame frame = frames.get(0); final GifFrame frame = frames.get(0);
if (frame.hasLocColTbl) { if (frame.hasLocColTbl) {
return frame.localColTbl[bgColIndex]; return frame.localColTbl[bgColIndex];
@@ -324,6 +404,9 @@ public final class GifDecoder {
return globalColTbl[bgColIndex]; return globalColTbl[bgColIndex];
} }
return 0; return 0;
} finally {
renderLock.readLock().unlock();
}
} }
/** /**
@@ -334,7 +417,12 @@ public final class GifDecoder {
* @return Delay as number of hundredths (1/100) of a second * @return Delay as number of hundredths (1/100) of a second
*/ */
public final int getDelay(final int index) { public final int getDelay(final int index) {
renderLock.readLock().lock();
try {
return frames.get(index).delay; return frames.get(index).delay;
} finally {
renderLock.readLock().unlock();
}
} }
/** /**
@@ -349,12 +437,25 @@ public final class GifDecoder {
* @return A BufferedImage for the specified frame. * @return A BufferedImage for the specified frame.
*/ */
public BufferedImage getFrame(final int index) { public BufferedImage getFrame(final int index) {
if (img == null) { // Init // Initialize rendering context if needed
img = new BufferedImage(w, h, 2); // 2 = ARGB if (img == null) {
synchronized (initLock) {
if (img == null) { // Double-checked locking
img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
g = img.createGraphics(); g = img.createGraphics();
g.setBackground(new Color(0, true)); // Transparent color 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); 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) { if (fr.img == null) {
// Draw all frames until and including the requested frame // Draw all frames until and including the requested frame
for (int i = 0; i <= index; i++) { for (int i = 0; i <= index; i++) {
@@ -364,6 +465,9 @@ public final class GifDecoder {
} }
} }
} }
}
}
return fr.img; return fr.img;
} }
@@ -407,6 +511,8 @@ public final class GifDecoder {
img.globalColTbl = new int[img.sizeOfGlobColTbl]; img.globalColTbl = new int[img.sizeOfGlobColTbl];
pos = readColTbl(in, img.globalColTbl, pos); pos = readColTbl(in, img.globalColTbl, pos);
} }
// Main parsing loop with bounds checking optimization
while (pos < in.length) { while (pos < in.length) {
final int block = in[pos] & 0xFF; final int block = in[pos] & 0xFF;
switch (block) { switch (block) {
@@ -456,7 +562,7 @@ public final class GifDecoder {
// and wait for a valid block. Experience: It'll get worse. b) // and wait for a valid block. Experience: It'll get worse. b)
// Throw exception. c) Return gracefully if we are almost done // Throw exception. c) Return gracefully if we are almost done
// processing. The frames we have so far should be error-free. // processing. The frames we have so far should be error-free.
final double progress = 1.0 * pos / in.length; final double progress = (double) pos / in.length;
if (progress < 0.9) { if (progress < 0.9) {
throw new IOException("Unknown block at: " + pos); throw new IOException("Unknown block at: " + pos);
} }
@@ -489,6 +595,7 @@ public final class GifDecoder {
* @return Index of the first byte after this extension * @return Index of the first byte after this extension
*/ */
static int readAppExt(final GifImage img, final byte[] in, int i) { 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.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
img.appAuthCode = new String(in, i + 11, 3); // should be "2.0" 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 i += 14; // Go to sub-block size, it's value should be 3
@@ -496,7 +603,7 @@ public final class GifDecoder {
// The only app extension widely used is NETSCAPE, it's got 3 data bytes // The only app extension widely used is NETSCAPE, it's got 3 data bytes
if (subBlockSize == 3) { if (subBlockSize == 3) {
// in[i+1] should have value 01, in[i+5] should be block terminator // 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 img.repetitions = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // Short
return i + 5; return i + 5;
} // Skip unknown application extensions } // Skip unknown application extensions
while ((in[i] & 0xFF) != 0) { // While sub-block size != 0 while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
@@ -514,11 +621,11 @@ public final class GifDecoder {
static int readColTbl(final byte[] in, final int[] colors, int i) { static int readColTbl(final byte[] in, final int[] colors, int i) {
final int numColors = colors.length; final int numColors = colors.length;
for (int c = 0; c < numColors; c++) { for (int c = 0; c < numColors; c++) {
final int a = 0xFF; // Alpha 255 (opaque)
final int r = in[i++] & 0xFF; // 1st byte is red final int r = in[i++] & 0xFF; // 1st byte is red
final int g = in[i++] & 0xFF; // 2nd byte is green final int g = in[i++] & 0xFF; // 2nd byte is green
final int b = in[i++] & 0xFF; // 3rd byte is blue final int b = in[i++] & 0xFF; // 3rd byte is blue
colors[c] = ((a << 8 | r) << 8 | g) << 8 | b; // Optimized color packing - alpha is always 0xFF for opaque
colors[c] = 0xFF000000 | (r << 16) | (g << 8) | b;
} }
return i; return i;
} }
@@ -530,9 +637,10 @@ public final class GifDecoder {
* @return Index of the first byte after this block * @return Index of the first byte after this block
*/ */
static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) { static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2 final int packed = in[i + 3] & 0xFF;
fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0 fr.disposalMethod = (packed & 0b00011100) >>> 2; // Bits 4-2
fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB 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 fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00 return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
} }
@@ -548,6 +656,7 @@ public final class GifDecoder {
if (in.length < 6) { // Check first 6 bytes if (in.length < 6) { // Check first 6 bytes
throw new IOException("Image is truncated."); throw new IOException("Image is truncated.");
} }
// Use ASCII encoding for header - faster than default charset
img.header = new String(in, 0, 6); img.header = new String(in, 0, 6);
if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) { if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
throw new IOException("Invalid GIF header."); throw new IOException("Invalid GIF header.");
@@ -568,23 +677,28 @@ public final class GifDecoder {
fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
fr.clearCode = clearCode; fr.clearCode = clearCode;
fr.endOfInfoCode = clearCode + 1; fr.endOfInfoCode = clearCode + 1;
// Pre-calculate image data size for better memory allocation
final int imgDataSize = readImgDataSize(in, i); final int imgDataSize = readImgDataSize(in, i);
final byte[] imgData = new byte[imgDataSize + 2]; final byte[] imgData = new byte[imgDataSize + 2];
int imgDataPos = 0; int imgDataPos = 0;
int subBlockSize = in[i] & 0xFF; int subBlockSize = in[i] & 0xFF;
while (subBlockSize > 0) { // While block has data while (subBlockSize > 0) { // While block has data
try { // Next line may throw exception if sub-block size is fake try { // Next line may throw exception if sub-block size is fake
final int nextSubBlockSizePos = i + subBlockSize + 1; final int nextSubBlockSizePos = i + subBlockSize + 1;
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF; final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position imgDataPos += subBlockSize; // Move output data position
i = nextSubBlockSizePos; // Move to next sub-block size i = nextSubBlockSizePos; // Move to next sub-block size
subBlockSize = nextSubBlockSize; subBlockSize = nextSubBlockSize;
} catch (final Exception e) { } catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes // Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes subBlockSize = fileSize - i - 1; // Remaining bytes
arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); if (subBlockSize > 0) {
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position imgDataPos += subBlockSize; // Move output data position
}
i += subBlockSize + 1; // Move to next sub-block size i += subBlockSize + 1; // Move to next sub-block size
break; break;
} }
@@ -608,7 +722,9 @@ public final class GifDecoder {
} catch (final Exception e) { } catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes // Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes subBlockSize = fileSize - i - 1; // Remaining bytes
if (subBlockSize > 0) {
imgDataPos += subBlockSize; // Move output data position imgDataPos += subBlockSize; // Move output data position
}
break; break;
} }
} }
@@ -622,16 +738,16 @@ public final class GifDecoder {
* @return Index of the first byte after this block * @return Index of the first byte after this block
*/ */
static int readImgDescr(final GifFrame fr, final byte[] in, int i) { 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.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.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.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.h = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 7-8: height
fr.wh = fr.w * fr.h; fr.wh = fr.w * fr.h;
final byte b = in[++i]; // Byte 9 is a packed byte final int packed = in[++i] & 0xFF; // Byte 9 is a packed byte
fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7 fr.hasLocColTbl = (packed & 0b10000000) != 0; // Bit 7
fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6 fr.interlaceFlag = (packed & 0b01000000) != 0; // Bit 6
fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5 fr.sortFlag = (packed & 0b00100000) != 0; // Bit 5
final int colTblSizePower = (b & 7) + 1; // Bits 2-0 final int colTblSizePower = (packed & 7) + 1; // Bits 2-0
fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
return ++i; return ++i;
} }
@@ -642,15 +758,15 @@ public final class GifDecoder {
* @return Index of the first byte after this block. * @return Index of the first byte after this block.
*/ */
static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) { 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.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.h = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // 16 bit
img.wh = img.w * img.h; img.wh = img.w * img.h;
final byte b = in[i + 4]; // Byte 4 is a packed byte final int packed = in[i + 4] & 0xFF; // Byte 4 is a packed byte
img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7 img.hasGlobColTbl = (packed & 0b10000000) != 0; // Bit 7
final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4 final int colResPower = ((packed & 0b01110000) >>> 4) + 1; // Bits 6-4
img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3 img.sortFlag = (packed & 0b00001000) != 0; // Bit 3
final int globColTblSizePower = (b & 7) + 1; // Bits 0-2 final int globColTblSizePower = (packed & 7) + 1; // Bits 0-2
img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
@@ -667,6 +783,7 @@ public final class GifDecoder {
int subBlockSize = in[i++] & 0xFF; int subBlockSize = in[i++] & 0xFF;
while (subBlockSize != 0 && i < in.length) { while (subBlockSize != 0 && i < in.length) {
i += subBlockSize; i += subBlockSize;
if (i >= in.length) break; // Safety check
subBlockSize = in[i++] & 0xFF; subBlockSize = in[i++] & 0xFF;
} }
return i; return i;
@@ -4,20 +4,30 @@ import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.texture.NativeImage; import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import org.lwjgl.BufferUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
public class IdentifierUtils { 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) { public static void registerBufferedImageTexture(Identifier i, BufferedImage bi) {
try { try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // This is the performance killer:
ImageIO.write(bi, "png", baos); // 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(); byte[] bytes = baos.toByteArray();
ByteBuffer bb = BufferUtils.createByteBuffer(bytes.length).put(bytes); java.nio.ByteBuffer bb = org.lwjgl.BufferUtils.createByteBuffer(bytes.length).put(bytes);
bb.flip(); bb.flip();
NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb)); NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb));
SaturnClient.client.getTextureManager().registerTexture(i, nibt); SaturnClient.client.getTextureManager().registerTexture(i, nibt);
@@ -25,4 +35,64 @@ public class IdentifierUtils {
e.printStackTrace(); 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));
}
} }
@@ -26,7 +26,7 @@ public class HudEditor extends Screen {
public HudEditor() { public HudEditor() {
super(Text.literal("HUD Editor")); super(Text.literal("HUD Editor"));
for (Module m : ModManager.MODS) { for (Module m : ModManager.ENABLED_MODS) {
if (m.isEnabled() && m instanceof HudMod) { if (m.isEnabled() && m instanceof HudMod) {
hudMods.add((HudMod) m); hudMods.add((HudMod) m);
} }
@@ -25,6 +25,6 @@ public abstract class PlayerEntityRendererMixin
@Inject(method = "<init>", at = @At("TAIL")) @Inject(method = "<init>", at = @At("TAIL"))
private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) { private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) {
this.addFeature(new HatFeatureRenderer(this)); this.addFeature(new HatFeatureRenderer(this));
this.addFeature(new CloakFeatureRenderer(this, ctx.getEntityModels(), ctx.getEquipmentModelLoader())); this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader()));
} }
} }
@@ -65,6 +65,8 @@ public class RenderScope {
} }
public void drawRect(int x, int y, int width, int height, int color) { public void drawRect(int x, int y, int width, int height, int color) {
if (color == 0)
return;
color = getColor(color); color = getColor(color);
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
@@ -98,6 +100,8 @@ public class RenderScope {
} }
public void drawText(float scale, String text, int x, int y, int font, int color) { public void drawText(float scale, String text, int x, int y, int font, int color) {
if (color == 0)
return;
if (font == 0) { if (font == 0) {
scale *= 2; scale *= 2;
} }
@@ -108,7 +112,8 @@ public class RenderScope {
matrices.translate(x, y + (i * Fonts.getHeight()), 0); matrices.translate(x, y + (i * Fonts.getHeight()), 0);
matrices.scale(scale, scale, 1.0f); matrices.scale(scale, scale, 1.0f);
TextRenderer textRenderer = SaturnClient.client.textRenderer; TextRenderer textRenderer = SaturnClient.client.textRenderer;
textRenderer.draw(Fonts.setFont(line, Fonts.getFont(font)), 0, font == 0 ? 1: 7, color, false, this.matrices.peek().getPositionMatrix(), textRenderer.draw(Fonts.setFont(line, Fonts.getFont(font)), 0, font == 0 ? 1 : 7, color, false,
this.matrices.peek().getPositionMatrix(),
this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880); this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
matrices.pop(); matrices.pop();
i++; i++;
@@ -159,6 +164,8 @@ public class RenderScope {
} }
public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) { public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) {
if (color == 0)
return;
radius = Math.min(radius, Math.min(width, height)); radius = Math.min(radius, Math.min(width, height));
int cornerWidth = width / 2; int cornerWidth = width / 2;
int cornerHeight = height / 2; int cornerHeight = height / 2;
@@ -187,6 +194,7 @@ public class RenderScope {
public int getScaledWindowHeight() { public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight(); return SaturnClient.client.getWindow().getScaledHeight();
} }
public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int color) { public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int color) {
this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color); this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
} }
@@ -195,15 +203,22 @@ public class RenderScope {
this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height); this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height);
} }
public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith, int regionHeight, int textureWidth, int textureHeight) { public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith,
int regionHeight, int textureWidth, int textureHeight) {
this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1); this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1);
} }
public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) { public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth,
this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float)textureWidth, (u + (float)regionWidth) / (float)textureWidth, (v + 0.0F) / (float)textureHeight, (v + (float)regionHeight) / (float)textureHeight, color); int regionHeight, int textureWidth, int textureHeight, int color) {
this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float) textureWidth,
(u + (float) regionWidth) / (float) textureWidth, (v + 0.0F) / (float) textureHeight,
(v + (float) regionHeight) / (float) textureHeight, color);
} }
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1, float v2, int color) { private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1,
float v2, int color) {
if (color == 0)
return;
x1 *= 4; x1 *= 4;
x2 *= 4; x2 *= 4;
y1 *= 4; y1 *= 4;
@@ -224,7 +239,8 @@ public class RenderScope {
} }
public void enableScissor(int x1, int y1, int x2, int y2) { public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1)).transform(this.matrices.peek().getPositionMatrix()); ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1))
.transform(this.matrices.peek().getPositionMatrix());
this.setScissor(this.scissorStack.push(screenRect)); this.setScissor(this.scissorStack.push(screenRect));
} }
@@ -268,7 +284,8 @@ public class RenderScope {
public ScreenRect push(ScreenRect p_281812_) { public ScreenRect push(ScreenRect p_281812_) {
ScreenRect screenrectangle = this.stack.peekLast(); ScreenRect screenrectangle = this.stack.peekLast();
if (screenrectangle != null) { if (screenrectangle != null) {
ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle), ScreenRect.empty()); ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
ScreenRect.empty());
this.stack.addLast(screenrectangle1); this.stack.addLast(screenrectangle1);
return screenrectangle1; return screenrectangle1;
} else { } else {
@@ -316,15 +333,19 @@ public class RenderScope {
this.drawItem(entity, entity.getWorld(), stack, x, y, seed); this.drawItem(entity, entity.getWorld(), stack, x, y, seed);
} }
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed) { private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y,
int seed) {
this.drawItem(entity, world, stack, x, y, seed, 0); this.drawItem(entity, world, stack, x, y, seed, 0);
} }
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed, int z) { private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
int z) {
if (!stack.isEmpty()) { if (!stack.isEmpty()) {
SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI, false, world, entity, seed); SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI,
false, world, entity, seed);
this.matrices.push(); this.matrices.push();
this.matrices.translate((float)(x + 8), (float)(y + 8), (float)(150 + (this.itemRenderState.hasDepth() ? z : 0))); this.matrices.translate((float) (x + 8), (float) (y + 8),
(float) (150 + (this.itemRenderState.hasDepth() ? z : 0)));
try { try {
this.matrices.scale(16.0F, -16.0F, 16.0F); this.matrices.scale(16.0F, -16.0F, 16.0F);
@@ -363,8 +384,11 @@ public class RenderScope {
} }
public void drawSpriteStretched(Sprite sprite, int x, int y, int width, int height, int color) { public void drawSpriteStretched(Sprite sprite, int x, int y, int width, int height, int color) {
if (color == 0)
return;
if (width != 0 && height != 0) { if (width != 0 && height != 0) {
this.drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height, sprite.getMinU(), sprite.getMaxU(), sprite.getMinV(), sprite.getMaxV(), color); this.drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height, sprite.getMinU(), sprite.getMaxU(),
sprite.getMinV(), sprite.getMaxV(), color);
} }
} }
} }
@@ -4,6 +4,8 @@ import java.util.List;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui2.elements.Notification;
import org.saturnclient.ui2.elements.Notification.NotificationKind;
public class Utils { public class Utils {
public static boolean isHovering(int mouseX, int mouseY, int elementWidth, int elementHeight, float elementScale) { public static boolean isHovering(int mouseX, int mouseY, int elementWidth, int elementHeight, float elementScale) {
@@ -52,4 +54,13 @@ public class Utils {
SaturnClient.LOGGER.info("Accessing index {} in list of size {}", index, list.size()); SaturnClient.LOGGER.info("Accessing index {} in list of size {}", index, list.size());
return (index >= 0 && index < list.size()) ? list.get(index) : null; return (index >= 0 && index < list.size()) ? list.get(index) : null;
} }
public static void notify(NotificationKind kind, String title, String toast) {
if (SaturnClient.client.currentScreen instanceof SaturnScreen) {
((SaturnScreen) SaturnClient.client.currentScreen)
.draw(new Notification(SaturnClient.client.currentScreen.width,
SaturnClient.client.currentScreen.height, kind,
title, toast));
}
}
} }
@@ -24,7 +24,7 @@ public class ModMenu extends SaturnScreen {
int row = 0; int row = 0;
int col = 0; int col = 0;
for (Module mod : ModManager.MODS) { for (Module mod : ModManager.ALL_MODS) {
scroll.draw(new SaturnModule(mod).position((160 + g) * col, (50 + g) * row)); scroll.draw(new SaturnModule(mod).position((160 + g) * col, (50 + g) * row));
if (col == 2) { if (col == 2) {
@@ -5,9 +5,11 @@ import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.auth.SaturnPlayer; import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.ui2.SaturnScreen; import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.components.CosmeticPreview; import org.saturnclient.ui2.components.CosmeticPreview;
import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.elements.ImageTexture; import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.elements.Notification.NotificationKind;
import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.elements.TabMenu; import org.saturnclient.ui2.elements.TabMenu;
import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent; import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
@@ -16,10 +18,17 @@ import org.saturnclient.ui2.resources.Fonts;
import org.saturnclient.ui2.resources.Textures; import org.saturnclient.ui2.resources.Textures;
public class CloakStore extends SaturnScreen { public class CloakStore extends SaturnScreen {
private long lastPurchaseTime = 0;
public CloakStore() { public CloakStore() {
super("Hat Store"); super("Hat Store");
} }
public CloakStore(long lastPurchaseTime) {
super("Hat Store");
this.lastPurchaseTime = lastPurchaseTime;
}
@Override @Override
public void ui() { public void ui() {
int p = 10; int p = 10;
@@ -38,16 +47,30 @@ public class CloakStore extends SaturnScreen {
if (!Cloaks.availableCloaks.contains(cloak)) { if (!Cloaks.availableCloaks.contains(cloak)) {
int x = (50 + gx) * col; int x = (50 + gx) * col;
int y = (111 + gy) * row; int y = (111 + gy) * row;
scroll.draw(new CosmeticPreview(cloak == player.cloak, Textures.getCloakPreview(cloak), () -> { scroll.draw(new CosmeticPreview(cloak == player.cloak, Textures.getCloakPreview(cloak), () -> {
long now = System.currentTimeMillis();
if (now - lastPurchaseTime >= 3000) {
Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait 3 seconds");
Auth.buyCloak(cloak); Auth.buyCloak(cloak);
SaturnClient.client.setScreen(new CloakStore()); lastPurchaseTime = now;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SaturnClient.client.setScreen(new CloakStore(now));
} else {
Utils.notify(NotificationKind.Error, "Timeout error", "Please wait 3 seconds");
}
}).position(x, y)); }).position(x, y));
String t = "100"; String t = "100";
scroll.draw(new Text(t).position(x + Fonts.centerX(50, t, Text.font.value), y + 113).scale(0.5f)); scroll.draw(new Text(t).position(x + Fonts.centerX(50, t, Text.font.value), y + 113).scale(0.5f));
scroll.draw(new ImageTexture(Textures.COINS).dimensions(16, 16).position(x + Fonts.getWidth(t, Text.font.value) - 1, y + 112).scale(0.5f)); scroll.draw(new ImageTexture(Textures.COINS).dimensions(16, 16)
.position(x + Fonts.getWidth(t, Text.font.value) - 1, y + 112).scale(0.5f));
if (col == 8) { if (col == 8) {
col = 0; col = 0;
@@ -66,10 +89,10 @@ public class CloakStore extends SaturnScreen {
draw(new Sidebar(5, this::close).centerOffset(width, height, -((scrollWidth - 30) / 2 + 20), 0)); draw(new Sidebar(5, this::close).centerOffset(width, height, -((scrollWidth - 30) / 2 + 20), 0));
draw(new TabMenu(0, draw(new TabMenu(0,
new TabMenuComponent(Textures.CLOAK, () -> {}), new TabMenuComponent(Textures.CLOAK, () -> {
}),
new TabMenuComponent(Textures.HAT, () -> { new TabMenuComponent(Textures.HAT, () -> {
SaturnClient.client.setScreen(new HatStore()); SaturnClient.client.setScreen(new HatStore());
}) })).centerOffset(width, height, 0, -195));
).centerOffset(width, height, 0, -195));
} }
} }
@@ -5,9 +5,11 @@ import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.auth.SaturnPlayer; import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.ui2.SaturnScreen; import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.components.CosmeticPreview; import org.saturnclient.ui2.components.CosmeticPreview;
import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.elements.ImageTexture; import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.elements.Notification.NotificationKind;
import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.elements.TabMenu; import org.saturnclient.ui2.elements.TabMenu;
import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent; import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
@@ -16,10 +18,17 @@ import org.saturnclient.ui2.resources.Fonts;
import org.saturnclient.ui2.resources.Textures; import org.saturnclient.ui2.resources.Textures;
public class HatStore extends SaturnScreen { public class HatStore extends SaturnScreen {
private long lastPurchaseTime = 0;
public HatStore() { public HatStore() {
super("Hat Store"); super("Hat Store");
} }
public HatStore(long lastPurchaseTime) {
super("Hat Store");
this.lastPurchaseTime = lastPurchaseTime;
}
@Override @Override
public void ui() { public void ui() {
int p = 10; int p = 10;
@@ -38,16 +47,30 @@ public class HatStore extends SaturnScreen {
if (!Hats.availableHats.contains(hat)) { if (!Hats.availableHats.contains(hat)) {
int x = (50 + gx) * col; int x = (50 + gx) * col;
int y = (50 + gy) * row; int y = (50 + gy) * row;
scroll.draw(new CosmeticPreview(hat == player.hat, Textures.getHatPreview(hat), () -> { scroll.draw(new CosmeticPreview(hat == player.hat, Textures.getHatPreview(hat), () -> {
long now = System.currentTimeMillis();
if (now - lastPurchaseTime >= 3000) {
Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait 3 seconds");
Auth.buyHat(hat); Auth.buyHat(hat);
SaturnClient.client.setScreen(new HatStore()); lastPurchaseTime = now;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SaturnClient.client.setScreen(new HatStore(now));
} else {
Utils.notify(NotificationKind.Error, "Timeout error", "Please wait 3 seconds");
}
}).dimensions(50, 50).position(x, y)); }).dimensions(50, 50).position(x, y));
String t = "50"; String t = "50";
scroll.draw(new Text(t).position(x + Fonts.centerX(50, t, Text.font.value), y + 53).scale(0.5f)); scroll.draw(new Text(t).position(x + Fonts.centerX(50, t, Text.font.value), y + 53).scale(0.5f));
scroll.draw(new ImageTexture(Textures.COINS).dimensions(16, 16).position(x + Fonts.getWidth(t, Text.font.value) + 4, y + 52).scale(0.5f)); scroll.draw(new ImageTexture(Textures.COINS).dimensions(16, 16)
.position(x + Fonts.getWidth(t, Text.font.value) + 4, y + 52).scale(0.5f));
if (col == 8) { if (col == 8) {
col = 0; col = 0;
@@ -69,7 +92,7 @@ public class HatStore extends SaturnScreen {
new TabMenuComponent(Textures.CLOAK, () -> { new TabMenuComponent(Textures.CLOAK, () -> {
SaturnClient.client.setScreen(new CloakStore()); SaturnClient.client.setScreen(new CloakStore());
}), }),
new TabMenuComponent(Textures.HAT, () -> {}) new TabMenuComponent(Textures.HAT, () -> {
).centerOffset(width, height, 0, -195)); })).centerOffset(width, height, 0, -195));
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B