diff --git a/common/src/main/java/org/saturnclient/common/minecraft/MinecraftProvider.java b/common/src/main/java/org/saturnclient/common/minecraft/MinecraftProvider.java index b6ad252..79723c2 100644 --- a/common/src/main/java/org/saturnclient/common/minecraft/MinecraftProvider.java +++ b/common/src/main/java/org/saturnclient/common/minecraft/MinecraftProvider.java @@ -4,4 +4,6 @@ public abstract class MinecraftProvider { public static MinecraftProvider PROVIDER; public abstract Object createIdentifier(String namespace, String path); + + public abstract boolean isKeyPressed(int key); } diff --git a/common/src/main/java/org/saturnclient/config/AnimationConfig.java b/common/src/main/java/org/saturnclient/config/AnimationConfig.java new file mode 100644 index 0000000..ae37336 --- /dev/null +++ b/common/src/main/java/org/saturnclient/config/AnimationConfig.java @@ -0,0 +1,55 @@ +package org.saturnclient.config; + +import org.saturnclient.saturnclient.config.manager.ConfigManager; +import org.saturnclient.saturnclient.config.manager.Property; + +public class AnimationConfig { + public static ConfigManager config; + + // Logo + public static final AnimationConfig logo = new AnimationConfig(true, 700, 0); + + // Main menu + public static final AnimationConfig mainMenu = new AnimationConfig(true, 700, 120); + + // Mod menu + public static final AnimationConfig modMenu = new AnimationConfig(true, 700, 20); + + // Shift menu + public static final AnimationConfig shiftMenu = new AnimationConfig(true, 700, 100); + + // Cosmetics menu + public static final AnimationConfig cosmeticsMenu = new AnimationConfig(true, 700, 40); + + // Emote wheel + public static final AnimationConfig emoteWheel = new AnimationConfig(true, 700, 40); + + public static void init(ConfigManager parent) { + config = new ConfigManager(parent, "Animations"); + + logo.init("Logo"); + mainMenu.init("Main Menu"); + modMenu.init("Mod Menu"); + shiftMenu.init("Shift Menu"); + cosmeticsMenu.init("Cosmetics Menu"); + emoteWheel.init("Emote Wheel"); + } + + // Animation properties + public final Property duration; + public final Property stagger; + public final Property curve; + + public AnimationConfig(boolean enabled, int delay, int stagger) { + this.duration = Property.integer(delay); + this.stagger = Property.integer(stagger); + this.curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back"); + } + + public void init(String name) { + // config.property("Enable " + name + " Animations", this.enabled); + config.property(name + " Duration", this.duration); + config.property(name + " Stagger", this.stagger); + config.property(name + " Curve", this.curve); + } +} diff --git a/common/src/main/java/org/saturnclient/config/Config.java b/common/src/main/java/org/saturnclient/config/Config.java new file mode 100644 index 0000000..1f8df23 --- /dev/null +++ b/common/src/main/java/org/saturnclient/config/Config.java @@ -0,0 +1,37 @@ +package org.saturnclient.saturnclient.config; + +import org.lwjgl.glfw.GLFW; +import org.saturnclient.saturnclient.config.manager.ConfigManager; +import org.saturnclient.saturnclient.config.manager.Property; +import org.saturnclient.ui.resources.Textures; + +import net.minecraft.util.Identifier; + +public class Config { + public static ConfigManager config; + + public static Property realisticLogo = Property.bool(false); + public static Property saturnTitleScreen = Property.bool(true); + public static Property cloakPhysics = Property.bool(true); + public static Property openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B); + public static Property stagger = Property.bool(true); + + public static Identifier getLogo() { + return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO; + } + + public static void init() { + config = new ConfigManager("Saturn Client"); + config.property("Realistic logo", realisticLogo); + config.property("Saturn client title screen", saturnTitleScreen); + config.property("Open Emote Wheel", openEmoteWheel); + config.property("Cloak Physics", cloakPhysics); + config.property("Stagger Animations", stagger); + + // Initialize a sub namespace for theme + Theme.init(config); + + // Initialize a sub namespace for animations + AnimationConfig.init(config); + } +} diff --git a/common/src/main/java/org/saturnclient/config/Theme.java b/common/src/main/java/org/saturnclient/config/Theme.java new file mode 100644 index 0000000..6da4e1a --- /dev/null +++ b/common/src/main/java/org/saturnclient/config/Theme.java @@ -0,0 +1,91 @@ +package org.saturnclient.saturnclient.config; + +import org.saturnclient.saturnclient.config.manager.ConfigManager; +import org.saturnclient.saturnclient.config.manager.Property; + +public class Theme { + // Main UI background (slightly warm dark to match Saturn tone) + public static final Property BACKGROUND = Property.color(0xFF12100D); + + // Default text color + public static final Property FOREGROUND = Property.color(0xFFF2E6D2); + + // Widget default background (normal state) + public static final Property PRIMARY = Property.color(0xFF1C1915); + + // Widget text color + public static final Property PRIMARY_FG = Property.color(0xFFEAD9B8); + + // Saturn brand color (hovered / enabled / selected) + public static final Property ACCENT = Property.color(0xFFE2BF7D); + + // Text/icons shown on accent background + public static final Property ACCENT_FG = Property.color(0xFF1A140A); + + // Misc colors + public static final Property SCROLL = Property.color(0xFFE2BF7D); + + // UI metrics + public static final Property BG_RADIUS = Property.integer(12); + public static final Property WIDGET_RADIUS = Property.integer(10); + + // Font + public static final Property FONT = Property.font(2); + + public static void init(ConfigManager parent) { + ConfigManager config = new ConfigManager(parent, "UI Style"); + config.property("Background", BACKGROUND); + config.property("Foreground", FOREGROUND); + config.property("Primary", PRIMARY); + config.property("Primary Foreground", PRIMARY_FG); + config.property("Accent", ACCENT); + config.property("Accent Foreground", ACCENT_FG); + config.property("Scrollbar Color", SCROLL); + + config.property("Background Radius", BG_RADIUS); + config.property("Widget Radius", WIDGET_RADIUS); + + config.property("Font", FONT); + } + + /** + * primary by default, accent if true + */ + public static int getBg(boolean hover) { + return hover ? ACCENT.value : PRIMARY.value; + } + + /** + * (Foreground) primary by default, accent if true + */ + public static int getFg(boolean hover) { + return hover ? ACCENT_FG.value : PRIMARY_FG.value; + } + + /** + * Multiplies existing alpha with 0–255 alpha. + */ + public static int withAlpha(int alpha, int color) { + alpha = Math.max(0, Math.min(255, alpha)); + + int originalAlpha = (color >>> 24) & 0xFF; + + // Merge (multiply) alphas + int merged = (originalAlpha * alpha) / 255; + + return (color & 0x00FFFFFF) | (merged << 24); + } + + /** + * Multiplies existing alpha with 0.0–1.0 alpha. + */ + public static int withAlpha(float alpha, int color) { + alpha = Math.max(0f, Math.min(1f, alpha)); + + int originalAlpha = (color >>> 24) & 0xFF; + + int merged = (int) (originalAlpha * alpha); + + return (color & 0x00FFFFFF) | (merged << 24); + } +} diff --git a/common/src/main/java/org/saturnclient/config/manager/ConfigManager.java b/common/src/main/java/org/saturnclient/config/manager/ConfigManager.java new file mode 100644 index 0000000..284fb03 --- /dev/null +++ b/common/src/main/java/org/saturnclient/config/manager/ConfigManager.java @@ -0,0 +1,197 @@ +package org.saturnclient.config.manager; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.LinkedHashMap; +import java.util.Map; + +public class ConfigManager { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // SINGLE ROOT TREE + private static final Map> ROOT = new LinkedHashMap<>(); + + private static JsonNode cachedJson = null; + + private final Map> currentMap; + + private final String[] path; + + private File configFile; + + /** + * Creates a root namespace + */ + public ConfigManager(File configFile, String namespace) { + this.configFile = configFile; + this.currentMap = new LinkedHashMap<>(); + this.path = new String[] { namespace }; + + ROOT.put(namespace, Property.namespace(currentMap)); + } + + /** + * Creates a sub-namespace inside a parent + */ + public ConfigManager(ConfigManager parent, String namespace) { + this.configFile = parent.configFile; + this.currentMap = new LinkedHashMap<>(); + + this.path = new String[parent.path.length + 1]; + System.arraycopy(parent.path, 0, this.path, 0, parent.path.length); + this.path[this.path.length - 1] = namespace; + + parent.property(namespace, Property.namespace(currentMap)); + } + + /** + * Add a property + */ + public Property property(String name, Property value) { + currentMap.put(name, value); + loadSingleProperty(name, value); + return value; + } + + /** + * Add empty namespace + */ + public ConfigManager sub(String name) { + return new ConfigManager(this, name); + } + + /** + * Load entire config + */ + public void load() { + JsonNode json = loadAndCache(); + if (json == null || !json.isObject()) + return; + + loadRecursive((ObjectNode) json, ROOT); + } + + /** + * Save entire config + */ + public void save() { + + ObjectNode rootJson = MAPPER.createObjectNode(); + saveRecursive(rootJson, ROOT); + + try { + MAPPER.writerWithDefaultPrettyPrinter().writeValue(configFile, rootJson); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Recursive save + */ + private static void saveRecursive(ObjectNode json, Map> map) { + map.forEach((name, prop) -> { + if (prop.isNamespace()) { + + ObjectNode child = json.objectNode(); + saveRecursive(child, prop.getNamespaceMap()); + + json.set(name, child); + + } else { + + json.set(name, prop.toJson()); + + } + }); + } + + /** + * Recursive load + */ + private static void loadRecursive(ObjectNode json, Map> map) { + + map.forEach((name, prop) -> { + + JsonNode element = json.get(name); + if (element == null) + return; + + if (prop.isNamespace() && element.isObject()) { + + loadRecursive((ObjectNode) element, prop.getNamespaceMap()); + + } else { + + prop.loadFromJson(element); + + } + + }); + } + + /** + * Load only a single property immediately after creation + */ + private void loadSingleProperty(String name, Property prop) { + + JsonNode source = cachedJson != null ? cachedJson : loadAndCache(); + if (source == null || !source.isObject()) + return; + + JsonNode currentScope = source; + + for (String segment : path) { + + JsonNode next = currentScope.get(segment); + + if (next != null && next.isObject()) { + currentScope = next; + } else { + return; + } + + } + + JsonNode element = currentScope.get(name); + + if (element != null) { + prop.loadFromJson(element); + } + } + + /** + * Reads file and caches json + */ + private JsonNode loadAndCache() { + + try { + + if (!configFile.exists()) { + + Files.writeString(configFile.toPath(), "{}"); + cachedJson = MAPPER.createObjectNode(); + return cachedJson; + + } + + cachedJson = MAPPER.readTree(configFile); + return cachedJson; + + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + } + + public Map> getProperties() { + return currentMap; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/saturnclient/config/manager/Property.java b/common/src/main/java/org/saturnclient/config/manager/Property.java new file mode 100644 index 0000000..6575d0d --- /dev/null +++ b/common/src/main/java/org/saturnclient/config/manager/Property.java @@ -0,0 +1,321 @@ +package org.saturnclient.config.manager; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.saturnclient.common.minecraft.MinecraftProvider; + +import java.util.Map; + +public class Property { + + public static class NamedProperty { + public String name; + public Property prop; + + public NamedProperty(String name, Property value) { + this.name = name; + this.prop = value; + } + } + + public enum PropertyType { + BOOLEAN, + INTEGER, + FLOAT, + STRING, + HEX, + NAMESPACE, + SELECT, + KEYBINDING, + } + + public T value; + public boolean isReset = false; + private T defaultValue; + private String[] availableValues; + private final PropertyType type; + private boolean wasPressedLastTick = false; + + private Property(T value, PropertyType type) { + this.value = value; + this.defaultValue = value; + this.type = type; + } + + // ---------- Factory Methods ---------- + + public static Property from(T value) { + if (value instanceof Boolean) + return new Property<>(value, PropertyType.BOOLEAN); + if (value instanceof Integer) + return new Property<>(value, PropertyType.INTEGER); + if (value instanceof Float) + return new Property<>(value, PropertyType.FLOAT); + if (value instanceof String) + return new Property<>(value, PropertyType.STRING); + if (valueIsNamespace(value)) + return new Property<>(value, PropertyType.NAMESPACE); + return null; + } + + public static Property font(int value) { + return select(value, "Default", "Inter", "Inter bold"); + } + + public static Property select(Integer value, String... availableValues) { + Property property = new Property<>(value, PropertyType.SELECT); + property.availableValues = availableValues; + return property; + } + + public static Property>> namespace(Map> value) { + return new Property<>(value, PropertyType.NAMESPACE); + } + + public static Property bool(boolean value) { + return new Property<>(value, PropertyType.BOOLEAN); + } + + public static Property integer(int value) { + return new Property<>(value, PropertyType.INTEGER); + } + + public static Property floatProp(float value) { + return new Property<>(value, PropertyType.FLOAT); + } + + public static Property string(String value) { + return new Property<>(value, PropertyType.STRING); + } + + public static Property color(int value) { + return new Property<>(value, PropertyType.HEX); + } + + public static Property keybinding(int value) { + return new Property<>(value, PropertyType.KEYBINDING); + } + + // ---------- Select Helpers ---------- + + public void next() { + if (type == PropertyType.SELECT) { + int i = (Integer) value; + setValue((i < availableValues.length - 1) ? i + 1 : 0); + } + } + + public void prev() { + if (type == PropertyType.SELECT) { + int i = (Integer) value; + setValue((i > 0) ? i - 1 : availableValues.length - 1); + } + } + + public void setSelection(int selection) { + if (type == PropertyType.SELECT && selection >= 0 && selection < availableValues.length) { + setValue(selection); + } + } + + public String getSelection() { + return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null; + } + + // ---------- Lifecycle ---------- + + public void reset() { + value = defaultValue; + isReset = true; + } + + public PropertyType getType() { + return type; + } + + @SuppressWarnings("unchecked") + public Map> getNamespaceValue() { + if (type == PropertyType.NAMESPACE && value instanceof Map) { + return (Map>) value; + } + throw new IllegalStateException("Property is not a namespace"); + } + + // ---------- JSON Serialization ---------- + + public JsonNode toJson() { + + switch (type) { + + case BOOLEAN: + return JsonNodeFactory.instance.booleanNode((Boolean) value); + + case INTEGER: + return JsonNodeFactory.instance.numberNode((Integer) value); + + case FLOAT: + return JsonNodeFactory.instance.numberNode((Float) value); + + case STRING: + return JsonNodeFactory.instance.textNode((String) value); + + case HEX: + return JsonNodeFactory.instance.textNode( + String.format("#%08X", (Integer) value)); + + case KEYBINDING: + case SELECT: + return JsonNodeFactory.instance.numberNode((Integer) value); + + case NAMESPACE: + + ObjectNode nested = JsonNodeFactory.instance.objectNode(); + + getNamespaceValue().forEach((k, v) -> { + nested.set(k, v.toJson()); + }); + + return nested; + + default: + return JsonNodeFactory.instance.textNode(String.valueOf(value)); + } + } + + public void loadFromJson(JsonNode element) { + + if (element == null) + return; + + switch (type) { + + case BOOLEAN: + if (element.isBoolean()) + setValue(element.booleanValue()); + break; + + case INTEGER: + if (element.isInt()) + setValue(element.intValue()); + else if (element.isTextual()) + setValue(parseHexToInt(element.textValue())); + break; + + case FLOAT: + if (element.isNumber()) + setValue(element.floatValue()); + break; + + case STRING: + if (element.isTextual()) + setValue(element.textValue()); + break; + + case HEX: + if (element.isTextual()) + setValue(parseHexToInt(element.textValue())); + break; + + case NAMESPACE: + + if (element.isObject()) { + + ObjectNode obj = (ObjectNode) element; + + getNamespaceValue().forEach((k, v) -> { + v.loadFromJson(obj.get(k)); + }); + + } + + break; + + case SELECT: + if (element.isInt()) + setSelection(element.intValue()); + break; + + case KEYBINDING: + if (element.isInt()) + setValue(element.intValue()); + break; + } + } + + // ---------- Utility ---------- + + @Override + public String toString() { + return String.valueOf(value); + } + + public static boolean valueIsNamespace(Object obj) { + return obj instanceof Map; + } + + @SuppressWarnings("unchecked") + public void setValue(Object value) { + this.value = (T) value; + } + + @SuppressWarnings("unchecked") + public void setValue(Object value, Object defaultValue) { + this.value = (T) value; + this.defaultValue = (T) defaultValue; + } + + public NamedProperty named(String name) { + return new NamedProperty<>(name, this); + } + + // ---------- Keybindings (no SaturnClient) ---------- + + public boolean isKeyPressed(long windowHandle, boolean screenOpen) { + + return (Integer) value != -1 && + MinecraftProvider.PROVIDER.isKeyPressed((Integer) value) && !screenOpen; + } + + public boolean wasKeyPressed(long windowHandle, boolean screenOpen) { + + boolean pressed = isKeyPressed(windowHandle, screenOpen); + + boolean result = pressed && !wasPressedLastTick; + + wasPressedLastTick = pressed; + + return result; + } + + // ---------- HEX ---------- + + public static int parseHexToInt(String hex) { + + hex = hex.replace("#", ""); + + if (hex.length() == 6) + hex = "FF" + hex; + + if (hex.length() != 8) + throw new IllegalArgumentException( + "Hex must be 6 or 8 chars long, got '" + hex + "'"); + + return (int) Long.parseLong(hex, 16); + } + + // ---------- Misc ---------- + + public Property copy() { + return new Property<>(this.value, this.type); + } + + public boolean isNamespace() { + return type == PropertyType.NAMESPACE; + } + + @SuppressWarnings("unchecked") + public Map> getNamespaceMap() { + return isNamespace() ? (Map>) value : null; + } +} \ No newline at end of file