Refactor features to use module system

This commit is contained in:
2026-03-08 13:13:11 +01:00
parent da6576e4ed
commit ab353f5fe9
56 changed files with 2030 additions and 1700 deletions
@@ -3,12 +3,13 @@ package org.saturnclient.feature;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.feature.features.*;
public class FeatureManager {
public static List<Feature> ENABLED_MODS = new ArrayList<>();
public static Feature[] MODS = {};
public static void init(ModuleProvider provider) {
MODS = provider.getMods();
public static void init() {
updateEnabledModules();
}
@@ -20,12 +21,4 @@ public class FeatureManager {
}
}
}
public static Feature[] getAllMods() {
return MODS;
}
public interface ModuleProvider {
Feature[] getMods();
}
}
@@ -1,118 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.ArmorDisplayInterface;
import org.saturnclient.ui.RenderScope;
public class ArmorDisplay extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(40, 75);
private static Property<Boolean> useMainHand = Property.bool(true);
private final ArmorDisplayInterface minecraft;
public ArmorDisplay(ArmorDisplayInterface minecraft) {
super(new FeatureDetails("Armor Display", "armor")
.description("Displays armor durability")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
useMainHand.named("Use Main Hand"),
dimensions.prop());
this.minecraft = minecraft;
}
public void renderArmor(RenderScope scope,
ItemStackRef mainHand,
ItemStackRef helmet,
ItemStackRef chestplate,
ItemStackRef leggings,
ItemStackRef boots) {
int row = 0;
if (useMainHand.value && !mainHand.isEmpty()) {
scope.drawItem(mainHand, 0, 15 * row);
renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
row++;
}
if (!helmet.isEmpty()) {
scope.drawItem(helmet, 0, 15 * row);
renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
row++;
}
if (!chestplate.isEmpty()) {
scope.drawItem(chestplate, 0, 15 * row);
renderHealth(scope, row, chestplate.getMaxDamage(), chestplate.getDamage());
row++;
}
if (!leggings.isEmpty()) {
scope.drawItem(leggings, 0, 15 * row);
renderHealth(scope, row, leggings.getMaxDamage(), leggings.getDamage());
row++;
}
if (!boots.isEmpty()) {
scope.drawItem(boots, 0, 15 * row);
renderHealth(scope, row, boots.getMaxDamage(), boots.getDamage());
row++;
}
}
public void renderHealth(RenderScope scope, int i, int maxDamage, int damage) {
if (maxDamage > 0) {
scope.drawText(
0.5f,
"" + (maxDamage - damage),
17,
(15 * i) + 3,
dimensions.font.value,
dimensions.fgColor.value);
}
}
@Override
public void renderHud(RenderScope scope) {
renderArmor(scope,
minecraft.getMainHand(),
minecraft.getHelmet(),
minecraft.getChestplate(),
minecraft.getLeggings(),
minecraft.getBoots());
}
@Override
public void renderDummy(RenderScope scope) {
renderArmor(scope,
minecraft.getDummyMainHand(),
minecraft.getDummyHelmet(),
minecraft.getDummyChestplate(),
minecraft.getDummyLeggings(),
minecraft.getDummyBoots());
}
@Override
public FeatureLayout getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,126 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.ui.RenderScope;
/**
* ArmorDisplayFeature renders the durability of the player's equipped
* armor pieces (and optionally the main-hand item) as a HUD overlay.
*
* Engine access is provided exclusively through {@link ModuleProvider}.
*/
public class ArmorDisplayFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> useMainHand = Property.bool(true);
private static final FeatureLayout layout = new FeatureLayout(40, 75);
private final ModuleProvider modules;
public ArmorDisplayFeature(ModuleProvider modules) {
super(
new FeatureDetails("Armor Display", "armor")
.description("Displays armor durability")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
useMainHand.named("Use Main Hand"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
PlayerModule player = modules.player();
renderArmor(scope,
player.getMainHand(),
player.getHelmet(),
player.getChestplate(),
player.getLeggings(),
player.getBoots());
}
@Override
public void renderDummy(RenderScope scope) {
PlayerModule player = modules.player();
renderArmor(scope,
player.getDummyMainHand(),
player.getDummyHelmet(),
player.getDummyChestplate(),
player.getDummyLeggings(),
player.getDummyBoots());
}
// ---------------------------------------------------------------
// Rendering helpers
// ---------------------------------------------------------------
private void renderArmor(RenderScope scope,
ItemStackRef mainHand,
ItemStackRef helmet,
ItemStackRef chestplate,
ItemStackRef leggings,
ItemStackRef boots) {
int row = 0;
if (useMainHand.value && !mainHand.isEmpty()) {
row = renderSlot(scope, mainHand, row);
}
if (!helmet.isEmpty())
row = renderSlot(scope, helmet, row);
if (!chestplate.isEmpty())
row = renderSlot(scope, chestplate, row);
if (!leggings.isEmpty())
row = renderSlot(scope, leggings, row);
if (!boots.isEmpty())
row = renderSlot(scope, boots, row);
}
/**
* Renders a single equipment slot and returns the next row index.
*/
private int renderSlot(RenderScope scope, ItemStackRef item, int row) {
scope.drawItem(item, 0, 15 * row);
if (item.getMaxDamage() > 0) {
int remaining = item.getMaxDamage() - item.getDamage();
scope.drawText(0.5f, String.valueOf(remaining),
17, (15 * row) + 3,
layout.font.value, layout.fgColor.value);
}
return row + 1;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,50 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.AutoSprintInterface;
public class AutoSprint extends Feature {
private static Property<Boolean> enabled = Property.bool(false);
private final AutoSprintInterface minecraft;
public AutoSprint(AutoSprintInterface minecraft) {
super(
new FeatureDetails("Auto Sprint", "sprint")
.description("Makes the player always sprint")
.tags("Movement")
.version("v0.1.0"),
enabled.named("Enabled"));
this.minecraft = minecraft;
}
@Override
public void tick() {
if (!minecraft.hasPlayer() || !minecraft.hasNetwork()) {
return;
}
if (minecraft.isForwardPressed()
&& !minecraft.isBackPressed()
&& !minecraft.isSneaking()
&& !minecraft.hasHorizontalCollision()
&& !minecraft.isUsingItem()) {
minecraft.setSprinting(true);
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,63 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.NetworkModule;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* AutoSprintFeature automatically enables sprinting whenever the
* player moves forward and no sprint-blocking condition is active.
*/
public class AutoSprintFeature extends Feature {
private static final Property<Boolean> enabled = Property.bool(false);
private final ModuleProvider modules;
public AutoSprintFeature(ModuleProvider modules) {
super(
new FeatureDetails("Auto Sprint", "sprint")
.description("Makes the player always sprint")
.tags("Movement")
.version("v0.2.0"),
enabled.named("Enabled"));
this.modules = modules;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void tick() {
PlayerModule player = modules.player();
NetworkModule network = modules.network();
if (!player.hasPlayer() || !network.hasNetwork()) {
return;
}
if (player.isForwardPressed()
&& !player.isBackPressed()
&& !player.isSneaking()
&& !player.hasHorizontalCollision()
&& !player.isUsingItem()) {
player.setSprinting(true);
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,82 +0,0 @@
package org.saturnclient.feature.features;
import java.time.format.DateTimeFormatter;
import java.time.LocalTime;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Clock extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> format = Property.select(0, "24 hour", "12 hour");
private static Property<Boolean> seconds = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(60, Fonts.getHeight());
public Clock() {
super(new FeatureDetails("Clock", "clock")
.description("Displays the current real-world time")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
format.named("Format"),
seconds.named("Show seconds"),
dimensions.prop());
}
@Override
public void renderDummy(RenderScope scope) {
renderClock(scope);
}
@Override
public void renderHud(RenderScope scope) {
renderClock(scope);
}
public void renderClock(RenderScope scope) {
String text = "";
LocalTime currentTime = LocalTime.now();
switch (format.value) {
case 0:
if (seconds.value) {
text = currentTime.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
} else {
text = currentTime.format(DateTimeFormatter.ofPattern("HH:mm"));
}
break;
case 1:
if (seconds.value) {
text = currentTime.format(DateTimeFormatter.ofPattern("hh:mm:ss a"));
} else {
text = currentTime.format(DateTimeFormatter.ofPattern("hh:mm a"));
}
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 FeatureLayout getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,92 @@
package org.saturnclient.feature.features;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* ClockFeature displays the real-world time as a HUD element.
* No engine access is required; {@link LocalTime} is used directly.
*/
public class ClockFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Integer> format = Property.select(0, "24 hour", "12 hour");
private static final Property<Boolean> showSeconds = Property.bool(false);
private static final FeatureLayout layout = new FeatureLayout(60, Fonts.getHeight());
public ClockFeature() {
super(
new FeatureDetails("Clock", "clock")
.description("Displays the current real-world time")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
format.named("Format"),
showSeconds.named("Show seconds"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderClock(scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderClock(scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderClock(RenderScope scope) {
String text = buildTimeString(LocalTime.now());
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
layout.height = 18 * text.split("\n").length;
}
private String buildTimeString(LocalTime time) {
return switch (format.value) {
case 1 -> time.format(showSeconds.value
? DateTimeFormatter.ofPattern("hh:mm:ss a")
: DateTimeFormatter.ofPattern("hh:mm a"));
default -> time.format(showSeconds.value
? DateTimeFormatter.ofPattern("HH:mm:ss")
: DateTimeFormatter.ofPattern("HH:mm"));
};
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,98 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.CoordinatesInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Coordinates extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> displayMethod = Property.select(
0,
"Flat",
"Flat annotated",
"Newline",
"Newline annotated");
private static FeatureLayout dimensions = new FeatureLayout(40, 18);
private final CoordinatesInterface minecraft;
public Coordinates(CoordinatesInterface minecraft) {
super(new FeatureDetails("Coordinates", "coords")
.description("Displays your current coordinates")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMethod.named("Display Method"),
dimensions.prop());
this.minecraft = minecraft;
}
@Override
public void renderDummy(RenderScope scope) {
renderCoords(532, 69, 253, scope);
}
@Override
public void renderHud(RenderScope scope) {
int x = minecraft.getPlayerX();
int y = minecraft.getPlayerY();
int z = minecraft.getPlayerZ();
renderCoords(x, y, z, scope);
}
public void renderCoords(int x, int y, int z, RenderScope scope) {
String f = "%d %d %d";
switch (displayMethod.value) {
case 1:
f = "X: %d Y: %d Z: %d";
break;
case 2:
f = "%d\n%d\n%d";
break;
case 3:
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 FeatureLayout getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,96 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* CoordinatesFeature displays the player's current XYZ position as a
* HUD element with multiple configurable display formats.
*/
public class CoordinatesFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Integer> displayMethod = Property.select(0,
"Flat",
"Flat annotated",
"Newline",
"Newline annotated");
private static final FeatureLayout layout = new FeatureLayout(40, 18);
private final ModuleProvider modules;
public CoordinatesFeature(ModuleProvider modules) {
super(
new FeatureDetails("Coordinates", "coords")
.description("Displays your current coordinates")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMethod.named("Display Method"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderCoords(
modules.player().getX(),
modules.player().getY(),
modules.player().getZ(),
scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderCoords(532, 69, 253, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderCoords(int x, int y, int z, RenderScope scope) {
String fmt = switch (displayMethod.value) {
case 1 -> "X: %d Y: %d Z: %d";
case 2 -> "%d\n%d\n%d";
case 3 -> "X: %d\nY: %d\nZ: %d";
default -> "%d %d %d";
};
String text = String.format(fmt, x, y, z);
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
layout.height = 18 * text.split("\n").length;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,55 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.CrosshairInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Textures;
public class Crosshair extends Feature {
public static Property<Boolean> enabled = Property.bool(false);
private final CrosshairInterface minecraft;
public Crosshair(CrosshairInterface minecraft) {
super(
new FeatureDetails("Crosshair", "crosshair")
.description("Changes the crosshair if you're aiming at a entity")
.tags("Visuals", "Utility")
.version("v0.1.0"),
enabled.named("Enabled"));
this.minecraft = minecraft;
}
@Override
public void render(RenderScope scope) {
if (enabled.value && minecraft.isTargetingLivingEntity()) {
int scaledWidth = 15;
int scaledHeight = 15;
scope.drawTexture(
Textures.CROSSHAIR_RANGE,
(scope.getScaledWindowWidth() - scaledWidth) / 2,
(scope.getScaledWindowHeight() - scaledHeight) / 2,
0.0F,
0.0F,
scaledWidth,
scaledHeight);
}
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,64 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.RenderModule;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Textures;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* CrosshairFeature renders an alternative crosshair texture when the
* player is aiming at a living entity.
*/
public class CrosshairFeature extends Feature {
public static final Property<Boolean> enabled = Property.bool(false);
private static final int CROSSHAIR_SIZE = 15;
private final ModuleProvider modules;
public CrosshairFeature(ModuleProvider modules) {
super(
new FeatureDetails("Crosshair", "crosshair")
.description("Changes the crosshair when aiming at an entity")
.tags("Visuals", "Utility")
.version("v0.2.0"),
enabled.named("Enabled"));
this.modules = modules;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void render(RenderScope scope) {
if (!enabled.value || !modules.entity().isTargetingLivingEntity()) {
return;
}
RenderModule render = modules.render();
int x = (render.getScaledWindowWidth() - CROSSHAIR_SIZE) / 2;
int y = (render.getScaledWindowHeight() - CROSSHAIR_SIZE) / 2;
scope.drawTexture(
Textures.CROSSHAIR_RANGE,
x, y,
0.0F, 0.0F,
CROSSHAIR_SIZE, CROSSHAIR_SIZE);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,71 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.DayCounterInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class DayCounter extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(40, 18);
private final DayCounterInterface minecraft;
long day = 0;
public DayCounter(DayCounterInterface minecraft) {
super(new FeatureDetails("DayCounter", "day")
.description("Displays the number of days that have passed")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
this.minecraft = minecraft;
}
@Override
public void renderDummy(RenderScope scope) {
renderDay(271, scope);
}
@Override
public void renderHud(RenderScope scope) {
day = minecraft.getDay();
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 FeatureLayout getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,79 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* DayCounterFeature shows how many in-game days have elapsed.
* Day count is derived from the world age via {@link WorldModule}.
*/
public class DayCounterFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final FeatureLayout layout = new FeatureLayout(40, 18);
private final ModuleProvider modules;
public DayCounterFeature(ModuleProvider modules) {
super(
new FeatureDetails("Day Counter", "day")
.description("Displays the number of in-game days elapsed")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderDay(modules.world().getDay(), scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderDay(271L, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderDay(long day, RenderScope scope) {
String text = "Day: " + day;
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
layout.height = 18 * text.split("\n").length;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,64 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.FpsInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Fps extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(60, Fonts.getHeight());
private final FpsInterface minecraft;
public Fps(FpsInterface minecraft) {
super(new FeatureDetails("FPS Display", "fps")
.description("Displays current FPS")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
this.minecraft = minecraft;
}
@Override
public void renderHud(RenderScope scope) {
int fps = minecraft.getFps();
String text = fps + " 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 FeatureLayout getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,77 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* FpsFeature displays the current frames-per-second as a HUD element.
* FPS is obtained from {@link RenderModule} rather than cached in the feature.
*/
public class FpsFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final FeatureLayout layout = new FeatureLayout(60, Fonts.getHeight());
private final ModuleProvider modules;
public FpsFeature(ModuleProvider modules) {
super(
new FeatureDetails("FPS Display", "fps")
.description("Displays current FPS")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderFps(modules.render().getFps(), scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderFps(369, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderFps(int fps, RenderScope scope) {
String text = fps + " FPS";
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,88 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.FreelookInterface;
public class Freelook extends Feature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(true);
public static Property<Integer> freelookKey = Property.keybinding(GLFWProvider.GLFW_KEY_H);
public static boolean isFreeLooking = false;
private static boolean was1stPerson = false;
private final FreelookInterface minecraft;
public Freelook(FreelookInterface minecraft) {
super(
new FeatureDetails("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"));
this.minecraft = minecraft;
}
@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 = minecraft.isFirstPerson();
minecraft.setThirdPersonBack();
}
private void stopFreelook() {
if (was1stPerson) {
minecraft.setFirstPerson();
}
isFreeLooking = false;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,92 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.RenderModule;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* FreelookFeature lets the player look around freely (in third-person)
* without changing the direction their character walks.
*
* Camera perspective is controlled through {@link RenderModule} so
* that the feature itself contains no direct Minecraft API calls.
*/
public class FreelookFeature extends Feature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> toggle = Property.bool(true);
public static final Property<Integer> freelookKey = Property.keybinding(GLFWProvider.GLFW_KEY_H);
/** Whether freelook is currently active (read by the camera mixin). */
public static boolean isFreeLooking = false;
private static boolean wasFirstPerson = false;
private final ModuleProvider modules;
public FreelookFeature(ModuleProvider modules) {
super(
new FeatureDetails("Freelook", "freelook")
.description("Look around freely without moving your character")
.tags("Movement", "Camera")
.version("v0.2.0"),
enabled.named("Enabled"),
toggle.named("Toggle freelook"),
freelookKey.named("Freelook Keybinding"));
this.modules = modules;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@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();
}
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
if (!e && isFreeLooking)
stopFreelook();
}
@Override
public boolean isEnabled() {
return enabled.value;
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private void startFreelook() {
RenderModule render = modules.render();
wasFirstPerson = render.isFirstPerson();
render.setThirdPersonBack();
isFreeLooking = true;
}
private void stopFreelook() {
if (wasFirstPerson) {
modules.render().setFirstPerson();
}
isFreeLooking = false;
}
}
@@ -1,39 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
public class Fullbright extends Feature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Integer> brightness = Property.integer(100);
public Fullbright() {
super(
new FeatureDetails("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,63 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* FullbrightFeature overrides the game's gamma/brightness to let the
* player see in complete darkness.
*
* Actual brightness injection happens in a mixin; this class owns only
* the configuration and the two static query methods the mixin calls.
*
* No ModuleProvider is needed — this feature has no per-tick engine
* interaction; the mixin polls the static helpers directly.
*/
public class FullbrightFeature extends Feature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Integer> brightness = Property.integer(100);
public FullbrightFeature() {
super(
new FeatureDetails("Fullbright", "fullbright")
.description("Allows you to see in the dark")
.tags("Camera")
.version("v0.2.0"),
enabled.named("Enabled"),
brightness.named("Brightness %"));
}
// ---------------------------------------------------------------
// Mixin query API (static so the mixin can call without an instance)
// ---------------------------------------------------------------
/** Returns {@code true} when the brightness override should be applied. */
public static boolean shouldOverrideBrightness() {
return enabled.value;
}
/**
* Returns the brightness multiplier to apply.
* The property stores a percentage (0–100+); the mixin expects a
* normalised float where 1.0 ≈ 100 %.
*/
public static float getBrightnessValue() {
return brightness.value / 10.0f;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,96 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.HealthDisplayInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class HealthDisplay extends Feature implements HudFeature {
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 FeatureLayout dimensions = new FeatureLayout(40, 18);
private final HealthDisplayInterface minecraft;
public HealthDisplay(HealthDisplayInterface minecraft) {
super(new FeatureDetails("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());
this.minecraft = minecraft;
}
@Override
public void renderDummy(RenderScope scope) {
renderHealth(10, scope);
}
@Override
public void renderHud(RenderScope scope) {
float health = minecraft.getPlayerHealth();
switch (displayMode.value) {
case 1:
health = health / 2;
break;
}
renderHealth(health, scope);
}
public void renderHealth(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 FeatureLayout getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,93 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* HealthDisplayFeature renders the player's current health as a HUD
* element, with configurable display modes (raw health vs. hearts) and
* decimal precision.
*/
public class HealthDisplayFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Integer> displayMode = Property.select(0, "Health", "Hearts");
private static final Property<Integer> decimals = Property.select(0, "0", "1", "2");
private static final FeatureLayout layout = new FeatureLayout(40, 18);
private final ModuleProvider modules;
public HealthDisplayFeature(ModuleProvider modules) {
super(
new FeatureDetails("Health Display", "health")
.description("Displays your current health")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMode.named("Display mode"),
decimals.named("Decimals"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
float health = modules.player().getHealth();
if (displayMode.value == 1)
health /= 2f; // convert to hearts
renderHealth(health, scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderHealth(10f, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderHealth(float health, RenderScope scope) {
String fmt = switch (decimals.value) {
case 1 -> "%.1f ";
case 2 -> "%.2f ";
default -> "%.0f ";
};
String text = String.format(fmt, health);
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
layout.height = 18 * text.split("\n").length;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,156 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.KeystrokesInterface;
import org.saturnclient.ui.RenderScope;
public class Keystrokes extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> showMouse = Property.bool(true);
private static final Property<Boolean> showSpace = Property.bool(false);
private static final Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static final Property<Integer> clickFg = Property.color(0xFFFFFFFF);
private static final FeatureLayout dimensions = new FeatureLayout(78, 54);
private final KeystrokesInterface minecraft;
// Key states
private boolean w, a, s, d, lmb, rmb, space;
// Layout constants
private static final int KEY_SIZE = 24;
private static final int KEY_SPACING = 3;
private static final int MOUSE_WIDTH = 38;
private static final int MOUSE_HEIGHT = 24;
private static final int SPACE_HEIGHT = 19;
public Keystrokes(KeystrokesInterface minecraft) {
super(new FeatureDetails("Keystrokes", "keystrokes")
.description("Displays movement keystrokes")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
showMouse.named("Show mouse clicks"),
showSpace.named("Show space"),
dimensions.prop(),
clickFg.named("Clicked fg"),
clickBg.named("Clicked bg"));
this.minecraft = minecraft;
dimensions.renderBackground = false;
}
@Override
public void tick() {
w = minecraft.isForwardPressed();
a = minecraft.isLeftPressed();
s = minecraft.isBackPressed();
d = minecraft.isRightPressed();
lmb = minecraft.isAttackPressed();
rmb = minecraft.isUsePressed();
space = minecraft.isJumpPressed();
updateHeight();
}
private void updateHeight() {
int height = KEY_SIZE * 2 + KEY_SPACING;
if (showSpace.value)
height += SPACE_HEIGHT + KEY_SPACING;
if (showMouse.value)
height += MOUSE_HEIGHT + KEY_SPACING;
dimensions.height = height;
}
@Override
public void renderDummy(RenderScope scope) {
render(scope, false);
}
@Override
public void renderHud(RenderScope scope) {
render(scope, true);
}
private void render(RenderScope scope, boolean live) {
int yOffset = 0;
// --- W ---
renderKey(scope, live && w, 'W', KEY_SIZE + KEY_SPACING, yOffset);
// --- A S D ---
yOffset += KEY_SIZE + KEY_SPACING;
renderKey(scope, live && a, 'A', 0, yOffset);
renderKey(scope, live && s, 'S', KEY_SIZE + KEY_SPACING, yOffset);
renderKey(scope, live && d, 'D', (KEY_SIZE + KEY_SPACING) * 2, yOffset);
yOffset += KEY_SIZE + KEY_SPACING;
// --- SPACE ---
if (showSpace.value) {
renderKeySpace(scope, live && space, 0, yOffset);
yOffset += SPACE_HEIGHT + KEY_SPACING;
}
// --- MOUSE ---
if (showMouse.value) {
renderKeyMouse(scope, live && lmb, "LMB", 0, yOffset);
renderKeyMouse(scope, live && rmb, "RMB", MOUSE_WIDTH + KEY_SPACING, yOffset);
}
}
private void renderKey(RenderScope scope, boolean pressed, char c, int x, int y) {
int bg = pressed ? clickBg.value : dimensions.bgColor.value;
int fg = pressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, KEY_SIZE, KEY_SIZE, dimensions.radius.value, bg);
scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, fg);
}
private void renderKeyMouse(RenderScope scope, boolean pressed, String text, int x, int y) {
int bg = pressed ? clickBg.value : dimensions.bgColor.value;
int fg = pressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, MOUSE_WIDTH, MOUSE_HEIGHT, dimensions.radius.value, bg);
scope.drawText(0.6f, text, x + 9, y + 7, dimensions.font.value, fg);
}
private void renderKeySpace(RenderScope scope, boolean pressed, int x, int y) {
int bg = pressed ? clickBg.value : dimensions.bgColor.value;
int fg = pressed ? clickFg.value : dimensions.fgColor.value;
scope.drawRoundedRectangle(x, y, dimensions.width, SPACE_HEIGHT, dimensions.radius.value, bg);
int lineY = y + SPACE_HEIGHT / 2;
scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, fg);
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public FeatureLayout getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,168 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* KeystrokesFeature renders the WASD / mouse / space key states as an
* on-screen overlay.
*/
public class KeystrokesFeature extends Feature implements HudFeature {
// ---------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> showMouse = Property.bool(true);
private static final Property<Boolean> showSpace = Property.bool(false);
private static final Property<Integer> clickBg = Property.color(0xFFCCCCCC);
private static final Property<Integer> clickFg = Property.color(0xFFFFFFFF);
private static final FeatureLayout layout = new FeatureLayout(78, 54);
// ---------------------------------------------------------------
// Layout constants
// ---------------------------------------------------------------
private static final int KEY_SIZE = 24;
private static final int KEY_SPACING = 3;
private static final int MOUSE_WIDTH = 38;
private static final int MOUSE_H = 24;
private static final int SPACE_H = 19;
// ---------------------------------------------------------------
// Cached key states (updated in tick)
// ---------------------------------------------------------------
private boolean w, a, s, d, lmb, rmb, space;
private final ModuleProvider modules;
public KeystrokesFeature(ModuleProvider modules) {
super(
new FeatureDetails("Keystrokes", "keystrokes")
.description("Displays movement keystrokes")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
showMouse.named("Show mouse clicks"),
showSpace.named("Show space"),
layout.prop(),
clickFg.named("Clicked fg"),
clickBg.named("Clicked bg"));
this.modules = modules;
layout.renderBackground = false;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void tick() {
PlayerModule player = modules.player();
w = player.isForwardPressed();
a = player.isLeftPressed();
s = player.isBackPressed();
d = player.isRightPressed();
lmb = player.isAttackPressed();
rmb = player.isUsePressed();
space = player.isJumpPressed();
refreshHeight();
}
@Override
public void renderHud(RenderScope scope) {
render(scope, true);
}
@Override
public void renderDummy(RenderScope scope) {
render(scope, false);
}
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void render(RenderScope scope, boolean live) {
int yOffset = 0;
// W (top centre)
renderKey(scope, live && w, 'W', KEY_SIZE + KEY_SPACING, yOffset);
yOffset += KEY_SIZE + KEY_SPACING;
// A S D
renderKey(scope, live && a, 'A', 0, yOffset);
renderKey(scope, live && s, 'S', KEY_SIZE + KEY_SPACING, yOffset);
renderKey(scope, live && d, 'D', (KEY_SIZE + KEY_SPACING) * 2, yOffset);
yOffset += KEY_SIZE + KEY_SPACING;
// Spacebar (optional)
if (showSpace.value) {
renderSpaceKey(scope, live && space, 0, yOffset);
yOffset += SPACE_H + KEY_SPACING;
}
// Mouse buttons (optional)
if (showMouse.value) {
renderMouseKey(scope, live && lmb, "LMB", 0, yOffset);
renderMouseKey(scope, live && rmb, "RMB", MOUSE_WIDTH + KEY_SPACING, yOffset);
}
}
private void renderKey(RenderScope scope, boolean pressed, char c, int x, int y) {
int bg = pressed ? clickBg.value : layout.bgColor.value;
int fg = pressed ? clickFg.value : layout.fgColor.value;
scope.drawRoundedRectangle(x, y, KEY_SIZE, KEY_SIZE, layout.radius.value, bg);
scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, layout.font.value, fg);
}
private void renderMouseKey(RenderScope scope, boolean pressed, String label, int x, int y) {
int bg = pressed ? clickBg.value : layout.bgColor.value;
int fg = pressed ? clickFg.value : layout.fgColor.value;
scope.drawRoundedRectangle(x, y, MOUSE_WIDTH, MOUSE_H, layout.radius.value, bg);
scope.drawText(0.6f, label, x + 9, y + 7, layout.font.value, fg);
}
private void renderSpaceKey(RenderScope scope, boolean pressed, int x, int y) {
int bg = pressed ? clickBg.value : layout.bgColor.value;
int fg = pressed ? clickFg.value : layout.fgColor.value;
scope.drawRoundedRectangle(x, y, layout.width, SPACE_H, layout.radius.value, bg);
int lineY = y + SPACE_H / 2;
scope.drawRect(x + (layout.width - 30) / 2, lineY, 30, 1, fg);
}
private void refreshHeight() {
int h = KEY_SIZE * 2 + KEY_SPACING;
if (showSpace.value)
h += SPACE_H + KEY_SPACING;
if (showMouse.value)
h += MOUSE_H + KEY_SPACING;
layout.height = h;
}
}
@@ -1,121 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.NametagsInterface;
public class Nametags extends Feature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> healthDisplay = Property.bool(false);
private static Property<Boolean> players = Property.bool(true);
private static Property<Boolean> hostile = Property.bool(false);
private static Property<Boolean> passive = Property.bool(false);
private static Property<Boolean> heartEmoji = Property.bool(true);
private static Property<Boolean> obfuscate = Property.bool(false);
private static Property<Integer> unit = Property.select(0, "Health", "Hearts");
private static Property<Integer> format = Property.select(0, "Value", "Value / Total", "%");
private static Property<Integer> nameColor = Property.select(15,
"Black", "Dark Blue", "Dark Green", "Dark Aqua", "Dark Red", "Dark Purple", "Gold", "Gray",
"Dark Gray", "Blue", "Green", "Aqua", "Red", "Light Purple", "Yellow", "White");
private static Property<Integer> healthColor = Property.select(12,
"Black", "Dark Blue", "Dark Green", "Dark Aqua", "Dark Red", "Dark Purple", "Gold", "Gray",
"Dark Gray", "Blue", "Green", "Aqua", "Red", "Light Purple", "Yellow", "White");
private static final String[] COLOR_CODES = {
"§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7",
"§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f"
};
public Nametags(NametagsInterface minecraft) {
super(new FeatureDetails("Nametags", "nametags")
.description("Allows you to modify entity nametags")
.tags("Camera", "Utility")
.version("0.1.0"),
enabled.named("Enabled"),
healthDisplay.named("Display health"),
players.named("Players"),
hostile.named("Hostile"),
passive.named("Passive"),
heartEmoji.named("Heart emoji"),
obfuscate.named("Obfuscate enemy names"),
unit.named("Unit"),
format.named("Format"),
nameColor.named("Name color"),
healthColor.named("Health color"));
}
@Override
public void tick() {
// Currently unused
}
public static String getHealthString(NametagsInterface.EntityState state, float health, float maxHealth) {
String ret = "";
switch (format.value) {
case 0: // value
ret = String.format("%.1f", health / (unit.value + 1));
break;
case 1: // value / total
ret = String.format("%.1f", health / (unit.value + 1)) + " / "
+ String.format("%.1f", maxHealth / (unit.value + 1));
break;
case 2: // %
ret = String.format("%.0f%%", (health / maxHealth) * 100);
break;
}
return ret;
}
public static String getNametagString(NametagsInterface.EntityState state) {
if (state.getCustomName() == null)
return null;
if (!healthDisplay.value)
return state.getCustomName(); // name only
float health = state.getHealth();
float maxHealth = state.getMaxHealth();
NametagsInterface.EntityType type = state.getEntityType();
if (type == NametagsInterface.EntityType.PASSIVE && !passive.value)
return null;
if (type == NametagsInterface.EntityType.HOSTILE && !hostile.value)
return null;
if (type == NametagsInterface.EntityType.PLAYER && !players.value)
return null;
if (type == NametagsInterface.EntityType.OTHER)
return null;
if (health < 0f || maxHealth < 0f || health > maxHealth)
return null;
String hpColor = COLOR_CODES[healthColor.value];
String color = COLOR_CODES[nameColor.value];
String heart = heartEmoji.value ? " ❤" : "";
String obf = obfuscate.value ? "§k" : "";
String reset = "§r";
return color + obf + state.getCustomName() + reset + " " + hpColor
+ getHealthString(state, health, maxHealth) + heart;
}
public static boolean shouldReplaceName() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
}
@@ -0,0 +1,143 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* NametagsFeature modifies the nametags displayed above entities.
*
* Actual rendering is injected via a mixin; this class owns the
* configuration and exposes two static query methods the mixin uses:
* {@link #shouldReplaceName()} and
* {@link #getNametagString(EntityModule.EntityState)}.
*
* No ModuleProvider is required here because the mixin passes the
* already-resolved {@link EntityModule.EntityState} directly.
*/
public class NametagsFeature extends Feature {
// ---------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> healthDisplay = Property.bool(false);
private static final Property<Boolean> players = Property.bool(true);
private static final Property<Boolean> hostile = Property.bool(false);
private static final Property<Boolean> passive = Property.bool(false);
private static final Property<Boolean> heartEmoji = Property.bool(true);
private static final Property<Boolean> obfuscate = Property.bool(false);
private static final Property<Integer> unit = Property.select(0, "Health", "Hearts");
private static final Property<Integer> format = Property.select(0, "Value", "Value / Total", "%");
private static final Property<Integer> nameColor = Property.select(15,
"Black", "Dark Blue", "Dark Green", "Dark Aqua", "Dark Red", "Dark Purple",
"Gold", "Gray", "Dark Gray", "Blue", "Green", "Aqua", "Red", "Light Purple", "Yellow", "White");
private static final Property<Integer> healthColor = Property.select(12,
"Black", "Dark Blue", "Dark Green", "Dark Aqua", "Dark Red", "Dark Purple",
"Gold", "Gray", "Dark Gray", "Blue", "Green", "Aqua", "Red", "Light Purple", "Yellow", "White");
private static final String[] COLOR_CODES = {
"§0", "§1", "§2", "§3", "§4", "§5", "§6", "§7",
"§8", "§9", "§a", "§b", "§c", "§d", "§e", "§f"
};
// ---------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------
public NametagsFeature() {
super(
new FeatureDetails("Nametags", "nametags")
.description("Allows you to modify entity nametags")
.tags("Camera", "Utility")
.version("v0.2.0"),
enabled.named("Enabled"),
healthDisplay.named("Display health"),
players.named("Players"),
hostile.named("Hostile"),
passive.named("Passive"),
heartEmoji.named("Heart emoji"),
obfuscate.named("Obfuscate enemy names"),
unit.named("Unit"),
format.named("Format"),
nameColor.named("Name color"),
healthColor.named("Health color"));
}
// ---------------------------------------------------------------
// Mixin query API
// ---------------------------------------------------------------
/** Returns {@code true} when nametag replacement is active. */
public static boolean shouldReplaceName() {
return enabled.value;
}
/**
* Builds the replacement nametag string for the given entity state,
* or {@code null} if this entity should not have its tag replaced.
*/
public static String getNametagString(EntityModule.EntityState state) {
if (state.getCustomName() == null)
return null;
if (!healthDisplay.value)
return state.getCustomName();
float health = state.getHealth();
float maxHealth = state.getMaxHealth();
EntityModule.EntityType type = state.getEntityType();
if (type == EntityModule.EntityType.PASSIVE && !passive.value)
return null;
if (type == EntityModule.EntityType.HOSTILE && !hostile.value)
return null;
if (type == EntityModule.EntityType.PLAYER && !players.value)
return null;
if (type == EntityModule.EntityType.OTHER)
return null;
if (health < 0f || maxHealth < 0f || health > maxHealth)
return null;
String hpText = buildHealthString(health, maxHealth);
String hpClr = COLOR_CODES[healthColor.value];
String nameClr = COLOR_CODES[nameColor.value];
String obfCode = obfuscate.value ? "§k" : "";
String heart = heartEmoji.value ? " ❤" : "";
return nameClr + obfCode + state.getCustomName() + "§r "
+ hpClr + hpText + heart;
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static String buildHealthString(float health, float maxHealth) {
float divisor = unit.value + 1;
return switch (format.value) {
case 1 -> String.format("%.1f", health / divisor)
+ " / " + String.format("%.1f", maxHealth / divisor);
case 2 -> String.format("%.0f%%", (health / maxHealth) * 100f);
default -> String.format("%.1f", health / divisor);
};
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,39 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
public class NoFog extends Feature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> liquids = Property.bool(false);
public NoFog() {
super(
new FeatureDetails("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,56 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* NoFogFeature suppresses distance fog and (optionally) liquid fog.
*
* Like {@link FullbrightFeature}, the actual suppression is applied
* inside a mixin. This class owns the configuration and provides
* static query methods the mixin calls.
*/
public class NoFogFeature extends Feature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> liquids = Property.bool(false);
public NoFogFeature() {
super(
new FeatureDetails("No Fog", "nofog")
.description("Stops fog from rendering")
.tags("Camera")
.version("v0.2.0"),
enabled.named("Enabled"),
liquids.named("See through liquids"));
}
// ---------------------------------------------------------------
// Mixin query API
// ---------------------------------------------------------------
/** Returns {@code true} when fog should be suppressed. */
public static boolean isActive() {
return enabled.value;
}
/** Returns {@code true} when liquid-fog suppression is also active. */
public static boolean liquids() {
return liquids.value;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,59 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.PingInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Ping extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(60, Fonts.getHeight());
private final PingInterface pingProvider;
public Ping(PingInterface pingProvider) {
super(new FeatureDetails("Ping Display", "ping")
.description("Displays ping")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
this.pingProvider = pingProvider;
}
@Override
public void renderHud(RenderScope scope) {
int ping = pingProvider.getPing();
String text = ping + " ms";
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 = "10 ms";
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 FeatureLayout getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,77 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* PingFeature displays the current server round-trip time in
* milliseconds as a HUD element.
*/
public class PingFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final FeatureLayout layout = new FeatureLayout(60, Fonts.getHeight());
private final ModuleProvider modules;
public PingFeature(ModuleProvider modules) {
super(
new FeatureDetails("Ping Display", "ping")
.description("Displays ping to the server")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderPing(modules.network().getPing(), scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderPing(10, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderPing(int ping, RenderScope scope) {
String text = ping + " ms";
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,96 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.SpeedometerInterface;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Speedometer extends Feature implements HudFeature {
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 FeatureLayout dimensions = new FeatureLayout(40, 18);
private final SpeedometerInterface provider;
private double speed = 0.;
public Speedometer(SpeedometerInterface provider) {
super(new FeatureDetails("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());
this.provider = provider;
}
@Override
public void renderDummy(RenderScope scope) {
renderSpeed(2.71, scope);
}
@Override
public void renderHud(RenderScope scope) {
SpeedometerInterface.Velocity velocity = provider.getVelocity();
boolean onGround = provider.isOnGround();
double y = onGround ? 0.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 text = String.format("%.2f ", speed);
switch (unitText.value) {
case 1 -> text += "Blocks/s";
case 2 -> text += "blocks/s";
case 3 -> text += "b/s";
}
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 FeatureLayout getDimensions() {
return dimensions;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,111 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* SpeedometerFeature displays the player's current movement speed as
* a HUD element. Velocity data comes from {@link PlayerModule}.
*/
public class SpeedometerFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Integer> axis = Property.select(1, "Absolute", "Horizontal", "Vertical");
private static final Property<Integer> unitText = Property.select(0, "None", "Blocks/s", "blocks/s", "b/s");
private static final Property<Boolean> showLabel = Property.bool(true);
private static final FeatureLayout layout = new FeatureLayout(40, 18);
private double cachedSpeed = 0.0;
private final ModuleProvider modules;
public SpeedometerFeature(ModuleProvider modules) {
super(
new FeatureDetails("Speedometer", "speed")
.description("Displays your current movement speed")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
axis.named("Speed type"),
unitText.named("Unit text"),
showLabel.named("Show speed label"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
PlayerModule player = modules.player();
PlayerModule.Velocity v = player.getVelocity();
boolean onGround = player.isOnGround();
double effectiveY = onGround ? 0.0 : v.y;
cachedSpeed = switch (axis.value) {
case 0 -> Math.sqrt(v.x * v.x + effectiveY * effectiveY + v.z * v.z); // absolute 3-D
case 2 -> Math.abs(effectiveY); // vertical only
default -> Math.sqrt(v.x * v.x + v.z * v.z); // horizontal
};
renderSpeed(cachedSpeed * 20.0, scope); // blocks/tick → blocks/s
}
@Override
public void renderDummy(RenderScope scope) {
renderSpeed(2.71, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderSpeed(double bps, RenderScope scope) {
String text = String.format("%.2f ", bps);
text += switch (unitText.value) {
case 1 -> "Blocks/s";
case 2 -> "blocks/s";
case 3 -> "b/s";
default -> "";
};
if (showLabel.value) {
text = "Speed: " + text;
}
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
layout.height = 18 * text.split("\n").length;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,87 +0,0 @@
package org.saturnclient.feature.features;
import java.util.List;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.interfaces.StatusEffectsInterface;
import org.saturnclient.ui.RenderScope;
public class StatusEffects extends Feature implements HudFeature {
public static Property<Boolean> enabled = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(60, 0);
private final StatusEffectsInterface provider;
public StatusEffects(StatusEffectsInterface provider) {
super(new FeatureDetails("Status effects", "effect")
.description("Displays status effects")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
this.provider = provider;
}
private void renderEffects(RenderScope scope, List<StatusEffectsInterface.Effect> effects) {
int row = 0;
for (StatusEffectsInterface.Effect effect : effects) {
if (!effect.shouldShowIcon())
continue;
scope.drawSpriteStretched(effect.getIcon(), 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, provider.getActiveEffects());
}
@Override
public void renderDummy(RenderScope scope) {
renderEffects(scope, provider.getDummyEffects());
}
private String getDurationAsString(StatusEffectsInterface.Effect effect) {
if (effect.isInfinite()) {
return effect.getInfiniteText();
}
int totalSeconds = effect.getDurationSeconds();
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 totalSeconds + "s";
}
}
@Override
public FeatureLayout getDimensions() {
return dimensions;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,129 @@
package org.saturnclient.feature.features;
import java.util.List;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* StatusEffectsFeature displays the player's active potion effects as
* a HUD overlay.
*
* The effect list is supplied by the platform through an interface
* returned by a future {@code PlayerModule#getActiveEffects()} call.
* Until that is wired up, the existing {@link StatusEffectsInterface}
* bridge is used, accessed via {@link ModuleProvider}.
*/
public class StatusEffectsFeature extends Feature implements HudFeature {
public static final Property<Boolean> enabled = Property.bool(false);
private static final FeatureLayout layout = new FeatureLayout(60, 0);
private final ModuleProvider modules;
public StatusEffectsFeature(ModuleProvider modules) {
super(
new FeatureDetails("Status Effects", "effect")
.description("Displays active status effects")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
this.modules = modules;
}
// ---------------------------------------------------------------
// Effect data contract (inner interface keeps things self-contained)
// ---------------------------------------------------------------
/**
* Minimal view of a single active status effect.
* Implementations are provided by the platform bridge.
*/
public interface EffectView {
boolean shouldShowIcon();
SpriteRef getIcon();
boolean isInfinite();
String getInfiniteText();
int getDurationSeconds();
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderEffects(scope, modules.player().getActiveEffects());
}
@Override
public void renderDummy(RenderScope scope) {
renderEffects(scope, modules.player().getDummyEffects());
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderEffects(RenderScope scope, List<? extends EffectView> effects) {
int row = 0;
for (EffectView effect : effects) {
if (!effect.shouldShowIcon())
continue;
scope.drawSpriteStretched(effect.getIcon(), 0, 18 * row, 16, 16);
scope.drawText(0.5f, durationString(effect),
18, 18 * row + 3,
layout.font.value, -1);
row++;
}
layout.height = 18 * row;
}
private static String durationString(EffectView effect) {
if (effect.isInfinite())
return effect.getInfiniteText();
int totalSeconds = effect.getDurationSeconds();
if (totalSeconds >= 3600) {
return (totalSeconds / 3600) + "h";
} else if (totalSeconds >= 60) {
return String.format("%d:%02d", totalSeconds / 60, totalSeconds % 60);
} else {
return totalSeconds + "s";
}
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,100 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Tps extends Feature implements HudFeature {
private static Property<Boolean> enabled = Property.bool(false);
private static FeatureLayout dimensions = new FeatureLayout(60, Fonts.getHeight());
private static long lastWorldAge = -1;
private static long lastRealTime = -1;
private static double currentTps = 20.0;
private static final int SAMPLE_COUNT = 2;
private static final double[] samples = new double[SAMPLE_COUNT];
static {
java.util.Arrays.fill(samples, 20.0);
}
private static int sampleIndex = 0;
private static int samplesFilled = 0;
public Tps() {
super(new FeatureDetails("TPS Display", "tps")
.description("Displays the TPS")
.version("v0.1.0")
.tags("Utility"),
enabled.named("Enabled"),
dimensions.prop());
}
public static void onTimePacket(long worldAge) {
long now = System.currentTimeMillis();
if (lastWorldAge != -1 && lastRealTime != -1) {
long tickDelta = worldAge - lastWorldAge;
long timeDelta = now - lastRealTime;
if (timeDelta > 0 && tickDelta > 0) {
double sample = (tickDelta / (double) timeDelta) * 1000.0;
sample = Math.min(sample, 20.0);
samples[sampleIndex % SAMPLE_COUNT] = sample;
sampleIndex++;
samplesFilled = Math.min(samplesFilled + 1, SAMPLE_COUNT);
double sum = 0;
for (int i = 0; i < samplesFilled; i++)
sum += samples[i];
currentTps = sum / samplesFilled;
}
}
lastWorldAge = worldAge;
lastRealTime = now;
}
public static void reset() {
lastWorldAge = -1;
lastRealTime = -1;
currentTps = 20.0;
sampleIndex = 0;
samplesFilled = SAMPLE_COUNT;
for (int i = 0; i < SAMPLE_COUNT; i++)
samples[i] = 20.0;
}
@Override
public void renderHud(RenderScope scope) {
String text = String.format("%.1f TPS", currentTps);
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 = "20.0 TPS";
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 FeatureLayout getDimensions() {
return dimensions;
}
}
@@ -0,0 +1,141 @@
package org.saturnclient.feature.features;
import org.saturnclient.config.manager.Property;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.HudFeature;
/**
* TpsFeature estimates the server's ticks-per-second by measuring the
* elapsed real time between consecutive world-time packets.
*
* TPS calculation is event-driven: the networking layer (or mixin)
* calls {@link #onTimePacket(long)} whenever a time-update packet
* arrives. No ModuleProvider is required because there is no
* per-frame polling of the engine.
*/
public class TpsFeature extends Feature implements HudFeature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final FeatureLayout layout = new FeatureLayout(60, Fonts.getHeight());
// ---------------------------------------------------------------
// TPS sampling state (static — shared across all instances)
// ---------------------------------------------------------------
private static final int SAMPLE_COUNT = 2;
private static final double[] samples = new double[SAMPLE_COUNT];
static {
java.util.Arrays.fill(samples, 20.0);
}
private static long lastWorldAge = -1L;
private static long lastRealTime = -1L;
private static double currentTps = 20.0;
private static int sampleIndex = 0;
private static int samplesFilled = SAMPLE_COUNT;
// ---------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------
public TpsFeature() {
super(
new FeatureDetails("TPS Display", "tps")
.description("Displays the server's ticks per second")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
}
// ---------------------------------------------------------------
// Packet hook (called by the networking mixin)
// ---------------------------------------------------------------
/**
* Must be called every time the client receives a world-time packet
* carrying the updated {@code worldAge} value.
*/
public static void onTimePacket(long worldAge) {
long now = System.currentTimeMillis();
if (lastWorldAge != -1L && lastRealTime != -1L) {
long tickDelta = worldAge - lastWorldAge;
long timeDelta = now - lastRealTime;
if (timeDelta > 0 && tickDelta > 0) {
double sample = Math.min((tickDelta / (double) timeDelta) * 1000.0, 20.0);
samples[sampleIndex % SAMPLE_COUNT] = sample;
sampleIndex++;
samplesFilled = Math.min(samplesFilled + 1, SAMPLE_COUNT);
double sum = 0;
for (int i = 0; i < samplesFilled; i++)
sum += samples[i];
currentTps = sum / samplesFilled;
}
}
lastWorldAge = worldAge;
lastRealTime = now;
}
/** Resets TPS history (e.g. on server disconnect). */
public static void reset() {
lastWorldAge = -1L;
lastRealTime = -1L;
currentTps = 20.0;
sampleIndex = 0;
samplesFilled = SAMPLE_COUNT;
java.util.Arrays.fill(samples, 20.0);
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderTps(currentTps, scope);
}
@Override
public void renderDummy(RenderScope scope) {
renderTps(20.0, scope);
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderTps(double tps, RenderScope scope) {
String text = String.format("%.1f TPS", tps);
scope.drawText(text, 0, 0, layout.font.value, layout.fgColor.value);
layout.width = Fonts.getWidth(text, layout.font.value);
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public FeatureLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -1,59 +0,0 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
public class Zoom extends Feature {
private static Property<Boolean> enabled = Property.bool(false);
private static Property<Boolean> toggle = Property.bool(false);
public static Property<Integer> zoomKey = Property.keybinding(GLFWProvider.GLFW_KEY_C);
public static Property<Float> zoomLevel = Property.floatProp(3.0f);
public static boolean isZooming = false;
public Zoom() {
super(
new FeatureDetails("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,78 @@
package org.saturnclient.feature.features;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.config.manager.Property;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureDetails;
/**
* ZoomFeature multiplies the camera's field-of-view to create an
* optical-zoom effect.
*
* The FOV override is applied inside a mixin that polls
* {@link #shouldZoom()} and {@link #getZoomLevel()} each frame.
* No ModuleProvider is needed — there is no per-tick engine query.
*/
public class ZoomFeature extends Feature {
private static final Property<Boolean> enabled = Property.bool(false);
private static final Property<Boolean> toggle = Property.bool(false);
public static final Property<Integer> zoomKey = Property.keybinding(GLFWProvider.GLFW_KEY_C);
public static final Property<Float> zoomLevel = Property.floatProp(3.0f);
/** Whether zoom is currently active (read by the FOV mixin). */
public static boolean isZooming = false;
public ZoomFeature() {
super(
new FeatureDetails("Zoom", "zoom")
.description("Zoom in for a closer view")
.tags("Camera", "Utility")
.version("v0.2.0"),
enabled.named("Enabled"),
toggle.named("Toggle zoom"),
zoomKey.named("Zoom Keybinding"),
zoomLevel.named("Zoom Level"));
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void tick() {
if (toggle.value) {
if (zoomKey.wasKeyPressed()) {
isZooming = !isZooming;
}
} else {
isZooming = zoomKey.isKeyPressed();
}
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
if (!e)
isZooming = false;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
// ---------------------------------------------------------------
// Mixin query API
// ---------------------------------------------------------------
/** Returns {@code true} when the FOV override should be applied. */
public static boolean shouldZoom() {
return enabled.value && isZooming;
}
/** Returns the zoom divisor (e.g. 3.0 → one-third of normal FOV). */
public static float getZoomLevel() {
return zoomLevel.value;
}
}
@@ -1,26 +0,0 @@
package org.saturnclient.feature.interfaces;
import org.saturnclient.common.ref.game.ItemStackRef;
public interface ArmorDisplayInterface {
ItemStackRef getMainHand();
ItemStackRef getHelmet();
ItemStackRef getChestplate();
ItemStackRef getLeggings();
ItemStackRef getBoots();
ItemStackRef getDummyMainHand();
ItemStackRef getDummyHelmet();
ItemStackRef getDummyChestplate();
ItemStackRef getDummyLeggings();
ItemStackRef getDummyBoots();
}
@@ -1,20 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface AutoSprintInterface {
boolean hasPlayer();
boolean hasNetwork();
boolean isForwardPressed();
boolean isBackPressed();
boolean isSneaking();
boolean hasHorizontalCollision();
boolean isUsingItem();
void setSprinting(boolean sprinting);
}
@@ -1,11 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface CoordinatesInterface {
int getPlayerX();
int getPlayerY();
int getPlayerZ();
}
@@ -1,7 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface CrosshairInterface {
boolean isTargetingLivingEntity();
}
@@ -1,7 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface DayCounterInterface {
long getDay();
}
@@ -1,7 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface FpsInterface {
int getFps();
}
@@ -1,11 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface FreelookInterface {
boolean isFirstPerson();
void setFirstPerson();
void setThirdPersonBack();
}
@@ -1,7 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface HealthDisplayInterface {
float getPlayerHealth();
}
@@ -1,13 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface KeystrokesInterface {
boolean isForwardPressed();
boolean isBackPressed();
boolean isLeftPressed();
boolean isRightPressed();
boolean isJumpPressed();
boolean isAttackPressed();
boolean isUsePressed();
}
@@ -1,23 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface NametagsInterface {
public static interface EntityState {
String getCustomName();
float getHealth();
float getMaxHealth();
EntityType getEntityType();
}
public static enum EntityType {
PLAYER,
PASSIVE,
HOSTILE,
OTHER
}
}
@@ -1,8 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface PingInterface {
/** Returns current player ping in milliseconds */
int getPing();
}
@@ -1,20 +0,0 @@
package org.saturnclient.feature.interfaces;
public interface SpeedometerInterface {
/** Returns the current player or vehicle velocity in blocks per tick */
Velocity getVelocity();
/** Returns true if player or vehicle is on ground */
boolean isOnGround();
class Velocity {
public final double x, y, z;
public Velocity(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
}
}
@@ -1,51 +0,0 @@
package org.saturnclient.feature.interfaces;
import java.util.List;
import org.saturnclient.common.ref.asset.SpriteRef;
public interface StatusEffectsInterface {
/** Returns a list of currently active effects */
List<Effect> getActiveEffects();
/** Returns a dummy list of effects for dummy rendering */
List<Effect> getDummyEffects();
/** Represents a single effect for rendering purposes */
class Effect {
private final SpriteRef icon;
private final int durationSeconds;
private final boolean infinite;
private final boolean showIcon;
private final String infiniteText;
public Effect(SpriteRef icon, int durationSeconds, boolean infinite, boolean showIcon, String infiniteText) {
this.icon = icon;
this.durationSeconds = durationSeconds;
this.infinite = infinite;
this.showIcon = showIcon;
this.infiniteText = infiniteText;
}
public SpriteRef getIcon() {
return icon;
}
public int getDurationSeconds() {
return durationSeconds;
}
public boolean isInfinite() {
return infinite;
}
public boolean shouldShowIcon() {
return showIcon;
}
public String getInfiniteText() {
return infiniteText;
}
}
}