Compare commits

...
117 Commits
Author SHA1 Message Date
selimaj-dev c567b282a5 Merge pull request #12 from RudideC/master
added health display
2026-02-13 12:03:56 +01:00
RudideC 5f19abebbf added health display
ts tuff
2026-02-13 10:06:13 +01:00
Klesti Selimaj 58eda56f37 Merge pull request #11 from RudideC/master
zoom mod
2026-01-21 21:57:42 +01:00
RudideC 50cab402d4 image 2026-01-21 21:52:28 +01:00
RudideC fa7161d371 zoom module 2026-01-21 21:52:24 +01:00
Klesti Selimaj 252808a467 Merge pull request #10 from RudideC/master
added nofog, daycounter and speedometer
2026-01-20 21:56:36 +01:00
RudideC 31e9a8d56e speedometer module finished
logo included
2026-01-20 15:30:57 +01:00
RudideC ba1b68d8fa speedometer works in vehicles 2026-01-20 14:57:59 +01:00
RudideC dcd82fa58e default is x axis 2026-01-20 14:51:22 +01:00
RudideC 4132a05c8f blocks/tick -> blocks/s 2026-01-20 14:44:29 +01:00
RudideC 47f8dd5e22 testing speedometer 2026-01-20 14:35:49 +01:00
RudideC 9b30ec7016 small change 2026-01-20 14:18:44 +01:00
RudideC d10f229ffa daycounter image 2026-01-19 20:55:07 +01:00
RudideC 061ed674e3 daycounter mod
counts days
2026-01-19 20:54:59 +01:00
RudideC 5b5f781095 nofog mod
you can customise if you want to be able to see through liquids, but only lava and powdered snow work (not water)
2026-01-17 00:24:39 +01:00
Klesti Selimaj 48f7aefbf6 Merge pull request #9 from RudideC/master 2026-01-16 22:12:15 +01:00
RudideC 8f111badf6 oops, forgot 2026-01-16 22:05:35 +01:00
RudideC 687bda68a1 renamed potiondisplay to status effects
this is the name most clients use
2026-01-16 22:04:57 +01:00
RudideC 8e2c986ae4 fullbright
new mod
2026-01-16 22:02:48 +01:00
RudideC faa95e664a faster timeout
(so i don't have to wait minutes before testing)
2026-01-16 22:02:25 +01:00
RudideC c9230da168 faster builds
yes
2026-01-16 22:01:59 +01:00
RudideC 7002fbec0e prevent race condition 2026-01-16 19:41:37 +01:00
RudideC 496d27b861 enabled mod list updates
each mod enable/disable updates the list
2026-01-16 19:41:29 +01:00
RudideC 3c37b80bc2 dont sprint when using item
anticheat compliance
2026-01-16 19:02:51 +01:00
selimaj-dev 920657ddd3 Beta 2 2025-09-08 18:46:29 +02:00
selimaj-dev 32035a2f7d Fixed flat cloak with new optimised textures 2025-09-06 12:07:30 +02:00
Leo Codes c230f4f5ec Merge pull request #7 from saturnclientmc/optimised-cloak
Optimised cloak texture size
2025-09-06 02:24:46 +02:00
selimaj-dev 50d1028e53 Optimised cloak texture size 2025-09-06 02:21:40 +02:00
Leo Codes 780cb9ba9a Merge pull request #6 from saturnclientmc/cloak-edge-cases
Cloak edge cases
2025-09-05 18:05:24 +02:00
selimaj-dev 65df9e9a5e Fixed cloak when crouching #5 2025-09-05 17:49:45 +02:00
selimaj-dev 66f8a13faa Added a wave effect #5 2025-09-05 17:09:46 +02:00
selimaj-dev 1726542c84 Fixed cloak when swimming #5 2025-09-05 16:35:57 +02:00
selimaj-dev 71f6597624 0.0.5 beta 1 and a few changes to the cloak 2025-09-04 20:04:38 +02:00
selimaj-dev 45f8c65549 Bendy cloak option and slight cloak jiggle 2025-09-04 19:54:35 +02:00
selimaj-dev 82bb2859c9 Fixed store purchase 2025-09-04 19:30:25 +02:00
Leo Codes 4d87c4e694 Merge pull request #4 from saturnclientmc/updated-cloak
Optimised, added wavy cloaks and added new cloaks
2025-09-04 18:19:49 +02:00
selimaj-dev 5c7283e513 Wavey cloaks 2025-09-04 17:28:17 +02:00
selimaj-dev ee300b2bb2 Wavey cloaks (no bottom) 2025-09-04 16:35:26 +02:00
selimaj-dev 3b1f54a5b0 Okay getting somewhere 2025-09-04 15:57:36 +02:00
selimaj-dev e39c877b91 All sides except top and bottom 2025-09-04 15:43:40 +02:00
selimaj-dev 4bb4e46487 Testing for curved cloak 2025-09-04 15:15:34 +02:00
selimaj-dev d56bdfea8e Working custom renderer for cloaks 2025-09-04 11:41:57 +02:00
selimaj-dev afca9c01b7 Working custom renderer for cloaks 2025-09-04 03:26:37 +02:00
selimaj-dev 40b0177cf2 Working custom renderer for cloaks 2025-09-03 17:50:57 +02:00
selimaj-dev db27de497a Slightly better light for cloaks 2025-09-03 08:20:18 +02:00
selimaj-dev 53fd88df18 Optimised cloak loading 2025-09-03 08:01:32 +02:00
selimaj-dev 8e477f8852 Main hand on armor status 2025-09-03 05:28:41 +02:00
selimaj-dev 1f93f6cf5a Fixed red bull cloak 2025-09-03 05:11:14 +02:00
selimaj-dev a4c60dd5b0 Fixed cloak store 2025-09-03 05:10:00 +02:00
selimaj-dev 72766a47ac Added cloaks to list 2025-09-03 04:45:40 +02:00
selimaj-dev 4b7238e863 Black hole cloak previews 2025-09-03 04:28:19 +02:00
selimaj-dev 52456f2ca8 Black hole cloaks 2025-09-03 04:26:03 +02:00
selimaj-dev a464635075 Cloak fix 2025-09-03 02:57:41 +02:00
selimaj-dev 8089deb172 Optimisations 2025-09-03 02:51:19 +02:00
selimaj-dev ca9626f340 Testing for cloak 2025-09-03 01:34:22 +02:00
selimaj-dev 0d3a27b5e7 Beta 2 2025-08-22 17:36:44 +02:00
selimaj-dev b6eca22097 Fixed alignment in bmw and amg cloaks 2025-08-21 01:49:58 +02:00
selimaj-dev 4de4605ee8 Red Bull cloak (sick af) 2025-08-21 01:44:45 +02:00
selimaj-dev 8018caf26b Ferrari 2025-08-21 01:22:03 +02:00
selimaj-dev 39266df45e Amg petronas f1 cloak 2025-08-20 18:14:45 +02:00
selimaj-dev 25046b53f3 Amg cloak 2025-08-20 17:26:11 +02:00
selimaj-dev 8e89a850b3 Bmw preview 2025-08-19 20:27:55 +02:00
selimaj-dev ccaa79e89b Bmw cloak 2025-08-19 20:24:23 +02:00
selimaj-dev 30b375aa6f Bmw cloak 2025-08-19 20:16:39 +02:00
selimaj-dev 6d2cad1c7a Bmw cloak 2025-08-19 20:16:32 +02:00
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
Leo Codes 7e3b6cf7f8 Merge pull request #3 from saturnclientmc/refactored-clientcom
Refactored clientcom
2025-08-16 10:39:06 -04:00
selimaj-dev 6fb38e4f9b Updated ip 2025-08-16 16:33:19 +02:00
selimaj-dev 997fbbe9a9 Working emote system 2025-08-16 16:29:48 +02:00
selimaj-dev 01a8ea15a0 Better cosmetic update system 2025-08-16 16:05:44 +02:00
selimaj-dev 08cee2172c Removed the reload system 2025-08-16 14:17:55 +02:00
selimaj-dev 2c64eed329 new version 2025-07-07 01:01:08 +02:00
selimaj-dev 66d7d7eab9 Disabled online emotes 2025-07-07 00:54:56 +02:00
selimaj-dev 21b1dcc997 saturn client branding 2025-07-05 05:20:34 +02:00
selimaj-dev c2470378e1 saturn client branding 2025-07-05 05:20:27 +02:00
selimaj-dev f57e773a44 fixed hat still visible when invisible 2025-07-05 04:02:30 +02:00
selimaj-dev 6c0575a03f Notifications 2025-06-23 17:03:18 -05:00
selimaj-dev 9f93ea923e Cosmetic preview slight improvement 2025-06-23 17:16:28 +02:00
selimaj-dev ecf5987434 sidebar gap 2025-06-23 17:04:03 +02:00
selimaj-dev 89a5b2437e fully functional store 2025-06-23 16:49:09 +02:00
selimaj-dev c8d28133a0 Crimson Mark 2025-06-23 07:12:54 +02:00
selimaj-dev 0980e3a5d4 semi working Store 2025-06-22 16:56:18 -05:00
selimaj-dev 420ba82b44 more emotes 2025-06-22 11:24:38 -05:00
selimaj-dev 2f3c125bba Keystrokes mouse and space options 2025-06-19 15:13:29 -05:00
selimaj-dev 9e64b2332e added keystrokes 2025-06-18 13:47:50 -05:00
selimaj-dev 089cf4778c freelook 2025-06-18 08:51:14 -05:00
selimaj-dev b9984c35a2 coord display methods 2025-06-18 05:10:15 -05:00
selimaj-dev b035381ba7 removed unnecessary resources 2025-06-18 04:30:17 -05:00
selimaj-dev 87b326ce9f coords 2025-06-18 04:26:39 -05:00
selimaj-dev 786f9cf6f7 small change 2025-06-18 00:29:57 +02:00
selimaj-dev 464d0b502b NONE key selection when backspace 2025-06-17 13:13:56 -05:00
selimaj-dev e38f4480a3 config save bug fix 2025-06-17 11:54:50 -05:00
selimaj-dev d178f1504b config for keybidnings 2025-06-17 11:42:19 -05:00
selimaj-dev 5b77ea7363 Small property refactor 2025-06-17 10:19:49 -05:00
selimaj-dev 78097b1d8f prep for module mixins 2025-06-17 09:54:03 -05:00
selimaj-dev 51040900cf potion display 2025-06-16 16:12:40 -05:00
selimaj-dev f06e2a0aaa updated icon 2025-06-16 16:06:19 -05:00
selimaj-dev 70fcd64f25 potion display (needs dummy and proper dimensions) 2025-06-16 11:49:56 -05:00
selimaj-dev b2c7e9f03e default transparent hud mod background 2025-06-16 07:31:56 -05:00
selimaj-dev 08155bd49a fixed armor display 2025-06-16 07:31:14 -05:00
selimaj-dev 67a5b219f5 better font management 2025-06-16 07:06:15 -05:00
selimaj-dev 6c608409e0 Added emote cancel (shift) 2025-06-15 09:53:50 -05:00
selimaj-dev 4c72f8e39f online emotes (todo: make the emotes cancel across the players) 2025-06-15 01:32:28 +02:00
selimaj-dev 3d9d01a28f fps display 2025-06-14 14:09:27 -05:00
selimaj-dev 7e39532d1d almost fully working emotes 2025-06-14 13:52:29 -05:00
selimaj-dev 76c20d8733 emotes (real) 2025-06-14 06:14:25 +02:00
selimaj-dev a1c68609d9 working emote test 2025-06-14 04:21:34 +02:00
selimaj-dev d49afbb981 updated mercedes flow 2025-05-18 23:03:16 +02:00
selimaj-dev fd0bf75750 beta2 2025-05-17 15:31:22 +02:00
157 changed files with 11847 additions and 1264 deletions
+2 -1
View File
@@ -8,11 +8,12 @@
<b>Saturn Client is a free and open source Minecraft client</b> <b>Saturn Client is a free and open source Minecraft client</b>
</p> </p>
![image](https://github.com/user-attachments/assets/b0b23e72-c2eb-49aa-b49c-4d3cc4d60fcc)
# Credits # Credits
- Cloak rendering: `IIpho3nix` & `Dhyan Blum` - Cloak rendering: `IIpho3nix` & `Dhyan Blum`
- Who Am I: `FokshaWasTaken` - 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/ - 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 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
+9
View File
@@ -19,6 +19,10 @@ dependencies {
modApi "dev.felnull:special-model-loader:1.4.0+pre.1" modApi "dev.felnull:special-model-loader:1.4.0+pre.1"
include "dev.felnull:special-model-loader:1.4.0+pre.1" include "dev.felnull:special-model-loader:1.4.0+pre.1"
include modImplementation("dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}")
//You might want bendy-lib. playerAnimator will wrap it.
// include modRuntimeOnly("io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}")
} }
@@ -28,6 +32,11 @@ repositories {
name = "FelNull Maven" name = "FelNull Maven"
url = "https://maven.felnull.dev" url = "https://maven.felnull.dev"
} }
maven {
name "KosmX's maven"
url 'https://maven.kosmx.dev/'
}
} }
processResources { processResources {
File diff suppressed because one or more lines are too long
+14 -13
View File
@@ -1,17 +1,18 @@
# Done to increase the memory available to gradle. # Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G org.gradle.jvmargs=-Xmx1G -XX:+UseParallelGC
# Fabric Properties org.gradle.daemon=true
# check these on https://modmuss50.me/fabric.html org.gradle.parallel=true
minecraft_version=1.21.4 org.gradle.configureondemand=true
yarn_mappings=1.21.4+build.7 org.gradle.caching=true
loader_version=0.16.10
# Mod Properties minecraft_version=1.21.4
mod_version = 0.0.1-alpha4 yarn_mappings=1.21.4+build.7
maven_group = me.iipho3nix loader_version=0.16.10
archives_base_name = saturnclient fabric_version=0.114.0+1.21.4
# Dependencies mod_version = 0.0.5-beta2
# check this on https://modmuss50.me/fabric.html maven_group = org.saturnclient
fabric_version=0.114.0+1.21.4 archives_base_name = saturnclient
player_anim = 2.0.3+1.21.4
@@ -6,27 +6,32 @@ import org.saturnclient.saturnclient.config.NamedProperty;
import org.saturnclient.saturnclient.config.Property; import org.saturnclient.saturnclient.config.Property;
public class ModDimensions { public class ModDimensions {
public Property<Integer> x = new Property<>(0); public Property<Integer> x = Property.integer(0);
public Property<Integer> y = new Property<>(0); public Property<Integer> y = Property.integer(0);
public Property<Float> scale = new Property<>(1.0f); public Property<Float> scale = Property.floatProp(1.0f);
public Property<Integer> bgColor = new Property<>(0xFF000000, Property.PropertyType.HEX); public Property<Integer> bgColor = Property.color(0x00000000);
public Property<Integer> fgColor = new Property<>(0xFFffffff, Property.PropertyType.HEX); public Property<Integer> fgColor = Property.color(0xFFffffff);
public Property<Integer> radius = new Property<>(0); public Property<Integer> radius = Property.integer(0);
public Property<Integer> font = Property.font(0);
public boolean renderBackground = true;
public int width = 0; public int width = 0;
public int height = 0; public int height = 0;
public NamedProperty<Map<String, Property<?>>> prop() { public NamedProperty<Map<String, Property<?>>> prop() {
return new Property<Map<String, Property<?>>>(Map.of( return Property.namespace(Map.of(
"X", x, "X", x,
"Y", y, "Y", y,
"Scale", scale, "Scale", scale,
"Background Color", bgColor, "Background Color", bgColor,
"Foreground Color", fgColor, "Foreground Color", fgColor,
"Corner Radius", radius "Corner Radius", radius,
"Font", font
)).named("In-Game Display"); )).named("In-Game Display");
} }
public ModDimensions() {}
public ModDimensions(int width, int height) { public ModDimensions(int width, int height) {
this.width = width; this.width = width;
this.height = height; this.height = height;
@@ -1,59 +1,56 @@
package org.saturnclient.modules; package org.saturnclient.modules;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; import java.util.ArrayList;
import net.minecraft.client.font.TextRenderer; import java.util.List;
import org.saturnclient.modules.mods.*; 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 class ModManager {
public static Module[] MODS = { public static final List<Module> ENABLED_MODS = new ArrayList<>();
new Crosshair(), public static final Module[] ALL_MODS = {
new AutoSprint(), new Crosshair(),
new ArmorDisplay() new AutoSprint(),
new ArmorDisplay(),
new Fps(),
new StatusEffects(),
new Coordinates(),
new Freelook(),
new Keystrokes(),
new Fullbright(),
new NoFog(),
new DayCounter(),
new Speedometer(),
new Zoom(),
new HealthDisplay()
}; };
public static void init() { public static void init() {
// Pre-filter enabled modules to avoid checking every tick
updateEnabledModules();
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (Module m : MODS) { // Only iterate over enabled modules
if (m.isEnabled()) { for (Module m : ENABLED_MODS) {
m.tick(); m.tick();
}
}
});
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);
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value, dim.bgColor.value);
((HudMod) m).renderHud(renderScope);
renderScope.matrices.pop();
} else {
m.render(renderScope);
}
}
} }
}); });
} }
public static synchronized 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();
}
} }
@@ -24,7 +24,12 @@ public abstract class Module {
public abstract boolean isEnabled(); public abstract boolean isEnabled();
public abstract void setEnabled(boolean e); public final void setEnabled(boolean e) {
onEnabled(e);
ModManager.updateEnabledModules();
}
public abstract void onEnabled(boolean e);
public final String getName() { public final String getName() {
return details.name; return details.name;
@@ -0,0 +1,6 @@
# Credits
### Freelook
- https://github.com/Celibistrial/freelook
- https://modrinth.com/mod/freelook
**The other mods are coded by us, if you have a concern or we missed something feel free to submit a issue with the credits tag**
@@ -13,22 +13,32 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.Items; import net.minecraft.item.Items;
public class ArmorDisplay extends Module implements HudMod { public class ArmorDisplay extends Module implements HudMod {
private static Property<Boolean> enabled = new Property<>(true); private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 60); private static ModDimensions dimensions = new ModDimensions(40, 75);
private static Property<Boolean> useMainHand = Property.bool(false);
public ArmorDisplay() { public ArmorDisplay() {
super(new ModuleDetails("Armor Display", "armor").description("Displays armor durability") super(new ModuleDetails("Armor Display", "armor")
.version("v0.1.0") .description("Displays armor durability")
.tags("Utility"), .version("v0.1.0")
enabled.named("Enabled"), .tags("Utility"),
dimensions.prop()); 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; int row = 0;
if (useMainHand.value && !mainHand.isEmpty()) {
scope.drawItem(mainHand, 0, 15 * row);
renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
row++;
}
if (!helmet.isEmpty()) { if (!helmet.isEmpty()) {
scope.drawItem(helmet, 0, 0); scope.drawItem(helmet, 0, 15 * row);
renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage()); renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
row++; row++;
} }
@@ -51,22 +61,28 @@ public class ArmorDisplay extends Module implements HudMod {
} }
} }
public void renderHealth(RenderScope scope, int i, int max, int current) { public void renderHealth(RenderScope scope, int i, int max_damage, int damage) {
scope.drawText(0.5f, String.valueOf(((double) current / max) * 100) + "%", 17, (15 * i) + 3, false, 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 @Override
public void renderHud(RenderScope scope) { public void renderHud(RenderScope scope) {
renderArmor(scope, renderArmor(scope,
SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD), SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST), SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS), SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET)); SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS),
SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET));
} }
@Override @Override
public void renderDummy(RenderScope scope) { public void renderDummy(RenderScope scope) {
renderArmor(scope, new ItemStack(Items.DIAMOND_HELMET), new ItemStack(Items.DIAMOND_CHESTPLATE), new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS)); renderArmor(scope, new ItemStack(Items.DIAMOND_SWORD), new ItemStack(Items.DIAMOND_HELMET),
new ItemStack(Items.DIAMOND_CHESTPLATE),
new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS));
} }
@Override @Override
@@ -80,7 +96,7 @@ public class ArmorDisplay extends Module implements HudMod {
} }
@Override @Override
public void setEnabled(boolean e) { public void onEnabled(boolean e) {
enabled.value = e; enabled.value = e;
} }
} }
@@ -8,7 +8,7 @@ import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property; import org.saturnclient.saturnclient.config.Property;
public class AutoSprint extends Module { public class AutoSprint extends Module {
private static Property<Boolean> enabled = new Property<>(true); private static Property<Boolean> enabled = Property.bool(false);
public AutoSprint() { public AutoSprint() {
super( super(
@@ -24,8 +24,12 @@ public class AutoSprint extends Module {
if (SaturnClient.client.player == null || SaturnClient.client.getNetworkHandler() == null) { if (SaturnClient.client.player == null || SaturnClient.client.getNetworkHandler() == null) {
return; return;
} }
if (SaturnClient.client.options.forwardKey.isPressed() && !SaturnClient.client.options.backKey.isPressed() && !SaturnClient.client.player.isSneaking() && if (SaturnClient.client.options.forwardKey.isPressed()
!SaturnClient.client.player.horizontalCollision) { && !SaturnClient.client.options.backKey.isPressed()
&& !SaturnClient.client.player.isSneaking()
&& !SaturnClient.client.player.horizontalCollision
&& !SaturnClient.client.player.isUsingItem()) {
Objects.requireNonNull(SaturnClient.client.player).setSprinting(true); Objects.requireNonNull(SaturnClient.client.player).setSprinting(true);
} }
} }
@@ -36,7 +40,7 @@ public class AutoSprint extends Module {
} }
@Override @Override
public void setEnabled(boolean e) { public void onEnabled(boolean e) {
enabled.value = e; enabled.value = e;
} }
} }
@@ -0,0 +1,75 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
public class Coordinates extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMethod = Property.select(0, "Flat", "Flat annotated", "Newline", "Newline annotated");
private static ModDimensions dimensions = new ModDimensions(40, 18);
public Coordinates() {
super(new ModuleDetails("Coordinates", "coords")
.description("Displays your current coordinates")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMethod.named("Display Method"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderCoords(532, 69, 253, scope);
}
@Override
public void renderHud(RenderScope scope) {
Vec3d pos = SaturnClient.client.player.getPos();
renderCoords((int) pos.x, (int) pos.y, (int) pos.z, scope);
}
public void renderCoords(int x, int y, int z, RenderScope scope) {
String f = "%d %d %d"; // Flat
switch (displayMethod.value) {
case 1: // Flat annotated
f = "X: %d Y: %d Z: %d";
break;
case 2: // Newline
f = "%d\n%d\n%d";
break;
case 3: // Newline annotated
f = "X: %d\nY: %d\nZ: %d";
break;
}
String text = String.format(f, x, y, z);
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -10,8 +10,7 @@ import org.saturnclient.ui2.resources.Textures;
import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.RenderLayer;
public class Crosshair extends Module { public class Crosshair extends Module {
public static Property<Boolean> enabled = new Property<>(false); public static Property<Boolean> enabled = Property.bool(false);
public static Property<Boolean> range_indicator = new Property<>(false);
public Crosshair() { public Crosshair() {
super( super(
@@ -19,14 +18,12 @@ public class Crosshair extends Module {
.description("Changes the crosshair if you're aiming at a entity") .description("Changes the crosshair if you're aiming at a entity")
.tags("Visuals", "Utility") .tags("Visuals", "Utility")
.version("v0.1.0"), .version("v0.1.0"),
enabled.named("Enabled"), enabled.named("Enabled"));
range_indicator.named("Range Indicator"));
} }
@Override @Override
public void render(RenderScope scope) { public void render(RenderScope scope) {
if (enabled.value if (enabled.value
&& range_indicator.value
&& SaturnClient.client.targetedEntity != null && SaturnClient.client.targetedEntity != null
&& SaturnClient.client.targetedEntity.isAlive()) { && SaturnClient.client.targetedEntity.isAlive()) {
int scaledWidth = 15; int scaledWidth = 15;
@@ -45,7 +42,7 @@ public class Crosshair extends Module {
} }
@Override @Override
public void setEnabled(boolean e) { public void onEnabled(boolean e) {
enabled.value = e; enabled.value = e;
} }
} }
@@ -0,0 +1,61 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class DayCounter extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(40, 18);
long day = 0;
public DayCounter() {
super(new ModuleDetails("DayCounter", "day")
.description("Displays the number of days that have passed")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderDay(271, scope);
}
@Override
public void renderHud(RenderScope scope) {
day = (SaturnClient.client.world.getTimeOfDay() / 24000L);
renderDay(day, scope);
}
public void renderDay(long day, RenderScope scope) {
String text = "Day: " + day;
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,53 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class Fps extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
public Fps() {
super(new ModuleDetails("FPS Display", "fps")
.description("Displays current FPS")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
@Override
public void renderHud(RenderScope scope) {
String text = String.valueOf(SaturnClient.client.getCurrentFps()) + " FPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public void renderDummy(RenderScope scope) {
String text = "369 FPS";
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,71 @@
package org.saturnclient.modules.mods;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import net.minecraft.client.option.Perspective;
public class Freelook extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(true);
public static Property<Integer> freelookKey = Property.keybinding(GLFW.GLFW_KEY_H);
public static boolean isFreeLooking = false;
private static boolean was1stPerson = false;
public Freelook() {
super(
new ModuleDetails("Freelook", "freelook")
.description("Allows you to look around freely without moving your character")
.tags("Movement", "Camera")
.version("0.1.0"),
enabled.named("Enabled"),
toggle.named("Toggle freelook"),
freelookKey.named("Freelook Keybinding")
);
}
@Override
public void tick() {
if (toggle.value) {
if (freelookKey.wasKeyPressed()) {
if (isFreeLooking) {
stopFreelook();
} else {
startFreelook();
}
}
} else if (freelookKey.isKeyPressed()) {
if (!isFreeLooking) {
startFreelook();
}
} else if (isFreeLooking) {
stopFreelook();
}
}
private void startFreelook() {
isFreeLooking = true;
was1stPerson = SaturnClient.client.options.getPerspective() == Perspective.FIRST_PERSON;
SaturnClient.client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
private void stopFreelook() {
if (was1stPerson) {
SaturnClient.client.options.setPerspective(Perspective.FIRST_PERSON);
}
isFreeLooking = false;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,39 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.Property;
public class Fullbright extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> brightness = Property.integer(100);
public Fullbright() {
super(
new ModuleDetails("Fullbright", "fullbright")
.description("Allows you to see in the dark")
.tags("Camera")
.version("v0.1.0"),
enabled.named("Enabled"),
brightness.named("Brightness %"));
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
public static boolean shouldOverrideBrightness() {
return enabled.value;
}
public static float getBrightnessValue() {
return brightness.value / 10;
}
}
@@ -0,0 +1,85 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
public class HealthDisplay extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMode = Property.select(0, "Health", "Hearts");
private static Property<Integer> decimals = Property.select(0, "0", "1", "2");
private static ModDimensions dimensions = new ModDimensions(40, 18);
double speed = 0.;
Vec3d velocity;
double y = 0.;
public HealthDisplay() {
super(new ModuleDetails("Health display", "health")
.description("Displays your current health")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMode.named("Display mode"),
decimals.named("Decimals"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderSpeed(10, scope);
}
@Override
public void renderHud(RenderScope scope) {
float health = SaturnClient.client.player.getHealth();
switch (displayMode.value) {
case 1:
health = health / 2;
}
renderSpeed(health, scope);
}
public void renderSpeed(float health, RenderScope scope) {
String text = "";
switch (decimals.value) {
case 0:
text = String.format("%.0f ", health);
break;
case 1:
text = String.format("%.1f ", health);
break;
case 2:
text = String.format("%.2f ", health);
break;
}
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,174 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.modules.ModuleDetails;
public class Keystrokes extends Module implements HudMod {
public static Property<Boolean> enabled = Property.bool(false);
public static Property<Boolean> showMouse = Property.bool(true);
public static Property<Boolean> 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;
private boolean d = false;
private boolean lmb = false;
private boolean rmb = false;
private boolean space = false;
// Cache dimensions to avoid recalculation
private int cachedHeight = 54;
private boolean dimensionsDirty = true;
private static Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static Property<Integer> clickFg = Property.color(0xFFFFFFFF);
public Keystrokes() {
super(new ModuleDetails("Keystrokes", "keystrokes")
.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"));
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();
d = SaturnClient.client.options.rightKey.isPressed();
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) {
// Use cached dimensions
dimensions.height = cachedHeight;
// WASD keys (3px padding between each key)
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, false, "LMB", 0, 54); // Below A/S/D
renderKeyM(scope, false, "RMB", 40, 54); // With 3px between
}
if (showSpace.value) {
renderKeySpace(scope, false, 0, cachedHeight - (showSpace.value ? 22 : 0));
}
}
@Override
public void renderHud(RenderScope scope) {
// 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
if (showMouse.value) {
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, cachedHeight - (showSpace.value ? 22 : 0));
}
}
// Consistent 3px padding applied below:
private void renderKeySpace(RenderScope scope, boolean isPressed, int x, int y) {
int height = 19;
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, fgColor);
}
private void renderKeyM(RenderScope scope, boolean isPressed, String c, int x, int y) {
int width = 38, height = 24;
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, fgColor);
}
private void renderKey(RenderScope scope, boolean isPressed, char c, int x, int y) {
int size = 24;
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, fgColor);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
dimensionsDirty = true;
}
}
@@ -0,0 +1,39 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.Property;
public class NoFog extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> liquids = Property.bool(false);
public NoFog() {
super(
new ModuleDetails("No Fog", "nofog")
.description("Stops fog from rendering")
.tags("Camera")
.version("v0.1.0"),
enabled.named("Enabled"),
liquids.named("See through liquids"));
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
// static for the mixin
public static boolean isActive() {
return enabled.value;
}
public static boolean liquids() {
return liquids.value;
}
}
@@ -0,0 +1,114 @@
package org.saturnclient.modules.mods;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.util.math.Vec3d;
import net.minecraft.entity.Entity;
public class Speedometer extends Module implements HudMod {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> axis = Property.select(1, "Absolute", "X axis", "Y axis");
private static Property<Integer> unitText = Property.select(0, "None", "Blocks/s", "blocks/s", "b/s");
private static Property<Boolean> speedText = Property.bool(true);
private static ModDimensions dimensions = new ModDimensions(40, 18);
double speed = 0.;
Vec3d velocity;
double y = 0.;
public Speedometer() {
super(new ModuleDetails("Speedometer", "speed")
.description("Displays your current speed")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
axis.named("Speed type"),
unitText.named("Unit text"),
speedText.named("Show speed text"),
dimensions.prop()
);
}
@Override
public void renderDummy(RenderScope scope) {
renderSpeed(2.71, scope);
}
@Override
public void renderHud(RenderScope scope) {
Entity vehicle = SaturnClient.client.player.getVehicle();
Entity player = SaturnClient.client.player;
if (vehicle == null) {
velocity = player.getVelocity();
y = player.isOnGround() ? 0. : velocity.y;
} else {
velocity = vehicle.getVelocity();
y = vehicle.isOnGround() ? 0. : velocity.y;
}
switch (axis.value) {
case 0: // Absolute
speed = (Math.sqrt(Math.pow(velocity.x, 2) + Math.pow(y, 2) + Math.pow(velocity.z, 2)));
break;
case 1: // x axis
speed = (Math.sqrt(Math.pow(velocity.x, 2) + Math.pow(velocity.z, 2)));
break;
case 2: // y axis
speed = (Math.abs(y));
break;
}
renderSpeed(speed * 20, scope); // blocks/tick to blocks/s
}
public void renderSpeed(double speed, RenderScope scope) {
String f = String.format("%.2f ", speed);
switch (unitText.value) {
case 1:
f += "Blocks/s";
break;
case 2:
f += "blocks/s";
break;
case 3:
f += "b/s";
break;
}
String text = f;
if (speedText.value) {
text = "Speed: " + text;
}
scope.drawText(text,
0, 0, dimensions.font.value, dimensions.fgColor.value);
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
dimensions.height = 18 * text.split("\n").length;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,105 @@
package org.saturnclient.modules.mods;
import java.util.Arrays;
import org.saturnclient.modules.HudMod;
import org.saturnclient.modules.ModDimensions;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.RenderScope;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.texture.StatusEffectSpriteManager;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
public class StatusEffects extends Module implements HudMod {
public static Property<Boolean> enabled = Property.bool(false);
private static ModDimensions dimensions = new ModDimensions(60, 0);
private final StatusEffectInstance[] dummyEffects = {
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2),
};
public StatusEffects() {
super(new ModuleDetails("Status effects", "effect")
.description("Displays status effects")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
private void renderEffects(RenderScope scope, Iterable<StatusEffectInstance> effects) {
int row = 0;
StatusEffectSpriteManager statusEffectSpriteManager = SaturnClient.client.getStatusEffectSpriteManager();
for (StatusEffectInstance effect : effects) {
if (effect.shouldShowIcon()) {
RegistryEntry<StatusEffect> registryEntry = effect.getEffectType();
Sprite sprite = statusEffectSpriteManager.getSprite(registryEntry);
scope.drawSpriteStretched(sprite, 0, 18 * row, 16, 16);
scope.drawText(0.5f, getDurationAsString(effect), 18, 18 * row + 3, dimensions.font.value, -1);
row++;
}
}
dimensions.height = 18 * row;
}
@Override
public void renderHud(RenderScope scope) {
renderEffects(scope, SaturnClient.client.player.getStatusEffects());
}
@Override
public void renderDummy(RenderScope scope) {
renderEffects(scope, Arrays.asList(dummyEffects));
}
// This code was from: https://github.com/magicus/statuseffecttimer/blob/master/src/main/java/se/icus/mag/statuseffecttimer/mixin/StatusEffectTimerMixin.java
private String getDurationAsString(StatusEffectInstance statusEffectInstance) {
if (statusEffectInstance.isInfinite()) {
return I18n.translate("effect.duration.infinite");
}
int ticks = MathHelper.floor((float) statusEffectInstance.getDuration());
int totalSeconds = ticks / 20;
if (totalSeconds >= 3600) {
return totalSeconds / 3600 + "h";
} else if (totalSeconds >= 60) {
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return String.format("%d:%02d", minutes, seconds);
} else {
return String.valueOf(totalSeconds) + "s";
}
}
@Override
public ModDimensions getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,60 @@
package org.saturnclient.modules.mods;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.modules.Module;
import org.saturnclient.modules.ModuleDetails;
import org.saturnclient.saturnclient.config.Property;
public class Zoom extends Module {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(false);
public static Property<Integer> zoomKey = Property.keybinding(GLFW.GLFW_KEY_C);
public static Property<Float> zoomLevel = Property.floatProp(3.0f);
public static boolean isZooming = false;
public Zoom() {
super(
new ModuleDetails("Zoom", "zoom")
.description("Allows you to zoom in for a closer view")
.tags("Camera", "Utility")
.version("0.1.0"),
enabled.named("Enabled"),
toggle.named("Toggle zoom"),
zoomKey.named("Zoom Keybinding"),
zoomLevel.named("Zoom Level")
);
}
@Override
public void tick() {
if (toggle.value) {
if (zoomKey.wasKeyPressed()) {
isZooming = !isZooming;
}
} else {
isZooming = zoomKey.isKeyPressed();
}
}
public static float getZoomLevel() {
return zoomLevel.value;
}
public static boolean shouldZoom() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
if (!e) {
isZooming = false;
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,33 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.render.BackgroundRenderer;
import org.saturnclient.modules.mods.NoFog;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(BackgroundRenderer.class)
public class BackgroundRendererMixin {
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFog.isActive()) {
return Float.MAX_VALUE; // this could be customised
}
return viewDistance;
}
// liquids
@Redirect(
method = "applyFog",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isSpectator()Z")
)
private static boolean redirectIsSpectator(Entity entity) {
if (NoFog.isActive() && NoFog.liquids()) {
return true; // pretend to be spectator
}
return entity.isSpectator();
}
}
@@ -0,0 +1,44 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
import org.saturnclient.modules.mods.Freelook;
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing, float tickDelta, CallbackInfo ci) {
if (Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && MinecraftClient.getInstance().player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(MinecraftClient.getInstance().player.getPitch());
cameraOverriddenEntity.freelook$setCameraYaw(MinecraftClient.getInstance().player.getYaw());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(), cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -0,0 +1,62 @@
package org.saturnclient.modules.mods.mixins;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
import org.saturnclient.modules.mods.Freelook;
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
//noinspection ConstantValue// IntelliJ is incorrect here, this code block is reachable
if (Freelook.isFreeLooking && (Object) this instanceof ClientPlayerEntity) {
double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15);
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta;
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return this.cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return this.cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float pitch) {
this.cameraPitch = pitch;
}
@Override
@Unique
public void freelook$setCameraYaw(float yaw) {
this.cameraYaw = yaw;
}
}
@@ -0,0 +1,22 @@
package org.saturnclient.modules.mods.mixins;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.saturnclient.modules.mods.Zoom;
import net.minecraft.client.render.GameRenderer;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (Zoom.isZooming && Zoom.shouldZoom()) {
float originalFov = cir.getReturnValue();
float zoomedFov = originalFov / Zoom.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -0,0 +1,23 @@
// code modified from Gamma utils: https://github.com/Sjouwer/gamma-utils
package org.saturnclient.modules.mods.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager;
import org.saturnclient.modules.mods.Fullbright;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
@ModifyExpressionValue(
method = "update",
at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)
)
private float modifyGamma(float original) {
if (Fullbright.shouldOverrideBrightness()) {
return Fullbright.getBrightnessValue();
}
return original;
}
}
@@ -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.ENABLED_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,9 @@
package org.saturnclient.modules.mods.utils;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -2,14 +2,23 @@ package org.saturnclient.saturnclient;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.util.math.MatrixStack;
import org.saturnclient.modules.ModManager; import org.saturnclient.modules.ModManager;
import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.config.ConfigManager; import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.saturnclient.cosmetics.Emotes;
import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.saturnclient.event.KeyInputHandler; import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.ui2.screens.TitleMenu;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -37,7 +46,35 @@ public class SaturnClient implements ModInitializer {
return (resourceManager, location) -> MOD_ID.equals(location.getNamespace()); return (resourceManager, location) -> MOD_ID.equals(location.getNamespace());
}); });
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));
} else {
ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, tickDelta) -> {
RenderScope renderScope = new RenderScope(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
int width = context.getScaledWindowWidth();
int height = context.getScaledWindowHeight();
float scaleX = (float) screen1.width / width;
float scaleY = (float) screen1.height / height;
MatrixStack matrixStack = context.getMatrices();
matrixStack.push();
matrixStack.translate(width - 95, height - 14, 0);
matrixStack.scale(0.5f / scaleX, 0.5f / scaleY, 1.0f);
renderScope.drawTexture(Textures.LOGO_TEXT_BIG, 0, 0, 0, 0, 180, 18);
matrixStack.pop();
});
}
}
});
KeyInputHandler.register(); KeyInputHandler.register();
Emotes.initialize();
if (Auth.authenticate()) { if (Auth.authenticate()) {
Cloaks.initialize(); Cloaks.initialize();
Hats.initialize(); Hats.initialize();
@@ -1,6 +1,7 @@
package org.saturnclient.saturnclient; package org.saturnclient.saturnclient;
import org.saturnclient.saturnclient.config.Property; import org.saturnclient.saturnclient.config.Property;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.config.ConfigManager; import org.saturnclient.saturnclient.config.ConfigManager;
import org.saturnclient.ui2.resources.Textures; import org.saturnclient.ui2.resources.Textures;
@@ -10,11 +11,10 @@ import net.minecraft.util.Identifier;
public class SaturnClientConfig { public class SaturnClientConfig {
public static ConfigManager config; public static ConfigManager config;
public static Property<Boolean> realisticLogo = public static Property<Boolean> realisticLogo = Property.bool(false);
new Property<>(false); public static Property<Boolean> saturnTitleScreen = Property.bool(true);
public static Property<Boolean> bendyCloaks = Property.bool(true);
public static Property<Boolean> saturnTitleScreen = public static Property<Integer> openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B);
new Property<>(true);
public static Identifier getLogo() { public static Identifier getLogo() {
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO; return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
@@ -32,7 +32,7 @@ public class SaturnClientConfig {
* - Partners: Gold * - Partners: Gold
* - Contributor: Aqua * - Contributor: Aqua
* - Other/player: White * - Other/player: White
*/ */
public static Formatting getIconColor(String uuid) { public static Formatting getIconColor(String uuid) {
if (uuid.equals("d362a04228bd49e1a807ae74dbe8aba9")) { if (uuid.equals("d362a04228bd49e1a807ae74dbe8aba9")) {
return Formatting.DARK_RED; // Owner return Formatting.DARK_RED; // Owner
@@ -45,5 +45,7 @@ public class SaturnClientConfig {
config = new ConfigManager("Saturn Client"); config = new ConfigManager("Saturn Client");
config.property("Realistic logo", realisticLogo); config.property("Realistic logo", realisticLogo);
config.property("Saturn client title screen", saturnTitleScreen); config.property("Saturn client title screen", saturnTitleScreen);
config.property("Open Emote Wheel", openEmoteWheel);
config.property("Bendy Cloaks", bendyCloaks);
} }
} }
@@ -2,9 +2,21 @@ package org.saturnclient.saturnclient.auth;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.util.Identifier;
import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.saturnclient.cosmetics.Hats;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.elements.Notification.NotificationKind;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
public class Auth { public class Auth {
@@ -15,7 +27,6 @@ public class Auth {
private static Thread pingThread; private static Thread pingThread;
private static volatile boolean running = false; private static volatile boolean running = false;
@SuppressWarnings("resource")
public static boolean authenticate() { public static boolean authenticate() {
// Register shutdown hook ONCE // Register shutdown hook ONCE
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close()); ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close());
@@ -96,9 +107,65 @@ public class Auth {
} }
break; break;
case "successful_transaction":
String cloak = parser.getStringOrNull("cloak");
String hat = parser.getStringOrNull("hat");
if (cloak != null) {
Cloaks.availableCloaks.add(cloak);
}
if (hat != null) {
Hats.availableHats.add(hat);
}
case "update_cloak":
String targetUuid = parser.getStringOrNull("uuid");
String targetCloak = parser.getStringOrNull("cloak");
if (targetUuid != null && targetCloak != null) {
SaturnPlayer player = players.get(targetUuid);
if (player != null) {
player.cloak = targetCloak;
}
}
break;
case "update_hat":
targetUuid = parser.getStringOrNull("uuid");
String targetHat = parser.getStringOrNull("hat");
if (targetUuid != null && targetHat != null) {
SaturnPlayer player = players.get(targetUuid);
if (player != null) {
player.hat = targetHat;
}
}
break;
case "emote":
targetUuid = parser.getStringOrNull("uuid");
String emote = parser.getStringOrNull("name");
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
if (player.getUuidAsString().replace("-", "").equals(targetUuid)) {
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
if (emote != null && !emote.isEmpty()) {
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
animationStack.removeLayer(1000);
}
animationStack.addAnimLayer(1000,
PlayerAnimationRegistry
.getAnimation(Identifier.of("saturnclient", emote))
.playAnimation());
} else {
animationStack.removeLayer(1000);
}
}
}
break;
default: default:
if (parser.error != null) { if (parser.error != null) {
SaturnClient.LOGGER.error("Error from the server: " + parser.error); String[] a = parser.error.replace("!", "").split(": ");
Utils.notify(NotificationKind.Error, a[0], a[1]);
} }
break; break;
} }
@@ -161,7 +228,7 @@ public class Auth {
public static void setCloak(String cloakName) { public static void setCloak(String cloakName) {
try { try {
Network.write("set_cloak@cloak=" + cloakName); Network.write("set_cloak@cloak=" + cloakName + "@notify=" + stringifyPlayers());
} catch (Exception e) { } catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e); SaturnClient.LOGGER.error("Request failed", e);
} }
@@ -169,12 +236,26 @@ public class Auth {
public static void setHat(String hatName) { public static void setHat(String hatName) {
try { try {
Network.write("set_hat@hat=" + hatName); Network.write("set_hat@hat=" + hatName + "@notify=" + stringifyPlayers());
} catch (Exception e) { } catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e); SaturnClient.LOGGER.error("Request failed", e);
} }
} }
public static void emote(String name) {
try {
Network.write("emote@name=" + name + "@notify=" + stringifyPlayers());
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
}
}
public static String stringifyPlayers() {
return players.keySet().stream()
.filter(key -> key != uuid)
.collect(Collectors.joining("$"));
}
public static void player(String name, String uuid) { public static void player(String name, String uuid) {
try { try {
playerNames.put(name, uuid.replaceAll("-", "")); playerNames.put(name, uuid.replaceAll("-", ""));
@@ -184,14 +265,19 @@ public class Auth {
} }
} }
public static void sendReload() { public static void buyCloak(String name) {
for (Map.Entry<String, String> player : playerNames.entrySet()) { try {
if (players.containsKey(player.getValue()) && !player.getValue().equals(uuid)) { Network.write("buy_cloak@cloak=" + name);
SaturnClient.client.player.networkHandler } catch (Exception e) {
.sendChatCommand("msg " + player.getKey() + " " SaturnClient.LOGGER.error("Request failed", e);
+ "$SATURN_RELOAD if you are seeing this as a player, please report this to https://github.com/saturnclientmc/saturnclient/issues"); }
; }
}
public static void buyHat(String name) {
try {
Network.write("buy_hat@hat=" + name);
} catch (Exception e) {
SaturnClient.LOGGER.error("Request failed", e);
} }
} }
} }
@@ -15,6 +15,9 @@ public class Network {
static BufferedReader in; static BufferedReader in;
static byte[] key = new byte[32]; static byte[] key = new byte[32];
private static final int CONNECTION_TIMEOUT = 5000;
private static final int READ_TIMEOUT = 5000;
public static class AES { public static class AES {
public static byte[] encryptAES256(String message, byte[] key) throws Exception { public static byte[] encryptAES256(String message, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
@@ -37,7 +40,11 @@ public class Network {
// Generate AES key // Generate AES key
new SecureRandom().nextBytes(key); new SecureRandom().nextBytes(key);
socket = new Socket("77.247.92.168", 8080); socket = new Socket();
socket.connect(new InetSocketAddress("77.247.92.168", 8080), CONNECTION_TIMEOUT);
socket.setSoTimeout(READ_TIMEOUT);
// out = new DataOutputStream(socket.getOutputStream()); // out = new DataOutputStream(socket.getOutputStream());
out = new PrintWriter(socket.getOutputStream(), true); out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream())); in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
@@ -43,6 +43,11 @@ public class SaturnParser {
} }
} }
public String getStringOrNull(String i) throws IOException {
String val = params.get(i);
return val;
}
public boolean getBool(String i) throws IOException { public boolean getBool(String i) throws IOException {
String val = params.get(i); String val = params.get(i);
if (val != null) { if (val != null) {
@@ -1,244 +1,116 @@
package org.saturnclient.saturnclient.config; package org.saturnclient.saturnclient.config;
import com.google.gson.Gson; import com.google.gson.*;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.HashMap; import java.util.*;
import java.util.LinkedHashMap;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
public class ConfigManager { public class ConfigManager {
private static File configFile = new File( private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json");
SaturnClient.client.runDirectory, private static final Map<String, Map<String, Property<?>>> properties = new HashMap<>();
"saturn.json");
private static Map<String, Map<String, Property<?>>> properties = new HashMap<>();
private static JsonObject cachedThemeJson = null; private static JsonObject cachedThemeJson = null;
private Map<String, Property<?>> currentMap; private final Map<String, Property<?>> currentMap;
private String namespace; private final String namespace;
public ConfigManager(String namespace) { public ConfigManager(String namespace) {
currentMap = new LinkedHashMap<>(); this.namespace = namespace;
this.currentMap = new LinkedHashMap<>();
properties.put(namespace, currentMap); properties.put(namespace, currentMap);
System.out.println("Created namespace: " + namespace); System.out.println("Created namespace: " + namespace);
} }
public ConfigManager(ConfigManager config, String namespace) { public ConfigManager(ConfigManager parent, String namespace) {
currentMap = new LinkedHashMap<>(); this.namespace = namespace;
Property<Map<String, Property<?>>> namespaceProperty = new Property<>(currentMap, this.currentMap = new LinkedHashMap<>();
Property.PropertyType.NAMESPACE); Property<Map<String, Property<?>>> nsProp = Property.namespace(currentMap);
config.property(namespace, namespaceProperty); parent.property(namespace, nsProp);
// Update the parent's state
config.currentMap.put(namespace, namespaceProperty);
System.out.println("Created sub-namespace: " + namespace); System.out.println("Created sub-namespace: " + namespace);
} }
public void sub(String namespace) { public void sub(String name) {
Property<Map<String, Property<?>>> namespaceProperty = new Property<>(currentMap, property(name, Property.namespace(new LinkedHashMap<>()));
Property.PropertyType.NAMESPACE);
property(namespace, namespaceProperty);
} }
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> value) { public <T> Property<T> property(String name, Property<T> value) {
System.out.println("Adding property: " + name);
currentMap.put(name, value); currentMap.put(name, value);
loadProp(name, value); loadProp(name, value);
return value; return value;
} }
@SuppressWarnings("unchecked") private <T> void loadProp(String name, Property<T> prop) {
public <T> void loadProp(String name, Property<T> prop) { JsonObject source = cachedThemeJson != null ? cachedThemeJson : loadAndCache();
if (cachedThemeJson != null) { if (source == null)
JsonElement element = cachedThemeJson.get(namespace); return;
if (element != null && element.isJsonObject()) {
JsonObject theme = element.getAsJsonObject(); JsonElement namespaceJson = source.get(namespace);
JsonElement value = theme.get(name); if (namespaceJson == null || !namespaceJson.isJsonObject())
if (value != null && prop.matchesJson(value)) { return;
switch (prop.getType()) {
case BOOLEAN: JsonElement value = namespaceJson.getAsJsonObject().get(name);
((Property<Boolean>) prop).setValue(value.getAsBoolean()); if (value != null)
break; prop.loadFromJson(value);
case INTEGER: }
((Property<Integer>) prop).setValue(value.getAsInt());
break; private static JsonObject loadAndCache() {
case FLOAT: try {
((Property<Float>) prop).setValue(value.getAsFloat()); if (!configFile.exists()) {
break; Files.writeString(configFile.toPath(), "{}");
case STRING: return new JsonObject();
((Property<String>) prop).setValue(value.getAsString());
break;
case HEX:
((Property<Integer>) prop).setValue(value.getAsInt());
break;
default:
break;
}
}
} }
} else { String json = Files.readString(configFile.toPath());
load(); cachedThemeJson = JsonParser.parseString(json).getAsJsonObject();
return cachedThemeJson;
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading config file", e);
return null;
} }
} }
public static void load() { public static void load() {
try { SaturnClient.LOGGER.info("Loading config...");
SaturnClient.LOGGER.info("Starting to load config..."); JsonObject json = loadAndCache();
if (!configFile.exists()) { if (json == null)
configFile.createNewFile(); return;
Files.write(configFile.toPath(), "{}".getBytes());
properties.forEach((namespace, map) -> {
JsonElement nsElement = json.get(namespace);
if (nsElement != null && nsElement.isJsonObject()) {
loadProperties(nsElement.getAsJsonObject(), map);
} }
});
JsonObject jsonObject = JsonParser.parseString(
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)
continue;
JsonObject config = jsonObject.get(namespace).getAsJsonObject();
if (config == null) {
continue;
}
Map<String, Property<?>> propertyMap = properties.get(namespace);
loadProperties(config, propertyMap);
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the config file", e);
}
} }
@SuppressWarnings("unchecked") private static void loadProperties(JsonObject json, Map<String, Property<?>> props) {
private static void loadProperties(JsonObject config, Map<String, Property<?>> propertyMap) { props.forEach((name, prop) -> {
SaturnClient.LOGGER.info("Loading properties: " + propertyMap); JsonElement el = json.get(name);
if (el != null)
for (String propertyName : propertyMap.keySet()) { prop.loadFromJson(el);
JsonElement c = config.get(propertyName); });
if (c == null) {
continue;
}
Property<?> p = propertyMap.get(propertyName);
if (p.matchesJson(c)) {
if (p.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedConfig = c.getAsJsonObject();
Map<String, Property<?>> nestedProperties = p.getNamespaceValue();
loadProperties(nestedConfig, nestedProperties);
} else {
// Handle regular properties
switch (p.getType()) {
case BOOLEAN:
((Property<Boolean>) p).value = c.getAsBoolean();
break;
case INTEGER:
((Property<Integer>) p).value = c.getAsInt();
break;
case FLOAT:
((Property<Float>) p).value = c.getAsFloat();
break;
case STRING:
((Property<String>) p).value = c.getAsString();
break;
case HEX:
((Property<Integer>) p).value = c.getAsInt();
break;
default:
break;
}
}
} else {
SaturnClient.LOGGER.warn(
"Property does not match JSON: " + propertyName);
}
}
} }
public static void save() { public static void save() {
SaturnClient.LOGGER.info("Saving config...");
JsonObject root = new JsonObject();
properties.forEach((namespace, map) -> {
JsonObject nsJson = new JsonObject();
saveProperties(nsJson, map);
root.add(namespace, nsJson);
});
try { try {
SaturnClient.LOGGER.info("Starting to save config..."); String formatted = new GsonBuilder().setPrettyPrinting().create().toJson(root);
Files.writeString(configFile.toPath(), formatted);
JsonObject jsonObject = new JsonObject();
// Iterate through all namespaces and their properties
for (String namespace : properties.keySet()) {
JsonObject namespaceConfig = new JsonObject();
Map<String, Property<?>> propertyMap = properties.get(namespace);
// Save properties for this namespace
saveProperties(namespaceConfig, propertyMap);
// Add the namespace config to the main JSON object
jsonObject.add(namespace, namespaceConfig);
}
// Format the json so it can easily be edited
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String formattedJson = gson.toJson(jsonObject);
// Write the JSON object to the config file
Files.write(configFile.toPath(), formattedJson.getBytes());
SaturnClient.LOGGER.info("Config saved successfully."); SaturnClient.LOGGER.info("Config saved successfully.");
} catch (IOException e) { } catch (IOException e) {
SaturnClient.LOGGER.error("Error saving the config file", e); SaturnClient.LOGGER.error("Error saving config file", e);
} }
} }
@SuppressWarnings("unchecked") private static void saveProperties(JsonObject json, Map<String, Property<?>> props) {
private static void saveProperties(JsonObject config, Map<String, Property<?>> propertyMap) { props.forEach((name, prop) -> json.add(name, prop.toJson()));
// Iterate through each property in the namespace
for (String propertyName : propertyMap.keySet()) {
Property<?> property = propertyMap.get(propertyName);
JsonElement propertyValue = null;
if (property.getType() == Property.PropertyType.NAMESPACE) {
// Handle nested namespace
JsonObject nestedConfig = new JsonObject();
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) property.value;
saveProperties(nestedConfig, nestedProperties);
propertyValue = nestedConfig;
} 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) {
config.add(propertyName, propertyValue);
}
}
} }
public Map<String, Property<?>> getProperties() { public Map<String, Property<?>> getProperties() {
@@ -2,7 +2,11 @@ package org.saturnclient.saturnclient.config;
import java.util.Map; import java.util.Map;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
public class Property<T> { public class Property<T> {
@@ -13,85 +17,108 @@ public class Property<T> {
FLOAT, FLOAT,
STRING, STRING,
HEX, HEX,
NAMESPACE NAMESPACE,
SELECT,
KEYBINDING,
} }
public T value; public T value;
public boolean isReset = false; public boolean isReset = false;
private T defaultValue; private T defaultValue;
private PropertyType type; private String[] availableValues;
private final PropertyType type;
private boolean wasPressedLastTick = false;
public Property(T value) { private Property(T value, PropertyType type) {
this.value = value;
this.defaultValue = value;
if (value instanceof Boolean) {
type = PropertyType.BOOLEAN;
} else if (value instanceof Integer) {
type = PropertyType.INTEGER;
} else if (value instanceof Float) {
type = PropertyType.FLOAT;
} else if (value instanceof String) {
type = PropertyType.STRING;
} else if (valueIsNamespace(value)) {
type = PropertyType.NAMESPACE;
}
}
public Property(T value, PropertyType type) {
this.value = value; this.value = value;
this.defaultValue = value; this.defaultValue = value;
this.type = type; this.type = type;
} }
public boolean getBool() { // ---------- Factory Methods ----------
if (value instanceof Boolean) { public static <T> Property<T> from(T value) {
return (boolean) value; if (value instanceof Boolean)
} return new Property<>(value, PropertyType.BOOLEAN);
throw new IllegalStateException("Property does not contain a 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 int getInt() { public static Property<Integer> font(int value) {
if (value instanceof Integer) { return select(value, "Default", "Inter", "Inter bold");
return (int) value;
}
throw new IllegalStateException("Property does not contain an int");
} }
public float getFloat() { public static Property<Integer> select(Integer value, String... availableValues) {
if (value instanceof Float) { Property<Integer> property = new Property<>(value, PropertyType.SELECT);
return (float) value; property.availableValues = availableValues;
} return property;
throw new IllegalStateException("Property does not contain a float");
} }
public String getString() { public static Property<Map<String, Property<?>>> namespace(Map<String, Property<?>> value) {
if (value instanceof String) { return new Property<>(value, PropertyType.NAMESPACE);
return (String) value;
}
throw new IllegalStateException("Property does not contain a string");
} }
public static Property<Boolean> bool(boolean value) {
return new Property<>(value, PropertyType.BOOLEAN);
}
public static Property<Integer> integer(int value) {
return new Property<>(value, PropertyType.INTEGER);
}
public static Property<Float> floatProp(float value) {
return new Property<>(value, PropertyType.FLOAT);
}
public static Property<String> string(String value) {
return new Property<>(value, PropertyType.STRING);
}
public static Property<Integer> color(int value) {
return new Property<>(value, PropertyType.HEX);
}
public static Property<Integer> 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() { public void reset() {
value = defaultValue; value = defaultValue;
isReset = true; isReset = true;
} }
public int getHexString() {
if (value instanceof String) {
String str = (String) value;
if (str.startsWith("0x") || str.startsWith("0X")) {
try {
return Integer.parseInt(str.substring(2), 16);
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Invalid hexadecimal integer format");
}
}
}
throw new IllegalStateException(
"Property does not contain a valid hex integer");
}
public PropertyType getType() { public PropertyType getType() {
return type; return type;
} }
@@ -104,38 +131,93 @@ public class Property<T> {
throw new IllegalStateException("Property is not a namespace"); throw new IllegalStateException("Property is not a namespace");
} }
public boolean matchesJson(JsonElement element) { // ---------- JSON Serialization ----------
if (element.isJsonPrimitive()) { /*
JsonPrimitive primitive = element.getAsJsonPrimitive(); * BOOLEAN,
if (value instanceof Boolean && primitive.isBoolean()) * INTEGER,
return true; * FLOAT,
if (value instanceof Integer && primitive.isNumber()) * STRING,
return true; * HEX,
if (value instanceof Float && primitive.isNumber()) * NAMESPACE,
return true; * SELECT,
if (value instanceof String && primitive.isString()) * KEYBINDING,
return true; */
public JsonElement toJson() {
// Check if JSON string represents a valid hex integer switch (type) {
if (value instanceof String && primitive.isString()) { case BOOLEAN:
String str = primitive.getAsString(); return new JsonPrimitive((Boolean) value);
return str.matches("0x[0-9A-Fa-f]+"); case INTEGER:
} return new JsonPrimitive((Integer) value);
} else if (element.isJsonObject()) { case FLOAT:
return isNamespace(value); return new JsonPrimitive((Float) value);
case NAMESPACE:
JsonObject nested = new JsonObject();
getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson()));
return nested;
case HEX:
return new JsonPrimitive(String.format("#%08X", (Integer) value));
case KEYBINDING:
return new JsonPrimitive((Integer) value);
case SELECT:
return new JsonPrimitive((Integer) value);
default:
return new JsonPrimitive(String.valueOf(value));
} }
return false;
} }
public void loadFromJson(JsonElement element) {
if (element == null)
return;
switch (type) {
case BOOLEAN:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean())
setValue(element.getAsBoolean());
break;
case INTEGER:
if (element.isJsonPrimitive()) {
JsonPrimitive p = element.getAsJsonPrimitive();
if (p.isNumber())
setValue(p.getAsInt());
else if (p.isString())
setValue(parseHexToInt(p.getAsString()));
}
break;
case FLOAT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsFloat());
break;
case STRING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(element.getAsString());
break;
case HEX:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
setValue(parseHexToInt(element.getAsString()));
break;
case NAMESPACE:
if (element.isJsonObject()) {
JsonObject obj = element.getAsJsonObject();
getNamespaceValue().forEach((k, v) -> v.loadFromJson(obj.get(k)));
}
break;
case SELECT:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setSelection(element.getAsInt());
break;
case KEYBINDING:
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
setValue(element.getAsInt());
break;
}
}
// ---------- Utility ----------
@Override @Override
public String toString() { public String toString() {
return String.valueOf(value); return String.valueOf(value);
} }
public boolean isNamespace(Object obj) {
return type == PropertyType.NAMESPACE && value instanceof Map;
}
public static boolean valueIsNamespace(Object obj) { public static boolean valueIsNamespace(Object obj) {
return obj instanceof Map; return obj instanceof Map;
} }
@@ -154,4 +236,45 @@ public class Property<T> {
public NamedProperty<T> named(String name) { public NamedProperty<T> named(String name) {
return new NamedProperty<>(name, this); return new NamedProperty<>(name, this);
} }
// ---------- Keybindings ----------
public boolean isKeyPressed() {
return (Integer) value != -1 &&
GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS &&
SaturnClient.client.currentScreen == null;
}
public boolean wasKeyPressed() {
boolean pressed = isKeyPressed();
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<T> copy() {
return new Property<>(this.value, this.type);
}
public void load(String name, JsonElement element) {
if (element != null && element.isJsonObject()) {
load(name, element.getAsJsonObject());
}
}
public void load(String name, JsonObject element) {
JsonElement value = element.get(name);
if (value != null)
this.loadFromJson(value);
}
} }
@@ -3,126 +3,75 @@ package org.saturnclient.saturnclient.config;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.HashMap; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.saturnclient.saturnclient.SaturnClient; 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.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
public class ThemeManager { public class ThemeManager {
private static File themeFile = new File( private static File themeFile = new File(
SaturnClient.client.runDirectory, SaturnClient.client.runDirectory,
"saturn.theme.json"); "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 static JsonObject cachedThemeJson = null;
// private Map<String, Map<String, Property<?>>> states; private Map<String, Map<String, Object>> states;
private Map<String, Property<?>> currentStyling; private Map<String, Property<?>> currentStyling;
private String state;
private String[] states;
private String namespace; private String namespace;
public ThemeManager(String namespace, String... states) { public ThemeManager(String namespace, String... states) {
// this.states = new LinkedHashMap<>();
this.namespace = namespace; this.namespace = namespace;
this.states = states; this.states = new LinkedHashMap<>();
currentStyling = new LinkedHashMap<>(); this.currentStyling = new LinkedHashMap<>();
this.states.put(null, new LinkedHashMap<>());
properties.put(namespace, new LinkedHashMap<>());
for (String s : states) { for (String s : states) {
properties.put(namespace+"@"+s, new LinkedHashMap<>()); this.states.put(s, new LinkedHashMap<>());
} }
theme.add(this);
} }
public void setState(String state) { public void setState(String state) {
this.state = state; for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
if (this.state != null) { currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
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()) {
currentStyling.get(tProp.getKey()).setValue(tProp.getValue().value);
}
} }
} }
public void applyState(String state) { public void applyState(String state) {
if (this.state != null) { for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
for (Map.Entry<String, Property<?>> tProp : properties.get(namespace+"@"+state).entrySet()) { if (!states.get(null).get(tProp.getKey()).equals(tProp.getValue()))
Property<?> p = currentStyling.get(tProp.getKey()); currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
Property<?> currProp = properties.get(namespace).get(tProp.getKey());
if (p.value.equals(currProp.value)) {
p.setValue(tProp.getValue().value);
}
}
} else {
setState(state);
} }
} }
// Generic method to store any type of property
public <T> Property<T> property(String name, Property<T> prop) { public <T> Property<T> property(String name, Property<T> prop) {
for (String state : states) { for (Map.Entry<String, Map<String, Object>> state : this.states.entrySet()) {
Property<?> property = new Property<T>(prop.value); Property<?> property = prop.copy();
properties.get(namespace+"@"+state).put(name, property); if (cachedThemeJson == null) {
loadProp(state, name, property); 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, new Property<T>(prop.value));
currentStyling.put(name, prop); currentStyling.put(name, prop);
return prop; return prop;
} }
public <T> void propertyStateDefault(String state, String name, T value) { public <T> void propertyStateDefault(String state, String name, T value) {
Property<?> prop = properties.get(namespace+"@"+state).get(name); Property<?> prop = currentStyling.get(name).copy();
prop.setValue(value, value); // sets the current and default value prop.setValue(value, value);
loadProp(state, name, prop);
}
@SuppressWarnings("unchecked")
public <T> void loadProp(String state, String name, Property<T> prop) {
if (cachedThemeJson == null) { if (cachedThemeJson == null) {
load(); load();
} }
prop.load(name, cachedThemeJson.get(state != null ? namespace + "@" + state : namespace));
JsonElement element = cachedThemeJson.get(state != null ? namespace+"@"+state : namespace); states.get(state).put(name, prop.value);
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;
}
}
}
} }
public static void load() { public static void load() {
@@ -134,85 +83,11 @@ public class ThemeManager {
} }
cachedThemeJson = JsonParser.parseString( cachedThemeJson = JsonParser.parseString(
new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject(); new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject();
SaturnClient.LOGGER.info("Theme loaded"); SaturnClient.LOGGER.info("Theme loaded");
} catch (IOException e) { } catch (IOException e) {
SaturnClient.LOGGER.error("Error reading the theme file", 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);
}
}
}
} }
@@ -1,5 +1,6 @@
package org.saturnclient.saturnclient.cosmetics; package org.saturnclient.saturnclient.cosmetics;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
@@ -12,13 +13,8 @@ import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType; import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
import net.minecraft.client.render.entity.feature.FeatureRenderer; import net.minecraft.client.render.entity.feature.FeatureRenderer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext; import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.model.BipedEntityModel;
import net.minecraft.client.render.entity.model.EntityModelLayers;
import net.minecraft.client.render.entity.model.LoadedEntityModels;
import net.minecraft.client.render.entity.model.PlayerCapeModel;
import net.minecraft.client.render.entity.model.PlayerEntityModel; import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState; import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.SkinTextures;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.component.DataComponentTypes; import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.EquippableComponent; import net.minecraft.component.type.EquippableComponent;
@@ -26,21 +22,67 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.equipment.EquipmentAsset; import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey; import net.minecraft.registry.RegistryKey;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.Vec3d;
/**
* The cloak renderer
* - **Efficient**: uses a image only consisting of the cloak texture, 16x8
* whitespace.
* - **Curving**: Curves when player velocity increases.
* - **Wavey**: Makes the cloak feel like wind, when jump sprinting.
*/
public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> { public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
private final BipedEntityModel<PlayerEntityRenderState> model;
private final EquipmentModelLoader equipmentModelLoader; private final EquipmentModelLoader equipmentModelLoader;
private static final int PARTS = 16;
private float currentVelocity = 0.0f;
private static final float TEX_W = 176f;
private static final float TEX_H = 138f;
private static final class PxRect {
final int x, y, w, h;
PxRect(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
float u1() {
return x / TEX_W;
}
float v1() {
return y / TEX_H;
}
float u2() {
return (x + w) / TEX_W;
}
float v2() {
return (y + h) / TEX_H;
}
}
private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128);
private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128);
private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128);
private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128);
private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context, public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context,
LoadedEntityModels modelLoader, EquipmentModelLoader equipmentModelLoader) { EquipmentModelLoader equipmentModelLoader) {
super(context); super(context);
this.model = new PlayerCapeModel<PlayerEntityRenderState>(
modelLoader.getModelPart(EntityModelLayers.PLAYER_CAPE));
this.equipmentModelLoader = equipmentModelLoader; this.equipmentModelLoader = equipmentModelLoader;
} }
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) { private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
EquippableComponent equippableComponent = (EquippableComponent) stack.get(DataComponentTypes.EQUIPPABLE); EquippableComponent equippableComponent = (EquippableComponent) stack
.get(DataComponentTypes.EQUIPPABLE);
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) { if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
EquipmentModel equipmentModel = this.equipmentModelLoader EquipmentModel equipmentModel = this.equipmentModelLoader
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get()); .get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get());
@@ -50,35 +92,262 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
} }
} }
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
PlayerEntityRenderState playerEntityRenderState, float f, float g) { Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
if (!playerEntityRenderState.invisible && playerEntityRenderState.capeVisible) { float u1, float v1, float u2, float v2, int light, int overlay,
SkinTextures skinTextures = playerEntityRenderState.skinTextures; float normalX, float normalY, float normalZ, boolean flipWinding) {
MatrixStack.Entry entry = matrixStack.peek();
if (skinTextures.capeTexture() == null) { if (!flipWinding) {
String name = playerEntityRenderState.name; // CCW
String uuid = Auth.playerNames.get(name); vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
if (uuid == null || !Auth.players.containsKey(uuid)) { (float) bottomLeft.z)
return; .color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
} .normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
Identifier customCape = Cloaks.getCurrentCloakTexture(uuid); (float) bottomRight.z)
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
if (customCape != null .normal(normalX, normalY, normalZ);
&& !this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) { vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
matrixStack.push(); (float) topRight.z)
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) { .color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
matrixStack.translate(0.0F, -0.053125F, 0.06875F); .normal(normalX, normalY, normalZ);
} vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
(float) topLeft.z)
VertexConsumer vertexConsumer = vertexConsumerProvider .color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
.getBuffer(RenderLayer.getEntityAlpha(customCape)); .normal(normalX, normalY, normalZ);
((PlayerEntityModel) this.getContextModel()).copyTransforms(this.model); } else {
this.model.setAngles(playerEntityRenderState); // CW
this.model.render(matrixStack, vertexConsumer, 0xF000F0, OverlayTexture.DEFAULT_UV); vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
matrixStack.pop(); (float) topLeft.z)
} .color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
} .normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
(float) topRight.z)
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
(float) bottomRight.z)
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
(float) bottomLeft.z)
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
.normal(normalX, normalY, normalZ);
} }
} }
private float getCurve(int i, float curveMagnitude, PlayerEntityRenderState playerState) {
float powCurve = (float) Math.pow((double) (PARTS - i) / PARTS, 2) * curveMagnitude;
float waveFreq = 0.6f;
float phase = (playerState.age + playerState.handSwingProgress) * 0.4f;
float sineAmplitude = 0.02f;
float sineCurve = (float) Math.sin(i * waveFreq + phase) * sineAmplitude;
float t = Math.min(1.0f, Math.max(0.0f, (curveMagnitude - 0.3f) / 0.2f));
t = t * t * (3 - 2 * t);
return (1.0f - t) * powCurve + t * (powCurve + sineCurve);
}
private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer,
PlayerEntityRenderState playerState, int light, int overlay,
float rotation, float curveMagnitude) {
float capeWidth = 10.0f / 16.0f;
float capeHeight = 16.0f / 16.0f;
float capeDepth = 1.0f / 16.0f;
float capePartHeight = capeHeight / PARTS;
matrixStack.push();
matrixStack.translate(0.0f, 1.25, 0.125f);
matrixStack.translate(0, -capeHeight, 0);
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(rotation));
matrixStack.translate(0, capeHeight, 0);
float x1 = -capeWidth / 2.0f;
float x2 = capeWidth / 2.0f;
float y2 = -capeHeight;
float z1 = 0.0f;
float z2 = capeDepth;
final float frontU1 = FRONT_RECT.u1();
final float frontU2 = FRONT_RECT.u2();
final float frontV1 = FRONT_RECT.v1();
final float frontV2 = FRONT_RECT.v2();
final float frontPartV = (frontV2 - frontV1) / PARTS;
final float backU1 = BACK_RECT.u1();
final float backU2 = BACK_RECT.u2();
final float backV1 = BACK_RECT.v1();
final float backV2 = BACK_RECT.v2();
final float backPartV = (backV2 - backV1) / PARTS;
final float leftU1 = LEFT_EDGE_RECT.u1();
final float leftU2 = LEFT_EDGE_RECT.u2();
final float leftV1 = LEFT_EDGE_RECT.v1();
final float leftV2 = LEFT_EDGE_RECT.v2();
final float leftPartV = (leftV2 - leftV1) / PARTS;
final float rightU1 = RIGHT_EDGE_RECT.u1();
final float rightU2 = RIGHT_EDGE_RECT.u2();
final float rightV1 = RIGHT_EDGE_RECT.v1();
final float rightV2 = RIGHT_EDGE_RECT.v2();
final float rightPartV = (rightV2 - rightV1) / PARTS;
final float topU1 = TOP_RECT.u1();
final float topU2 = TOP_RECT.u2();
final float topV1 = TOP_RECT.v1();
final float topV2 = TOP_RECT.v2();
final float bottomU1 = BOTTOM_RECT.u1();
final float bottomU2 = BOTTOM_RECT.u2();
final float bottomV1 = BOTTOM_RECT.v1();
final float bottomV2 = BOTTOM_RECT.v2();
for (int i = 0; i < PARTS; i++) {
float y3 = -(capePartHeight * i);
float y4 = -(capePartHeight * (i + 1));
float curveOffsetTop = getCurve(i, curveMagnitude, playerState);
float curveOffsetBottom = getCurve(i + 1, curveMagnitude, playerState);
// FRONT FACE (outside)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, y4, z2 + curveOffsetBottom),
new Vec3d(x1, y4, z2 + curveOffsetBottom),
new Vec3d(x1, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z2 + curveOffsetTop),
frontU1, frontV2 - (frontPartV * i),
frontU2, frontV2 - (frontPartV * (i + 1)),
light, overlay,
0.0f, 0.0f, 1.0f, false);
// BACK FACE (inside)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x1, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z1 + curveOffsetTop),
backU1, backV2 - (backPartV * i),
backU2, backV2 - (backPartV * (i + 1)),
light, overlay,
0.0f, 0.0f, 1.0f, false);
// LEFT FACE (thickness at x2)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, y4, z1 + curveOffsetBottom),
new Vec3d(x2, y4, z2 + curveOffsetBottom),
new Vec3d(x2, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z1 + curveOffsetTop),
leftU1, leftV2 - (leftPartV * i),
leftU2, leftV2 - (leftPartV * (i + 1)),
light, overlay,
1.0f, 0.0f, 0.0f, false);
// RIGHT FACE (thickness at x1)
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x1, y4, z2 + curveOffsetBottom),
new Vec3d(x1, y4, z1 + curveOffsetBottom),
new Vec3d(x1, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z2 + curveOffsetTop),
rightU1, rightV2 - (rightPartV * i),
rightU2, rightV2 - (rightPartV * (i + 1)),
light, overlay,
-1.0f, 0.0f, 0.0f, false);
}
// TOP FACE (shoulders edge) – use its dedicated strip
matrixStack.push();
matrixStack.translate(0.0f, y2 + capeDepth, 0.0f);
matrixStack.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(90.0f));
matrixStack.translate(0.0f, -y2, 0.0f);
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, y2 + capeDepth, z2), new Vec3d(x1, y2 + capeDepth, z2),
new Vec3d(x1, y2, z2), new Vec3d(x2, y2, z2),
topU1, topV1, topU2, topV2,
light, overlay,
0.0f, 1.0f, 0.0f, true);
matrixStack.pop();
// BOTTOM FACE – use bottom strip
float curveOffset = getCurve(0, curveMagnitude, playerState);
this.renderCapeQuad(
vertexConsumer, matrixStack,
new Vec3d(x2, 0.0f, z2 + curveOffset), new Vec3d(x1, 0.0f, z2 + curveOffset),
new Vec3d(x1, 0.0f, z1 + curveOffset), new Vec3d(x2, 0.0f, z1 + curveOffset),
bottomU1, bottomV1, bottomU2, bottomV2,
light, overlay,
0.0f, 0.0f, 1.0f, false);
matrixStack.pop();
}
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible) {
return;
}
if (playerEntityRenderState.skinTextures.capeTexture() != null) {
return;
}
String name = playerEntityRenderState.name;
String uuid = Auth.playerNames.get(name);
if (uuid == null || !Auth.players.containsKey(uuid)) {
return;
}
Identifier customCape = Cloaks.getCurrentCloakTexture(uuid);
if (customCape == null || this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack,
LayerType.WINGS)) {
return;
}
int minBrightness = 7;
int blockLight = (light >> 4) & 0xF;
int skyLight = (light >> 20) & 0xF;
blockLight = Math.max(blockLight, minBrightness);
skyLight = Math.max(skyLight, minBrightness);
light = (skyLight << 20) | (blockLight << 4);
matrixStack.push();
VertexConsumer vertexConsumer = vertexConsumerProvider
.getBuffer(RenderLayer.getEntityAlpha(customCape));
float targetVelocity = ((6.0F + playerEntityRenderState.field_53537 / 2.0F
+ playerEntityRenderState.field_53536) * 0.02f);
targetVelocity = Math.max(0.0f, targetVelocity - 0.1f);
float accelerationRate = 0.02f;
this.currentVelocity = this.currentVelocity
+ (targetVelocity - this.currentVelocity) * accelerationRate;
float rotation = this.currentVelocity * 72.0f;
float curve = this.currentVelocity * 0.4f;
if (playerEntityRenderState.sneaking && playerEntityRenderState.isInSneakingPose) {
matrixStack.translate(0, 0.24f, 0);
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(27.0f));
curve += 0.2f;
}
matrixStack.translate(0.0f, -0.25f, 0.0f);
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack,
LayerType.HUMANOID)) {
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
}
if (playerEntityRenderState.isSwimming) {
curve = 0.0f;
rotation = 0.0f;
}
if (!SaturnClientConfig.bendyCloaks.value) {
curve = 0.0f;
}
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light,
OverlayTexture.DEFAULT_UV, Math.min(rotation, 60.0f), Math.min(curve, 0.5f));
matrixStack.pop();
}
} }
@@ -0,0 +1,17 @@
package org.saturnclient.saturnclient.cosmetics;
import java.util.ArrayList;
import java.util.List;
public class Emotes {
public static List<String> availableEmotes = new ArrayList<>();
public static void initialize() {
availableEmotes.add("tpose");
availableEmotes.add("back_flip");
availableEmotes.add("facepalm");
availableEmotes.add("cry");
availableEmotes.add("giga_chad");
availableEmotes.add("front_flip");
}
}
@@ -30,7 +30,7 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
PlayerEntityRenderState state, float limbAngle, float limbDistance) { PlayerEntityRenderState state, float limbAngle, float limbDistance) {
ItemStack headItem = state.equippedHeadStack; ItemStack headItem = state.equippedHeadStack;
String uuid = Auth.playerNames.get(state.name); String uuid = Auth.playerNames.get(state.name);
if (headItem.isEmpty() && uuid != null) { if (headItem.isEmpty() && uuid != null && !state.invisible) {
SaturnPlayer player = Auth.players.get(uuid); SaturnPlayer player = Auth.players.get(uuid);
if (player == null || player.hat == null || player.hat.isEmpty()) { if (player == null || player.hat == null || player.hat.isEmpty()) {
return; return;
@@ -6,6 +6,7 @@ import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.Auth;
public class Hats { public class Hats {
public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo", "halo_black" };
public static List<String> availableHats = new ArrayList<>(); public static List<String> availableHats = new ArrayList<>();
public static void initialize() { public static void initialize() {
@@ -15,7 +16,6 @@ public class Hats {
public static void setHat(String uuid, String hatName) { public static void setHat(String uuid, String hatName) {
setHatSilent(uuid, hatName); setHatSilent(uuid, hatName);
Auth.setHat(hatName); Auth.setHat(hatName);
Auth.sendReload();
} }
public static void setHatSilent(String uuid, String hatName) { public static void setHatSilent(String uuid, String hatName) {
@@ -11,7 +11,11 @@ import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
/** /**
@@ -20,7 +24,10 @@ import org.saturnclient.saturnclient.SaturnClient;
* Originally created by IIpho3nix and modified for Saturn Client by leo. * Originally created by IIpho3nix and modified for Saturn Client by leo.
*/ */
public class Cloaks { public class Cloaks {
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/"; private static final String CLOAKS_RESOURCE_PATH = "assets/saturnclient/textures/cloaks/";
public static final List<String> availableCloaks = new ArrayList<>(); public static final List<String> availableCloaks = new ArrayList<>();
@@ -28,6 +35,17 @@ public class Cloaks {
public static final Map<String, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>(); public static final Map<String, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>();
private static final Map<String, Long> lastFrameTime = new ConcurrentHashMap<>(); private static final Map<String, Long> lastFrameTime = new ConcurrentHashMap<>();
private static final ExecutorService CLOAK_LOADER_EXECUTOR = Executors.newFixedThreadPool(
Math.min(4, Runtime.getRuntime().availableProcessors()),
r -> {
Thread t = new Thread(r, "cloak-loader");
t.setDaemon(true);
t.setPriority(Thread.NORM_PRIORITY - 1);
return t;
});
private static final ConcurrentHashMap<String, List<AnimatedCloakData>> CLOAK_CACHE = new ConcurrentHashMap<>();
/** /**
* Initializes the cloak system. * Initializes the cloak system.
* Loads cloak textures from resources. * Loads cloak textures from resources.
@@ -44,10 +62,20 @@ public class Cloaks {
public static void setCloak(String uuid, String cloakName) { public static void setCloak(String uuid, String cloakName) {
Auth.setCloak(cloakName); Auth.setCloak(cloakName);
setCloakSilent(uuid, cloakName); setCloakSilent(uuid, cloakName);
Auth.sendReload();
SaturnClient.LOGGER.info("Cloak set to " + cloakName); SaturnClient.LOGGER.info("Cloak set to " + cloakName);
} }
/**
* Sets cloak of the current player
*
* @param cloakName Name of the cloak file to load
*/
public static void setCloak(String cloakName) {
if (availableCloaks.contains(cloakName)) {
setCloak(Auth.uuid, cloakName);
}
}
/** /**
* Handles loading and caching of a new cloak texture, without connecting to the * Handles loading and caching of a new cloak texture, without connecting to the
* server. * server.
@@ -76,47 +104,13 @@ public class Cloaks {
if (player != null && player.cloak != null) { if (player != null && player.cloak != null) {
if (!player.cloak.isEmpty()) { if (!player.cloak.isEmpty()) {
if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) { if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) {
// Load animated cloak data in background thread loadAnimatedCloakAsync(uuid, player.cloak)
new Thread(() -> { .exceptionally(throwable -> {
try { SaturnClient.LOGGER.error("Failed to load animated cloak for player: " + player.cloak,
String fileName = player.cloak + ".gif"; throwable);
String resourcePath = CLOAKS_RESOURCE_PATH + fileName; return null;
InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath); });
if (inputStream != null) {
byte[] data = inputStream.readAllBytes();
GifDecoder.GifImage gif = GifDecoder.read(data);
List<BufferedImage> frames = new ArrayList<>();
List<Integer> delays = new ArrayList<>();
int frameCount = gif.getFrameCount();
for (int i = 0; i < frameCount; i++) {
frames.add(gif.getFrame(i));
delays.add(gif.getDelay(i) * 10);
}
// Register textures on main thread
SaturnClient.client.execute(() -> {
try {
List<AnimatedCloakData> animatedFrames = new ArrayList<>();
for (int i = 0; i < frameCount; i++) {
String frameId = fileName.replace(".gif", "") + "_frame_" + i;
Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId);
IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frames.get(i));
animatedFrames.add(new AnimatedCloakData(frameIdentifier, delays.get(i)));
}
animatedCloaks.put(uuid, animatedFrames);
lastFrameTime.put(uuid, System.currentTimeMillis());
SaturnClient.LOGGER.info("Loaded " + frames.size() + " frames for animated cloak: " + fileName);
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register animated cloak textures: " + fileName, e);
}
});
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + player.cloak + ".gif", e);
}
}).start();
} else { } else {
SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png")); SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png"));
} }
@@ -152,6 +146,86 @@ public class Cloaks {
} }
} }
public static CompletableFuture<Void> loadAnimatedCloakAsync(String uuid, String cloakName) {
return CompletableFuture.runAsync(() -> {
String fileName = cloakName + ".gif";
// Check cache first
List<AnimatedCloakData> cached = CLOAK_CACHE.get(fileName);
if (cached != null) {
animatedCloaks.put(uuid, cached);
lastFrameTime.put(uuid, System.currentTimeMillis());
return;
}
String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
try (InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath)) {
if (inputStream == null) {
SaturnClient.LOGGER.warn("Cloak resource not found: " + resourcePath);
return;
}
// Read all bytes at once - more efficient than multiple reads
byte[] data = inputStream.readAllBytes();
GifDecoder.GifImage gif = GifDecoder.read(data);
int frameCount = gif.getFrameCount();
if (frameCount == 0) {
SaturnClient.LOGGER.warn("No frames found in animated cloak: " + fileName);
return;
}
// Pre-allocate collections with known size
List<AnimatedCloakData> animatedFrames = new ArrayList<>(frameCount);
String baseFrameId = fileName.replace(".gif", "");
// Process frames in batch
for (int i = 0; i < frameCount; i++) {
BufferedImage frame = gif.getFrame(i);
int delay = Math.max(gif.getDelay(i) * 10, 50); // Minimum 50ms delay
String frameId = baseFrameId + "_frame_" + i;
Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId);
try {
IdentifierUtils.registerBufferedImageTextureFast(frameIdentifier, frame);
// Register texture on main thread if required by the graphics system
if (SaturnClient.client.isOnThread()) {
// IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame);
} else {
// Queue for main thread execution
// SaturnClient.client.execute(
// () -> IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame));
}
animatedFrames.add(new AnimatedCloakData(frameIdentifier, delay));
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register frame " + i + " for cloak: " + fileName, e);
// Continue with other frames instead of failing completely
}
}
if (!animatedFrames.isEmpty()) {
// Cache the result for future use
CLOAK_CACHE.put(fileName, animatedFrames);
animatedCloaks.put(uuid, animatedFrames);
lastFrameTime.put(uuid, System.currentTimeMillis());
SaturnClient.LOGGER.info("Loaded {} frames for animated cloak: {} (cached)",
animatedFrames.size(), fileName);
} else {
SaturnClient.LOGGER.error("No valid frames could be loaded for cloak: " + fileName);
}
} catch (IOException e) {
SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + fileName, e);
} catch (Exception e) {
SaturnClient.LOGGER.error("Unexpected error loading animated cloak: " + fileName, e);
}
}, CLOAK_LOADER_EXECUTOR);
}
public static Identifier getCurrentCloakTexture(String uuid) { public static Identifier getCurrentCloakTexture(String uuid) {
if (!Auth.players.containsKey(uuid)) { if (!Auth.players.containsKey(uuid)) {
return null; return null;
@@ -188,7 +262,13 @@ public class Cloaks {
currentFrame = 0; currentFrame = 0;
} }
return frames.get(currentFrame).getTextureId(); Identifier identifier = frames.get(currentFrame).getTextureId();
if (identifier == null) {
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
}
return identifier;
} else { } else {
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png"); return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
} }
@@ -6,9 +6,10 @@ import java.awt.image.DataBufferInt;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import static java.lang.System.arraycopy; import java.util.concurrent.locks.ReentrantReadWriteLock;
/* /*
* Copyright 2014 Dhyan Blum * Copyright 2014 Dhyan Blum
@@ -28,7 +29,7 @@ import static java.lang.System.arraycopy;
/** /**
* <p> * <p>
* A decoder capable of processing a GIF data stream to render the graphics * A thread-safe decoder capable of processing a GIF data stream to render the graphics
* contained in it. This implementation follows the official * contained in it. This implementation follows the official
* <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF * <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF
* specification</A>. * specification</A>.
@@ -54,9 +55,51 @@ import static java.lang.System.arraycopy;
* </p> * </p>
* *
* @author Dhyan Blum * @author Dhyan Blum
* @version 1.09 November 2017 * @version 1.09 November 2017 (Thread-safe version)
*/ */
public final class GifDecoder { public final class GifDecoder {
// Thread-local storage for reusable objects to avoid synchronization overhead
private static final ThreadLocal<DecodingContext> DECODING_CONTEXT =
ThreadLocal.withInitial(DecodingContext::new);
/**
* Thread-local context containing reusable objects for decoding operations.
* This eliminates the need for synchronization while maintaining performance benefits
* of object reuse within each thread.
*/
static final class DecodingContext {
final BitReader bitReader = new BitReader();
final CodeTable codeTable = new CodeTable();
int[] pixelBuffer;
int[] prevPixelBuffer;
int[] tempBuffer;
// Get or create a pixel buffer of the specified size
int[] getPixelBuffer(int size) {
if (pixelBuffer == null || pixelBuffer.length < size) {
pixelBuffer = new int[Math.max(size, 1024)]; // Minimum size to reduce allocations
}
return pixelBuffer;
}
// Get or create a previous pixel buffer of the specified size
int[] getPrevPixelBuffer(int size) {
if (prevPixelBuffer == null || prevPixelBuffer.length < size) {
prevPixelBuffer = new int[Math.max(size, 1024)];
}
return prevPixelBuffer;
}
// Get or create a temporary buffer of the specified size
int[] getTempBuffer(int size) {
if (tempBuffer == null || tempBuffer.length < size) {
tempBuffer = new int[Math.max(size, 1024)];
}
return tempBuffer;
}
}
static final class BitReader { static final class BitReader {
private int nextBitToRead; private int nextBitToRead;
private int numberOfBitsToRead; private int numberOfBitsToRead;
@@ -71,14 +114,16 @@ public final class GifDecoder {
private int read() { private int read() {
// Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2 // Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8 final int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8
int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8 final int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8
int byte0, byte1, byte2;
byte0 = bytes[byteIndex++] & 0xFF; // & 0xFF gives us the unsigned values // Use int operations to avoid repeated array access
byte1 = bytes[byteIndex++] & 0xFF; final int byte0 = bytes[byteIndex] & 0xFF;
byte2 = bytes[byteIndex] & 0xFF; final int byte1 = bytes[byteIndex + 1] & 0xFF;
// Glue the bytes together, don't do more shifting than necessary final int byte2 = bytes[byteIndex + 2] & 0xFF;
int buffer = ((byte2 << 8 | byte1) << 8 | byte0) >>> bitsToShiftRight;
// Optimized bit manipulation
final int buffer = ((byte2 << 16) | (byte1 << 8) | byte0) >>> bitsToShiftRight;
nextBitToRead += numberOfBitsToRead; nextBitToRead += numberOfBitsToRead;
return buffer & bitMask; // Kill the unwanted higher bits return buffer & bitMask; // Kill the unwanted higher bits
} }
@@ -100,7 +145,7 @@ public final class GifDecoder {
private BitReader bitReader; // Notify when code sizes increases private BitReader bitReader; // Notify when code sizes increases
public CodeTable() { public CodeTable() {
table = new int[4096][1]; table = new int[4096][];
} }
private int add(final int[] indices) { private int add(final int[] indices) {
@@ -130,14 +175,18 @@ public final class GifDecoder {
initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1 initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1
initTableSize = fr.endOfInfoCode + 1; initTableSize = fr.endOfInfoCode + 1;
nextCode = initTableSize; nextCode = initTableSize;
for (int c = numColors - 1; c >= 0; c--) {
table[c][0] = activeColTbl[c]; // Translated color // Initialize single color entries
} // A gap may follow with no colors assigned if numCols < CLEAR for (int c = 0; c < numColors; c++) {
table[c] = new int[]{activeColTbl[c]}; // Single element array
}
table[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR table[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR
table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
// Locate transparent color in code table and set to 0 // Locate transparent color in code table and set to 0
if (fr.transpColFlag && fr.transpColIndex < numColors) { if (fr.transpColFlag && fr.transpColIndex < numColors) {
table[fr.transpColIndex][0] = 0; table[fr.transpColIndex] = new int[]{0};
} }
} }
} }
@@ -145,73 +194,87 @@ public final class GifDecoder {
final class GifFrame { final class GifFrame {
// Graphic control extension (optional) // Graphic control extension (optional)
// Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV // Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
private int disposalMethod; // 0-3 as above, 4-7 undefined private volatile int disposalMethod; // 0-3 as above, 4-7 undefined
private boolean transpColFlag; // 1 Bit private volatile boolean transpColFlag; // 1 Bit
private int delay; // Unsigned, LSByte first, n * 1/100 * s private volatile int delay; // Unsigned, LSByte first, n * 1/100 * s
private int transpColIndex; // 1 Byte private volatile int transpColIndex; // 1 Byte
// Image descriptor // Image descriptor
private int x; // Position on the canvas from the left private volatile int x; // Position on the canvas from the left
private int y; // Position on the canvas from the top private volatile int y; // Position on the canvas from the top
private int w; // May be smaller than the base image private volatile int w; // May be smaller than the base image
private int h; // May be smaller than the base image private volatile int h; // May be smaller than the base image
private int wh; // width * height private volatile int wh; // width * height
private boolean hasLocColTbl; // Has local color table? 1 Bit private volatile boolean hasLocColTbl; // Has local color table? 1 Bit
private boolean interlaceFlag; // Is an interlace image? 1 Bit private volatile boolean interlaceFlag; // Is an interlace image? 1 Bit
@SuppressWarnings("unused") @SuppressWarnings("unused")
private boolean sortFlag; // True if local colors are sorted, 1 Bit private volatile boolean sortFlag; // True if local colors are sorted, 1 Bit
private int sizeOfLocColTbl; // Size of the local color table, 3 Bits private volatile int sizeOfLocColTbl; // Size of the local color table, 3 Bits
private int[] localColTbl; // Local color table (optional) private volatile int[] localColTbl; // Local color table (optional)
// Image data // Image data
private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI private volatile int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
private int clearCode; private volatile int clearCode;
private int endOfInfoCode; private volatile int endOfInfoCode;
private byte[] data; // Holds LZW encoded data private volatile byte[] data; // Holds LZW encoded data
private BufferedImage img; // Full drawn image, not just the frame area private volatile BufferedImage img; // Full drawn image, not just the frame area
// Synchronization for frame image creation
private final Object imageLock = new Object();
} }
public final class GifImage { public final class GifImage {
public String header; // Bytes 0-5, GIF87a or GIF89a public volatile String header; // Bytes 0-5, GIF87a or GIF89a
private int w; // Unsigned 16 Bit, the least significant byte first private volatile int w; // Unsigned 16 Bit, the least significant byte first
private int h; // Unsigned 16 Bit, the least significant byte first private volatile int h; // Unsigned 16 Bit, the least significant byte first
private int wh; // Image width * image height private volatile int wh; // Image width * image height
public boolean hasGlobColTbl; // 1 Bit public volatile boolean hasGlobColTbl; // 1 Bit
public int colorResolution; // 3 Bits public volatile int colorResolution; // 3 Bits
public boolean sortFlag; // True if global colors are sorted, 1 Bit public volatile boolean sortFlag; // True if global colors are sorted, 1 Bit
public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec public volatile int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
public int bgColIndex; // Background color index, 1 Byte public volatile int bgColIndex; // Background color index, 1 Byte
public int pxAspectRatio; // Pixel aspect ratio, 1 Byte public volatile int pxAspectRatio; // Pixel aspect ratio, 1 Byte
public int[] globalColTbl; // Global color table public volatile int[] globalColTbl; // Global color table
private final List<GifFrame> frames = new ArrayList<GifFrame>(64); private final List<GifFrame> frames = new ArrayList<>(64);
public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE" public volatile String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0" public volatile String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
public int repetitions = 0; // 0: infinite loop, N: number of loops public volatile int repetitions = 0; // 0: infinite loop, N: number of loops
private BufferedImage img = null; // Currently, drawn frame
private final BitReader bits = new BitReader(); // Thread-safe frame rendering state
private final CodeTable codes = new CodeTable(); private volatile BufferedImage img = null; // Currently, drawn frame
private Graphics2D g; private volatile Graphics2D g;
private final ReadWriteLock renderLock = new ReentrantReadWriteLock();
// Synchronization objects
private final Object initLock = new Object();
private int[] decode(final GifFrame fr, final int[] activeColTbl) { private int[] decode(final GifFrame fr, final int[] activeColTbl) {
codes.init(fr, activeColTbl, bits); final DecodingContext ctx = DECODING_CONTEXT.get();
bits.init(fr.data); // Incoming codes ctx.codeTable.init(fr, activeColTbl, ctx.bitReader);
ctx.bitReader.init(fr.data); // Incoming codes
final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode; final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
final int[] out = new int[wh]; // Target image pixel array
final int[][] tbl = codes.table; // Code table // Use thread-local pixel buffer
final int[] out = ctx.getPixelBuffer(wh);
final int[][] tbl = ctx.codeTable.table; // Code table
int outPos = 0; // Next pixel position in the output image array int outPos = 0; // Next pixel position in the output image array
codes.clear(); // Init code table ctx.codeTable.clear(); // Init code table
bits.read(); // Skip leading clear code ctx.bitReader.read(); // Skip leading clear code
int code = bits.read(); // Read first code int code = ctx.bitReader.read(); // Read first code
int[] pixels = tbl[code]; // Output pixel for first code int[] pixels = tbl[code]; // Output pixel for first code
arraycopy(pixels, 0, out, outPos, pixels.length);
// Use System.arraycopy for better performance
System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length; outPos += pixels.length;
try { try {
while (true) { while (true) {
final int prevCode = code; final int prevCode = code;
code = bits.read(); // Get next code in stream code = ctx.bitReader.read(); // Get next code in stream
if (code == clearCode) { // After a CLEAR table, there is if (code == clearCode) { // After a CLEAR table, there is
codes.clear(); // no previous code, we need to read ctx.codeTable.clear(); // no previous code, we need to read
code = bits.read(); // a new one code = ctx.bitReader.read(); // a new one
pixels = tbl[code]; // Output pixels pixels = tbl[code]; // Output pixels
arraycopy(pixels, 0, out, outPos, pixels.length); System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length; outPos += pixels.length;
continue; // Back to the loop with a valid previous code continue; // Back to the loop with a valid previous code
} else if (code == endCode) { } else if (code == endCode) {
@@ -219,92 +282,107 @@ public final class GifDecoder {
} }
final int[] prevVals = tbl[prevCode]; final int[] prevVals = tbl[prevCode];
final int[] prevValsAndK = new int[prevVals.length + 1]; final int[] prevValsAndK = new int[prevVals.length + 1];
arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length); System.arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
if (code < codes.nextCode) { // Code table contains code if (code < ctx.codeTable.nextCode) { // Code table contains code
pixels = tbl[code]; // Output pixels pixels = tbl[code]; // Output pixels
arraycopy(pixels, 0, out, outPos, pixels.length); System.arraycopy(pixels, 0, out, outPos, pixels.length);
outPos += pixels.length; outPos += pixels.length;
prevValsAndK[prevVals.length] = tbl[code][0]; // K prevValsAndK[prevVals.length] = tbl[code][0]; // K
} else { } else {
prevValsAndK[prevVals.length] = prevVals[0]; // K prevValsAndK[prevVals.length] = prevVals[0]; // K
arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length); System.arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
outPos += prevValsAndK.length; outPos += prevValsAndK.length;
} }
codes.add(prevValsAndK); // Previous indices + K ctx.codeTable.add(prevValsAndK); // Previous indices + K
} }
} catch (final ArrayIndexOutOfBoundsException ignored) { } catch (final ArrayIndexOutOfBoundsException ignored) {
} }
return out;
// Return a properly sized array
return Arrays.copyOf(out, outPos);
} }
private int[] deinterlace(final int[] src, final GifFrame fr) { private int[] deinterlace(final int[] src, final GifFrame fr) {
final int w = fr.w, h = fr.h, wh = fr.wh; final int w = fr.w, h = fr.h, wh = fr.wh;
final int[] dest = new int[src.length]; final DecodingContext ctx = DECODING_CONTEXT.get();
final int[] dest = ctx.getTempBuffer(src.length);
// Interlaced images are organized in 4 sets of pixel lines // Interlaced images are organized in 4 sets of pixel lines
final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0) final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0)
final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0) final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0) final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
// Sets' start indices in source array // Sets' start indices in source array
final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y; final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
// Line skips in destination array
// Line skips in destination array - use bit shifts for multiplication
final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1; final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
// Group 1 contains every 8th line starting from 0 // Group 1 contains every 8th line starting from 0
int from = 0, to = 0; int from = 0, to = 0;
for (; from < set2; from += w, to += w8) { for (; from < set2; from += w, to += w8) {
arraycopy(src, from, dest, to, w); System.arraycopy(src, from, dest, to, w);
} // Group 2 contains every 8th line starting from 4
for (to = w4; from < set3; from += w, to += w8) {
arraycopy(src, from, dest, to, w);
} // Group 3 contains every 4th line starting from 2
for (to = w2; from < set4; from += w, to += w4) {
arraycopy(src, from, dest, to, w);
} // Group 4 contains every 2nd line starting from 1 (biggest group)
for (to = w; from < wh; from += w, to += w2) {
arraycopy(src, from, dest, to, w);
} }
return dest; // All pixel lines have now been rearranged // Group 2 contains every 8th line starting from 4
for (to = w4; from < set3; from += w, to += w8) {
System.arraycopy(src, from, dest, to, w);
}
// Group 3 contains every 4th line starting from 2
for (to = w2; from < set4; from += w, to += w4) {
System.arraycopy(src, from, dest, to, w);
}
// Group 4 contains every 2nd line starting from 1 (biggest group)
for (to = w; from < wh; from += w, to += w2) {
System.arraycopy(src, from, dest, to, w);
}
return Arrays.copyOf(dest, src.length); // Return a copy, not the shared buffer
} }
private void drawFrame(final GifFrame fr) { private void drawFrame(final GifFrame fr) {
// Determine the color table that will be active for this frame renderLock.writeLock().lock();
final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl; try {
// Get pixels from data stream // Determine the color table that will be active for this frame
int[] pixels = decode(fr, activeColTbl); final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
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);
// Visualize frame boundaries during testing // Get pixels from data stream
// if (DEBUG_MODE) { int[] pixels = decode(fr, activeColTbl);
// if (prev != null) { if (fr.interlaceFlag) {
// g.setColor(Color.RED); // Previous frame color pixels = deinterlace(pixels, fr); // Rearrange pixel lines
// 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 in case we need to restore the // Create image of type TYPE_INT_ARGB for frame area
// frame final BufferedImage frame = new BufferedImage(fr.w, fr.h, BufferedImage.TYPE_INT_ARGB);
int[] prevPx = new int[wh]; final int[] frameData = ((DataBufferInt) frame.getRaster().getDataBuffer()).getData();
arraycopy(((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, prevPx, 0, wh); System.arraycopy(pixels, 0, frameData, 0, Math.min(pixels.length, fr.wh));
// Create another copy for the end user to not expose internal state // Draw frame area on top of working image
fr.img = new BufferedImage(w, h, 2); // 2 = ARGB g.drawImage(frame, fr.x, fr.y, null);
arraycopy(prevPx, 0, ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(), 0, wh);
// Handle disposal of current frame // Keep a copy of the previous frame's pixels
if (fr.disposalMethod == 2) { final DecodingContext ctx = DECODING_CONTEXT.get();
// Restore to background color (clear frame area only) final int[] prevPixelBuffer = ctx.getPrevPixelBuffer(wh);
g.clearRect(fr.x, fr.y, fr.w, fr.h); final int[] imgData = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
} else if (fr.disposalMethod == 3) { System.arraycopy(imgData, 0, prevPixelBuffer, 0, wh);
// Restore previous frame
arraycopy(prevPx, 0, ((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, wh); // Create another copy for the end user to not expose internal state
synchronized (fr.imageLock) {
if (fr.img == null) { // Double-checked locking
fr.img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
final int[] frameImgData = ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData();
System.arraycopy(prevPixelBuffer, 0, frameImgData, 0, wh);
}
}
// Handle disposal of current frame
if (fr.disposalMethod == 2) {
// Restore to background color (clear frame area only)
g.clearRect(fr.x, fr.y, fr.w, fr.h);
} else if (fr.disposalMethod == 3) {
// Restore previous frame
System.arraycopy(prevPixelBuffer, 0, imgData, 0, wh);
}
} finally {
renderLock.writeLock().unlock();
} }
} }
@@ -317,13 +395,18 @@ public final class GifDecoder {
* @return 32 bit ARGB color in the form 0xAARRGGBB * @return 32 bit ARGB color in the form 0xAARRGGBB
*/ */
public final int getBackgroundColor() { public final int getBackgroundColor() {
final GifFrame frame = frames.get(0); renderLock.readLock().lock();
if (frame.hasLocColTbl) { try {
return frame.localColTbl[bgColIndex]; final GifFrame frame = frames.get(0);
} else if (hasGlobColTbl) { if (frame.hasLocColTbl) {
return globalColTbl[bgColIndex]; 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 * @return Delay as number of hundredths (1/100) of a second
*/ */
public final int getDelay(final int index) { public final int getDelay(final int index) {
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. * @return A BufferedImage for the specified frame.
*/ */
public BufferedImage getFrame(final int index) { public BufferedImage getFrame(final int index) {
if (img == null) { // Init // Initialize rendering context if needed
img = new BufferedImage(w, h, 2); // 2 = ARGB if (img == null) {
g = img.createGraphics(); synchronized (initLock) {
g.setBackground(new Color(0, true)); // Transparent color if (img == null) { // Double-checked locking
} img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
GifFrame fr = frames.get(index); g = img.createGraphics();
if (fr.img == null) { g.setBackground(new Color(0, true)); // Transparent color
// Draw all frames until and including the requested frame // Set rendering hints for better performance
for (int i = 0; i <= index; i++) { g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
fr = frames.get(i); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
if (fr.img == null) {
drawFrame(fr);
} }
} }
} }
GifFrame fr = frames.get(index);
// Check if frame is already rendered using double-checked locking
if (fr.img == null) {
synchronized (fr.imageLock) {
if (fr.img == null) {
// Draw all frames until and including the requested frame
for (int i = 0; i <= index; i++) {
fr = frames.get(i);
if (fr.img == null) {
drawFrame(fr);
}
}
}
}
}
return fr.img; return fr.img;
} }
@@ -407,6 +511,8 @@ public final class GifDecoder {
img.globalColTbl = new int[img.sizeOfGlobColTbl]; img.globalColTbl = new int[img.sizeOfGlobColTbl];
pos = readColTbl(in, img.globalColTbl, pos); pos = readColTbl(in, img.globalColTbl, pos);
} }
// Main parsing loop with bounds checking optimization
while (pos < in.length) { while (pos < in.length) {
final int block = in[pos] & 0xFF; final int block = in[pos] & 0xFF;
switch (block) { switch (block) {
@@ -456,7 +562,7 @@ public final class GifDecoder {
// and wait for a valid block. Experience: It'll get worse. b) // and wait for a valid block. Experience: It'll get worse. b)
// Throw exception. c) Return gracefully if we are almost done // Throw exception. c) Return gracefully if we are almost done
// processing. The frames we have so far should be error-free. // processing. The frames we have so far should be error-free.
final double progress = 1.0 * pos / in.length; final double progress = (double) pos / in.length;
if (progress < 0.9) { if (progress < 0.9) {
throw new IOException("Unknown block at: " + pos); throw new IOException("Unknown block at: " + pos);
} }
@@ -489,6 +595,7 @@ public final class GifDecoder {
* @return Index of the first byte after this extension * @return Index of the first byte after this extension
*/ */
static int readAppExt(final GifImage img, final byte[] in, int i) { static int readAppExt(final GifImage img, final byte[] in, int i) {
// Use StandardCharsets for better performance if available, otherwise fallback
img.appId = new String(in, i + 3, 8); // should be "NETSCAPE" img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
img.appAuthCode = new String(in, i + 11, 3); // should be "2.0" img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
i += 14; // Go to sub-block size, it's value should be 3 i += 14; // Go to sub-block size, it's value should be 3
@@ -496,7 +603,7 @@ public final class GifDecoder {
// The only app extension widely used is NETSCAPE, it's got 3 data bytes // The only app extension widely used is NETSCAPE, it's got 3 data bytes
if (subBlockSize == 3) { if (subBlockSize == 3) {
// in[i+1] should have value 01, in[i+5] should be block terminator // in[i+1] should have value 01, in[i+5] should be block terminator
img.repetitions = in[i + 2] & 0xFF | in[i + 3] & 0xFF << 8; // Short img.repetitions = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // Short
return i + 5; return i + 5;
} // Skip unknown application extensions } // Skip unknown application extensions
while ((in[i] & 0xFF) != 0) { // While sub-block size != 0 while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
@@ -514,11 +621,11 @@ public final class GifDecoder {
static int readColTbl(final byte[] in, final int[] colors, int i) { static int readColTbl(final byte[] in, final int[] colors, int i) {
final int numColors = colors.length; final int numColors = colors.length;
for (int c = 0; c < numColors; c++) { for (int c = 0; c < numColors; c++) {
final int a = 0xFF; // Alpha 255 (opaque)
final int r = in[i++] & 0xFF; // 1st byte is red final int r = in[i++] & 0xFF; // 1st byte is red
final int g = in[i++] & 0xFF; // 2nd byte is green final int g = in[i++] & 0xFF; // 2nd byte is green
final int b = in[i++] & 0xFF; // 3rd byte is blue final int b = in[i++] & 0xFF; // 3rd byte is blue
colors[c] = ((a << 8 | r) << 8 | g) << 8 | b; // Optimized color packing - alpha is always 0xFF for opaque
colors[c] = 0xFF000000 | (r << 16) | (g << 8) | b;
} }
return i; return i;
} }
@@ -530,9 +637,10 @@ public final class GifDecoder {
* @return Index of the first byte after this block * @return Index of the first byte after this block
*/ */
static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) { static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2 final int packed = in[i + 3] & 0xFF;
fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0 fr.disposalMethod = (packed & 0b00011100) >>> 2; // Bits 4-2
fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB fr.transpColFlag = (packed & 1) == 1; // Bit 0
fr.delay = (in[i + 4] & 0xFF) | ((in[i + 5] & 0xFF) << 8); // 16 bit LSB
fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6 fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00 return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
} }
@@ -548,6 +656,7 @@ public final class GifDecoder {
if (in.length < 6) { // Check first 6 bytes if (in.length < 6) { // Check first 6 bytes
throw new IOException("Image is truncated."); throw new IOException("Image is truncated.");
} }
// Use ASCII encoding for header - faster than default charset
img.header = new String(in, 0, 6); img.header = new String(in, 0, 6);
if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) { if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
throw new IOException("Invalid GIF header."); throw new IOException("Invalid GIF header.");
@@ -568,23 +677,28 @@ public final class GifDecoder {
fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
fr.clearCode = clearCode; fr.clearCode = clearCode;
fr.endOfInfoCode = clearCode + 1; fr.endOfInfoCode = clearCode + 1;
// Pre-calculate image data size for better memory allocation
final int imgDataSize = readImgDataSize(in, i); final int imgDataSize = readImgDataSize(in, i);
final byte[] imgData = new byte[imgDataSize + 2]; final byte[] imgData = new byte[imgDataSize + 2];
int imgDataPos = 0; int imgDataPos = 0;
int subBlockSize = in[i] & 0xFF; int subBlockSize = in[i] & 0xFF;
while (subBlockSize > 0) { // While block has data while (subBlockSize > 0) { // While block has data
try { // Next line may throw exception if sub-block size is fake try { // Next line may throw exception if sub-block size is fake
final int nextSubBlockSizePos = i + subBlockSize + 1; final int nextSubBlockSizePos = i + subBlockSize + 1;
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF; final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position imgDataPos += subBlockSize; // Move output data position
i = nextSubBlockSizePos; // Move to next sub-block size i = nextSubBlockSizePos; // Move to next sub-block size
subBlockSize = nextSubBlockSize; subBlockSize = nextSubBlockSize;
} catch (final Exception e) { } catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes // Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes subBlockSize = fileSize - i - 1; // Remaining bytes
arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize); if (subBlockSize > 0) {
imgDataPos += subBlockSize; // Move output data position System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position
}
i += subBlockSize + 1; // Move to next sub-block size i += subBlockSize + 1; // Move to next sub-block size
break; break;
} }
@@ -608,7 +722,9 @@ public final class GifDecoder {
} catch (final Exception e) { } catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes // Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes subBlockSize = fileSize - i - 1; // Remaining bytes
imgDataPos += subBlockSize; // Move output data position if (subBlockSize > 0) {
imgDataPos += subBlockSize; // Move output data position
}
break; break;
} }
} }
@@ -622,16 +738,16 @@ public final class GifDecoder {
* @return Index of the first byte after this block * @return Index of the first byte after this block
*/ */
static int readImgDescr(final GifFrame fr, final byte[] in, int i) { static int readImgDescr(final GifFrame fr, final byte[] in, int i) {
fr.x = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 1-2: left fr.x = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 1-2: left
fr.y = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 3-4: top fr.y = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 3-4: top
fr.w = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 5-6: width fr.w = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 5-6: width
fr.h = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 7-8: height fr.h = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 7-8: height
fr.wh = fr.w * fr.h; fr.wh = fr.w * fr.h;
final byte b = in[++i]; // Byte 9 is a packed byte final int packed = in[++i] & 0xFF; // Byte 9 is a packed byte
fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7 fr.hasLocColTbl = (packed & 0b10000000) != 0; // Bit 7
fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6 fr.interlaceFlag = (packed & 0b01000000) != 0; // Bit 6
fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5 fr.sortFlag = (packed & 0b00100000) != 0; // Bit 5
final int colTblSizePower = (b & 7) + 1; // Bits 2-0 final int colTblSizePower = (packed & 7) + 1; // Bits 2-0
fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
return ++i; return ++i;
} }
@@ -642,15 +758,15 @@ public final class GifDecoder {
* @return Index of the first byte after this block. * @return Index of the first byte after this block.
*/ */
static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) { static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) {
img.w = in[i] & 0xFF | (in[i + 1] & 0xFF) << 8; // 16 bit, LSB 1st img.w = (in[i] & 0xFF) | ((in[i + 1] & 0xFF) << 8); // 16 bit, LSB 1st
img.h = in[i + 2] & 0xFF | (in[i + 3] & 0xFF) << 8; // 16 bit img.h = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // 16 bit
img.wh = img.w * img.h; img.wh = img.w * img.h;
final byte b = in[i + 4]; // Byte 4 is a packed byte final int packed = in[i + 4] & 0xFF; // Byte 4 is a packed byte
img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7 img.hasGlobColTbl = (packed & 0b10000000) != 0; // Bit 7
final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4 final int colResPower = ((packed & 0b01110000) >>> 4) + 1; // Bits 6-4
img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3 img.sortFlag = (packed & 0b00001000) != 0; // Bit 3
final int globColTblSizePower = (b & 7) + 1; // Bits 0-2 final int globColTblSizePower = (packed & 7) + 1; // Bits 0-2
img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
@@ -667,6 +783,7 @@ public final class GifDecoder {
int subBlockSize = in[i++] & 0xFF; int subBlockSize = in[i++] & 0xFF;
while (subBlockSize != 0 && i < in.length) { while (subBlockSize != 0 && i < in.length) {
i += subBlockSize; i += subBlockSize;
if (i >= in.length) break; // Safety check
subBlockSize = in[i++] & 0xFF; subBlockSize = in[i++] & 0xFF;
} }
return i; return i;
@@ -4,20 +4,30 @@ import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.texture.NativeImage; import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import org.lwjgl.BufferUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
public class IdentifierUtils { public class IdentifierUtils {
/**
* ORIGINAL METHOD - EXTREMELY SLOW - causes 10-20 second freezes
* This converts BufferedImage -> PNG bytes -> ByteBuffer -> NativeImage ->
* Texture
* This is completely unnecessary and wastes massive amounts of CPU time!
*/
public static void registerBufferedImageTexture(Identifier i, BufferedImage bi) { public static void registerBufferedImageTexture(Identifier i, BufferedImage bi) {
try { try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // This is the performance killer:
ImageIO.write(bi, "png", baos); // 1. Converts BufferedImage to PNG (expensive compression)
// 2. Creates ByteBuffer copy (memory allocation)
// 3. NativeImage.read() parses PNG back to raw pixels (expensive decompression)
// This is doing compression -> decompression for no reason!
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
javax.imageio.ImageIO.write(bi, "png", baos);
byte[] bytes = baos.toByteArray(); byte[] bytes = baos.toByteArray();
ByteBuffer bb = BufferUtils.createByteBuffer(bytes.length).put(bytes); java.nio.ByteBuffer bb = org.lwjgl.BufferUtils.createByteBuffer(bytes.length).put(bytes);
bb.flip(); bb.flip();
NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb)); NativeImageBackedTexture nibt = new NativeImageBackedTexture(NativeImage.read(bb));
SaturnClient.client.getTextureManager().registerTexture(i, nibt); SaturnClient.client.getTextureManager().registerTexture(i, nibt);
@@ -25,4 +35,64 @@ public class IdentifierUtils {
e.printStackTrace(); e.printStackTrace();
} }
} }
/**
* OPTIMIZED METHOD - Direct pixel conversion without unnecessary PNG
* encoding/decoding
* This should reduce loading time from 10-20 seconds to under 1 second
*/
public static void registerBufferedImageTextureFast(Identifier identifier, BufferedImage bufferedImage) {
try {
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
// Create NativeImage directly with the correct format
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
// Direct pixel copy - much faster than PNG conversion
if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
// Fast path for ARGB images - direct memory access
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixel = pixels[y * width + x];
nativeImage.setColorArgb(x, y, pixel);
}
}
} else {
// Fallback for other image types - still faster than PNG conversion
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = bufferedImage.getRGB(x, y);
nativeImage.setColorArgb(x, y, rgb);
}
}
}
// Register the texture
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
SaturnClient.client.execute(
() -> SaturnClient.client.getTextureManager().registerTexture(identifier, texture));
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register texture: " + identifier, e);
// Fallback to slow method if fast method fails
registerBufferedImageTexture(identifier, bufferedImage);
}
}
/**
* EVEN FASTER VERSION - Bulk texture registration
* Use this when registering many textures at once (like animated cloak frames)
*/
public static void registerBufferedImageTexturesBulk(java.util.Map<Identifier, BufferedImage> textures) {
long startTime = System.currentTimeMillis();
for (java.util.Map.Entry<Identifier, BufferedImage> entry : textures.entrySet()) {
registerBufferedImageTextureFast(entry.getKey(), entry.getValue());
}
long endTime = System.currentTimeMillis();
SaturnClient.LOGGER.info("Registered {} textures in {}ms", textures.size(), (endTime - startTime));
}
} }
@@ -3,12 +3,17 @@ package org.saturnclient.saturnclient.event;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.option.KeyBinding; import net.minecraft.client.option.KeyBinding;
// import net.minecraft.client.option.Perspective;
import net.minecraft.client.util.InputUtil; import net.minecraft.client.util.InputUtil;
// import org.saturnclient.modules.mods.FreeLook; import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.ui2.screens.EmoteWheel;
import org.saturnclient.ui2.screens.ShiftMenu; import org.saturnclient.ui2.screens.ShiftMenu;
// import org.saturnclient.saturnclient.menus.SaturnMenu;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
/** /**
@@ -21,55 +26,30 @@ public class KeyInputHandler {
private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT; private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT;
private static KeyBinding mainMenuKeyBinding; private static KeyBinding mainMenuKeyBinding;
// private static final String FREELOOK_KEY_ID = "key.saturnclient.freelook";
// private static final int FREELOOK_KEY = GLFW.GLFW_KEY_M;
// private static KeyBinding freelookKeyBinding;
/**
* Registers all keybindings and their associated event handlers.
* Should be called during mod initialization.
*/
public static void register() { public static void register() {
// Register the main menu key binding
mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding( mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
MAIN_MENU_KEY_ID, MAIN_MENU_KEY_ID,
InputUtil.Type.KEYSYM, InputUtil.Type.KEYSYM,
DEFAULT_MENU_KEY, DEFAULT_MENU_KEY,
KEY_CATEGORY)); KEY_CATEGORY));
// freelookKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
// FREELOOK_KEY_ID,
// InputUtil.Type.KEYSYM,
// FREELOOK_KEY,
// KEY_CATEGORY));
// Register the event handler for the main menu key // Register the event handler for the main menu key
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (mainMenuKeyBinding.wasPressed()) { if (mainMenuKeyBinding.wasPressed()) {
client.setScreen(new ShiftMenu()); client.setScreen(new ShiftMenu());
} }
// else if (freelookKeyBinding.wasPressed() && FreeLook.enabled.value && FreeLook.toggle.value) {
// FreeLook.isFreeLooking = !FreeLook.isFreeLooking; if (SaturnClientConfig.openEmoteWheel.wasKeyPressed() && client.currentScreen == null) {
// if (FreeLook.isFreeLooking) { client.setScreen(new EmoteWheel());
// FreeLook.lastPerspective = client.options.getPerspective(); }
// client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
// } else if (FreeLook.lastPerspective != null) { if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
// client.options.setPerspective(FreeLook.lastPerspective); AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
// FreeLook.lastPerspective = null; if (animationStack.isActive() && animationStack.getPriority() == 1000) {
// } animationStack.removeLayer(1000);
// } else if (FreeLook.enabled.value && !FreeLook.toggle.value) { Auth.emote("");
// if (freelookKeyBinding.isPressed()) { }
// if (!FreeLook.isFreeLooking) { }
// FreeLook.lastPerspective = client.options.getPerspective();
// client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
// FreeLook.isFreeLooking = true;
// }
// } else if (FreeLook.isFreeLooking && FreeLook.lastPerspective != null) {
// client.options.setPerspective(FreeLook.lastPerspective);
// FreeLook.isFreeLooking = false;
// FreeLook.lastPerspective = null;
// }
// }
}); });
} }
} }
@@ -26,7 +26,7 @@ public class HudEditor extends Screen {
public HudEditor() { public HudEditor() {
super(Text.literal("HUD Editor")); super(Text.literal("HUD Editor"));
for (Module m : ModManager.MODS) { for (Module m : ModManager.ENABLED_MODS) {
if (m.isEnabled() && m instanceof HudMod) { if (m.isEnabled() && m instanceof HudMod) {
hudMods.add((HudMod) m); hudMods.add((HudMod) m);
} }
@@ -25,6 +25,6 @@ public abstract class PlayerEntityRendererMixin
@Inject(method = "<init>", at = @At("TAIL")) @Inject(method = "<init>", at = @At("TAIL"))
private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) { private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) {
this.addFeature(new HatFeatureRenderer(this)); this.addFeature(new HatFeatureRenderer(this));
this.addFeature(new CloakFeatureRenderer(this, ctx.getEntityModels(), ctx.getEquipmentModelLoader())); this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader()));
} }
} }
@@ -1,33 +0,0 @@
package org.saturnclient.saturnclient.mixin;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.saturnclient.auth.Auth;
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.gui.hud.ChatHud;
import net.minecraft.client.gui.hud.MessageIndicator;
import net.minecraft.network.message.MessageSignatureData;
import net.minecraft.text.Text;
@Mixin(ChatHud.class)
public class SaturnReload {
@Inject(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V", at = @At("HEAD"), cancellable = true)
public void onAddMessage(Text message, @Nullable MessageSignatureData signatureData,
@Nullable MessageIndicator indicator, CallbackInfo ci) {
String msg = message.toString();
if (msg.contains("$SATURN_RELOAD")) {
for (Map.Entry<String, String> player : Auth.playerNames.entrySet()) {
String name = player.getKey();
if (msg.contains(name)) {
Auth.player(name, player.getValue());
}
ci.cancel();
}
}
}
}
@@ -15,6 +15,7 @@ public class Element {
public Animation animation; public Animation animation;
public float scale = 1.0f; public float scale = 1.0f;
public Function<Double, Double> curve = Curve::easeInOutCubic; public Function<Double, Double> curve = Curve::easeInOutCubic;
public Integer duration = null; // Element duration, will fade out if not null
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
} }
@@ -31,45 +32,45 @@ public class Element {
public void charTyped(char typedChar) { public void charTyped(char typedChar) {
} }
public final Element dimensions(int width, int height) { public Element dimensions(int width, int height) {
this.width = width; this.width = width;
this.height = height; this.height = height;
return this; return this;
} }
public final Element position(int x, int y) { public Element position(int x, int y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
return this; return this;
} }
public final Element centerHorizontal(int w, int h, int offsetX, int offsetY) { public Element centerHorizontal(int w, int h, int offsetX, int offsetY) {
this.x = (w - (int) (width * scale)) / 2 + offsetX; this.x = (w - (int) (width * scale)) / 2 + offsetX;
this.y = offsetY; this.y = offsetY;
return this; return this;
} }
public final Element center(int w, int h) { public Element center(int w, int h) {
this.centerOffset(w, h, 0, 0); this.centerOffset(w, h, 0, 0);
return this; return this;
} }
public final Element centerOffset(int w, int h, int offsetX, int offsetY) { public Element centerOffset(int w, int h, int offsetX, int offsetY) {
this.x = (w - (int) (width * scale)) / 2 + offsetX; this.x = (w - (int) (width * scale)) / 2 + offsetX;
this.y = (h - (int) (height * scale)) / 2 + offsetY; this.y = (h - (int) (height * scale)) / 2 + offsetY;
return this; return this;
} }
public final Element animation(Animation animation) { public Element animation(Animation animation) {
this.animation = animation; this.animation = animation;
return this; return this;
} }
public final Element animation(Animation animation, Function<Double, Double> curve) { public Element animation(Animation animation, Function<Double, Double> curve) {
this.animation = animation; this.animation = animation;
this.curve = curve; this.curve = curve;
return this; return this;
@@ -21,6 +21,7 @@ import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer; import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.item.ItemRenderState; import net.minecraft.client.render.item.ItemRenderState;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.Window; import net.minecraft.client.util.Window;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.LivingEntity; import net.minecraft.entity.LivingEntity;
@@ -36,7 +37,7 @@ import net.minecraft.world.World;
public class RenderScope { public class RenderScope {
public MatrixStack matrices; public MatrixStack matrices;
public VertexConsumerProvider.Immediate vertexConsumers; public VertexConsumerProvider.Immediate vertexConsumers;
private Function<Identifier, RenderLayer> renderLayers; // private Function<Identifier, RenderLayer> renderLayers;
private ScissorStack scissorStack = new ScissorStack(); private ScissorStack scissorStack = new ScissorStack();
private int opacity = 255 << 24; private int opacity = 255 << 24;
private final ItemRenderState itemRenderState; private final ItemRenderState itemRenderState;
@@ -60,10 +61,12 @@ public class RenderScope {
} }
public void setRenderLayer(Function<Identifier, RenderLayer> renderLayers) { public void setRenderLayer(Function<Identifier, RenderLayer> renderLayers) {
this.renderLayers = renderLayers; // this.renderLayers = renderLayers;
} }
public void drawRect(int x, int y, int width, int height, int color) { public void drawRect(int x, int y, int width, int height, int color) {
if (color == 0)
return;
color = getColor(color); color = getColor(color);
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
@@ -92,20 +95,25 @@ public class RenderScope {
vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, 0).color(color); vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, 0).color(color);
} }
public void drawText(String text, int x, int y, boolean bold, int color) { public void drawText(String text, int x, int y, int font, int color) {
drawText(1.0f, text, x, y, bold, color); drawText(1.0f, text, x, y, font, color);
} }
public void drawText(float scale, String text, int x, int y, boolean bold, int color) { public void drawText(float scale, String text, int x, int y, int font, int color) {
if (color == 0)
return;
if (font == 0) {
scale *= 2;
}
int i = 0; int i = 0;
for (String line : text.split("\n")) { for (String line : text.split("\n")) {
color = getColor(color); color = getColor(color);
Identifier font = bold ? Fonts.INTER_BOLD : Fonts.INTER;
matrices.push(); matrices.push();
matrices.translate(x, y + (i * Fonts.getHeight()), 0); matrices.translate(x, y + (i * Fonts.getHeight()), 0);
matrices.scale(scale, scale, 1.0f); matrices.scale(scale, scale, 1.0f);
TextRenderer textRenderer = SaturnClient.client.textRenderer; TextRenderer textRenderer = SaturnClient.client.textRenderer;
textRenderer.draw(Fonts.setFont(line, font), 0, 7, color, false, this.matrices.peek().getPositionMatrix(), textRenderer.draw(Fonts.setFont(line, Fonts.getFont(font)), 0, font == 0 ? 1 : 7, color, false,
this.matrices.peek().getPositionMatrix(),
this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880); this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
matrices.pop(); matrices.pop();
i++; i++;
@@ -156,6 +164,8 @@ public class RenderScope {
} }
public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) { public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) {
if (color == 0)
return;
radius = Math.min(radius, Math.min(width, height)); radius = Math.min(radius, Math.min(width, height));
int cornerWidth = width / 2; int cornerWidth = width / 2;
int cornerHeight = height / 2; int cornerHeight = height / 2;
@@ -179,28 +189,36 @@ public class RenderScope {
public int getScaledWindowWidth() { public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getScaledWidth(); return SaturnClient.client.getWindow().getScaledWidth();
} }
public int getScaledWindowHeight() { public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight(); return SaturnClient.client.getWindow().getScaledHeight();
} }
public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int color) { public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int color) {
this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color); this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
} }
public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height) { 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) { public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith,
this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1); 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) { public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth,
this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float)textureWidth, (u + (float)regionWidth) / (float)textureWidth, (v + 0.0F) / (float)textureHeight, (v + (float)regionHeight) / (float)textureHeight, color); int regionHeight, int textureWidth, int textureHeight, int color) {
this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float) textureWidth,
(u + (float) regionWidth) / (float) textureWidth, (v + 0.0F) / (float) textureHeight,
(v + (float) regionHeight) / (float) textureHeight, color);
} }
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1, float v2, int color) { private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1,
float v2, int color) {
if (color == 0)
return;
x1 *= 4; x1 *= 4;
x2 *= 4; x2 *= 4;
y1 *= 4; y1 *= 4;
@@ -209,43 +227,44 @@ public class RenderScope {
matrices.push(); matrices.push();
matrices.scale(0.25f, 0.25f, 1.0f); matrices.scale(0.25f, 0.25f, 1.0f);
RenderSystem.setShaderTexture(0, sprite); RenderSystem.setShaderTexture(0, sprite);
RenderLayer renderLayer = (RenderLayer)renderLayers.apply(sprite); RenderLayer renderLayer = (RenderLayer) RenderLayer.getGuiTextured(sprite);
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(renderLayer); 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) 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) 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) 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) x2, (float) y1, 0.0F).texture(u2, v1).color(color);
matrices.pop(); matrices.pop();
} }
public void enableScissor(int x1, int y1, int x2, int y2) { public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1)).transform(this.matrices.peek().getPositionMatrix()); ScreenRect screenRect = (new ScreenRect(x1, y1, x2 - x1, y2 - y1))
.transform(this.matrices.peek().getPositionMatrix());
this.setScissor(this.scissorStack.push(screenRect)); this.setScissor(this.scissorStack.push(screenRect));
} }
public void disableScissor() { public void disableScissor() {
this.setScissor(this.scissorStack.pop()); this.setScissor(this.scissorStack.pop());
} }
public boolean scissorContains(int x, int y) { public boolean scissorContains(int x, int y) {
return this.scissorStack.containsPoint(x, y); return this.scissorStack.containsPoint(x, y);
} }
private void setScissor(@Nullable ScreenRect rect) { private void setScissor(@Nullable ScreenRect rect) {
this.draw(); this.draw();
if (rect != null) { if (rect != null) {
Window window = SaturnClient.client.getWindow(); Window window = SaturnClient.client.getWindow();
int i = window.getFramebufferHeight(); int i = window.getFramebufferHeight();
double d = window.getScaleFactor(); double d = window.getScaleFactor();
double e = (double)rect.getLeft() * d; double e = (double) rect.getLeft() * d;
double f = (double)i - (double)rect.getBottom() * d; double f = (double) i - (double) rect.getBottom() * d;
double g = (double)rect.width() * d; double g = (double) rect.width() * d;
double h = (double)rect.height() * d; double h = (double) rect.height() * d;
RenderSystem.enableScissor((int)e, (int)f, Math.max(0, (int)g), Math.max(0, (int)h)); RenderSystem.enableScissor((int) e, (int) f, Math.max(0, (int) g), Math.max(0, (int) h));
} else { } else {
RenderSystem.disableScissor(); RenderSystem.disableScissor();
} }
} }
@@ -257,7 +276,7 @@ public class RenderScope {
public void draw(Consumer<VertexConsumerProvider> drawer) { public void draw(Consumer<VertexConsumerProvider> drawer) {
drawer.accept(this.vertexConsumers); drawer.accept(this.vertexConsumers);
this.vertexConsumers.draw(); this.vertexConsumers.draw();
} }
static class ScissorStack { static class ScissorStack {
private final Deque<ScreenRect> stack = new ArrayDeque<>(); private final Deque<ScreenRect> stack = new ArrayDeque<>();
@@ -265,7 +284,8 @@ public class RenderScope {
public ScreenRect push(ScreenRect p_281812_) { public ScreenRect push(ScreenRect p_281812_) {
ScreenRect screenrectangle = this.stack.peekLast(); ScreenRect screenrectangle = this.stack.peekLast();
if (screenrectangle != null) { if (screenrectangle != null) {
ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle), ScreenRect.empty()); ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
ScreenRect.empty());
this.stack.addLast(screenrectangle1); this.stack.addLast(screenrectangle1);
return screenrectangle1; return screenrectangle1;
} else { } else {
@@ -290,68 +310,85 @@ public class RenderScope {
} }
public void drawItem(ItemStack item, int x, int y) { public void drawItem(ItemStack item, int x, int y) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0); this.drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0);
} }
public void drawItem(ItemStack stack, int x, int y, int seed) { public void drawItem(ItemStack stack, int x, int y, int seed) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed); this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed);
} }
public void drawItem(ItemStack stack, int x, int y, int seed, int z) { public void drawItem(ItemStack stack, int x, int y, int seed, int z) {
this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed, z); this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed, z);
} }
public void drawItemWithoutEntity(ItemStack stack, int x, int y) { public void drawItemWithoutEntity(ItemStack stack, int x, int y) {
this.drawItemWithoutEntity(stack, x, y, 0); this.drawItemWithoutEntity(stack, x, y, 0);
} }
public void drawItemWithoutEntity(ItemStack stack, int x, int y, int seed) { 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) { public void drawItem(LivingEntity entity, ItemStack stack, int x, int y, int seed) {
this.drawItem(entity, entity.getWorld(), stack, x, y, seed); this.drawItem(entity, entity.getWorld(), stack, x, y, seed);
} }
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed) { private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y,
this.drawItem(entity, world, stack, x, y, seed, 0); 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,
if (!stack.isEmpty()) { int z) {
SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI, false, world, entity, seed); if (!stack.isEmpty()) {
this.matrices.push(); SaturnClient.client.getItemModelManager().update(this.itemRenderState, stack, ModelTransformationMode.GUI,
this.matrices.translate((float)(x + 8), (float)(y + 8), (float)(150 + (this.itemRenderState.hasDepth() ? z : 0))); false, world, entity, seed);
this.matrices.push();
this.matrices.translate((float) (x + 8), (float) (y + 8),
(float) (150 + (this.itemRenderState.hasDepth() ? z : 0)));
try { try {
this.matrices.scale(16.0F, -16.0F, 16.0F); this.matrices.scale(16.0F, -16.0F, 16.0F);
boolean bl = !this.itemRenderState.isSideLit(); boolean bl = !this.itemRenderState.isSideLit();
if (bl) { if (bl) {
this.draw(); this.draw();
DiffuseLighting.disableGuiDepthLighting(); DiffuseLighting.disableGuiDepthLighting();
}
this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV);
this.draw();
if (bl) {
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());
});
crashReportSection.add("Item Components", () -> {
return String.valueOf(stack.getComponents());
});
crashReportSection.add("Item Foil", () -> {
return String.valueOf(stack.hasGlint());
});
throw new CrashException(crashReport);
} }
this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV); this.matrices.pop();
this.draw(); }
if (bl) { }
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());
});
crashReportSection.add("Item Components", () -> {
return String.valueOf(stack.getComponents());
});
crashReportSection.add("Item Foil", () -> {
return String.valueOf(stack.hasGlint());
});
throw new CrashException(crashReport);
}
this.matrices.pop(); public void drawSpriteStretched(Sprite sprite, int x, int y, int width, int height) {
} this.drawSpriteStretched(sprite, x, y, width, height, -1);
} }
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);
}
}
} }
@@ -5,6 +5,7 @@ import java.util.List;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor; import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui2.anim.Animation; import org.saturnclient.ui2.anim.Animation;
import org.saturnclient.ui2.anim.SlideXAbsolute;
import org.saturnclient.ui2.components.ElementRenderer; import org.saturnclient.ui2.components.ElementRenderer;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
@@ -50,6 +51,25 @@ public abstract class SaturnScreen extends Screen {
element.animation.tick(progress, element); element.animation.tick(progress, element);
}, element.animation.duration); }, element.animation.duration);
} }
if (element.duration != null) {
new Thread(() -> {
try {
Animation fadeOut = new SlideXAbsolute(700, -(width - element.x));
Thread.sleep(element.duration);
fadeOut.init(element);
Animation.executeSync((Float progress) -> {
fadeOut.tick(progress, element);
}, fadeOut.duration);
synchronized (elements) {
elements.remove(element);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
} }
@Override @Override
@@ -93,7 +113,7 @@ public abstract class SaturnScreen extends Screen {
renderScope.matrices.scale(0.5f, 0.5f, 0.5f); renderScope.matrices.scale(0.5f, 0.5f, 0.5f);
ElementRenderer.render(elements, renderScope, mouseX, mouseY); ElementRenderer.render(new ArrayList<>(elements), renderScope, mouseX, mouseY);
renderScope.matrices.pop(); renderScope.matrices.pop();
} }
+22 -3
View File
@@ -1,13 +1,18 @@
package org.saturnclient.ui2; package org.saturnclient.ui2;
import java.util.List;
import org.lwjgl.glfw.GLFW; 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 class Utils {
public static boolean isHovering(int mouseX, int mouseY, int elementWidth, int elementHeight, float elementScale) { public static boolean isHovering(int mouseX, int mouseY, int elementWidth, int elementHeight, float elementScale) {
return mouseX >= 0 && return mouseX >= 0 &&
mouseX <= (elementWidth * elementScale) && mouseX <= (elementWidth * elementScale) &&
mouseY >= 0 && mouseY >= 0 &&
mouseY <= (elementHeight * elementScale); mouseY <= (elementHeight * elementScale);
} }
public static char getCharFromKey(int keyCode, int modifiers) { public static char getCharFromKey(int keyCode, int modifiers) {
@@ -44,4 +49,18 @@ public class Utils {
default -> '\0'; default -> '\0';
}; };
} }
public static <T> T getOrNull(List<T> list, int index) {
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));
}
}
} }
@@ -44,4 +44,32 @@ public abstract class Animation {
} }
}).start(); }).start();
} }
public static void executeSync(Consumer<Float> tick, int duration) {
Function<Double, Double> curveFunction = Curve::easeInOutCubic;
float progress = 0.0f;
double incremental = (1000 - duration) / 10000.0;
for (int i = 0; i <= 100; i++) {
float newProgress = (float) (double) curveFunction.apply(i / 100.0);
while (Math.abs(newProgress - progress) > 1e-6) {
if (progress < newProgress) {
progress += incremental;
if (progress > newProgress) progress = newProgress;
} else {
progress -= incremental;
if (progress < newProgress) progress = newProgress;
}
tick.accept(newProgress);
try {
Thread.sleep(duration / 100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
} }
@@ -0,0 +1,22 @@
package org.saturnclient.ui2.anim;
import org.saturnclient.ui2.Element;
public class FadeOut extends Animation {
float targetOpacity;
public FadeOut(int duration) {
super(duration);
}
@Override
public void tick(float progress, Element element) {
element.opacity = targetOpacity - progress;
}
@Override
public void init(Element element) {
targetOpacity = element.opacity;
// element.opacity = 1.0f;
}
}
@@ -0,0 +1,36 @@
package org.saturnclient.ui2.anim;
import org.saturnclient.ui2.Element;
public class SlideX extends Animation {
int offset = 20;
int maxOffset = 20;
int elementX;
public SlideX(int duration) {
super(duration);
}
public SlideX(int duration, int offset) {
super(duration);
this.offset = offset;
this.maxOffset = offset;
}
public SlideX(int duration, int offset, int maxOffset) {
super(duration);
this.offset = offset;
this.maxOffset = maxOffset; // ✅ fixed
}
@Override
public void tick(float progress, Element element) {
element.x = elementX + (int) (offset * progress);
}
@Override
public void init(Element element) {
element.x -= offset;
elementX = element.x;
}
}
@@ -0,0 +1,35 @@
package org.saturnclient.ui2.anim;
import org.saturnclient.ui2.Element;
public class SlideXAbsolute extends Animation {
int offset = 20;
int maxOffset = 20;
int elementX;
public SlideXAbsolute(int duration) {
super(duration);
}
public SlideXAbsolute(int duration, int offset) {
super(duration);
this.offset = offset;
this.maxOffset = offset;
}
public SlideXAbsolute(int duration, int offset, int maxOffset) {
super(duration);
this.offset = offset;
this.maxOffset = maxOffset;
}
@Override
public void tick(float progress, Element element) {
element.x = elementX + (int) (offset * progress);
}
@Override
public void init(Element element) {
elementX = element.x;
}
}
@@ -11,10 +11,10 @@ import net.minecraft.util.Identifier;
public class CosmeticPreview extends Element { public class CosmeticPreview extends Element {
private static ThemeManager theme = new ThemeManager("CosmeticPreview", "selected", "hovering"); private static ThemeManager theme = new ThemeManager("CosmeticPreview", "selected", "hovering");
private static Property<Integer> bgColor = theme.property("bg-color", new Property<Integer>(0xAA000000)); private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0x89000000));
private static Property<Integer> fgColor = theme.property("fg-color", new Property<Integer>(0xFFFFFFFF)); private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xFFFFFFFF));
private static Property<Integer> padding = theme.property("padding", new Property<Integer>(14)); private static Property<Integer> padding = theme.property("padding", Property.integer(14));
private static Property<Integer> cornerRadius = theme.property("corner-radius", new Property<Integer>(10)); private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(10));
static {} static {}
@@ -16,13 +16,14 @@ import net.minecraft.client.render.RenderLayer;
public class SaturnModule extends Element { public class SaturnModule extends Element {
private static ThemeManager theme = new ThemeManager("Module", "enabled", "hovering"); private static ThemeManager theme = new ThemeManager("Module", "enabled", "hovering");
private static Property<Integer> bgColor = theme.property("bg", new Property<>(0xFF1d202d)); private static Property<Integer> bgColor = theme.property("bg", Property.color(0xFF1d202d));
private static Property<Integer> fgColor = theme.property("fg", new Property<>(0xFFA1A2B8)); private static Property<Integer> fgColor = theme.property("fg", Property.color(0xFFA1A2B8));
private static Property<Integer> iconFg = theme.property("icon-fg", new Property<>(0xFFA1A2B8)); private static Property<Integer> iconFg = theme.property("icon-fg", Property.color(0xFFA1A2B8));
private static Property<Integer> iconBg = theme.property("icon-bg", new Property<>(0xFF2e3248)); private static Property<Integer> iconBg = theme.property("icon-bg", Property.color(0xFF2e3248));
private static Property<Integer> tagBg = theme.property("tag-bg", new Property<>(0xFF2e3248)); private static Property<Integer> tagBg = theme.property("tag-bg", Property.color(0xFF2e3248));
private static Property<Integer> iconRadius = theme.property("icon-radius", new Property<>(10)); private static Property<Integer> iconRadius = theme.property("icon-radius", Property.color(10));
private static Property<Integer> radius = theme.property("radius", new Property<>(10)); private static Property<Integer> font = theme.property("font", Property.font(1));
private static Property<Integer> radius = theme.property("radius", Property.color(10));
static { static {
theme.propertyStateDefault("enabled", "bg", 0xFF1d202d); theme.propertyStateDefault("enabled", "bg", 0xFF1d202d);
@@ -64,15 +65,15 @@ public class SaturnModule extends Element {
renderScope.drawTexture(mod.getIconTexture(), p + (p / 2), p + (p / 2), 0, 0, h - p, h - p, iconFg.value); renderScope.drawTexture(mod.getIconTexture(), p + (p / 2), p + (p / 2), 0, 0, h - p, h - p, iconFg.value);
renderScope.drawText(0.6f, mod.getName(), p+h+4, p + 1, true, fgColor.value); renderScope.drawText(0.6f, mod.getName(), p+h+4, p + 1, font.value, fgColor.value);
renderScope.drawText(0.35f, mod.getVersion(), width - p - (int) (Fonts.getWidth(mod.getVersion(), false) * 0.35f), p + 1, false, fgColor.value); renderScope.drawText(0.35f, mod.getVersion(), width - p - (int) (Fonts.getWidth(mod.getVersion(), font.value) * 0.35f), p + 1, font.value, fgColor.value);
int xt = p + h + 4; int xt = p + h + 4;
for (String tag : mod.getTags()) { for (String tag : mod.getTags()) {
int xtb = (int) (Fonts.getWidth(tag, false) * 0.4f); int xtb = (int) (Fonts.getWidth(tag, font.value) * 0.4f);
renderScope.drawRoundedRectangle(xt, p + (h/2), xtb + 6, 12, 5, tagBg.value); renderScope.drawRoundedRectangle(xt, p + (h/2), xtb + 6, 12, 5, tagBg.value);
renderScope.drawText(0.4f, tag, xt+2, p + (h/2) + 3, false, fgColor.value); renderScope.drawText(0.4f, tag, xt+2, p + (h/2) + 3, font.value, fgColor.value);
xt += xtb + 9; xt += xtb + 9;
} }
@@ -1,6 +1,7 @@
package org.saturnclient.ui2.components; package org.saturnclient.ui2.components;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.Property; import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ThemeManager; import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.resources.Textures; import org.saturnclient.ui2.resources.Textures;
@@ -8,23 +9,23 @@ import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext; import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope; import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.Utils; import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.screens.CloakMenu; import org.saturnclient.ui2.screens.ConfigEditor;
import org.saturnclient.ui2.screens.HatMenu;
import org.saturnclient.ui2.screens.ModMenu; import org.saturnclient.ui2.screens.ModMenu;
import org.saturnclient.ui2.screens.SaturnConfigEditor; import org.saturnclient.ui2.screens.cosmetics.CloakMenu;
import org.saturnclient.ui2.screens.store.CloakStore;
import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.RenderLayer;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
public class Sidebar extends Element { public class Sidebar extends Element {
private static ThemeManager theme = new ThemeManager("SideBar", "hovering"); private static ThemeManager theme = new ThemeManager("SideBar", "hovering");
private static Property<Integer> bgColor = theme.property("bg-color", new Property<Integer>(0x90000000)); private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0x90000000));
private static Property<Integer> iconFg = theme.property("icon-fg", new Property<Integer>(-1)); private static Property<Integer> iconFg = theme.property("icon-fg", Property.color(0xFFFFFFFF));
private static Property<Integer> iconBg = theme.property("icon-bg", new Property<Integer>(0x00000000)); private static Property<Integer> iconBg = theme.property("icon-bg", Property.color(0x00000000));
private static Property<Integer> cornerRadius = theme.property("corner-radius", new Property<Integer>(10)); private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(10));
private static Property<Integer> iconRadius = theme.property("icon-radius", new Property<Integer>(10)); private static Property<Integer> iconRadius = theme.property("icon-radius", Property.integer(10));
private static Property<Integer> padding = theme.property("padding", new Property<Integer>(10)); private static Property<Integer> padding = theme.property("padding", Property.integer(10));
static { static {
theme.propertyStateDefault("hovering", "icon-bg", -7643914); theme.propertyStateDefault("hovering", "icon-bg", -7643914);
@@ -48,20 +49,20 @@ public class Sidebar extends Element {
SaturnClient.client.setScreen(new ModMenu()); SaturnClient.client.setScreen(new ModMenu());
}, false), }, false),
new Sidebar.SidebarComponent(Textures.SETTINGS, () -> { new Sidebar.SidebarComponent(Textures.SETTINGS, () -> {
SaturnClient.client.setScreen(new SaturnConfigEditor()); SaturnClient.client.setScreen(new ConfigEditor(SaturnClientConfig.config));
}, false), }, false),
new Sidebar.SidebarComponent(Textures.CLOAK, () -> { new Sidebar.SidebarComponent(Textures.SHIRT, () -> {
SaturnClient.client.setScreen(new CloakMenu()); SaturnClient.client.setScreen(new CloakMenu());
}, false), }, false),
new Sidebar.SidebarComponent(Textures.HAT, () -> {
SaturnClient.client.setScreen(new HatMenu());
}, false),
new Sidebar.SidebarComponent(Textures.CLOSE, () -> { new Sidebar.SidebarComponent(Textures.CLOSE, () -> {
onClose.run(); onClose.run();
}, true) }, true),
new Sidebar.SidebarComponent(Textures.STORE, () -> {
SaturnClient.client.setScreen(new CloakStore());
}, true),
}; };
int active = 0; int active = 0;
@@ -77,9 +78,9 @@ public class Sidebar extends Element {
int topRow = 0; int topRow = 0;
int bottomRow = 0; int bottomRow = 0;
int sp = 30 - padding.value; int gap = 5; // 5px gap between icons
int sp = 30 - padding.value + gap;
int s = 30 - (padding.value * 2); int s = 30 - (padding.value * 2);
int s2 = s + padding.value; int s2 = s + padding.value;
renderScope.setRenderLayer(RenderLayer::getGuiTextured); renderScope.setRenderLayer(RenderLayer::getGuiTextured);
@@ -100,7 +101,7 @@ public class Sidebar extends Element {
renderScope.drawTexture(component.sprite, padding.value, y, 0, 0, s, s, iconFg.value); renderScope.drawTexture(component.sprite, padding.value, y, 0, 0, s, s, iconFg.value);
bottomRow += 1; bottomRow += 1;
} else { } else {
int y = padding.value + ((sp) * topRow); int y = padding.value + (sp * topRow);
if (ctx.isHovering(0, y - (padding.value / 2), s2, s2) || active == i) { if (ctx.isHovering(0, y - (padding.value / 2), s2, s2) || active == i) {
theme.setState("hovering"); theme.setState("hovering");
} else { } else {
@@ -120,19 +121,22 @@ public class Sidebar extends Element {
int topRow = 0; int topRow = 0;
int bottomRow = 0; int bottomRow = 0;
int sp = 30 - padding.value; int gap = 5; // same 5px gap here
int sp = 30 - padding.value + gap;
int s = 30 - (padding.value * 2); int s = 30 - (padding.value * 2);
int s2 = s + padding.value; int s2 = s + padding.value;
for (SidebarComponent component : components) { for (SidebarComponent component : components) {
if (component.end) { if (component.end) {
if (Utils.isHovering(mouseX, mouseY - (height - (sp * bottomRow) - (s * 2)), s2, s2, scale)) { int y = (height - (sp * bottomRow)) - (s * 2);
if (Utils.isHovering(mouseX - (padding.value / 2), mouseY - (y - (padding.value / 2)), s2, s2, scale)) {
component.onClick.run(); component.onClick.run();
break; break;
} }
bottomRow += 1; bottomRow += 1;
} else { } else {
if (Utils.isHovering(mouseX, mouseY - (padding.value + ((sp) * topRow)), s2, s2, scale)) { int y = padding.value + (sp * topRow);
if (Utils.isHovering(mouseX - (padding.value / 2), mouseY - (y - (padding.value / 2)), s2, s2, scale)) {
component.onClick.run(); component.onClick.run();
break; break;
} }
@@ -2,12 +2,17 @@ package org.saturnclient.ui2.components.inputs;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.Element; import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext; import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope; import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts; import org.saturnclient.ui2.resources.Fonts;
public abstract class Input extends Element { public abstract class Input extends Element {
private static ThemeManager theme = new ThemeManager("Input");
private static Property<Integer> font = theme.property("font", Property.font(1));
String text = ""; String text = "";
public int cursorPosition = 0; public int cursorPosition = 0;
public abstract void checkReset(); public abstract void checkReset();
@@ -66,12 +71,11 @@ public abstract class Input extends Element {
int scrollOffset = getScrollOffset(); int scrollOffset = getScrollOffset();
String visibleText = getVisibleText(scrollOffset); String visibleText = getVisibleText(scrollOffset);
int textColor = focused ? 0xFFFFFF : 0xAAAAAA; int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
renderScope.drawText(0.6f, visibleText, 4, 4, false, textColor); renderScope.drawText(0.6f, visibleText, 4, 4, font.value, textColor);
if (focused) { if (focused) {
int cursorX = 4 + int cursorX = 4 +
(int) (SaturnClient.client.textRenderer.getWidth( (int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset), font.value) * 0.6f);
Fonts.setFont(visibleText.substring(0, cursorPosition - scrollOffset))) * 0.6f);
renderScope.drawRect(cursorX, 2, 1, height - 4, 0xFFFFFFFF); renderScope.drawRect(cursorX, 2, 1, height - 4, 0xFFFFFFFF);
} }
@@ -86,7 +90,7 @@ public abstract class Input extends Element {
int maxWidth = width - 4; // Available width for text int maxWidth = width - 4; // Available width for text
while (offset < cursorPosition) { // Ensure cursor remains visible while (offset < cursorPosition) { // Ensure cursor remains visible
String visibleText = text.substring(offset, cursorPosition); String visibleText = text.substring(offset, cursorPosition);
if (SaturnClient.client.textRenderer.getWidth(Fonts.setFont(visibleText)) > maxWidth) { if (Fonts.getWidth(visibleText, font.value) > maxWidth) {
offset++; offset++;
} else { } else {
break; break;
@@ -100,8 +104,7 @@ public abstract class Input extends Element {
// Ensure text does not overflow past the available width // Ensure text does not overflow past the available width
for (int i = 1; i <= visibleText.length(); i++) { for (int i = 1; i <= visibleText.length(); i++) {
if (SaturnClient.client.textRenderer.getWidth( if (Fonts.getWidth(visibleText.substring(0, i), font.value) > width - 4) {
Fonts.setFont(visibleText.substring(0, i))) > width - 4) {
return visibleText.substring(0, i - 1); // Trim overflow return visibleText.substring(0, i - 1); // Trim overflow
} }
} }
@@ -0,0 +1,40 @@
package org.saturnclient.ui2.components.inputs;
import org.lwjgl.glfw.GLFW;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class KeybindingSelector extends Element {
private static ThemeManager theme = new ThemeManager("Input");
private static Property<Integer> font = theme.property("font", Property.font(1));
Property<Integer> prop;
public KeybindingSelector(Property<Integer> prop) {
this.prop = prop;
this.width = 120;
this.height = Fonts.getHeight();
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
renderScope.drawRoundedRectangle(0, 0, width, height, 10, 0xFF000000);
renderScope.drawText(0.6f, prop.value == -1 ? "<NONE>" :
GLFW.glfwGetKeyName(prop.value, GLFW.glfwGetKeyScancode(prop.value)).toUpperCase(),
4, 4, font.value, textColor);
}
@Override
public void keyPressed(int keyCode, int scanCode, int modifiers) {
if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
prop.value = -1;
} else {
prop.value = keyCode;
}
focused = false;
}
}
@@ -0,0 +1,49 @@
package org.saturnclient.ui2.components.inputs;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.resources.Fonts;
import net.minecraft.client.render.RenderLayer;
public class Select extends Element {
private static ThemeManager theme = new ThemeManager("Toggle");
private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xAAAAAA));
private static Property<Integer> arrowColor = theme.property("arrow-color", Property.color(0xff8B5CF6));
private static Property<Integer> font = theme.property("font", Property.font(1));
private Property<Integer> prop;
public Select(Property<Integer> prop) {
this.prop = prop;
this.width = 150;
this.height = 15;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.setRenderLayer(RenderLayer::getGuiTextured);
renderScope.drawText(0.8f, "<", 0, 0, font.value, arrowColor.value);
String text = prop.getSelection();
renderScope.drawText(0.6f, text, (int) (width - ((float) Fonts.getWidth(text, font.value) * 0.6f)) / 2, 1, font.value, fgColor.value);
renderScope.drawText(0.8f, ">", width - 10, 0, font.value, arrowColor.value);
theme.setState(null);
}
@Override
public void click(int mouseX, int mouseY) {
if (Utils.isHovering(mouseX - (width - 10), mouseY, 10, height, 1.0f)) {
prop.next();
} else if (Utils.isHovering(mouseX, mouseY, 10, height, 1.0f)) {
prop.prev();
}
}
}
@@ -8,9 +8,9 @@ import org.saturnclient.ui2.RenderScope;
public class Toggle extends Element { public class Toggle extends Element {
private static ThemeManager theme = new ThemeManager("Toggle", "hovering", "enabled"); private static ThemeManager theme = new ThemeManager("Toggle", "hovering", "enabled");
private static Property<Integer> bgColor = theme.property("bg-color", new Property<Integer>(0xFF808080)); private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0xFF808080));
private static Property<Integer> fgColor = theme.property("fg-color", new Property<Integer>(0xFF909090)); private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xFF909090));
private static Property<Integer> cornerRadius = theme.property("corner-radius", new Property<Integer>(30)); private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(30));
static { static {
theme.propertyStateDefault("enabled", "bg-color", 0xaf8B5CF6); theme.propertyStateDefault("enabled", "bg-color", 0xaf8B5CF6);
@@ -27,6 +27,8 @@ public class Toggle extends Element {
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
theme.setState(null);
if (prop.value) { if (prop.value) {
theme.setState("enabled"); theme.setState("enabled");
} }
@@ -38,8 +40,6 @@ public class Toggle extends Element {
renderScope.drawRoundedRectangle(0, 0, width, height, cornerRadius.value, bgColor.value); renderScope.drawRoundedRectangle(0, 0, width, height, cornerRadius.value, bgColor.value);
renderScope.drawRoundedRectangle(prop.value ? 30 : 0, 0, 30, 30, 30, fgColor.value); renderScope.drawRoundedRectangle(prop.value ? 30 : 0, 0, 30, 30, 30, fgColor.value);
theme.setState(null);
} }
@Override @Override
@@ -9,10 +9,10 @@ import org.saturnclient.ui2.resources.Fonts;
public class Button extends Element { public class Button extends Element {
private static ThemeManager theme = new ThemeManager("Button", "hovering"); private static ThemeManager theme = new ThemeManager("Button", "hovering");
private static Property<Integer> bgColor = theme.property("bg-color", new Property<Integer>(0xFF000000)); private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0xFF000000));
private static Property<Integer> fgColor = theme.property("fg-color", new Property<Integer>(0xFFFFFFFF)); private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xFFFFFFFF));
private static Property<Integer> cornerRadius = theme.property("corner-radius", new Property<Integer>(10)); private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(10));
private static Property<Boolean> bold = theme.property("fg-bold", new Property<Boolean>(false)); private static Property<Integer> font = theme.property("font", Property.font(1));
static { static {
theme.propertyStateDefault("hovering", "fg-color", 0xFF845eee); theme.propertyStateDefault("hovering", "fg-color", 0xFF845eee);
@@ -24,7 +24,7 @@ public class Button extends Element {
public Button(String text, Runnable onClick) { public Button(String text, Runnable onClick) {
this.text = text; this.text = text;
this.onClick = onClick; this.onClick = onClick;
this.dimensions(Fonts.getWidth(text, bold.value) + 50, 38); this.dimensions(Fonts.getWidth(text, font.value) + 50, 38);
} }
@Override @Override
@@ -37,7 +37,7 @@ public class Button extends Element {
renderScope.drawRoundedRectangle(0, 0, width, height, cornerRadius.value, bgColor.value); renderScope.drawRoundedRectangle(0, 0, width, height, cornerRadius.value, bgColor.value);
// renderScope.drawText(text, (width - Fonts.getWidth(text, font)) / 2, (height - Fonts.getHeight()) / 2, bold.value, fgColor.value); // renderScope.drawText(text, (width - Fonts.getWidth(text, font)) / 2, (height - Fonts.getHeight()) / 2, bold.value, fgColor.value);
renderScope.drawText(text, Fonts.centerX(width, text, bold.value), Fonts.centerY(height), bold.value, fgColor.value); renderScope.drawText(text, Fonts.centerX(width, text, font.value), Fonts.centerY(height), font.value, fgColor.value);
} }
@Override @Override
@@ -11,7 +11,7 @@ import net.minecraft.util.Identifier;
public class ClickableTexture extends Element { public class ClickableTexture extends Element {
private static ThemeManager theme = new ThemeManager("ClickableTexture", "hovering"); private static ThemeManager theme = new ThemeManager("ClickableTexture", "hovering");
private static Property<Integer> fgColor = theme.property("fg-color", new Property<Integer>(0xFFFFFFFF)); private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xFFFFFFFF));
Identifier sprite; Identifier sprite;
private Runnable onClick; private Runnable onClick;
@@ -0,0 +1,60 @@
package org.saturnclient.ui2.elements;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.anim.SlideX;
import org.saturnclient.ui2.resources.Fonts;
import org.saturnclient.ui2.resources.Textures;
public class Notification extends Element {
private static ThemeManager theme = new ThemeManager("Notification");
private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0xFF000000));
private static Property<Integer> error = theme.property("error", Property.color(0xFFbf212f));
private static Property<Integer> info = theme.property("info", Property.color(0xFF264b96));
private static Property<Integer> success = theme.property("success", Property.color(0xFF27b376));
public static enum NotificationKind {
Error,
Info,
Success,
}
NotificationKind kind;
String text;
String title;
public Notification(int width, int height, NotificationKind kind, String title, String text) {
this.kind = kind;
this.text = text;
this.title = title;
this.width = (int) Math.max(Fonts.getWidth(text, 1) * 0.6f, Fonts.getWidth(title, 1)) + 20;
this.height = 50;
this.x = width - this.width - 15;
this.y = height - this.height - 15;
this.animation = new SlideX(700, -width);
this.duration = 3000;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, 10, bgColor.value);
switch (kind) {
case Error:
renderScope.drawTexture(Textures.CIRCLE_X, 9, 9, 0, 0, 15, 15, error.value);
break;
case Info:
renderScope.drawTexture(Textures.INFO, 9, 9, 0, 0, 15, 15, info.value);
break;
case Success:
renderScope.drawTexture(Textures.CHECK, 9, 9, 0, 0, 15, 15, success.value);
break;
}
renderScope.drawText(0.7f, title, 32, 10, 1, -1);
renderScope.drawText(0.5f, text, 32, 28, 1, -1);
}
}
@@ -12,13 +12,13 @@ import org.saturnclient.ui2.components.ElementRenderer;
public class Scroll extends Element { public class Scroll extends Element {
private static ThemeManager theme = new ThemeManager("Scroll"); private static ThemeManager theme = new ThemeManager("Scroll");
private static Property<Integer> bgColor = theme.property("bg-color", new Property<Integer>(0x90000000)); private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0x90000000));
private static Property<Integer> scrollBarColor = theme.property("scrollbar-color", new Property<Integer>(-7643914)); private static Property<Integer> scrollBarColor = theme.property("scrollbar-color", Property.color(-7643914));
private static Property<Integer> scrollBarRadius = theme.property("scrollbar-radius", new Property<Integer>(10)); private static Property<Integer> scrollBarRadius = theme.property("scrollbar-radius", Property.integer(10));
private static Property<Integer> scrollBarWidth = theme.property("scrollbar-width", new Property<Integer>(5)); private static Property<Integer> scrollBarWidth = theme.property("scrollbar-width", Property.integer(5));
private static Property<Integer> scrollBarPadding = theme.property("scrollbar-padding", new Property<Integer>(5)); private static Property<Integer> scrollBarPadding = theme.property("scrollbar-padding", Property.integer(5));
private static Property<Integer> cornerRadius = theme.property("corner-radius", new Property<Integer>(10)); private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(10));
int padding = 0; int padding = 0;
@@ -61,16 +61,14 @@ public class Scroll extends Element {
public void draw(Element element) { public void draw(Element element) {
children.add(element); children.add(element);
}
if (element.width > width) { @Override
width = element.width; public Element dimensions(int width, int height) {
for (Element element : children) {
maxScroll = Math.max(maxScroll, element.y + element.height - height);
} }
return super.dimensions(width, height);
if (element.height > height) {
height = element.height;
}
maxScroll = Math.max(maxScroll, element.y - maxScroll);
} }
int calculateScrollBarHeight() { int calculateScrollBarHeight() {
@@ -0,0 +1,73 @@
package org.saturnclient.ui2.elements;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.Utils;
import net.minecraft.util.Identifier;
public class TabMenu extends Element {
private static ThemeManager theme = new ThemeManager("TabMenu", "hovering", "selected");
private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0x90000000));
private static Property<Integer> iconFg = theme.property("icon-fg", Property.color(0xFFFFFFFF));
private static Property<Integer> iconBg = theme.property("icon-bg", Property.color(0x00000000));
private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(10));
static {
theme.propertyStateDefault("hovering", "icon-bg", -7643914);
}
public static class TabMenuComponent {
Runnable onClick;
Identifier sprite;
public TabMenuComponent(Identifier sprite, Runnable onClick) {
this.onClick = onClick;
this.sprite = sprite;
}
}
TabMenuComponent[] components;
public int selectedScreen = 0;
public TabMenu(int active, TabMenuComponent... components) {
this.components = components;
width = 25 * components.length + 6;
height = 30;
this.selectedScreen = active;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, cornerRadius.value, bgColor.value);
int col = 0;
for (TabMenuComponent component : components) {
int x = 25 * col + 5;
if (ctx.isHovering(x, 5, 20, 20) || selectedScreen == col) {
theme.setState("hovering");
}
renderScope.drawRoundedRectangle(x, 5, 20, 20, cornerRadius.value, iconBg.value);
renderScope.drawTexture(component.sprite, x + 5, 10, 0, 0, 10, 10, iconFg.value);
theme.setState(null);
col++;
}
}
@Override
public void click(int mouseX, int mouseY) {
int col = 0;
for (TabMenuComponent component : components) {
int x = 25 * col + 5;
if (Utils.isHovering(mouseX - x, mouseY - 5, 20, 20, 1.0f)) {
component.onClick.run();
}
col++;
}
}
}
@@ -5,19 +5,23 @@ import org.saturnclient.saturnclient.config.ThemeManager;
import org.saturnclient.ui2.Element; import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.ElementContext; import org.saturnclient.ui2.ElementContext;
import org.saturnclient.ui2.RenderScope; import org.saturnclient.ui2.RenderScope;
import org.saturnclient.ui2.resources.Fonts;
public class Text extends Element { public class Text extends Element {
private static ThemeManager theme = new ThemeManager("Text"); private static ThemeManager theme = new ThemeManager("Text");
private static Property<Integer> fgColor = theme.property("fg-color", new Property<Integer>(0xFFFFFFFF)); private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xFFFFFFFF));
public static Property<Integer> font = theme.property("font", Property.font(1));
private String text; private String text;
public Text(String text) { public Text(String text) {
this.text = text; this.text = text;
width = Fonts.getWidth(text, font.value);
height = Fonts.getHeight();
} }
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawText(this.text, 0, 0, false, fgColor.value); renderScope.drawText(this.text, 0, 0, font.value, fgColor.value);
} }
} }
@@ -11,10 +11,10 @@ import net.minecraft.util.Identifier;
public class TextureButton extends Element { public class TextureButton extends Element {
private static ThemeManager theme = new ThemeManager("TextureButton", "hovering"); private static ThemeManager theme = new ThemeManager("TextureButton", "hovering");
private static Property<Integer> bgColor = theme.property("bg-color", new Property<Integer>(0xFF000000)); private static Property<Integer> bgColor = theme.property("bg-color", Property.color(0xFF000000));
private static Property<Integer> fgColor = theme.property("fg-color", new Property<Integer>(0xFFFFFFFF)); private static Property<Integer> fgColor = theme.property("fg-color", Property.color(0xFFFFFFFF));
private static Property<Integer> padding = theme.property("padding", new Property<Integer>(14)); private static Property<Integer> padding = theme.property("padding", Property.integer(14));
private static Property<Integer> cornerRadius = theme.property("corner-radius", new Property<Integer>(10)); private static Property<Integer> cornerRadius = theme.property("corner-radius", Property.integer(10));
static { static {
@@ -8,12 +8,16 @@ import net.minecraft.util.Identifier;
public class Fonts { public class Fonts {
public static final Identifier INTER = Identifier.of("saturnclient", "inter"); public static final Identifier INTER = Identifier.of("saturnclient", "inter");
public static final Identifier INTER_BOLD = Identifier.of("saturnclient", "inter_bold"); public static final Identifier INTER_BOLD = Identifier.of("saturnclient", "inter_bold");
public static final Identifier DEFAULT = Identifier.ofVanilla("default");
public static Identifier getFont(boolean bold) { public static Identifier getFont(int font) {
if (bold) { switch (font) {
return INTER_BOLD; case 0:
} else { return DEFAULT;
return INTER; case 1:
return INTER;
default:
return INTER_BOLD;
} }
} }
@@ -22,35 +26,28 @@ public class Fonts {
Style.EMPTY.withFont(font)); Style.EMPTY.withFont(font));
} }
public static Text setFont(String text, boolean bold) { public static int getWidth(String text, int font) {
return Text.literal(text).setStyle( int w = 0;
Style.EMPTY.withFont(bold ? INTER_BOLD : INTER)); for (String line : text.split("\n")) {
w = Math.max(w, getWidth(line, getFont(font)));
}
if (font == 0) {
return w * 2;
} else {
return w;
}
} }
public static Text setFont(String text) { private static int getWidth(String text, Identifier font) {
return Text.literal(text).setStyle( return SaturnClient.client.textRenderer.getWidth(setFont(text, font));
Style.EMPTY.withFont(INTER));
}
// public static int getWidth(String text, Identifier font) {
// return SaturnClient.client.textRenderer.getWidth(Text.literal(text).setStyle(
// Style.EMPTY.withFont(font)));
// }
// public static int getHeight() {
// return SaturnClient.client.textRenderer.fontHeight;
// }
public static int getWidth(String text, boolean bold) {
return SaturnClient.client.textRenderer.getWidth(setFont(text, getFont(bold)));
} }
public static int getHeight() { public static int getHeight() {
return 18; return 18;
} }
public static int centerX(int w, String text, boolean bold) { public static int centerX(int w, String text, int font) {
return (w - Fonts.getWidth(text, bold)) / 2; return (w - Fonts.getWidth(text, font)) / 2;
} }
public static int centerY(int h) { public static int centerY(int h) {
@@ -15,11 +15,19 @@ public class Textures {
public static final Identifier RESET = Identifier.of("saturnclient", "textures/gui/components/reset.png"); public static final Identifier RESET = Identifier.of("saturnclient", "textures/gui/components/reset.png");
public static final Identifier TOGGLE_INDICATOR = Identifier.of("saturnclient", public static final Identifier TOGGLE_INDICATOR = Identifier.of("saturnclient",
"textures/gui/components/toggle_indicator.png"); "textures/gui/components/toggle_indicator.png");
public static final Identifier TOGGLE_BG = Identifier.of("saturnclient", "textures/gui/components/toggle_bg.png"); public static final Identifier LEFT = Identifier.of("saturnclient", "textures/gui/components/left.png");
public static final Identifier RIGHT = Identifier.of("saturnclient", "textures/gui/components/right.png");
public static final Identifier STORE = Identifier.of("saturnclient", "textures/gui/components/store.png");
public static final Identifier COINS = Identifier.of("saturnclient", "textures/gui/components/coins.png");
public static final Identifier SHIRT = Identifier.of("saturnclient", "textures/gui/components/shirt.png");
public static final Identifier CIRCLE_X = Identifier.of("saturnclient", "textures/gui/components/circle_x.png");
public static final Identifier INFO = Identifier.of("saturnclient", "textures/gui/components/info.png");
public static final Identifier CHECK = Identifier.of("saturnclient", "textures/gui/components/check.png");
public static final Identifier LOGO = Identifier.of("saturnclient", "textures/logo/logo.png"); public static final Identifier LOGO = Identifier.of("saturnclient", "textures/logo/logo.png");
public static final Identifier REALISTIC_LOGO = Identifier.of("saturnclient", "textures/logo/realistic.png"); public static final Identifier REALISTIC_LOGO = Identifier.of("saturnclient", "textures/logo/realistic.png");
public static final Identifier LOGO_TEXT = Identifier.of("saturnclient", "textures/logo/text.png"); public static final Identifier LOGO_TEXT = Identifier.of("saturnclient", "textures/logo/text.png");
public static final Identifier LOGO_TEXT_BIG = Identifier.of("saturnclient", "textures/logo/text_big.png");
// Widgets // Widgets
public static final Identifier BUTTON_BORDER = Identifier.of("saturnclient", "button_border"); public static final Identifier BUTTON_BORDER = Identifier.of("saturnclient", "button_border");
@@ -39,7 +47,12 @@ public class Textures {
return Identifier.of("saturnclient", "textures/gui/hat/" + (hat.isEmpty() ? "none" : hat) + ".png"); return Identifier.of("saturnclient", "textures/gui/hat/" + (hat.isEmpty() ? "none" : hat) + ".png");
} }
public static Identifier getEmotePreview(String emote) {
return Identifier.of("saturnclient", "textures/gui/emote/" + (emote == null ? "none" : emote) + ".png");
}
public static Identifier getModIcon(String modId) { public static Identifier getModIcon(String modId) {
return Identifier.of("saturnclient", "textures/gui/mod/" + modId + ".png"); return Identifier.of("saturnclient", "textures/gui/mod/" + modId + ".png");
} }
} }
@@ -9,6 +9,8 @@ import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.components.inputs.FloatInput; import org.saturnclient.ui2.components.inputs.FloatInput;
import org.saturnclient.ui2.components.inputs.HexInput; import org.saturnclient.ui2.components.inputs.HexInput;
import org.saturnclient.ui2.components.inputs.IntInput; import org.saturnclient.ui2.components.inputs.IntInput;
import org.saturnclient.ui2.components.inputs.KeybindingSelector;
import org.saturnclient.ui2.components.inputs.Select;
import org.saturnclient.ui2.components.inputs.Toggle; import org.saturnclient.ui2.components.inputs.Toggle;
import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.elements.Text; import org.saturnclient.ui2.elements.Text;
@@ -36,7 +38,7 @@ public class ConfigEditor extends SaturnScreen {
draw(scroll.dimensions(scrollWidth, 350).center(width, height)); draw(scroll.dimensions(scrollWidth, 350).center(width, height));
draw(new Sidebar(0, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0)); draw(new Sidebar(1, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -59,7 +61,7 @@ public class ConfigEditor extends SaturnScreen {
configScroll.draw( configScroll.draw(
new Text( new Text(
propName propName
).position(Fonts.centerX(520, propName, false), modY + 1).scale(0.7f)); ).position(Fonts.centerX(520, propName, Text.font.value), modY + 1).scale(0.7f));
} else { } else {
configScroll.draw( configScroll.draw(
new Text( new Text(
@@ -91,6 +93,15 @@ public class ConfigEditor extends SaturnScreen {
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) prop.value; Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) prop.value;
row = drawProperties(configScroll, nestedProperties, row + 1, col, w); row = drawProperties(configScroll, nestedProperties, row + 1, col, w);
break; break;
case SELECT:
configScroll.draw(new Select((Property<Integer>) prop).position(w / 2, modY));
break;
case KEYBINDING:
configScroll.draw(new KeybindingSelector((Property<Integer>) prop).position(w / 2, modY));
break;
case STRING: case STRING:
} }
@@ -0,0 +1,92 @@
package org.saturnclient.ui2.screens;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.auth.Auth;
// import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.cosmetics.Emotes;
import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.Utils;
import org.saturnclient.ui2.anim.Fade;
import org.saturnclient.ui2.anim.SlideY;
import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.elements.TextureButton;
import org.saturnclient.ui2.resources.Textures;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import net.minecraft.util.Identifier;
public class EmoteWheel extends SaturnScreen {
int page = 1;
public EmoteWheel() {
super("Emote Wheel");
}
@Override
public void ui() {
int row = 0;
int col = 0;
draw(new ImageTexture(Textures.LOGO_TEXT).dimensions(98, 10).centerOffset(width, height, 0, -105)
.animation(new Fade(700)));
draw(new ImageTexture(SaturnClientConfig.getLogo()).dimensions(98, 98).centerOffset(width, height, 0, -149)
.animation(new SlideY(700, -20)));
draw(new TextureButton(Textures.LEFT, () -> {
if (page > 1) {
page--;
elements.clear();
ui();
}
}).dimensions(30, 30).centerOffset(width, height, -20, 45).animation(new Fade(700)));
draw(new TextureButton(Textures.RIGHT, () -> {
if ((8 * page) < Emotes.availableEmotes.size()) {
page++;
elements.clear();
ui();
}
}).dimensions(30, 30).centerOffset(width, height, 20, 45).animation(new Fade(700)));
while (row < 3) {
int idx = (row * 3 + col) * page;
if (row == 1 && col == 1) {
col++;
}
String emote = Utils.getOrNull(Emotes.availableEmotes, idx);
draw(new TextureButton(Textures.getEmotePreview(emote), () -> {
if (emote == null) {
return;
}
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
animationStack.removeLayer(1000);
}
animationStack.addAnimLayer(1000,
PlayerAnimationRegistry.getAnimation(Identifier.of("saturnclient",
emote)).playAnimation());
Auth.emote(emote);
close();
}).dimensions(70, 70).centerOffset(width, height, -80 + (col * 80), -35 + (row * 80))
.animation(new Fade(700)));
if (col < 2) {
col++;
} else {
col = 0;
row++;
}
}
}
@Override
public boolean shouldPause() {
return false;
}
}
@@ -5,7 +5,9 @@ import org.saturnclient.modules.Module;
import org.saturnclient.ui2.SaturnScreen; import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.components.SaturnModule; import org.saturnclient.ui2.components.SaturnModule;
import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.elements.ImageTexture;
import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.resources.Textures;
public class ModMenu extends SaturnScreen { public class ModMenu extends SaturnScreen {
public ModMenu() { public ModMenu() {
@@ -22,7 +24,7 @@ public class ModMenu extends SaturnScreen {
int row = 0; int row = 0;
int col = 0; int col = 0;
for (Module mod : ModManager.MODS) { for (Module mod : ModManager.ALL_MODS) {
scroll.draw(new SaturnModule(mod).position((160 + g) * col, (50 + g) * row)); scroll.draw(new SaturnModule(mod).position((160 + g) * col, (50 + g) * row));
if (col == 2) { if (col == 2) {
@@ -38,5 +40,7 @@ public class ModMenu extends SaturnScreen {
draw(scroll.dimensions(scrollWidth, 350).center(width, height)); draw(scroll.dimensions(scrollWidth, 350).center(width, height));
draw(new Sidebar(0, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0)); draw(new Sidebar(0, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20));
} }
} }
@@ -1,117 +0,0 @@
package org.saturnclient.ui2.screens;
import java.util.Map;
import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.config.Property;
import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.components.inputs.FloatInput;
import org.saturnclient.ui2.components.inputs.HexInput;
import org.saturnclient.ui2.components.inputs.IntInput;
import org.saturnclient.ui2.components.inputs.Toggle;
import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.elements.Text;
import org.saturnclient.ui2.resources.Fonts;
public class SaturnConfigEditor extends SaturnScreen {
public SaturnConfigEditor() {
super("Saturn Config Editor");
}
@Override
public void ui() {
int p = 10;
int g = 10;
Scroll scroll = new Scroll(p);
drawProperties(scroll, SaturnClientConfig.config.getProperties(), 0, 0, 480 + 10 + (g * 2));
int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
draw(scroll.dimensions(scrollWidth, 350).center(width, height));
draw(new Sidebar(1, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
}
@SuppressWarnings("unchecked")
private int drawProperties(Scroll configScroll, Map<String, Property<?>> properties, int row, int col, int w) {
for (Map.Entry<String, Property<?>> propEntry : properties.entrySet()) {
Property<?> prop = propEntry.getValue();
String propName = propEntry.getKey();
boolean full = isFull(prop);
// Always reset to col 0 if full-width component needs to be drawn mid-row
if (full && col != 0) {
col = 0;
row++;
}
int modX = (w / 2) * col; // two-column layout
int modY = 25 * row;
if (prop.getType() == Property.PropertyType.NAMESPACE) {
configScroll.draw(
new Text(
propName
).position(Fonts.centerX(520, propName, false), modY + 1).scale(0.7f));
} else {
configScroll.draw(
new Text(
propName
).position(modX, modY + 1).scale(0.7f));
}
switch (prop.getType()) {
case BOOLEAN:
configScroll.draw(
new Toggle((Property<Boolean>) prop)
.position(modX + (w / 2) - 40, modY)
.scale(0.5f));
break;
case HEX:
configScroll.draw(new HexInput((Property<Integer>) prop, this).position(w / 2, modY));
break;
case INTEGER:
configScroll.draw(new IntInput((Property<Integer>) prop).position(w / 2, modY));
break;
case FLOAT:
configScroll.draw(new FloatInput((Property<Float>) prop).position(w / 2, modY));
break;
case NAMESPACE:
Map<String, Property<?>> nestedProperties = (Map<String, Property<?>>) prop.value;
row = drawProperties(configScroll, nestedProperties, row + 1, col, w);
break;
case STRING:
}
if (full) {
row++; // full-width takes a whole row
col = 0;
} else {
col++;
if (col > 1) { // max 2 columns
col = 0;
row++;
}
}
}
return row;
}
public static boolean isFull(Property<?> prop) {
switch (prop.getType()) {
case BOOLEAN:
return false;
default:
return true;
}
}
}
@@ -4,6 +4,7 @@ import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig; import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.menus.HudEditor; import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.ui2.resources.Textures; import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.ui2.screens.cosmetics.CloakMenu;
import org.saturnclient.ui2.Element; import org.saturnclient.ui2.Element;
import org.saturnclient.ui2.SaturnScreen; import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.elements.Button; import org.saturnclient.ui2.elements.Button;
@@ -4,6 +4,7 @@ import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.SaturnClientConfig; import org.saturnclient.saturnclient.SaturnClientConfig;
import org.saturnclient.saturnclient.menus.HudEditor; import org.saturnclient.saturnclient.menus.HudEditor;
import org.saturnclient.ui2.resources.Textures; import org.saturnclient.ui2.resources.Textures;
import org.saturnclient.ui2.screens.cosmetics.CloakMenu;
import org.saturnclient.ui2.SaturnScreen; import org.saturnclient.ui2.SaturnScreen;
import org.saturnclient.ui2.anim.Fade; import org.saturnclient.ui2.anim.Fade;
import org.saturnclient.ui2.anim.SlideY; import org.saturnclient.ui2.anim.SlideY;
@@ -1,5 +1,6 @@
package org.saturnclient.ui2.screens; package org.saturnclient.ui2.screens.cosmetics;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.auth.SaturnPlayer; import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks; import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
@@ -9,6 +10,8 @@ import org.saturnclient.ui2.components.CosmeticPreview;
import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.components.SkinPreview; import org.saturnclient.ui2.components.SkinPreview;
import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.elements.TabMenu;
import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
public class CloakMenu extends SaturnScreen { public class CloakMenu extends SaturnScreen {
public CloakMenu() { public CloakMenu() {
@@ -30,7 +33,7 @@ public class CloakMenu extends SaturnScreen {
if (player != null) { if (player != null) {
for (String cloak : Cloaks.availableCloaks) { for (String cloak : Cloaks.availableCloaks) {
scroll.draw(new CosmeticPreview(cloak == player.cloak, Textures.getCloakPreview(cloak), () -> { scroll.draw(new CosmeticPreview(cloak == player.cloak, Textures.getCloakPreview(cloak), () -> {
Cloaks.setCloak(Auth.uuid, cloak); Cloaks.setCloak(cloak);
}).position((50 + g) * col, (111 + g) * row)); }).position((50 + g) * col, (111 + g) * row));
if (col == 5) { if (col == 5) {
@@ -49,5 +52,12 @@ public class CloakMenu extends SaturnScreen {
draw(new SkinPreview(-30.0f, false).scale(3.5f).position(scroll.x + (scrollWidth - 220), scroll.y+40)); draw(new SkinPreview(-30.0f, false).scale(3.5f).position(scroll.x + (scrollWidth - 220), scroll.y+40));
draw(new Sidebar(2, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0)); draw(new Sidebar(2, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
draw(new TabMenu(0,
new TabMenuComponent(Textures.CLOAK, () -> {}),
new TabMenuComponent(Textures.HAT, () -> {
SaturnClient.client.setScreen(new HatMenu());
})
).centerOffset(width, height, 0, -195));
} }
} }
@@ -1,5 +1,6 @@
package org.saturnclient.ui2.screens; package org.saturnclient.ui2.screens.cosmetics;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.auth.Auth; import org.saturnclient.saturnclient.auth.Auth;
import org.saturnclient.saturnclient.auth.SaturnPlayer; import org.saturnclient.saturnclient.auth.SaturnPlayer;
import org.saturnclient.saturnclient.cosmetics.Hats; import org.saturnclient.saturnclient.cosmetics.Hats;
@@ -9,6 +10,8 @@ import org.saturnclient.ui2.components.CosmeticPreview;
import org.saturnclient.ui2.components.Sidebar; import org.saturnclient.ui2.components.Sidebar;
import org.saturnclient.ui2.components.SkinPreview; import org.saturnclient.ui2.components.SkinPreview;
import org.saturnclient.ui2.elements.Scroll; import org.saturnclient.ui2.elements.Scroll;
import org.saturnclient.ui2.elements.TabMenu;
import org.saturnclient.ui2.elements.TabMenu.TabMenuComponent;
public class HatMenu extends SaturnScreen { public class HatMenu extends SaturnScreen {
public HatMenu() { public HatMenu() {
@@ -48,6 +51,13 @@ public class HatMenu extends SaturnScreen {
draw(new SkinPreview(170f, true).scale(3.5f).position(scroll.x + (scrollWidth - 220), scroll.y+40)); draw(new SkinPreview(170f, true).scale(3.5f).position(scroll.x + (scrollWidth - 220), scroll.y+40));
draw(new Sidebar(3, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0)); draw(new Sidebar(2, this::close).centerOffset(width, height, -(scrollWidth / 2 + 20), 0));
draw(new TabMenu(1,
new TabMenuComponent(Textures.CLOAK, () -> {
SaturnClient.client.setScreen(new CloakMenu());
}),
new TabMenuComponent(Textures.HAT, () -> {})
).centerOffset(width, height, 0, -195));
} }
} }
@@ -0,0 +1,106 @@
package org.saturnclient.ui2.screens.store;
import org.saturnclient.saturnclient.SaturnClient;
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;
import org.saturnclient.ui2.elements.Text;
import org.saturnclient.ui2.resources.Fonts;
import org.saturnclient.ui2.resources.Textures;
import com.mojang.blaze3d.systems.RenderSystem;
public class CloakStore extends SaturnScreen {
private long lastPurchaseTime = 0;
public CloakStore() {
super("Cloak Store");
}
public CloakStore(long lastPurchaseTime) {
super("Cloak Store");
this.lastPurchaseTime = lastPurchaseTime;
}
@Override
public void ui() {
int p = 10;
int gx = 10;
int gy = 20;
Scroll scroll = new Scroll(p);
int row = 0;
int col = 0;
SaturnPlayer player = Auth.players.get(Auth.uuid);
if (player != null) {
for (String cloak : Cloaks.ALL_CLOAKS) {
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), () -> {
long now = System.currentTimeMillis();
if (now - lastPurchaseTime >= 3000) {
Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
Auth.buyCloak(cloak);
lastPurchaseTime = now;
new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
RenderSystem.recordRenderCall(() -> {
SaturnClient.client.setScreen(new CloakStore(now));
Utils.notify(NotificationKind.Success, "Purchase complete",
"Congrats, enjoy your new cloak!");
});
}).start();
} 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));
if (col == 8) {
col = 0;
row++;
} else {
col++;
}
}
}
}
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));
}
}
@@ -0,0 +1,106 @@
package org.saturnclient.ui2.screens.store;
import org.saturnclient.saturnclient.SaturnClient;
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;
import org.saturnclient.ui2.elements.Text;
import org.saturnclient.ui2.resources.Fonts;
import org.saturnclient.ui2.resources.Textures;
import com.mojang.blaze3d.systems.RenderSystem;
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;
int gx = 10;
int gy = 20;
Scroll scroll = new Scroll(p);
int row = 0;
int col = 0;
SaturnPlayer player = Auth.players.get(Auth.uuid);
if (player != null) {
for (String hat : Hats.ALL_HATS) {
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), () -> {
long now = System.currentTimeMillis();
if (now - lastPurchaseTime >= 3000) {
Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
Auth.buyHat(hat);
lastPurchaseTime = now;
new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
RenderSystem.recordRenderCall(() -> {
SaturnClient.client.setScreen(new HatStore(now));
Utils.notify(NotificationKind.Success, "Purchase complete",
"Congrats, enjoy your new hat!");
});
}).start();
} 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));
if (col == 8) {
col = 0;
row++;
} else {
col++;
}
}
}
}
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));
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 419 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
{
"version": 1,
"valid": true,
"name": "cry",
"description": "T_T",
"author": "dsty",
"emote": {
"beginTick": 0,
"endTick": 60,
"stopTick": 61,
"isLoop": true,
"returnTick": 5,
"nsfw": false,
"degrees": false,
"moves": [
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"x": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"x": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"y": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"y": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"z": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"z": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.34134695
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.22808763
}
},
{
"tick": 25,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.21269174
}
},
{
"tick": 35,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.32248768
}
},
{
"tick": 45,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.0874397
}
},
{
"tick": 55,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.17582592
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.21476103
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"yaw": -6.320644E-4
}
},
{
"tick": 25,
"easing": "INOUTQUAD",
"head": {
"yaw": 0.029434478
}
},
{
"tick": 35,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.039604172
}
},
{
"tick": 45,
"easing": "INOUTQUAD",
"head": {
"yaw": 0.037607383
}
},
{
"tick": 55,
"easing": "INOUTQUAD",
"head": {
"yaw": 0.050604735
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"roll": -0.00930274
}
},
{
"tick": 25,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0010032838
}
},
{
"tick": 35,
"easing": "INOUTQUAD",
"head": {
"roll": -0.009209918
}
},
{
"tick": 45,
"easing": "INOUTQUAD",
"head": {
"roll": -0.012478679
}
},
{
"tick": 55,
"easing": "INOUTQUAD",
"head": {
"roll": 0.031160546
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"x": -5.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"y": 2.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -1.8700758
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.4349194
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.4970688
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.9622451
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"x": 5.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"y": 2.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"z": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": -1.9203311
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": 0.6307226
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"roll": -0.2971223
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"bend": -0.8864816
}
}
]
}
}
@@ -0,0 +1,389 @@
{
"version": 1,
"valid": true,
"name": "facepalm",
"description": "just facepalm",
"author": "dsty",
"emote": {
"beginTick": 0,
"endTick": 60,
"stopTick": 61,
"isLoop": false,
"returnTick": 2,
"nsfw": false,
"degrees": false,
"moves": [
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"x": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"x": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"y": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"y": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"z": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"z": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.067995355
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.14453278
}
},
{
"tick": 30,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.11871816
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.19018666
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.20064461
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"pitch": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"yaw": 0.060454715
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.086299464
}
},
{
"tick": 30,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.087619215
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.083825104
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"head": {
"yaw": 0.036273066
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0041144555
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"head": {
"roll": 0.12942825
}
},
{
"tick": 30,
"easing": "INOUTQUAD",
"head": {
"roll": -0.050149817
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"head": {
"roll": 0.08701207
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"head": {
"roll": 0.08676299
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"x": -3.8523226
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"x": -3.8523226
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"x": -5.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"y": 2.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"y": 2.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -2.0664942
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -2.074402
}
},
{
"tick": 30,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -2.074402
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -2.02932
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -2.02932
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.47555572
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.5093512
}
},
{
"tick": 30,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.5093512
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.38563538
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.38563538
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.20147677
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.20521602
}
},
{
"tick": 30,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.3274244
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.30787703
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.40478125
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"roll": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.49199954
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.54730254
}
},
{
"tick": 40,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.52019864
}
},
{
"tick": 60,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.0
}
}
]
}
}
@@ -0,0 +1,697 @@
{
"version": 1,
"valid": true,
"name": "front_flip",
"description": "Parkour!!!!1",
"author": "DKinchik",
"emote": {
"beginTick": 0,
"endTick": 17,
"stopTick": 18,
"isLoop": false,
"returnTick": 1,
"nsfw": false,
"degrees": false,
"moves": [
{
"tick": 2,
"easing": "INOUTQUAD",
"head": {
"x": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"x": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"head": {
"y": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"y": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"head": {
"z": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"z": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"head": {
"pitch": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.7020477
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"head": {
"pitch": 0.7020477
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"pitch": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"yaw": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"head": {
"roll": 0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"x": -1.9
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightLeg": {
"x": -1.9
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"y": 11.169379
}
},
{
"tick": 3,
"easing": "INOUTQUAD",
"rightLeg": {
"y": 12.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightLeg": {
"y": 12.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"z": -0.39214998
}
},
{
"tick": 3,
"easing": "INOUTQUAD",
"rightLeg": {
"z": 0.1
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightLeg": {
"z": 0.1
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"pitch": -0.0
}
},
{
"tick": 3,
"easing": "INOUTQUAD",
"rightLeg": {
"pitch": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightLeg": {
"pitch": -1.9707682
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightLeg": {
"pitch": -1.9707682
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightLeg": {
"pitch": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"yaw": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightLeg": {
"yaw": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"roll": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightLeg": {
"roll": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightLeg": {
"bend": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"x": -5.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightArm": {
"x": -5.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"y": 2.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightArm": {
"y": 2.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -1.3665518
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -1.3665518
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"roll": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"rightArm": {
"roll": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"x": 5.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftArm": {
"x": 5.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"y": 2.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftArm": {
"y": 2.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"z": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftArm": {
"z": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": 0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": -1.0793308
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": -1.0793308
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": 0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"roll": 0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftArm": {
"roll": 0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftArm": {
"bend": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"x": 1.9
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftLeg": {
"x": 1.9
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"y": 10.861001
}
},
{
"tick": 3,
"easing": "INOUTQUAD",
"leftLeg": {
"y": 12.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftLeg": {
"y": 12.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"z": -0.5082285
}
},
{
"tick": 3,
"easing": "INOUTQUAD",
"leftLeg": {
"z": -0.1
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftLeg": {
"z": -0.1
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"pitch": -0.0
}
},
{
"tick": 3,
"easing": "INOUTQUAD",
"leftLeg": {
"pitch": -0.0
}
},
{
"tick": 5,
"easing": "INOUTQUAD",
"leftLeg": {
"pitch": -1.9850811
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftLeg": {
"pitch": -1.9850811
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftLeg": {
"pitch": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"yaw": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftLeg": {
"yaw": -0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"roll": -0.0
}
},
{
"tick": 15,
"easing": "INOUTQUAD",
"leftLeg": {
"roll": 0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"leftLeg": {
"bend": -0.0
}
},
{
"tick": 2,
"easing": "INQUAD",
"body": {
"x": 0.0
}
},
{
"tick": 16,
"easing": "INQUAD",
"body": {
"x": 0.0
}
},
{
"tick": 2,
"easing": "INQUAD",
"body": {
"y": -0.061333913
}
},
{
"tick": 7,
"easing": "OUTQUAD",
"body": {
"y": 0.48852682
}
},
{
"tick": 15,
"easing": "CONSTANT",
"body": {
"y": 0.0
}
},
{
"tick": 16,
"easing": "INQUAD",
"body": {
"y": 0.0
}
},
{
"tick": 2,
"easing": "INQUAD",
"body": {
"z": -0.0
}
},
{
"tick": 16,
"easing": "INQUAD",
"body": {
"z": -0.0
}
},
{
"tick": 2,
"easing": "INQUAD",
"body": {
"pitch": 0.0
}
},
{
"tick": 7,
"easing": "OUTQUAD",
"body": {
"pitch": -2.8362873
}
},
{
"tick": 15,
"easing": "CONSTANT",
"body": {
"pitch": -6.2831855
}
},
{
"tick": 16,
"easing": "INQUAD",
"body": {
"pitch": 0.0
}
},
{
"tick": 2,
"easing": "INQUAD",
"body": {
"yaw": 0.0
}
},
{
"tick": 16,
"easing": "INQUAD",
"body": {
"yaw": 0.0
}
},
{
"tick": 2,
"easing": "INQUAD",
"body": {
"roll": -0.0
}
},
{
"tick": 16,
"easing": "INQUAD",
"body": {
"roll": 0.0
}
},
{
"tick": 2,
"easing": "INOUTQUAD",
"body": {
"bend": -0.0
}
}
]
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,354 @@
{
"version": 1,
"valid": true,
"name": "tpose",
"description": "MENACING",
"author": "stephanieisdumb",
"emote": {
"beginTick": 0,
"endTick": 80,
"stopTick": 81,
"isLoop": true,
"returnTick": 20,
"nsfw": false,
"degrees": false,
"moves": [
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"x": -5.0
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"x": -6.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightArm": {
"x": -6.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"y": 2.0
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"y": 3.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightArm": {
"y": 3.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightArm": {
"z": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -1.0704756
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -0.0
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": 0.059108086
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightArm": {
"pitch": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.61077195
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.0
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.008776078
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightArm": {
"yaw": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 0.6060502
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 1.5707964
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 1.5765934
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightArm": {
"roll": 1.5707964
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -1.3806202
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"rightArm": {
"bend": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"x": 5.0
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"x": 6.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftArm": {
"x": 6.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"y": 2.0
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"y": 3.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftArm": {
"y": 3.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"z": -0.0
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"z": -0.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftArm": {
"z": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": -1.0045553
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": 0.0
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": 0.010133168
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftArm": {
"pitch": 0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": 0.44322997
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": -0.0
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": 0.038214765
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftArm": {
"yaw": -0.0
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"roll": -0.5571526
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"roll": -1.5707964
}
},
{
"tick": 50,
"easing": "INOUTQUAD",
"leftArm": {
"roll": -1.5692185
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftArm": {
"roll": -1.5707964
}
},
{
"tick": 10,
"easing": "INOUTQUAD",
"leftArm": {
"bend": -1.3810014
}
},
{
"tick": 20,
"easing": "INOUTQUAD",
"leftArm": {
"bend": -0.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"leftLeg": {
"pitch": 0.0
}
},
{
"tick": 80,
"easing": "INOUTQUAD",
"rightLeg": {
"pitch": 0.0
}
}
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Some files were not shown because too many files have changed in this diff Show More