diff --git a/src/main/java/org/saturnclient/modules/ModManager.java b/src/main/java/org/saturnclient/modules/ModManager.java index 459e661..b8a8f62 100644 --- a/src/main/java/org/saturnclient/modules/ModManager.java +++ b/src/main/java/org/saturnclient/modules/ModManager.java @@ -1,11 +1,14 @@ package org.saturnclient.modules; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import java.util.ArrayList; +import java.util.List; import org.saturnclient.modules.mods.*; public class ModManager { - public static Module[] MODS = { + public static final List ENABLED_MODS = new ArrayList<>(); + public static final Module[] ALL_MODS = { new Crosshair(), new AutoSprint(), new ArmorDisplay(), @@ -17,12 +20,31 @@ public class ModManager { }; public static void init() { + // Pre-filter enabled modules to avoid checking every tick + updateEnabledModules(); + ClientTickEvents.END_CLIENT_TICK.register(client -> { - for (Module m : MODS) { - if (m.isEnabled()) { - m.tick(); - } + // Only iterate over enabled modules + for (Module m : ENABLED_MODS) { + 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(); + } } diff --git a/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java b/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java index a9d26e7..87458fb 100644 --- a/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java +++ b/src/main/java/org/saturnclient/modules/mods/ArmorDisplay.java @@ -14,22 +14,31 @@ import net.minecraft.item.Items; public class ArmorDisplay extends Module implements HudMod { private static Property enabled = Property.bool(false); - private static ModDimensions dimensions = new ModDimensions(40, 60); + private static ModDimensions dimensions = new ModDimensions(40, 75); + private static Property useMainHand = Property.bool(false); public ArmorDisplay() { super(new ModuleDetails("Armor Display", "armor") - .description("Displays armor durability") - .version("v0.1.0") - .tags("Utility"), - enabled.named("Enabled"), - dimensions.prop()); + .description("Displays armor durability") + .version("v0.1.0") + .tags("Utility"), + enabled.named("Enabled"), + useMainHand.named("Use Main Hand"), + dimensions.prop()); } - public void renderArmor(RenderScope scope, ItemStack 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; + if (useMainHand.value && !mainHand.isEmpty()) { + scope.drawItem(mainHand, 0, 15 * row); + renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage()); + row++; + } + if (!helmet.isEmpty()) { - scope.drawItem(helmet, 0, 0); + scope.drawItem(helmet, 0, 15 * row); renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage()); row++; } @@ -53,23 +62,27 @@ public class ArmorDisplay extends Module implements HudMod { } public void renderHealth(RenderScope scope, int i, int max_damage, int damage) { - if (max_damage > 0 ) { - scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value, dimensions.fgColor.value); + if (max_damage > 0) { + scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value, + dimensions.fgColor.value); } } @Override public void renderHud(RenderScope scope) { renderArmor(scope, - SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD), - SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST), - SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS), - SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET)); + SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND), + SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD), + SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST), + SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS), + SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET)); } @Override public void renderDummy(RenderScope scope) { - renderArmor(scope, new ItemStack(Items.DIAMOND_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 diff --git a/src/main/java/org/saturnclient/modules/mods/Keystrokes.java b/src/main/java/org/saturnclient/modules/mods/Keystrokes.java index 44731e5..0eac3b0 100644 --- a/src/main/java/org/saturnclient/modules/mods/Keystrokes.java +++ b/src/main/java/org/saturnclient/modules/mods/Keystrokes.java @@ -13,6 +13,8 @@ public class Keystrokes extends Module implements HudMod { public static Property showMouse = Property.bool(true); public static Property showSpace = Property.bool(false); public static ModDimensions dimensions = new ModDimensions(78, 46); + + // Cache key states to avoid repeated calls private boolean w = false; private boolean a = false; private boolean s = false; @@ -21,27 +23,32 @@ public class Keystrokes extends Module implements HudMod { private boolean rmb = false; private boolean space = false; + // Cache dimensions to avoid recalculation + private int cachedHeight = 54; + private boolean dimensionsDirty = true; + private static Property clickBg = Property.color(0xFFCCCCCC); private static Property clickFg = Property.color(0xFFFFFFFF); public Keystrokes() { super(new ModuleDetails("Keystrokes", "keystrokes") - .description("Displays the keystrokes for movement") - .version("v0.1.0") - .tags("Utility"), + .description("Displays the keystrokes for movement") + .version("v0.1.0") + .tags("Utility"), - enabled.named("Enabled"), - showMouse.named("Show mouse clicks"), - showSpace.named("Show space clicks"), - dimensions.prop(), - clickFg.named("Clicked fg"), - clickBg.named("Clicked bg")); + enabled.named("Enabled"), + showMouse.named("Show mouse clicks"), + showSpace.named("Show space clicks"), + dimensions.prop(), + clickFg.named("Clicked fg"), + clickBg.named("Clicked bg")); dimensions.renderBackground = false; } - + @Override public void tick() { + // Cache key states to reduce method calls w = SaturnClient.client.options.forwardKey.isPressed(); a = SaturnClient.client.options.leftKey.isPressed(); s = SaturnClient.client.options.backKey.isPressed(); @@ -49,49 +56,67 @@ public class Keystrokes extends Module implements HudMod { lmb = SaturnClient.client.options.attackKey.isPressed(); rmb = SaturnClient.client.options.useKey.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 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) - renderKey(scope, true, 'W', 27, 0); // Top center - renderKey(scope, false, 'A', 0, 27); // Bottom left - renderKey(scope, false, 'S', 27, 27); // Bottom center - renderKey(scope, false, 'D', 54, 27); // Bottom right + renderKey(scope, false, 'W', 27, 0); // Top center + renderKey(scope, false, 'A', 0, 27); // Bottom left + renderKey(scope, false, 'S', 27, 27); // Bottom center + renderKey(scope, false, 'D', 54, 27); // Bottom right if (showMouse.value) { - renderKeyM(scope, true, "LMB", 0, 54); // Below A/S/D - renderKeyM(scope, false, "RMB", 40, 54); // With 3px between - dimensions.height += 27; + renderKeyM(scope, false, "LMB", 0, 54); // Below A/S/D + renderKeyM(scope, false, "RMB", 40, 54); // With 3px between } if (showSpace.value) { - renderKeySpace(scope, false, 0, dimensions.height); - dimensions.height += 22; + renderKeySpace(scope, false, 0, cachedHeight - (showSpace.value ? 22 : 0)); } } @Override 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) - renderKey(scope, w, 'W', 27, 0); // Top center - renderKey(scope, a, 'A', 0, 27); // Bottom left - renderKey(scope, s, 'S', 27, 27); // Bottom center - renderKey(scope, d, 'D', 54, 27); // Bottom right + renderKey(scope, w, 'W', 27, 0); // Top center + renderKey(scope, a, 'A', 0, 27); // Bottom left + renderKey(scope, s, 'S', 27, 27); // Bottom center + renderKey(scope, d, 'D', 54, 27); // Bottom right if (showMouse.value) { - renderKeyM(scope, lmb, "LMB", 0, 54); // Below A/S/D - renderKeyM(scope, rmb, "RMB", 40, 54); // With 3px between - dimensions.height += 27; + renderKeyM(scope, lmb, "LMB", 0, 54); // Below A/S/D + renderKeyM(scope, rmb, "RMB", 40, 54); // With 3px between } if (showSpace.value) { - renderKeySpace(scope, space, 0, dimensions.height); - dimensions.height += 22; + renderKeySpace(scope, space, 0, cachedHeight - (showSpace.value ? 22 : 0)); } } @@ -99,29 +124,38 @@ public class Keystrokes extends Module implements HudMod { private void renderKeySpace(RenderScope scope, boolean isPressed, int x, int y) { 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 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) { 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 - 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) { 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 - 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 public boolean isEnabled() { return enabled.value; @@ -135,5 +169,6 @@ public class Keystrokes extends Module implements HudMod { @Override public void setEnabled(boolean e) { enabled.value = e; + dimensionsDirty = true; } } \ No newline at end of file diff --git a/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java b/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java index e32f64f..a552698 100644 --- a/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java +++ b/src/main/java/org/saturnclient/modules/mods/mixins/RenderMixin.java @@ -29,7 +29,7 @@ public class RenderMixin { RenderScope renderScope = new RenderScope(context.getMatrices(), ((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()) { ModDimensions dim = ((HudMod) m).getDimensions(); diff --git a/src/main/java/org/saturnclient/saturnclient/auth/Auth.java b/src/main/java/org/saturnclient/saturnclient/auth/Auth.java index 5ecdf8c..3bcd7af 100644 --- a/src/main/java/org/saturnclient/saturnclient/auth/Auth.java +++ b/src/main/java/org/saturnclient/saturnclient/auth/Auth.java @@ -10,8 +10,7 @@ import net.minecraft.util.Identifier; import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; -import org.saturnclient.ui2.SaturnScreen; -import org.saturnclient.ui2.elements.Notification; +import org.saturnclient.ui2.Utils; import org.saturnclient.ui2.elements.Notification.NotificationKind; import dev.kosmx.playerAnim.api.layered.AnimationStack; @@ -28,7 +27,6 @@ public class Auth { private static Thread pingThread; private static volatile boolean running = false; - @SuppressWarnings("resource") public static boolean authenticate() { // Register shutdown hook ONCE ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close()); @@ -166,15 +164,8 @@ public class Auth { default: 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(": "); - ((SaturnScreen) SaturnClient.client.currentScreen) - .draw(new Notification(SaturnClient.client.currentScreen.width, - SaturnClient.client.currentScreen.height, NotificationKind.Error, - a[0], a[1])); - } + String[] a = parser.error.replace("!", "").split(": "); + Utils.notify(NotificationKind.Error, a[0], a[1]); } break; } diff --git a/src/main/java/org/saturnclient/saturnclient/config/ConfigManager.java b/src/main/java/org/saturnclient/saturnclient/config/ConfigManager.java index a13ec16..4123bbf 100644 --- a/src/main/java/org/saturnclient/saturnclient/config/ConfigManager.java +++ b/src/main/java/org/saturnclient/saturnclient/config/ConfigManager.java @@ -1,223 +1,116 @@ package org.saturnclient.saturnclient.config; -import com.google.gson.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 com.google.gson.*; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; import org.saturnclient.saturnclient.SaturnClient; public class ConfigManager { - private static File configFile = new File( - SaturnClient.client.runDirectory, - "saturn.json"); - private static Map>> properties = new HashMap<>(); + private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json"); + private static final Map>> properties = new HashMap<>(); private static JsonObject cachedThemeJson = null; - private Map> currentMap; - private String namespace; + private final Map> currentMap; + private final String namespace; public ConfigManager(String namespace) { - currentMap = new LinkedHashMap<>(); + this.namespace = namespace; + this.currentMap = new LinkedHashMap<>(); properties.put(namespace, currentMap); System.out.println("Created namespace: " + namespace); } - public ConfigManager(ConfigManager config, String namespace) { - currentMap = new LinkedHashMap<>(); - Property>> namespaceProperty = Property.namespace(currentMap); - config.property(namespace, namespaceProperty); - // Update the parent's state - config.currentMap.put(namespace, namespaceProperty); + public ConfigManager(ConfigManager parent, String namespace) { + this.namespace = namespace; + this.currentMap = new LinkedHashMap<>(); + Property>> nsProp = Property.namespace(currentMap); + parent.property(namespace, nsProp); System.out.println("Created sub-namespace: " + namespace); } - public void sub(String namespace) { - Property>> namespaceProperty = Property.namespace(currentMap); - property(namespace, namespaceProperty); + public void sub(String name) { + property(name, Property.namespace(new LinkedHashMap<>())); } - // Generic method to store any type of property public Property property(String name, Property value) { currentMap.put(name, value); loadProp(name, value); return value; } - @SuppressWarnings("unchecked") - public void loadProp(String name, Property prop) { - if (cachedThemeJson != null) { - JsonElement element = cachedThemeJson.get(namespace); - if (element != null && element.isJsonObject()) { - JsonObject theme = element.getAsJsonObject(); - JsonElement value = theme.get(name); - if (value != null && prop.matchesJson(value)) { - switch (prop.getType()) { - case BOOLEAN: - ((Property) prop).setValue(value.getAsBoolean()); - break; - case INTEGER: - ((Property) prop).setValue(value.getAsInt()); - break; - case FLOAT: - ((Property) prop).setValue(value.getAsFloat()); - break; - case STRING: - ((Property) prop).setValue(value.getAsString()); - break; - case HEX: - ((Property) prop).setValue(value.getAsInt()); - break; - default: - break; - } - } + private void loadProp(String name, Property prop) { + JsonObject source = cachedThemeJson != null ? cachedThemeJson : loadAndCache(); + if (source == null) + return; + + JsonElement namespaceJson = source.get(namespace); + if (namespaceJson == null || !namespaceJson.isJsonObject()) + return; + + JsonElement value = namespaceJson.getAsJsonObject().get(name); + if (value != null) + prop.loadFromJson(value); + } + + private static JsonObject loadAndCache() { + try { + if (!configFile.exists()) { + Files.writeString(configFile.toPath(), "{}"); + return new JsonObject(); } - } else { - load(); + String json = Files.readString(configFile.toPath()); + cachedThemeJson = JsonParser.parseString(json).getAsJsonObject(); + return cachedThemeJson; + } catch (IOException e) { + SaturnClient.LOGGER.error("Error reading config file", e); + return null; } } public static void load() { - try { - SaturnClient.LOGGER.info("Starting to load config..."); - if (!configFile.exists()) { - configFile.createNewFile(); - Files.write(configFile.toPath(), "{}".getBytes()); + SaturnClient.LOGGER.info("Loading config..."); + JsonObject json = loadAndCache(); + if (json == null) + return; + + properties.forEach((namespace, map) -> { + JsonElement nsElement = json.get(namespace); + if (nsElement != null && nsElement.isJsonObject()) { + loadProperties(nsElement.getAsJsonObject(), map); } - - JsonObject jsonObject = JsonParser.parseString( - new String(Files.readAllBytes(configFile.toPath()))).getAsJsonObject(); - - for (String namespace : properties.keySet()) { - JsonElement configElement = jsonObject.get(namespace); - - if (configElement == null) - continue; - - JsonObject config = jsonObject.get(namespace).getAsJsonObject(); - - if (config == null) { - continue; - } - - Map> 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> 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> nestedProperties = p.getNamespaceValue(); - loadProperties(nestedConfig, nestedProperties); - } else { - if (p.value instanceof Integer) { - ((Property) p).value = c.getAsInt(); - } else if (p.value instanceof String) { - ((Property) p).value = c.getAsString(); - } else if (p.value instanceof Float) { - ((Property) p).value = c.getAsFloat(); - } else if (p.value instanceof Boolean) { - ((Property) p).value = c.getAsBoolean(); - } else { - SaturnClient.LOGGER.warn( - "Unknown property type for: " + propertyName); - } - } - } else { - SaturnClient.LOGGER.warn( - "Property does not match JSON: " + propertyName); - } - } + private static void loadProperties(JsonObject json, Map> props) { + props.forEach((name, prop) -> { + JsonElement el = json.get(name); + if (el != null) + prop.loadFromJson(el); + }); } 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 { - SaturnClient.LOGGER.info("Starting to save config..."); - - JsonObject jsonObject = new JsonObject(); - - // Iterate through all namespaces and their properties - for (String namespace : properties.keySet()) { - JsonObject namespaceConfig = new JsonObject(); - Map> 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()); + String formatted = new GsonBuilder().setPrettyPrinting().create().toJson(root); + Files.writeString(configFile.toPath(), formatted); SaturnClient.LOGGER.info("Config saved successfully."); } 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 config, Map> propertyMap) { - // 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> nestedProperties = (Map>) 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); - } - } + private static void saveProperties(JsonObject json, Map> props) { + props.forEach((name, prop) -> json.add(name, prop.toJson())); } public Map> getProperties() { diff --git a/src/main/java/org/saturnclient/saturnclient/config/Property.java b/src/main/java/org/saturnclient/saturnclient/config/Property.java index 28ac3de..0a8aa01 100644 --- a/src/main/java/org/saturnclient/saturnclient/config/Property.java +++ b/src/main/java/org/saturnclient/saturnclient/config/Property.java @@ -6,6 +6,7 @@ import org.lwjgl.glfw.GLFW; import org.saturnclient.saturnclient.SaturnClient; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; public class Property { @@ -25,7 +26,7 @@ public class Property { public boolean isReset = false; private T defaultValue; private String[] availableValues; - private PropertyType type; + private final PropertyType type; private boolean wasPressedLastTick = false; private Property(T value, PropertyType type) { @@ -34,24 +35,19 @@ public class Property { this.type = type; } + // ---------- Factory Methods ---------- public static Property from(T value) { - if (value instanceof Boolean) { + if (value instanceof Boolean) return new Property<>(value, PropertyType.BOOLEAN); - } else if (value instanceof Integer) { + if (value instanceof Integer) return new Property<>(value, PropertyType.INTEGER); - } else if (value instanceof Float) { + if (value instanceof Float) return new Property<>(value, PropertyType.FLOAT); - } else if (value instanceof String) { + if (value instanceof String) return new Property<>(value, PropertyType.STRING); - } else if (valueIsNamespace(value)) { + if (valueIsNamespace(value)) return new Property<>(value, PropertyType.NAMESPACE); - } else { - return null; - } - } - - public Property copy() { - return new Property<>(this.value, this.type); + return null; } public static Property font(int value) { @@ -92,55 +88,37 @@ public class Property { return new Property<>(value, PropertyType.KEYBINDING); } + // ---------- Select Helpers ---------- public void next() { - if ((Integer) value < availableValues.length - 1) - setValue((Integer) value + 1); - else - setValue(0); + if (type == PropertyType.SELECT) { + int i = (Integer) value; + setValue((i < availableValues.length - 1) ? i + 1 : 0); + } } public void prev() { - if ((Integer) value > 0) - setValue((Integer) value - 1); - else - setValue(availableValues.length - 1); + if (type == PropertyType.SELECT) { + int i = (Integer) value; + setValue((i > 0) ? i - 1 : availableValues.length - 1); + } } 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); } } public String getSelection() { - if (type == PropertyType.SELECT) { - return availableValues[(Integer) value]; - } else { - return null; - } + return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null; } + // ---------- Lifecycle ---------- public void reset() { value = defaultValue; 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() { return type; } @@ -153,34 +131,93 @@ public class Property { throw new IllegalStateException("Property is not a namespace"); } - public boolean matchesJson(JsonElement element) { - if (element.isJsonPrimitive()) { - JsonPrimitive primitive = element.getAsJsonPrimitive(); - if (value instanceof Boolean && primitive.isBoolean()) - return true; - if (value instanceof Integer && primitive.isNumber()) - return true; - if (value instanceof Float && primitive.isNumber()) - return true; - if (value instanceof String && primitive.isString()) - return true; - if (type == PropertyType.HEX && primitive.isString()) - return true; - } else if (element.isJsonObject()) { - return isNamespace(value); + // ---------- JSON Serialization ---------- + /* + * BOOLEAN, + * INTEGER, + * FLOAT, + * STRING, + * HEX, + * NAMESPACE, + * SELECT, + * KEYBINDING, + */ + public JsonElement toJson() { + switch (type) { + case BOOLEAN: + return new JsonPrimitive((Boolean) value); + case INTEGER: + return new JsonPrimitive((Integer) value); + case FLOAT: + return new JsonPrimitive((Float) value); + case NAMESPACE: + JsonObject nested = new JsonObject(); + getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson())); + return nested; + case HEX: + return new JsonPrimitive(String.format("#%08X", (Integer) value)); + case KEYBINDING: + return new JsonPrimitive((Integer) value); + case SELECT: + return new JsonPrimitive((Integer) value); + default: + return new JsonPrimitive(String.valueOf(value)); } - 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 public String toString() { return String.valueOf(value); } - public boolean isNamespace(Object obj) { - return type == PropertyType.NAMESPACE && value instanceof Map; - } - public static boolean valueIsNamespace(Object obj) { return obj instanceof Map; } @@ -200,59 +237,44 @@ public class Property { return new NamedProperty<>(name, this); } + // ---------- Keybindings ---------- public boolean isKeyPressed() { - return (Integer) value == -1 ? false - : GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), - (Integer) (Object) value) == GLFW.GLFW_PRESS && SaturnClient.client.currentScreen == null; + return (Integer) value != -1 && + GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS && + SaturnClient.client.currentScreen == null; } public boolean wasKeyPressed() { - boolean isPressed = this.isKeyPressed(); - wasPressedLastTick = isPressed && !wasPressedLastTick; - return wasPressedLastTick; - } - - @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) this).setValue(value.getAsBoolean()); - break; - case INTEGER: - JsonPrimitive primitive = value.getAsJsonPrimitive(); - if (primitive.isNumber()) - ((Property) this).setValue(value.getAsInt()); - else if (primitive.isString()) - ((Property) this).setValue(parseHexToInt(value.getAsString())); - break; - case FLOAT: - ((Property) this).setValue(value.getAsFloat()); - break; - case STRING: - ((Property) this).setValue(value.getAsString()); - break; - case HEX: - ((Property) this).setValue(parseHexToInt(value.getAsString())); - break; - default: - break; - } - } - } + boolean pressed = isKeyPressed(); + boolean result = pressed && !wasPressedLastTick; + wasPressedLastTick = pressed; + return result; } + // ---------- HEX ---------- public static int parseHexToInt(String hex) { - hex = hex.replaceAll("#", ""); - - if (hex.length() == 6) { + hex = hex.replace("#", ""); + if (hex.length() == 6) hex = "FF" + hex; - } else if (hex.length() != 8) { - throw new IllegalArgumentException("Hex string must be 6 or 8 characters long"); - } - + if (hex.length() != 8) + throw new IllegalArgumentException("Hex must be 6 or 8 chars long, got \'" + hex + "\'"); return (int) Long.parseLong(hex, 16); } + + // Misc + public Property copy() { + return new Property<>(this.value, this.type); + } + + public void load(String name, JsonElement element) { + if (element != null && element.isJsonObject()) { + load(name, element.getAsJsonObject()); + } + } + + public void load(String name, JsonObject element) { + JsonElement value = element.get(name); + if (value != null) + this.loadFromJson(value); + } } diff --git a/src/main/java/org/saturnclient/saturnclient/cosmetics/CloakFeatureRenderer.java b/src/main/java/org/saturnclient/saturnclient/cosmetics/CloakFeatureRenderer.java index 93f5105..d687687 100644 --- a/src/main/java/org/saturnclient/saturnclient/cosmetics/CloakFeatureRenderer.java +++ b/src/main/java/org/saturnclient/saturnclient/cosmetics/CloakFeatureRenderer.java @@ -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.feature.FeatureRenderer; 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.state.PlayerEntityRenderState; import net.minecraft.client.util.SkinTextures; @@ -26,16 +22,17 @@ import net.minecraft.item.ItemStack; import net.minecraft.item.equipment.EquipmentAsset; import net.minecraft.registry.RegistryKey; import net.minecraft.util.Identifier; +import net.minecraft.util.math.RotationAxis; +import net.minecraft.util.math.Vec3d; public class CloakFeatureRenderer extends FeatureRenderer { - private final BipedEntityModel model; private final EquipmentModelLoader equipmentModelLoader; + private static final int PARTS = 16; + private float currentVelocity = 0.0f; public CloakFeatureRenderer(FeatureRendererContext context, - LoadedEntityModels modelLoader, EquipmentModelLoader equipmentModelLoader) { + EquipmentModelLoader equipmentModelLoader) { super(context); - this.model = new PlayerCapeModel( - modelLoader.getModelPart(EntityModelLayers.PLAYER_CAPE)); this.equipmentModelLoader = equipmentModelLoader; } @@ -50,7 +47,220 @@ public class CloakFeatureRenderer extends FeatureRenderer> 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.translate(0.0f, -0.25f, 0.0f); if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) { matrixStack.translate(0.0F, -0.053125F, 0.06875F); } - VertexConsumer vertexConsumer = vertexConsumerProvider .getBuffer(RenderLayer.getEntityAlpha(customCape)); - ((PlayerEntityModel) this.getContextModel()).copyTransforms(this.model); - this.model.setAngles(playerEntityRenderState); - this.model.render(matrixStack, vertexConsumer, 0xF000F0, OverlayTexture.DEFAULT_UV); + renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV, + Math.min(rotation, 70.0f), Math.min(curve, 0.46f)); matrixStack.pop(); } } } } -} +} \ No newline at end of file diff --git a/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/Cloaks.java b/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/Cloaks.java index 8222541..556a7c4 100644 --- a/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/Cloaks.java +++ b/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/Cloaks.java @@ -11,7 +11,11 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.saturnclient.saturnclient.SaturnClient; /** @@ -20,8 +24,10 @@ import org.saturnclient.saturnclient.SaturnClient; * Originally created by IIpho3nix and modified for Saturn Client by leo. */ public class Cloaks { - public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas", "ferrari", "redbull" }; - private static final String[] ANIMATED_CLOAKS = { "glitch" }; + public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas", + "ferrari", "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white" }; + private static final String[] ANIMATED_CLOAKS = { "glitch", "black_hole_amethyst", "black_hole_flame", + "black_hole_white" }; private static final String CLOAKS_RESOURCE_PATH = "assets/saturnclient/textures/cloaks/"; public static final List availableCloaks = new ArrayList<>(); @@ -29,6 +35,17 @@ public class Cloaks { public static final Map> animatedCloaks = new ConcurrentHashMap<>(); private static final Map lastFrameTime = new ConcurrentHashMap<>(); + private static final ExecutorService CLOAK_LOADER_EXECUTOR = Executors.newFixedThreadPool( + Math.min(4, Runtime.getRuntime().availableProcessors()), + r -> { + Thread t = new Thread(r, "cloak-loader"); + t.setDaemon(true); + t.setPriority(Thread.NORM_PRIORITY - 1); + return t; + }); + + private static final ConcurrentHashMap> CLOAK_CACHE = new ConcurrentHashMap<>(); + /** * Initializes the cloak system. * Loads cloak textures from resources. @@ -87,52 +104,13 @@ public class Cloaks { if (player != null && player.cloak != null) { if (!player.cloak.isEmpty()) { if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) { - // Load animated cloak data in background thread - new Thread(() -> { - try { - String fileName = player.cloak + ".gif"; - String resourcePath = CLOAKS_RESOURCE_PATH + fileName; - InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath); + loadAnimatedCloakAsync(uuid, player.cloak) + .exceptionally(throwable -> { + SaturnClient.LOGGER.error("Failed to load animated cloak for player: " + player.cloak, + throwable); + return null; + }); - if (inputStream != null) { - byte[] data = inputStream.readAllBytes(); - GifDecoder.GifImage gif = GifDecoder.read(data); - List frames = new ArrayList<>(); - List 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 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 { SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png")); } @@ -168,6 +146,86 @@ public class Cloaks { } } + public static CompletableFuture loadAnimatedCloakAsync(String uuid, String cloakName) { + return CompletableFuture.runAsync(() -> { + String fileName = cloakName + ".gif"; + + // Check cache first + List cached = CLOAK_CACHE.get(fileName); + if (cached != null) { + animatedCloaks.put(uuid, cached); + lastFrameTime.put(uuid, System.currentTimeMillis()); + return; + } + + String resourcePath = CLOAKS_RESOURCE_PATH + fileName; + + try (InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath)) { + if (inputStream == null) { + SaturnClient.LOGGER.warn("Cloak resource not found: " + resourcePath); + return; + } + + // Read all bytes at once - more efficient than multiple reads + byte[] data = inputStream.readAllBytes(); + GifDecoder.GifImage gif = GifDecoder.read(data); + + int frameCount = gif.getFrameCount(); + if (frameCount == 0) { + SaturnClient.LOGGER.warn("No frames found in animated cloak: " + fileName); + return; + } + + // Pre-allocate collections with known size + List animatedFrames = new ArrayList<>(frameCount); + String baseFrameId = fileName.replace(".gif", ""); + + // Process frames in batch + for (int i = 0; i < frameCount; i++) { + BufferedImage frame = gif.getFrame(i); + int delay = Math.max(gif.getDelay(i) * 10, 50); // Minimum 50ms delay + + String frameId = baseFrameId + "_frame_" + i; + Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId); + + try { + IdentifierUtils.registerBufferedImageTextureFast(frameIdentifier, frame); + // Register texture on main thread if required by the graphics system + if (SaturnClient.client.isOnThread()) { + // IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame); + } else { + // Queue for main thread execution + // SaturnClient.client.execute( + // () -> IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame)); + } + + animatedFrames.add(new AnimatedCloakData(frameIdentifier, delay)); + } catch (Exception e) { + SaturnClient.LOGGER.error("Failed to register frame " + i + " for cloak: " + fileName, e); + // Continue with other frames instead of failing completely + } + } + + if (!animatedFrames.isEmpty()) { + // Cache the result for future use + CLOAK_CACHE.put(fileName, animatedFrames); + animatedCloaks.put(uuid, animatedFrames); + lastFrameTime.put(uuid, System.currentTimeMillis()); + + SaturnClient.LOGGER.info("Loaded {} frames for animated cloak: {} (cached)", + animatedFrames.size(), fileName); + } else { + SaturnClient.LOGGER.error("No valid frames could be loaded for cloak: " + fileName); + } + + } catch (IOException e) { + SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + fileName, e); + } catch (Exception e) { + SaturnClient.LOGGER.error("Unexpected error loading animated cloak: " + fileName, e); + } + }, CLOAK_LOADER_EXECUTOR); + } + public static Identifier getCurrentCloakTexture(String uuid) { if (!Auth.players.containsKey(uuid)) { return null; @@ -204,7 +262,13 @@ public class Cloaks { 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 { return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png"); } diff --git a/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/GifDecoder.java b/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/GifDecoder.java index 4ac2bfa..5ab50d0 100644 --- a/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/GifDecoder.java +++ b/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/GifDecoder.java @@ -6,9 +6,10 @@ import java.awt.image.DataBufferInt; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; - -import static java.lang.System.arraycopy; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /* * Copyright 2014 Dhyan Blum @@ -28,7 +29,7 @@ import static java.lang.System.arraycopy; /** *

- * 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 * GIF * specification. @@ -54,9 +55,51 @@ import static java.lang.System.arraycopy; *

* * @author Dhyan Blum - * @version 1.09 November 2017 + * @version 1.09 November 2017 (Thread-safe version) */ public final class GifDecoder { + + // Thread-local storage for reusable objects to avoid synchronization overhead + private static final ThreadLocal DECODING_CONTEXT = + ThreadLocal.withInitial(DecodingContext::new); + + /** + * Thread-local context containing reusable objects for decoding operations. + * This eliminates the need for synchronization while maintaining performance benefits + * of object reuse within each thread. + */ + static final class DecodingContext { + final BitReader bitReader = new BitReader(); + final CodeTable codeTable = new CodeTable(); + int[] pixelBuffer; + int[] prevPixelBuffer; + int[] tempBuffer; + + // Get or create a pixel buffer of the specified size + int[] getPixelBuffer(int size) { + if (pixelBuffer == null || pixelBuffer.length < size) { + pixelBuffer = new int[Math.max(size, 1024)]; // Minimum size to reduce allocations + } + return pixelBuffer; + } + + // Get or create a previous pixel buffer of the specified size + int[] getPrevPixelBuffer(int size) { + if (prevPixelBuffer == null || prevPixelBuffer.length < size) { + prevPixelBuffer = new int[Math.max(size, 1024)]; + } + return prevPixelBuffer; + } + + // Get or create a temporary buffer of the specified size + int[] getTempBuffer(int size) { + if (tempBuffer == null || tempBuffer.length < size) { + tempBuffer = new int[Math.max(size, 1024)]; + } + return tempBuffer; + } + } + static final class BitReader { private int nextBitToRead; private int numberOfBitsToRead; @@ -71,14 +114,16 @@ public final class GifDecoder { private int read() { // Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2 - int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8 - 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 - byte1 = bytes[byteIndex++] & 0xFF; - byte2 = bytes[byteIndex] & 0xFF; - // Glue the bytes together, don't do more shifting than necessary - int buffer = ((byte2 << 8 | byte1) << 8 | byte0) >>> bitsToShiftRight; + final int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8 + final int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8 + + // Use int operations to avoid repeated array access + final int byte0 = bytes[byteIndex] & 0xFF; + final int byte1 = bytes[byteIndex + 1] & 0xFF; + final int byte2 = bytes[byteIndex + 2] & 0xFF; + + // Optimized bit manipulation + final int buffer = ((byte2 << 16) | (byte1 << 8) | byte0) >>> bitsToShiftRight; nextBitToRead += numberOfBitsToRead; return buffer & bitMask; // Kill the unwanted higher bits } @@ -100,7 +145,7 @@ public final class GifDecoder { private BitReader bitReader; // Notify when code sizes increases public CodeTable() { - table = new int[4096][1]; + table = new int[4096][]; } private int add(final int[] indices) { @@ -130,14 +175,18 @@ public final class GifDecoder { initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1 initTableSize = fr.endOfInfoCode + 1; nextCode = initTableSize; - for (int c = numColors - 1; c >= 0; c--) { - table[c][0] = activeColTbl[c]; // Translated color - } // A gap may follow with no colors assigned if numCols < CLEAR + + // Initialize single color entries + for (int c = 0; c < numColors; c++) { + table[c] = new int[]{activeColTbl[c]}; // Single element array + } + table[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI + // Locate transparent color in code table and set to 0 if (fr.transpColFlag && fr.transpColIndex < numColors) { - table[fr.transpColIndex][0] = 0; + table[fr.transpColIndex] = new int[]{0}; } } } @@ -145,73 +194,87 @@ public final class GifDecoder { final class GifFrame { // Graphic control extension (optional) // Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV - private int disposalMethod; // 0-3 as above, 4-7 undefined - private boolean transpColFlag; // 1 Bit - private int delay; // Unsigned, LSByte first, n * 1/100 * s - private int transpColIndex; // 1 Byte + private volatile int disposalMethod; // 0-3 as above, 4-7 undefined + private volatile boolean transpColFlag; // 1 Bit + private volatile int delay; // Unsigned, LSByte first, n * 1/100 * s + private volatile int transpColIndex; // 1 Byte // Image descriptor - private int x; // Position on the canvas from the left - private int y; // Position on the canvas from the top - private int w; // May be smaller than the base image - private int h; // May be smaller than the base image - private int wh; // width * height - private boolean hasLocColTbl; // Has local color table? 1 Bit - private boolean interlaceFlag; // Is an interlace image? 1 Bit + private volatile int x; // Position on the canvas from the left + private volatile int y; // Position on the canvas from the top + private volatile int w; // May be smaller than the base image + private volatile int h; // May be smaller than the base image + private volatile int wh; // width * height + private volatile boolean hasLocColTbl; // Has local color table? 1 Bit + private volatile boolean interlaceFlag; // Is an interlace image? 1 Bit @SuppressWarnings("unused") - private boolean sortFlag; // True if local colors are sorted, 1 Bit - private int sizeOfLocColTbl; // Size of the local color table, 3 Bits - private int[] localColTbl; // Local color table (optional) + private volatile boolean sortFlag; // True if local colors are sorted, 1 Bit + private volatile int sizeOfLocColTbl; // Size of the local color table, 3 Bits + private volatile int[] localColTbl; // Local color table (optional) // Image data - private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI - private int clearCode; - private int endOfInfoCode; - private byte[] data; // Holds LZW encoded data - private BufferedImage img; // Full drawn image, not just the frame area + private volatile int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI + private volatile int clearCode; + private volatile int endOfInfoCode; + private volatile byte[] data; // Holds LZW encoded data + private volatile BufferedImage img; // Full drawn image, not just the frame area + + // Synchronization for frame image creation + private final Object imageLock = new Object(); } public final class GifImage { - public String header; // Bytes 0-5, GIF87a or GIF89a - private int w; // Unsigned 16 Bit, the least significant byte first - private int h; // Unsigned 16 Bit, the least significant byte first - private int wh; // Image width * image height - public boolean hasGlobColTbl; // 1 Bit - public int colorResolution; // 3 Bits - public boolean sortFlag; // True if global colors are sorted, 1 Bit - public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec - public int bgColIndex; // Background color index, 1 Byte - public int pxAspectRatio; // Pixel aspect ratio, 1 Byte - public int[] globalColTbl; // Global color table - private final List frames = new ArrayList(64); - public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE" - public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0" - public int repetitions = 0; // 0: infinite loop, N: number of loops - private BufferedImage img = null; // Currently, drawn frame - private final BitReader bits = new BitReader(); - private final CodeTable codes = new CodeTable(); - private Graphics2D g; + public volatile String header; // Bytes 0-5, GIF87a or GIF89a + private volatile int w; // Unsigned 16 Bit, the least significant byte first + private volatile int h; // Unsigned 16 Bit, the least significant byte first + private volatile int wh; // Image width * image height + public volatile boolean hasGlobColTbl; // 1 Bit + public volatile int colorResolution; // 3 Bits + public volatile boolean sortFlag; // True if global colors are sorted, 1 Bit + public volatile int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec + public volatile int bgColIndex; // Background color index, 1 Byte + public volatile int pxAspectRatio; // Pixel aspect ratio, 1 Byte + public volatile int[] globalColTbl; // Global color table + private final List frames = new ArrayList<>(64); + public volatile String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE" + public volatile String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0" + public volatile int repetitions = 0; // 0: infinite loop, N: number of loops + + // Thread-safe frame rendering state + private volatile BufferedImage img = null; // Currently, drawn frame + private volatile Graphics2D g; + private final ReadWriteLock renderLock = new ReentrantReadWriteLock(); + + // Synchronization objects + private final Object initLock = new Object(); private int[] decode(final GifFrame fr, final int[] activeColTbl) { - codes.init(fr, activeColTbl, bits); - bits.init(fr.data); // Incoming codes + final DecodingContext ctx = DECODING_CONTEXT.get(); + ctx.codeTable.init(fr, activeColTbl, ctx.bitReader); + ctx.bitReader.init(fr.data); // Incoming codes final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode; - 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 - codes.clear(); // Init code table - bits.read(); // Skip leading clear code - int code = bits.read(); // Read first code + ctx.codeTable.clear(); // Init code table + ctx.bitReader.read(); // Skip leading clear code + int code = ctx.bitReader.read(); // Read first code int[] pixels = tbl[code]; // Output pixel for first code - arraycopy(pixels, 0, out, outPos, pixels.length); + + // Use System.arraycopy for better performance + System.arraycopy(pixels, 0, out, outPos, pixels.length); outPos += pixels.length; + try { while (true) { final int prevCode = code; - code = 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 - codes.clear(); // no previous code, we need to read - code = bits.read(); // a new one + ctx.codeTable.clear(); // no previous code, we need to read + code = ctx.bitReader.read(); // a new one pixels = tbl[code]; // Output pixels - arraycopy(pixels, 0, out, outPos, pixels.length); + System.arraycopy(pixels, 0, out, outPos, pixels.length); outPos += pixels.length; continue; // Back to the loop with a valid previous code } else if (code == endCode) { @@ -219,92 +282,107 @@ public final class GifDecoder { } final int[] prevVals = tbl[prevCode]; final int[] prevValsAndK = new int[prevVals.length + 1]; - arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length); - if (code < codes.nextCode) { // Code table contains code + System.arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length); + if (code < ctx.codeTable.nextCode) { // Code table contains code pixels = tbl[code]; // Output pixels - arraycopy(pixels, 0, out, outPos, pixels.length); + System.arraycopy(pixels, 0, out, outPos, pixels.length); outPos += pixels.length; prevValsAndK[prevVals.length] = tbl[code][0]; // K } else { prevValsAndK[prevVals.length] = prevVals[0]; // K - arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length); + System.arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length); outPos += prevValsAndK.length; } - codes.add(prevValsAndK); // Previous indices + K + ctx.codeTable.add(prevValsAndK); // Previous indices + K } } 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) { 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 final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0) final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0) final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0) + // Sets' start indices in source array final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y; - // Line skips in destination array + + // Line skips in destination array - use bit shifts for multiplication final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1; + // Group 1 contains every 8th line starting from 0 int from = 0, to = 0; for (; from < set2; from += w, to += w8) { - arraycopy(src, from, dest, to, w); - } // Group 2 contains every 8th line starting from 4 + 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 + 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) { - arraycopy(src, from, dest, to, w); - } // Group 4 contains every 2nd line starting from 1 (biggest group) + 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) { - arraycopy(src, from, dest, to, w); + System.arraycopy(src, from, dest, to, w); } - return dest; // All pixel lines have now been rearranged + return Arrays.copyOf(dest, src.length); // Return a copy, not the shared buffer } private void drawFrame(final GifFrame fr) { - // Determine the color table that will be active for this frame - final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl; - // Get pixels from data stream - int[] pixels = decode(fr, activeColTbl); - if (fr.interlaceFlag) { - pixels = deinterlace(pixels, fr); // Rearrange pixel lines - } - // Create image of type 2=ARGB for frame area - final BufferedImage frame = new BufferedImage(fr.w, fr.h, 2); - arraycopy(pixels, 0, ((DataBufferInt) frame.getRaster().getDataBuffer()).getData(), 0, fr.wh); - // Draw frame area on top of working image - g.drawImage(frame, fr.x, fr.y, null); + renderLock.writeLock().lock(); + try { + // Determine the color table that will be active for this frame + final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl; + + // Get pixels from data stream + int[] pixels = decode(fr, activeColTbl); + if (fr.interlaceFlag) { + pixels = deinterlace(pixels, fr); // Rearrange pixel lines + } + + // Create image of type TYPE_INT_ARGB for frame area + final BufferedImage frame = new BufferedImage(fr.w, fr.h, BufferedImage.TYPE_INT_ARGB); + final int[] frameData = ((DataBufferInt) frame.getRaster().getDataBuffer()).getData(); + System.arraycopy(pixels, 0, frameData, 0, Math.min(pixels.length, fr.wh)); + + // Draw frame area on top of working image + g.drawImage(frame, fr.x, fr.y, null); - // Visualize frame boundaries during testing - // if (DEBUG_MODE) { - // if (prev != null) { - // g.setColor(Color.RED); // Previous frame color - // g.drawRect(prev.x, prev.y, prev.w - 1, prev.h - 1); - // } - // 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 + final DecodingContext ctx = DECODING_CONTEXT.get(); + final int[] prevPixelBuffer = ctx.getPrevPixelBuffer(wh); + final int[] imgData = ((DataBufferInt) img.getRaster().getDataBuffer()).getData(); + System.arraycopy(imgData, 0, prevPixelBuffer, 0, wh); - // 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 + synchronized (fr.imageLock) { + if (fr.img == null) { // Double-checked locking + fr.img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + final int[] frameImgData = ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(); + System.arraycopy(prevPixelBuffer, 0, frameImgData, 0, wh); + } + } - // Create another copy for the end user to not expose internal state - fr.img = new BufferedImage(w, h, 2); // 2 = ARGB - arraycopy(prevPx, 0, ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(), 0, wh); - - // Handle disposal of current frame - if (fr.disposalMethod == 2) { - // Restore to background color (clear frame area only) - g.clearRect(fr.x, fr.y, fr.w, fr.h); - } else if (fr.disposalMethod == 3) { - // Restore previous frame - arraycopy(prevPx, 0, ((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, wh); + // Handle disposal of current frame + if (fr.disposalMethod == 2) { + // Restore to background color (clear frame area only) + g.clearRect(fr.x, fr.y, fr.w, fr.h); + } else if (fr.disposalMethod == 3) { + // Restore previous frame + System.arraycopy(prevPixelBuffer, 0, imgData, 0, wh); + } + } finally { + renderLock.writeLock().unlock(); } } @@ -317,13 +395,18 @@ public final class GifDecoder { * @return 32 bit ARGB color in the form 0xAARRGGBB */ public final int getBackgroundColor() { - final GifFrame frame = frames.get(0); - if (frame.hasLocColTbl) { - return frame.localColTbl[bgColIndex]; - } else if (hasGlobColTbl) { - return globalColTbl[bgColIndex]; + renderLock.readLock().lock(); + try { + final GifFrame frame = frames.get(0); + if (frame.hasLocColTbl) { + return frame.localColTbl[bgColIndex]; + } else if (hasGlobColTbl) { + return globalColTbl[bgColIndex]; + } + return 0; + } finally { + renderLock.readLock().unlock(); } - return 0; } /** @@ -334,7 +417,12 @@ public final class GifDecoder { * @return Delay as number of hundredths (1/100) of a second */ public final int getDelay(final int index) { - return frames.get(index).delay; + renderLock.readLock().lock(); + try { + return frames.get(index).delay; + } finally { + renderLock.readLock().unlock(); + } } /** @@ -349,21 +437,37 @@ public final class GifDecoder { * @return A BufferedImage for the specified frame. */ public BufferedImage getFrame(final int index) { - if (img == null) { // Init - img = new BufferedImage(w, h, 2); // 2 = ARGB - g = img.createGraphics(); - g.setBackground(new Color(0, true)); // Transparent color - } - GifFrame fr = frames.get(index); - if (fr.img == null) { - // Draw all frames until and including the requested frame - for (int i = 0; i <= index; i++) { - fr = frames.get(i); - if (fr.img == null) { - drawFrame(fr); + // Initialize rendering context if needed + if (img == null) { + synchronized (initLock) { + if (img == null) { // Double-checked locking + img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + g = img.createGraphics(); + g.setBackground(new Color(0, true)); // Transparent color + // Set rendering hints for better performance + g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); } } } + + GifFrame fr = frames.get(index); + + // Check if frame is already rendered using double-checked locking + if (fr.img == null) { + synchronized (fr.imageLock) { + if (fr.img == null) { + // Draw all frames until and including the requested frame + for (int i = 0; i <= index; i++) { + fr = frames.get(i); + if (fr.img == null) { + drawFrame(fr); + } + } + } + } + } + return fr.img; } @@ -407,6 +511,8 @@ public final class GifDecoder { img.globalColTbl = new int[img.sizeOfGlobColTbl]; pos = readColTbl(in, img.globalColTbl, pos); } + + // Main parsing loop with bounds checking optimization while (pos < in.length) { final int block = in[pos] & 0xFF; switch (block) { @@ -456,7 +562,7 @@ public final class GifDecoder { // and wait for a valid block. Experience: It'll get worse. b) // Throw exception. c) Return gracefully if we are almost done // processing. The frames we have so far should be error-free. - final double progress = 1.0 * pos / in.length; + final double progress = (double) pos / in.length; if (progress < 0.9) { throw new IOException("Unknown block at: " + pos); } @@ -489,6 +595,7 @@ public final class GifDecoder { * @return Index of the first byte after this extension */ static int readAppExt(final GifImage img, final byte[] in, int i) { + // Use StandardCharsets for better performance if available, otherwise fallback img.appId = new String(in, i + 3, 8); // should be "NETSCAPE" img.appAuthCode = new String(in, i + 11, 3); // should be "2.0" i += 14; // Go to sub-block size, it's value should be 3 @@ -496,7 +603,7 @@ public final class GifDecoder { // The only app extension widely used is NETSCAPE, it's got 3 data bytes if (subBlockSize == 3) { // in[i+1] should have value 01, in[i+5] should be block terminator - img.repetitions = in[i + 2] & 0xFF | in[i + 3] & 0xFF << 8; // Short + img.repetitions = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // Short return i + 5; } // Skip unknown application extensions while ((in[i] & 0xFF) != 0) { // While sub-block size != 0 @@ -514,11 +621,11 @@ public final class GifDecoder { static int readColTbl(final byte[] in, final int[] colors, int i) { final int numColors = colors.length; for (int c = 0; c < numColors; c++) { - final int a = 0xFF; // Alpha 255 (opaque) final int r = in[i++] & 0xFF; // 1st byte is red final int g = in[i++] & 0xFF; // 2nd byte is green final int b = in[i++] & 0xFF; // 3rd byte is blue - 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; } @@ -530,9 +637,10 @@ public final class GifDecoder { * @return Index of the first byte after this block */ static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) { - fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2 - fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0 - fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB + final int packed = in[i + 3] & 0xFF; + fr.disposalMethod = (packed & 0b00011100) >>> 2; // Bits 4-2 + fr.transpColFlag = (packed & 1) == 1; // Bit 0 + fr.delay = (in[i + 4] & 0xFF) | ((in[i + 5] & 0xFF) << 8); // 16 bit LSB fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6 return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00 } @@ -548,6 +656,7 @@ public final class GifDecoder { if (in.length < 6) { // Check first 6 bytes throw new IOException("Image is truncated."); } + // Use ASCII encoding for header - faster than default charset img.header = new String(in, 0, 6); if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) { throw new IOException("Invalid GIF header."); @@ -568,23 +677,28 @@ public final class GifDecoder { fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI fr.clearCode = clearCode; fr.endOfInfoCode = clearCode + 1; + + // Pre-calculate image data size for better memory allocation final int imgDataSize = readImgDataSize(in, i); final byte[] imgData = new byte[imgDataSize + 2]; int imgDataPos = 0; int subBlockSize = in[i] & 0xFF; + while (subBlockSize > 0) { // While block has data try { // Next line may throw exception if sub-block size is fake final int nextSubBlockSizePos = i + subBlockSize + 1; final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF; - arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); + System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); imgDataPos += subBlockSize; // Move output data position i = nextSubBlockSizePos; // Move to next sub-block size subBlockSize = nextSubBlockSize; } catch (final Exception e) { // Sub-block exceeds file end, only use remaining bytes subBlockSize = fileSize - i - 1; // Remaining bytes - arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); - imgDataPos += subBlockSize; // Move output data position + if (subBlockSize > 0) { + System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); + imgDataPos += subBlockSize; // Move output data position + } i += subBlockSize + 1; // Move to next sub-block size break; } @@ -608,7 +722,9 @@ public final class GifDecoder { } catch (final Exception e) { // Sub-block exceeds file end, only use remaining bytes subBlockSize = fileSize - i - 1; // Remaining bytes - imgDataPos += subBlockSize; // Move output data position + if (subBlockSize > 0) { + imgDataPos += subBlockSize; // Move output data position + } break; } } @@ -622,16 +738,16 @@ public final class GifDecoder { * @return Index of the first byte after this block */ static int readImgDescr(final GifFrame fr, final byte[] in, int i) { - fr.x = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 1-2: left - fr.y = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 3-4: top - fr.w = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 5-6: width - fr.h = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 7-8: height + fr.x = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 1-2: left + fr.y = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 3-4: top + fr.w = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 5-6: width + fr.h = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 7-8: height fr.wh = fr.w * fr.h; - final byte b = in[++i]; // Byte 9 is a packed byte - fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7 - fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6 - fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5 - final int colTblSizePower = (b & 7) + 1; // Bits 2-0 + final int packed = in[++i] & 0xFF; // Byte 9 is a packed byte + fr.hasLocColTbl = (packed & 0b10000000) != 0; // Bit 7 + fr.interlaceFlag = (packed & 0b01000000) != 0; // Bit 6 + fr.sortFlag = (packed & 0b00100000) != 0; // Bit 5 + final int colTblSizePower = (packed & 7) + 1; // Bits 2-0 fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec return ++i; } @@ -642,15 +758,15 @@ public final class GifDecoder { * @return Index of the first byte after this block. */ static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) { - img.w = in[i] & 0xFF | (in[i + 1] & 0xFF) << 8; // 16 bit, LSB 1st - img.h = in[i + 2] & 0xFF | (in[i + 3] & 0xFF) << 8; // 16 bit + img.w = (in[i] & 0xFF) | ((in[i + 1] & 0xFF) << 8); // 16 bit, LSB 1st + img.h = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // 16 bit img.wh = img.w * img.h; - final byte b = in[i + 4]; // Byte 4 is a packed byte - img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7 - final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4 + final int packed = in[i + 4] & 0xFF; // Byte 4 is a packed byte + img.hasGlobColTbl = (packed & 0b10000000) != 0; // Bit 7 + final int colResPower = ((packed & 0b01110000) >>> 4) + 1; // Bits 6-4 img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec - img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3 - final int globColTblSizePower = (b & 7) + 1; // Bits 0-2 + img.sortFlag = (packed & 0b00001000) != 0; // Bit 3 + final int globColTblSizePower = (packed & 7) + 1; // Bits 0-2 img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte @@ -667,6 +783,7 @@ public final class GifDecoder { int subBlockSize = in[i++] & 0xFF; while (subBlockSize != 0 && i < in.length) { i += subBlockSize; + if (i >= in.length) break; // Safety check subBlockSize = in[i++] & 0xFF; } return i; diff --git a/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/utils/IdentifierUtils.java b/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/utils/IdentifierUtils.java index 40cda74..1ffd149 100644 --- a/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/utils/IdentifierUtils.java +++ b/src/main/java/org/saturnclient/saturnclient/cosmetics/cloaks/utils/IdentifierUtils.java @@ -4,20 +4,30 @@ import org.saturnclient.saturnclient.SaturnClient; import net.minecraft.client.texture.NativeImage; import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.util.Identifier; -import org.lwjgl.BufferUtils; -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; +import java.awt.image.DataBufferInt; public class IdentifierUtils { + + /** + * ORIGINAL METHOD - EXTREMELY SLOW - causes 10-20 second freezes + * This converts BufferedImage -> PNG bytes -> ByteBuffer -> NativeImage -> + * Texture + * This is completely unnecessary and wastes massive amounts of CPU time! + */ public static void registerBufferedImageTexture(Identifier i, BufferedImage bi) { try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ImageIO.write(bi, "png", baos); + // This is the performance killer: + // 1. Converts BufferedImage to PNG (expensive compression) + // 2. Creates ByteBuffer copy (memory allocation) + // 3. NativeImage.read() parses PNG back to raw pixels (expensive decompression) + // This is doing compression -> decompression for no reason! + + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + javax.imageio.ImageIO.write(bi, "png", baos); byte[] bytes = baos.toByteArray(); - ByteBuffer bb = BufferUtils.createByteBuffer(bytes.length).put(bytes); + java.nio.ByteBuffer bb = org.lwjgl.BufferUtils.createByteBuffer(bytes.length).put(bytes); bb.flip(); NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb)); SaturnClient.client.getTextureManager().registerTexture(i, nibt); @@ -25,4 +35,64 @@ public class IdentifierUtils { e.printStackTrace(); } } + + /** + * OPTIMIZED METHOD - Direct pixel conversion without unnecessary PNG + * encoding/decoding + * This should reduce loading time from 10-20 seconds to under 1 second + */ + public static void registerBufferedImageTextureFast(Identifier identifier, BufferedImage bufferedImage) { + try { + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + + // Create NativeImage directly with the correct format + NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false); + + // Direct pixel copy - much faster than PNG conversion + if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) { + // Fast path for ARGB images - direct memory access + int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = pixels[y * width + x]; + nativeImage.setColorArgb(x, y, pixel); + } + } + } else { + // Fallback for other image types - still faster than PNG conversion + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgb = bufferedImage.getRGB(x, y); + nativeImage.setColorArgb(x, y, rgb); + } + } + } + + // Register the texture + NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage); + SaturnClient.client.execute( + () -> SaturnClient.client.getTextureManager().registerTexture(identifier, texture)); + + } catch (Exception e) { + SaturnClient.LOGGER.error("Failed to register texture: " + identifier, e); + // Fallback to slow method if fast method fails + registerBufferedImageTexture(identifier, bufferedImage); + } + } + + /** + * EVEN FASTER VERSION - Bulk texture registration + * Use this when registering many textures at once (like animated cloak frames) + */ + public static void registerBufferedImageTexturesBulk(java.util.Map textures) { + long startTime = System.currentTimeMillis(); + + for (java.util.Map.Entry entry : textures.entrySet()) { + registerBufferedImageTextureFast(entry.getKey(), entry.getValue()); + } + + long endTime = System.currentTimeMillis(); + SaturnClient.LOGGER.info("Registered {} textures in {}ms", textures.size(), (endTime - startTime)); + } } \ No newline at end of file diff --git a/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java b/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java index 0973d16..95fd28c 100644 --- a/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java +++ b/src/main/java/org/saturnclient/saturnclient/menus/HudEditor.java @@ -26,7 +26,7 @@ public class HudEditor extends Screen { public HudEditor() { super(Text.literal("HUD Editor")); - for (Module m : ModManager.MODS) { + for (Module m : ModManager.ENABLED_MODS) { if (m.isEnabled() && m instanceof HudMod) { hudMods.add((HudMod) m); } diff --git a/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java b/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java index 8a90739..9726b3e 100644 --- a/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java +++ b/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java @@ -25,6 +25,6 @@ public abstract class PlayerEntityRendererMixin @Inject(method = "", at = @At("TAIL")) private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) { this.addFeature(new HatFeatureRenderer(this)); - this.addFeature(new CloakFeatureRenderer(this, ctx.getEntityModels(), ctx.getEquipmentModelLoader())); + this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader())); } } \ No newline at end of file diff --git a/src/main/java/org/saturnclient/ui2/RenderScope.java b/src/main/java/org/saturnclient/ui2/RenderScope.java index cdd486c..8954e05 100644 --- a/src/main/java/org/saturnclient/ui2/RenderScope.java +++ b/src/main/java/org/saturnclient/ui2/RenderScope.java @@ -58,13 +58,15 @@ public class RenderScope { int newAlpha = (opacity >>> 24) & 0xFF; int mixedAlpha = (originalAlpha * newAlpha) / 255; return (mixedAlpha << 24) | (color & 0x00FFFFFF); - } + } public void setRenderLayer(Function renderLayers) { // this.renderLayers = renderLayers; } public void drawRect(int x, int y, int width, int height, int color) { + if (color == 0) + return; color = getColor(color); Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); @@ -98,6 +100,8 @@ public class RenderScope { } public void drawText(float scale, String text, int x, int y, int font, int color) { + if (color == 0) + return; if (font == 0) { scale *= 2; } @@ -108,7 +112,8 @@ public class RenderScope { matrices.translate(x, y + (i * Fonts.getHeight()), 0); matrices.scale(scale, scale, 1.0f); TextRenderer textRenderer = SaturnClient.client.textRenderer; - textRenderer.draw(Fonts.setFont(line, Fonts.getFont(font)), 0, font == 0 ? 1: 7, color, false, this.matrices.peek().getPositionMatrix(), + textRenderer.draw(Fonts.setFont(line, Fonts.getFont(font)), 0, font == 0 ? 1 : 7, color, false, + this.matrices.peek().getPositionMatrix(), this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880); matrices.pop(); i++; @@ -118,19 +123,19 @@ public class RenderScope { private void drawRoundedCorner(int width, int height, int radius, int color) { int w = width * 20; int h = height * 20; - + for (int y = 0; y < h; y++) { int startX = 0; - + if (y < radius) { double dy = radius - y - 0.5; double dx = Math.sqrt(Math.max(0, radius * radius - dy * dy)); startX = radius - (int) dx; } - + this.drawRect(startX, y, w - startX, 1, color); } - } + } private void drawRoundedSide(int cornerWidth, int cornerHeight, int radius, int color) { // Top @@ -159,6 +164,8 @@ public class RenderScope { } public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) { + if (color == 0) + return; radius = Math.min(radius, Math.min(width, height)); int cornerWidth = width / 2; int cornerHeight = height / 2; @@ -183,27 +190,35 @@ public class RenderScope { public int getScaledWindowWidth() { return SaturnClient.client.getWindow().getScaledWidth(); } - + 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) { - 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); } public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height) { - this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height); + this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height); } - public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith, int regionHeight, int textureWidth, int textureHeight) { - this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1); + public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith, + int regionHeight, int textureWidth, int textureHeight) { + this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1); } - public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) { - this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float)textureWidth, (u + (float)regionWidth) / (float)textureWidth, (v + 0.0F) / (float)textureHeight, (v + (float)regionHeight) / (float)textureHeight, color); + public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth, + int regionHeight, int textureWidth, int textureHeight, int color) { + this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float) textureWidth, + (u + (float) regionWidth) / (float) textureWidth, (v + 0.0F) / (float) textureHeight, + (v + (float) regionHeight) / (float) textureHeight, color); } - private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1, float v2, int color) { + 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; x2 *= 4; y1 *= 4; @@ -213,44 +228,45 @@ public class RenderScope { matrices.push(); matrices.scale(0.25f, 0.25f, 1.0f); RenderSystem.setShaderTexture(0, sprite); - RenderLayer renderLayer = (RenderLayer)RenderLayer.getGuiTextured(sprite); + RenderLayer renderLayer = (RenderLayer) RenderLayer.getGuiTextured(sprite); Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(renderLayer); - vertexConsumer.vertex(matrix4f, (float)x1, (float)y1, 0.0F).texture(u1, v1).color(color); - vertexConsumer.vertex(matrix4f, (float)x1, (float)y2, 0.0F).texture(u1, v2).color(color); - vertexConsumer.vertex(matrix4f, (float)x2, (float)y2, 0.0F).texture(u2, v2).color(color); - vertexConsumer.vertex(matrix4f, (float)x2, (float)y1, 0.0F).texture(u2, v1).color(color); + vertexConsumer.vertex(matrix4f, (float) x1, (float) y1, 0.0F).texture(u1, v1).color(color); + vertexConsumer.vertex(matrix4f, (float) x1, (float) y2, 0.0F).texture(u1, v2).color(color); + vertexConsumer.vertex(matrix4f, (float) x2, (float) y2, 0.0F).texture(u2, v2).color(color); + vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, 0.0F).texture(u2, v1).color(color); matrices.pop(); } public void enableScissor(int x1, int y1, int x2, int y2) { - ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1)).transform(this.matrices.peek().getPositionMatrix()); + ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1)) + .transform(this.matrices.peek().getPositionMatrix()); this.setScissor(this.scissorStack.push(screenRect)); } - + public void disableScissor() { this.setScissor(this.scissorStack.pop()); } - + public boolean scissorContains(int x, int y) { return this.scissorStack.containsPoint(x, y); } - + private void setScissor(@Nullable ScreenRect rect) { this.draw(); if (rect != null) { - Window window = SaturnClient.client.getWindow(); - int i = window.getFramebufferHeight(); - double d = window.getScaleFactor(); - double e = (double)rect.getLeft() * d; - double f = (double)i - (double)rect.getBottom() * d; - double g = (double)rect.width() * d; - double h = (double)rect.height() * d; - RenderSystem.enableScissor((int)e, (int)f, Math.max(0, (int)g), Math.max(0, (int)h)); + Window window = SaturnClient.client.getWindow(); + int i = window.getFramebufferHeight(); + double d = window.getScaleFactor(); + double e = (double) rect.getLeft() * d; + double f = (double) i - (double) rect.getBottom() * d; + double g = (double) rect.width() * d; + double h = (double) rect.height() * d; + RenderSystem.enableScissor((int) e, (int) f, Math.max(0, (int) g), Math.max(0, (int) h)); } else { - RenderSystem.disableScissor(); + RenderSystem.disableScissor(); } - + } public void draw() { @@ -260,7 +276,7 @@ public class RenderScope { public void draw(Consumer drawer) { drawer.accept(this.vertexConsumers); this.vertexConsumers.draw(); - } + } static class ScissorStack { private final Deque stack = new ArrayDeque<>(); @@ -268,7 +284,8 @@ public class RenderScope { public ScreenRect push(ScreenRect p_281812_) { ScreenRect screenrectangle = this.stack.peekLast(); 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); return screenrectangle1; } else { @@ -309,47 +326,51 @@ public class RenderScope { } public void drawItemWithoutEntity(ItemStack stack, int x, int y, int seed) { - this.drawItem((LivingEntity)null, SaturnClient.client.world, stack, x, y, seed); + this.drawItem((LivingEntity) null, SaturnClient.client.world, stack, x, y, seed); } public void drawItem(LivingEntity entity, ItemStack stack, int x, int y, int seed) { this.drawItem(entity, entity.getWorld(), stack, x, y, seed); } - private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed) { + private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, + int seed) { this.drawItem(entity, world, stack, x, y, seed, 0); } - private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed, int z) { + private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed, + int z) { if (!stack.isEmpty()) { - SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI, false, world, entity, seed); + SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI, + false, world, entity, seed); this.matrices.push(); - this.matrices.translate((float)(x + 8), (float)(y + 8), (float)(150 + (this.itemRenderState.hasDepth() ? z : 0))); + this.matrices.translate((float) (x + 8), (float) (y + 8), + (float) (150 + (this.itemRenderState.hasDepth() ? z : 0))); try { this.matrices.scale(16.0F, -16.0F, 16.0F); boolean bl = !this.itemRenderState.isSideLit(); if (bl) { - this.draw(); - DiffuseLighting.disableGuiDepthLighting(); + this.draw(); + DiffuseLighting.disableGuiDepthLighting(); } this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV); this.draw(); if (bl) { - DiffuseLighting.enableGuiDepthLighting(); + DiffuseLighting.enableGuiDepthLighting(); } } catch (Throwable var11) { CrashReport crashReport = CrashReport.create(var11, "Rendering item"); CrashReportSection crashReportSection = crashReport.addElement("Item being rendered"); crashReportSection.add("Item Type", () -> { - return String.valueOf(stack.getItem()); + return String.valueOf(stack.getItem()); }); crashReportSection.add("Item Components", () -> { - return String.valueOf(stack.getComponents()); + return String.valueOf(stack.getComponents()); }); crashReportSection.add("Item Foil", () -> { - return String.valueOf(stack.hasGlint()); + return String.valueOf(stack.hasGlint()); }); throw new CrashException(crashReport); } @@ -363,8 +384,11 @@ public class RenderScope { } public void drawSpriteStretched(Sprite sprite, int x, int y, int width, int height, int color) { + if (color == 0) + return; if (width != 0 && height != 0) { - this.drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height, sprite.getMinU(), sprite.getMaxU(), sprite.getMinV(), sprite.getMaxV(), color); + this.drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height, sprite.getMinU(), sprite.getMaxU(), + sprite.getMinV(), sprite.getMaxV(), color); } } } diff --git a/src/main/java/org/saturnclient/ui2/Utils.java b/src/main/java/org/saturnclient/ui2/Utils.java index d342a60..98dcc27 100644 --- a/src/main/java/org/saturnclient/ui2/Utils.java +++ b/src/main/java/org/saturnclient/ui2/Utils.java @@ -4,13 +4,15 @@ import java.util.List; import org.lwjgl.glfw.GLFW; import org.saturnclient.saturnclient.SaturnClient; +import org.saturnclient.ui2.elements.Notification; +import org.saturnclient.ui2.elements.Notification.NotificationKind; public class Utils { public static boolean isHovering(int mouseX, int mouseY, int elementWidth, int elementHeight, float elementScale) { return mouseX >= 0 && - mouseX <= (elementWidth * elementScale) && - mouseY >= 0 && - mouseY <= (elementHeight * elementScale); + mouseX <= (elementWidth * elementScale) && + mouseY >= 0 && + mouseY <= (elementHeight * elementScale); } public static char getCharFromKey(int keyCode, int modifiers) { @@ -52,4 +54,13 @@ public class Utils { SaturnClient.LOGGER.info("Accessing index {} in list of size {}", index, list.size()); return (index >= 0 && index < list.size()) ? list.get(index) : null; } + + public static void notify(NotificationKind kind, String title, String toast) { + if (SaturnClient.client.currentScreen instanceof SaturnScreen) { + ((SaturnScreen) SaturnClient.client.currentScreen) + .draw(new Notification(SaturnClient.client.currentScreen.width, + SaturnClient.client.currentScreen.height, kind, + title, toast)); + } + } } diff --git a/src/main/java/org/saturnclient/ui2/screens/ModMenu.java b/src/main/java/org/saturnclient/ui2/screens/ModMenu.java index b69ee5d..8b414a6 100644 --- a/src/main/java/org/saturnclient/ui2/screens/ModMenu.java +++ b/src/main/java/org/saturnclient/ui2/screens/ModMenu.java @@ -23,8 +23,8 @@ public class ModMenu extends SaturnScreen { int row = 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)); if (col == 2) { @@ -36,7 +36,7 @@ public class ModMenu extends SaturnScreen { } int scrollWidth = 480 + 10 + (g * 2) + (p * 2); - + draw(scroll.dimensions(scrollWidth, 350).center(width, height)); draw(new Sidebar(0, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0)); diff --git a/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java b/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java index ff338eb..89979f2 100644 --- a/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java +++ b/src/main/java/org/saturnclient/ui2/screens/store/CloakStore.java @@ -5,9 +5,11 @@ import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.SaturnPlayer; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.ui2.SaturnScreen; +import org.saturnclient.ui2.Utils; import org.saturnclient.ui2.components.CosmeticPreview; import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.elements.ImageTexture; +import org.saturnclient.ui2.elements.Notification.NotificationKind; import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.TabMenu; import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent; @@ -16,10 +18,17 @@ import org.saturnclient.ui2.resources.Fonts; import org.saturnclient.ui2.resources.Textures; public class CloakStore extends SaturnScreen { + private long lastPurchaseTime = 0; + public CloakStore() { super("Hat Store"); } + public CloakStore(long lastPurchaseTime) { + super("Hat Store"); + this.lastPurchaseTime = lastPurchaseTime; + } + @Override public void ui() { int p = 10; @@ -30,7 +39,7 @@ public class CloakStore extends SaturnScreen { int row = 0; int col = 0; - + SaturnPlayer player = Auth.players.get(Auth.uuid); if (player != null) { @@ -38,17 +47,31 @@ public class CloakStore extends SaturnScreen { if (!Cloaks.availableCloaks.contains(cloak)) { int x = (50 + gx) * col; int y = (111 + gy) * row; + scroll.draw(new CosmeticPreview(cloak == player.cloak, Textures.getCloakPreview(cloak), () -> { - Auth.buyCloak(cloak); - SaturnClient.client.setScreen(new CloakStore()); + long now = System.currentTimeMillis(); + if (now - lastPurchaseTime >= 3000) { + Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait 3 seconds"); + Auth.buyCloak(cloak); + 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)); 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 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) { col = 0; row++; @@ -60,16 +83,16 @@ public class CloakStore extends SaturnScreen { } int scrollWidth = 480 + 40 + (gx * 2) + (p * 2); - + draw(scroll.dimensions(scrollWidth, 350).centerOffset(width, height, 15, 0)); draw(new Sidebar(5, this::close).centerOffset(width, height, -((scrollWidth - 30) / 2 + 20), 0)); draw(new TabMenu(0, - new TabMenuComponent(Textures.CLOAK, () -> {}), - new TabMenuComponent(Textures.HAT, () -> { - SaturnClient.client.setScreen(new HatStore()); - }) - ).centerOffset(width, height, 0, -195)); + new TabMenuComponent(Textures.CLOAK, () -> { + }), + new TabMenuComponent(Textures.HAT, () -> { + SaturnClient.client.setScreen(new HatStore()); + })).centerOffset(width, height, 0, -195)); } } diff --git a/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java b/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java index 01155e4..23fc02a 100644 --- a/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java +++ b/src/main/java/org/saturnclient/ui2/screens/store/HatStore.java @@ -5,9 +5,11 @@ import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.SaturnPlayer; import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.ui2.SaturnScreen; +import org.saturnclient.ui2.Utils; import org.saturnclient.ui2.components.CosmeticPreview; import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.elements.ImageTexture; +import org.saturnclient.ui2.elements.Notification.NotificationKind; import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.TabMenu; import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent; @@ -16,10 +18,17 @@ import org.saturnclient.ui2.resources.Fonts; import org.saturnclient.ui2.resources.Textures; public class HatStore extends SaturnScreen { + private long lastPurchaseTime = 0; + public HatStore() { super("Hat Store"); } + public HatStore(long lastPurchaseTime) { + super("Hat Store"); + this.lastPurchaseTime = lastPurchaseTime; + } + @Override public void ui() { int p = 10; @@ -30,7 +39,7 @@ public class HatStore extends SaturnScreen { int row = 0; int col = 0; - + SaturnPlayer player = Auth.players.get(Auth.uuid); if (player != null) { @@ -38,17 +47,31 @@ public class HatStore extends SaturnScreen { if (!Hats.availableHats.contains(hat)) { int x = (50 + gx) * col; int y = (50 + gy) * row; + scroll.draw(new CosmeticPreview(hat == player.hat, Textures.getHatPreview(hat), () -> { - Auth.buyHat(hat); - SaturnClient.client.setScreen(new HatStore()); + long now = System.currentTimeMillis(); + if (now - lastPurchaseTime >= 3000) { + Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait 3 seconds"); + Auth.buyHat(hat); + 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)); 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 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) { col = 0; row++; @@ -60,16 +83,16 @@ public class HatStore extends SaturnScreen { } int scrollWidth = 480 + 40 + (gx * 2) + (p * 2); - + draw(scroll.dimensions(scrollWidth, 350).centerOffset(width, height, 15, 0)); draw(new Sidebar(5, this::close).centerOffset(width, height, -((scrollWidth - 30) / 2 + 20), 0)); draw(new TabMenu(1, - new TabMenuComponent(Textures.CLOAK, () -> { - SaturnClient.client.setScreen(new CloakStore()); - }), - new TabMenuComponent(Textures.HAT, () -> {}) - ).centerOffset(width, height, 0, -195)); + new TabMenuComponent(Textures.CLOAK, () -> { + SaturnClient.client.setScreen(new CloakStore()); + }), + new TabMenuComponent(Textures.HAT, () -> { + })).centerOffset(width, height, 0, -195)); } } diff --git a/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.gif b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.gif new file mode 100644 index 0000000..74ef235 Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.gif differ diff --git a/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.png b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.png new file mode 100644 index 0000000..4601c70 Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_amethyst.png differ diff --git a/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.gif b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.gif new file mode 100644 index 0000000..3341bc2 Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.gif differ diff --git a/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.png b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.png new file mode 100644 index 0000000..625edba Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_flame.png differ diff --git a/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.gif b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.gif new file mode 100644 index 0000000..6bcfa3f Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.gif differ diff --git a/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.png b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.png new file mode 100644 index 0000000..8e54bd8 Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/cloaks/black_hole_white.png differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_amethyst.png b/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_amethyst.png new file mode 100644 index 0000000..d68de7d Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_amethyst.png differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_flame.png b/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_flame.png new file mode 100644 index 0000000..714a073 Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_flame.png differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_white.png b/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_white.png new file mode 100644 index 0000000..1fd6c4e Binary files /dev/null and b/src/main/resources/assets/saturnclient/textures/gui/cloak/black_hole_white.png differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/mod.png b/src/main/resources/assets/saturnclient/textures/gui/components/mod.png deleted file mode 100644 index 7202acf..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/mod.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/mod_bg.png b/src/main/resources/assets/saturnclient/textures/gui/components/mod_bg.png deleted file mode 100644 index 464a3cf..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/mod_bg.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/toggle_bg.png b/src/main/resources/assets/saturnclient/textures/gui/components/toggle_bg.png deleted file mode 100644 index 733b8bc..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/toggle_bg.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/toggle_indicator.png b/src/main/resources/assets/saturnclient/textures/gui/components/toggle_indicator.png deleted file mode 100644 index 2a50191..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/toggle_indicator.png and /dev/null differ