Compare commits

..
Author SHA1 Message Date
selimaj-dev 720ee3d8bd change version to 0.0.4-beta1 2025-08-18 14:00:40 +02:00
selimaj-dev b4b348a54c change version to 0.0.2-beta1 2025-08-18 14:00:23 +02:00
selimaj-dev a3be72c443 Loading in theme manager with refactor 2025-08-18 11:58:14 +02:00
selimaj-dev 1c553d14bf Fixed theme manager bug 2025-08-18 09:25:38 +02:00
selimaj-dev cd967a907c Changed emote icons 2025-08-17 20:34:40 +02:00
selimaj-dev ba04c45b75 readme change 2025-08-17 15:26:45 +02:00
selimaj-dev ecb07ce730 readme change 2025-08-17 15:25:41 +02:00
selimaj-dev 6cd10240fc Merge branch 'refactored-clientcom' 2025-08-17 15:23:24 +02:00
selimaj-dev d162db3464 Fixed keypress and hud rendering 2025-08-17 15:22:00 +02:00
22 changed files with 169 additions and 235 deletions
+2 -1
View File
@@ -8,11 +8,12 @@
<b>Saturn Client is a free and open source Minecraft client</b>
</p>
![image](https://github.com/user-attachments/assets/b0b23e72-c2eb-49aa-b49c-4d3cc4d60fcc)
# Credits
- Cloak rendering: `IIpho3nix` & `Dhyan Blum`
- Who Am I: `FokshaWasTaken`
- Panton font (ui): `Ivan Petrov` & `Fontfabric`
- Panorama: https://www.planetminecraft.com/texture-pack/minecraft-cavesand-cliffs-part-ii-main-menu-panorama/
If any of the code or assets are in your name and you want to remove them, please submit an issue, we are still in alpha to have any artists so we would also appreciate if you could help us make some cosmetics
+1 -1
View File
@@ -6,7 +6,7 @@ yarn_mappings=1.21.4+build.7
loader_version=0.16.10
fabric_version=0.114.0+1.21.4
mod_version = 0.0.3-beta0
mod_version = 0.0.4-beta1
maven_group = org.saturnclient
archives_base_name = saturnclient
@@ -1,26 +1,19 @@
package org.saturnclient.modules;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.render.RenderLayer;
import org.saturnclient.modules.mods.*;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
public class ModManager {
public static Module[] MODS = {
new Crosshair(),
new AutoSprint(),
new ArmorDisplay(),
new Fps(),
new PotionDisplay(),
new Coordinates(),
new Freelook(),
new Keystrokes()
new Crosshair(),
new AutoSprint(),
new ArmorDisplay(),
new Fps(),
new PotionDisplay(),
new Coordinates(),
new Freelook(),
new Keystrokes()
};
public static void init() {
@@ -31,39 +24,5 @@ public class ModManager {
}
}
});
HudRenderCallback.EVENT.register((context, _o) -> {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
if (textRenderer != null &&
!(SaturnClient.client.currentScreen instanceof HudEditor)) {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (Module m : MODS) {
if (m instanceof HudMod && m.isEnabled()) {
ModDimensions dim = ((HudMod) m).getDimensions();
renderScope.matrices.push();
renderScope.matrices.translate(dim.x.value, (float) dim.y.value, 0);
renderScope.matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value, dim.bgColor.value);
}
renderScope.setRenderLayer(RenderLayer::getGuiTextured);
((HudMod) m).renderHud(renderScope);
renderScope.matrices.pop();
} else {
m.render(renderScope);
}
}
}
});
}
}
@@ -0,0 +1,58 @@
package org.saturnclient.modules.mods.mixins;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderTickCounter;
@Mixin(InGameHud.class)
public class RenderMixin {
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
if (textRenderer != null &&
!(SaturnClient.client.currentScreen instanceof HudEditor)) {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (org.saturnclient.modules.Module m : ModManager.MODS) {
if (m instanceof HudMod && m.isEnabled()) {
ModDimensions dim = ((HudMod) m).getDimensions();
renderScope.matrices.push();
renderScope.matrices.translate(dim.x.value, (float) dim.y.value, 0);
renderScope.matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value,
dim.bgColor.value);
}
renderScope.setRenderLayer(RenderLayer::getGuiTextured);
((HudMod) m).renderHud(renderScope);
renderScope.matrices.pop();
} else {
m.render(renderScope);
}
}
}
}
}
@@ -49,11 +49,12 @@ public class SaturnClient implements ModInitializer {
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
if (screen != null && !(screen instanceof TitleMenu)) {
if (screen instanceof SaturnScreen) {
((SaturnScreen) screen).draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(screen.width - 180 - 20, screen.height - 18 - 20));
((SaturnScreen) screen).draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18)
.position(screen.width - 180 - 20, screen.height - 18 - 20));
} else {
ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, tickDelta) -> {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
((DrawContextAccessor) context).getVertexConsumers());
int width = context.getScaledWindowWidth();
int height = context.getScaledWindowHeight();
@@ -46,7 +46,6 @@ public class ConfigManager {
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> value) {
System.out.println("Adding property: " + name);
currentMap.put(name, value);
loadProp(name, value);
return value;
@@ -98,7 +97,6 @@ public class ConfigManager {
new String(Files.readAllBytes(configFile.toPath()))).getAsJsonObject();
for (String namespace : properties.keySet()) {
SaturnClient.LOGGER.info("Loading namespace: " + namespace);
JsonElement configElement = jsonObject.get(namespace);
if (configElement == null)
@@ -120,8 +118,6 @@ public class ConfigManager {
@SuppressWarnings("unchecked")
private static void loadProperties(JsonObject config, Map<String, Property<?>> propertyMap) {
SaturnClient.LOGGER.info("Loading properties: " + propertyMap);
for (String propertyName : propertyMap.keySet()) {
JsonElement c = config.get(propertyName);
@@ -50,6 +50,10 @@ public class Property<T> {
}
}
public Property<T> copy() {
return new Property<>(this.value, this.type);
}
public static Property<Integer> font(int value) {
return select(value, "Default", "Inter", "Inter bold");
}
@@ -89,13 +93,17 @@ public class Property<T> {
}
public void next() {
if ((Integer) value < availableValues.length-1) setValue((Integer)value+1);
else setValue(0);
if ((Integer) value < availableValues.length - 1)
setValue((Integer) value + 1);
else
setValue(0);
}
public void prev() {
if ((Integer) value > 0) setValue((Integer)value-1);
else setValue(availableValues.length - 1);
if ((Integer) value > 0)
setValue((Integer) value - 1);
else
setValue(availableValues.length - 1);
}
public void setSelection(int selection) {
@@ -156,12 +164,8 @@ public class Property<T> {
return true;
if (value instanceof String && primitive.isString())
return true;
// Check if JSON string represents a valid hex integer
if (value instanceof String && primitive.isString()) {
String str = primitive.getAsString();
return str.matches("0x[0-9A-Fa-f]+");
}
if (type == PropertyType.HEX && primitive.isString())
return true;
} else if (element.isJsonObject()) {
return isNamespace(value);
}
@@ -197,18 +201,58 @@ public class Property<T> {
}
public boolean isKeyPressed() {
return (Integer) value == -1 ? false : GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) (Object) value) == GLFW.GLFW_PRESS;
return (Integer) value == -1 ? false
: GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(),
(Integer) (Object) value) == GLFW.GLFW_PRESS && SaturnClient.client.currentScreen == null;
}
public boolean wasKeyPressed() {
long window = SaturnClient.client.getWindow().getHandle();
int key = (Integer) value;
boolean isPressed = this.isKeyPressed();
wasPressedLastTick = isPressed && !wasPressedLastTick;
return wasPressedLastTick;
}
boolean isPressed = key != -1 && GLFW.glfwGetKey(window, key) == GLFW.GLFW_PRESS;
@SuppressWarnings("unchecked")
public void load(String name, JsonElement element) {
if (element != null && element.isJsonObject()) {
JsonElement value = element.getAsJsonObject().get(name);
if (value != null) {
switch (this.getType()) {
case BOOLEAN:
((Property<Boolean>) this).setValue(value.getAsBoolean());
break;
case INTEGER:
JsonPrimitive primitive = value.getAsJsonPrimitive();
if (primitive.isNumber())
((Property<Integer>) this).setValue(value.getAsInt());
else if (primitive.isString())
((Property<Integer>) this).setValue(parseHexToInt(value.getAsString()));
break;
case FLOAT:
((Property<Float>) this).setValue(value.getAsFloat());
break;
case STRING:
((Property<String>) this).setValue(value.getAsString());
break;
case HEX:
((Property<Integer>) this).setValue(parseHexToInt(value.getAsString()));
break;
default:
break;
}
}
}
}
boolean wasJustPressed = isPressed && !wasPressedLastTick;
wasPressedLastTick = isPressed;
public static int parseHexToInt(String hex) {
hex = hex.replaceAll("#", "");
return wasJustPressed;
if (hex.length() == 6) {
hex = "FF" + hex;
} else if (hex.length() != 8) {
throw new IllegalArgumentException("Hex string must be 6 or 8 characters long");
}
return (int) Long.parseLong(hex, 16);
}
}
@@ -3,127 +3,75 @@ package org.saturnclient.saturnclient.config;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient;
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;
public class ThemeManager {
private static File themeFile = new File(
SaturnClient.client.runDirectory,
"saturn.theme.json");
private static Map<String, Map<String, Property<?>>> properties = new HashMap<>();
private static List<ThemeManager> theme = new ArrayList<>();
private static JsonObject cachedThemeJson = null;
// private Map<String, Map<String, Property<?>>> states;
private Map<String, Map<String, Object>> states;
private Map<String, Property<?>> currentStyling;
private String state;
private String[] states;
private String namespace;
public ThemeManager(String namespace, String... states) {
// this.states = new LinkedHashMap<>();
this.namespace = namespace;
this.states = states;
currentStyling = new LinkedHashMap<>();
properties.put(namespace, new LinkedHashMap<>());
this.states = new LinkedHashMap<>();
this.currentStyling = new LinkedHashMap<>();
this.states.put(null, new LinkedHashMap<>());
for (String s : states) {
properties.put(namespace+"@"+s, new LinkedHashMap<>());
this.states.put(s, new LinkedHashMap<>());
}
theme.add(this);
}
public void setState(String state) {
this.state = state;
if (this.state != null) {
for (Map.Entry<String, Property<?>> tProp : properties.get(namespace+"@"+state).entrySet()) {
currentStyling.get(tProp.getKey()).setValue(tProp.getValue().value);
}
} else {
for (Map.Entry<String, Property<?>> tProp : properties.get(namespace).entrySet()) {
Property<?> p = currentStyling.get(tProp.getKey());
if (p != null) p.setValue(tProp.getValue().value);
}
for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
}
}
public void applyState(String state) {
if (this.state != null) {
for (Map.Entry<String, Property<?>> tProp : properties.get(namespace+"@"+state).entrySet()) {
Property<?> p = currentStyling.get(tProp.getKey());
Property<?> currProp = properties.get(namespace).get(tProp.getKey());
if (currProp != null && p.value.equals(currProp.value)) {
p.setValue(tProp.getValue().value);
}
}
} else {
setState(state);
for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
if (!states.get(null).get(tProp.getKey()).equals(tProp.getValue()))
currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
}
}
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> prop) {
for (String state : states) {
Property<?> property = Property.from(prop.value);
properties.get(namespace+"@"+state).put(name, property);
loadProp(state, name, property);
for (Map.Entry<String, Map<String, Object>> state : this.states.entrySet()) {
Property<?> property = prop.copy();
if (cachedThemeJson == null) {
load();
}
String stateName = state.getKey();
property.load(name, cachedThemeJson.get(stateName != null ? namespace + "@" + stateName : namespace));
state.getValue().put(name, property.value);
}
loadProp(null, name, prop);
properties.get(namespace).put(name, Property.from(prop.value));
currentStyling.put(name, prop);
return prop;
}
public <T> void propertyStateDefault(String state, String name, T value) {
Property<?> prop = properties.get(namespace+"@"+state).get(name);
prop.setValue(value, value); // sets the current and default value
loadProp(state, name, prop);
}
@SuppressWarnings("unchecked")
public <T> void loadProp(String state, String name, Property<T> prop) {
Property<?> prop = currentStyling.get(name).copy();
prop.setValue(value, value);
if (cachedThemeJson == null) {
load();
}
JsonElement element = cachedThemeJson.get(state != null ? namespace+"@"+state : 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<Boolean>) prop).setValue(value.getAsBoolean());
break;
case INTEGER:
((Property<Integer>) prop).setValue(value.getAsInt());
break;
case FLOAT:
((Property<Float>) prop).setValue(value.getAsFloat());
break;
case STRING:
((Property<String>) prop).setValue(value.getAsString());
break;
case HEX:
((Property<Integer>) prop).setValue(value.getAsInt());
break;
default:
break;
}
}
}
prop.load(name, cachedThemeJson.get(state != null ? namespace + "@" + state : namespace));
states.get(state).put(name, prop.value);
}
public static void load() {
@@ -135,85 +83,11 @@ public class ThemeManager {
}
cachedThemeJson = JsonParser.parseString(
new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject();
new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject();
SaturnClient.LOGGER.info("Theme loaded");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the theme file", e);
}
}
public static void save() {
try {
SaturnClient.LOGGER.info("Starting to save theme...");
JsonObject jsonObject = new JsonObject();
// Iterate through all namespaces and their properties
for (String namespace : properties.keySet()) {
JsonObject namespaceTheme = new JsonObject();
Map<String, Property<?>> propertyMap = properties.get(namespace);
// Save properties for this namespace
saveProperties(namespaceTheme, propertyMap);
// Add the namespace theme to the main JSON object
jsonObject.add(namespace, namespaceTheme);
}
// 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 theme file
Files.write(themeFile.toPath(), formattedJson.getBytes());
SaturnClient.LOGGER.info("Theme saved successfully.");
} catch (IOException e) {
SaturnClient.LOGGER.error("Error saving the theme file", e);
}
}
@SuppressWarnings("unchecked")
private static void saveProperties(JsonObject theme, Map<String, Property<?>> propertyMap) {
// Iterate through each property in the namespace
for (String propertyName : propertyMap.keySet()) {
Property<?> property = propertyMap.get(propertyName);
JsonElement propertyValue = null;
if (property.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedTheme = new JsonObject();
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) property.value;
saveProperties(nestedTheme, nestedProperties);
propertyValue = nestedTheme;
} else {
// Convert the property value based on its type
switch (property.getType()) {
case BOOLEAN:
propertyValue = new JsonPrimitive((Boolean) property.value);
break;
case INTEGER:
propertyValue = new JsonPrimitive((Integer) property.value);
break;
case FLOAT:
propertyValue = new JsonPrimitive((Float) property.value);
break;
case STRING:
propertyValue = new JsonPrimitive((String) property.value);
break;
case HEX:
propertyValue = new JsonPrimitive((Integer) property.value);
break;
default:
SaturnClient.LOGGER.warn(
"Unknown property type for: " + propertyName);
break;
}
}
if (propertyValue != null) {
theme.add(propertyName, propertyValue);
}
}
}
}
@@ -39,7 +39,7 @@ public class KeyInputHandler {
client.setScreen(new ShiftMenu());
}
if (SaturnClientConfig.openEmoteWheel.wasKeyPressed()) {
if (SaturnClientConfig.openEmoteWheel.wasKeyPressed() && client.currentScreen == null) {
client.setScreen(new EmoteWheel());
}
@@ -27,6 +27,8 @@ public class Toggle extends Element {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
theme.setState(null);
if (prop.value) {
theme.setState("enabled");
}
@@ -38,8 +40,6 @@ public class Toggle extends Element {
renderScope.drawRoundedRectangle(0, 0, width, height, cornerRadius.value, bgColor.value);
renderScope.drawRoundedRectangle(prop.value ? 30 : 0, 0, 30, 30, 30, fgColor.value);
theme.setState(null);
}
@Override
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+2 -1
View File
@@ -5,7 +5,8 @@
"compatibilityLevel": "JAVA_21",
"client": [
"CameraMixin",
"EntityMixin"
"EntityMixin",
"RenderMixin"
],
"injectors": {
"defaultRequire": 1