Refactored Feature to Mod

This commit is contained in:
2026-03-24 11:48:04 +01:00
parent 174c95240c
commit 3138f7ec62
28 changed files with 196 additions and 196 deletions
@@ -0,0 +1,11 @@
package org.saturnclient.mod;
import org.saturnclient.ui.RenderScope;
public interface HudMod {
public ModLayout getDimensions();
public void renderDummy(RenderScope scope);
public void renderHud(RenderScope scope);
}
@@ -0,0 +1,59 @@
package org.saturnclient.mod;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.ConfigManager;
import org.saturnclient.config.property.NamedProperty;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Textures;
public abstract class Mod {
private final ModSpec details;
private final ConfigManager configManager;
public Mod(ModSpec details, NamedProperty... props) {
this.details = details;
configManager = new ConfigManager(details.name);
for (NamedProperty prop : props) {
configManager.property(prop.name, prop.prop);
}
}
public void render(RenderScope scope) {
}
public void tick() {
}
public abstract boolean isEnabled();
public final void setEnabled(boolean e) {
onEnabled(e);
ModManager.updateEnabledModules();
}
public abstract void onEnabled(boolean e);
public final String getName() {
return details.name;
}
public IdentifierRef getIconTexture() {
return Textures.getModIcon(details.namespace);
}
public ConfigManager getConfig() {
return configManager;
}
public String getDescription() {
return details.description;
}
public String[] getTags() {
return details.tags;
}
public String getVersion() {
return details.version;
}
}
@@ -0,0 +1,58 @@
package org.saturnclient.mod;
import java.util.Map;
import org.saturnclient.config.property.Property;
import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.config.property.ColorProperty;
import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.config.property.IntProperty;
import org.saturnclient.config.property.NamedProperty;
public class ModLayout {
public IntProperty x = Property.integer(0);
public IntProperty y = Property.integer(0);
public FloatProperty scale = Property.floatProp(1.0f);
public ColorProperty bgColor = Property.color(0x00000000);
public ColorProperty fgColor = Property.color(0xFFffffff);
public IntProperty radius = Property.integer(0);
public SelectProperty font = Property.font(0);
public boolean renderBackground = true;
public int width = 0;
public int height = 0;
public NamedProperty prop() {
return Property.namespace(Map.of(
"X", x,
"Y", y,
"Scale", scale,
"Background Color", bgColor,
"Foreground Color", fgColor,
"Corner Radius", radius,
"Font", font)).named("In-Game Display");
}
public ModLayout() {
}
public ModLayout(int width, int height) {
this.width = width;
this.height = height;
}
public ModLayout(int x, int y, int width, int height) {
this.x.value = x;
this.y.value = y;
this.width = width;
this.height = height;
}
public ModLayout(int x, int y, int width, int height, float scale) {
this.x.value = x;
this.y.value = y;
this.scale.value = scale;
this.width = width;
this.height = height;
}
}
@@ -0,0 +1,30 @@
package org.saturnclient.mod;
import java.util.ArrayList;
import java.util.List;
import org.saturnclient.mod.mods.*;
public class ModManager {
public static List<Mod> ENABLED_MODS = new ArrayList<>();
public static Mod[] MODS = {
new ArmorDisplayFeature(), new DayCounterFeature(), new KeystrokesFeature(), new StatusEffectsFeature(),
new AutoSprintFeature(), new FpsFeature(), new NametagsFeature(), new TpsFeature(),
new ClockFeature(), new FreelookFeature(), new NoFogFeature(), new ZoomFeature(),
new CoordinatesFeature(), new FullbrightFeature(), new PingFeature(),
new CrosshairFeature(), new HealthDisplayFeature(), new SpeedometerFeature(),
};
public static void init() {
updateEnabledModules();
}
public static synchronized void updateEnabledModules() {
ENABLED_MODS.clear();
for (Mod m : MODS) {
if (m.isEnabled()) {
ENABLED_MODS.add(m);
}
}
}
}
@@ -0,0 +1,39 @@
package org.saturnclient.mod;
public class ModSpec {
public String name;
public String namespace;
public String description;
public String version;
public String[] tags = {};
public ModSpec(String name, String namespace) {
this.name = name;
this.namespace = namespace;
}
public ModSpec name(String name) {
this.name = name;
return this;
}
public ModSpec namespace(String namespace) {
this.namespace = namespace;
return this;
}
public ModSpec description(String description) {
this.description = description;
return this;
}
public ModSpec version(String version) {
this.version = version;
return this;
}
public ModSpec tags(String... tags) {
this.tags = tags;
return this;
}
}
@@ -0,0 +1,123 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
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 Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final BoolProperty useMainHand = Property.bool(true);
private static final ModLayout layout = new ModLayout(40, 75);
public ArmorDisplayFeature() {
super(
new ModSpec("Armor Display", "armor")
.description("Displays armor durability")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
useMainHand.named("Use Main Hand"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
PlayerModule player = Providers.module.player();
renderArmor(scope,
player.getMainHand(),
player.getHelmet(),
player.getChestplate(),
player.getLeggings(),
player.getBoots());
}
@Override
public void renderDummy(RenderScope scope) {
PlayerModule player = Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,60 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.common.module.NetworkModule;
import org.saturnclient.common.module.PlayerModule;
/**
* AutoSprintFeature automatically enables sprinting whenever the
* player moves forward and no sprint-blocking condition is active.
*/
public class AutoSprintFeature extends Mod {
private static final BoolProperty enabled = Property.bool(false);
public AutoSprintFeature() {
super(
new ModSpec("Auto Sprint", "sprint")
.description("Makes the player always sprint")
.tags("Movement")
.version("v0.2.0"),
enabled.named("Enabled"));
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void tick() {
PlayerModule player = Providers.module.player();
NetworkModule network = Providers.module.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;
}
}
@@ -0,0 +1,94 @@
package org.saturnclient.mod.mods;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
/**
* ClockFeature displays the real-world time as a HUD element.
* No engine access is required; {@link LocalTime} is used directly.
*/
public class ClockFeature extends Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final SelectProperty format = Property.select(0, "24 hour", "12 hour");
private static final BoolProperty showSeconds = Property.bool(false);
private static final ModLayout layout = new ModLayout(60, Fonts.getHeight());
public ClockFeature() {
super(
new ModSpec("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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,94 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* CoordinatesFeature displays the player's current XYZ position as a
* HUD element with multiple configurable display formats.
*/
public class CoordinatesFeature extends Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final SelectProperty displayMethod = Property.select(0,
"Flat",
"Flat annotated",
"Newline",
"Newline annotated");
private static final ModLayout layout = new ModLayout(40, 18);
public CoordinatesFeature() {
super(
new ModSpec("Coordinates", "coords")
.description("Displays your current coordinates")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
displayMethod.named("Display Method"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderCoords(
Providers.module.player().getX(),
Providers.module.player().getY(),
Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,61 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.common.module.RenderModule;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Textures;
/**
* CrosshairFeature renders an alternative crosshair texture when the
* player is aiming at a living entity.
*/
public class CrosshairFeature extends Mod {
public static final BoolProperty enabled = Property.bool(false);
private static final int CROSSHAIR_SIZE = 15;
public CrosshairFeature() {
super(
new ModSpec("Crosshair", "crosshair")
.description("Changes the crosshair when aiming at an entity")
.tags("Visuals", "Utility")
.version("v0.2.0"),
enabled.named("Enabled"));
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void render(RenderScope scope) {
if (!enabled.value || !Providers.module.entity().isTargetingLivingEntity()) {
return;
}
RenderModule render = Providers.module.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;
}
}
@@ -0,0 +1,77 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.module.WorldModule;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* DayCounterFeature shows how many in-game days have elapsed.
* Day count is derived from the world age via {@link WorldModule}.
*/
public class DayCounterFeature extends Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final ModLayout layout = new ModLayout(40, 18);
public DayCounterFeature() {
super(
new ModSpec("Day Counter", "day")
.description("Displays the number of in-game days elapsed")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderDay(Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,75 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.module.RenderModule;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* 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 Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final ModLayout layout = new ModLayout(60, Fonts.getHeight());
public FpsFeature() {
super(
new ModSpec("FPS Display", "fps")
.description("Displays current FPS")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderFps(Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,90 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.KeybindingProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.common.module.RenderModule;
import org.saturnclient.common.provider.GLFWProvider;
/**
* 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 Mod {
private static final BoolProperty enabled = Property.bool(false);
private static final BoolProperty toggle = Property.bool(true);
public static final KeybindingProperty 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;
public FreelookFeature() {
super(
new ModSpec("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"));
}
// ---------------------------------------------------------------
// 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 = Providers.module.render();
wasFirstPerson = render.isFirstPerson();
render.setThirdPersonBack();
isFreeLooking = true;
}
private void stopFreelook() {
if (wasFirstPerson) {
Providers.module.render().setFirstPerson();
}
isFreeLooking = false;
}
}
@@ -0,0 +1,65 @@
package org.saturnclient.mod.mods;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.IntProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
/**
* 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 Mod {
private static final BoolProperty enabled = Property.bool(false);
private static final IntProperty brightness = Property.integer(100);
public FullbrightFeature() {
super(
new ModSpec("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;
}
}
@@ -0,0 +1,91 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* 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 Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final SelectProperty displayMode = Property.select(0, "Health", "Hearts");
private static final SelectProperty decimals = Property.select(0, "0", "1", "2");
private static final ModLayout layout = new ModLayout(40, 18);
public HealthDisplayFeature() {
super(
new ModSpec("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());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
float health = Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,166 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.ColorProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.ui.RenderScope;
/**
* KeystrokesFeature renders the WASD / mouse / space key states as an
* on-screen overlay.
*/
public class KeystrokesFeature extends Mod implements HudMod {
// ---------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------
private static final BoolProperty enabled = Property.bool(false);
private static final BoolProperty showMouse = Property.bool(true);
private static final BoolProperty showSpace = Property.bool(false);
private static final ColorProperty clickBg = Property.color(0xFFCCCCCC);
private static final ColorProperty clickFg = Property.color(0xFFFFFFFF);
private static final ModLayout layout = new ModLayout(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;
public KeystrokesFeature() {
super(
new ModSpec("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"));
layout.renderBackground = false;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public void tick() {
PlayerModule player = Providers.module.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 ModLayout 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;
}
}
@@ -0,0 +1,145 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
/**
* 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 Mod {
// ---------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------
private static final BoolProperty enabled = Property.bool(false);
private static final BoolProperty healthDisplay = Property.bool(false);
private static final BoolProperty players = Property.bool(true);
private static final BoolProperty hostile = Property.bool(false);
private static final BoolProperty passive = Property.bool(false);
private static final BoolProperty heartEmoji = Property.bool(true);
private static final BoolProperty obfuscate = Property.bool(false);
private static final SelectProperty unit = Property.select(0, "Health", "Hearts");
private static final SelectProperty format = Property.select(0, "Value", "Value / Total", "%");
private static final SelectProperty 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 SelectProperty 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 ModSpec("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;
}
}
@@ -0,0 +1,46 @@
package org.saturnclient.mod.mods;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
/**
* 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 Mod {
private static final BoolProperty enabled = Property.bool(false);
public NoFogFeature() {
super(
new ModSpec("No Fog", "nofog")
.description("Stops fog from rendering")
.tags("Camera")
.version("v0.2.0"),
enabled.named("Enabled"));
}
/** Returns {@code true} when fog should be suppressed. */
public static boolean isActive() {
return enabled.value;
}
// ---------------------------------------------------------------
// Feature contract
// ---------------------------------------------------------------
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,74 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* PingFeature displays the current server round-trip time in
* milliseconds as a HUD element.
*/
public class PingFeature extends Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final ModLayout layout = new ModLayout(60, Fonts.getHeight());
public PingFeature() {
super(
new ModSpec("Ping Display", "ping")
.description("Displays ping to the server")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderPing(Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,109 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* SpeedometerFeature displays the player's current movement speed as
* a HUD element. Velocity data comes from {@link PlayerModule}.
*/
public class SpeedometerFeature extends Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final SelectProperty axis = Property.select(1, "Absolute", "Horizontal", "Vertical");
private static final SelectProperty unitText = Property.select(0, "None", "Blocks/s", "blocks/s", "b/s");
private static final BoolProperty showLabel = Property.bool(true);
private static final ModLayout layout = new ModLayout(40, 18);
private double cachedSpeed = 0.0;
public SpeedometerFeature() {
super(
new ModSpec("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());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
PlayerModule player = Providers.module.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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,107 @@
package org.saturnclient.mod.mods;
import java.util.List;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.game.EffectRef;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
import org.saturnclient.ui.RenderScope;
/**
* 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 Mod implements HudMod {
public static final BoolProperty enabled = Property.bool(false);
private static final ModLayout layout = new ModLayout(60, 0);
public StatusEffectsFeature() {
super(
new ModSpec("Status Effects", "effect")
.description("Displays active status effects")
.version("v0.2.0")
.tags("Utility"),
enabled.named("Enabled"),
layout.prop());
}
// ---------------------------------------------------------------
// HudFeature
// ---------------------------------------------------------------
@Override
public void renderHud(RenderScope scope) {
renderEffects(scope, Providers.module.player().getActiveEffects());
}
@Override
public void renderDummy(RenderScope scope) {
renderEffects(scope, Providers.module.player().getDummyEffects());
}
// ---------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------
private void renderEffects(RenderScope scope, List<? extends EffectRef> effects) {
int row = 0;
for (EffectRef 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(EffectRef 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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,142 @@
package org.saturnclient.mod.mods;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModSpec;
/**
* 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 Mod implements HudMod {
private static final BoolProperty enabled = Property.bool(false);
private static final ModLayout layout = new ModLayout(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 ModSpec("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 ModLayout getDimensions() {
return layout;
}
@Override
public boolean isEnabled() {
return enabled.value;
}
@Override
public void onEnabled(boolean e) {
enabled.value = e;
}
}
@@ -0,0 +1,81 @@
package org.saturnclient.mod.mods;
import org.saturnclient.common.provider.GLFWProvider;
import org.saturnclient.config.property.BoolProperty;
import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.config.property.KeybindingProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModSpec;
/**
* 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 Mod {
private static final BoolProperty enabled = Property.bool(false);
private static final BoolProperty toggle = Property.bool(false);
public static final KeybindingProperty zoomKey = Property.keybinding(GLFWProvider.GLFW_KEY_C);
public static final FloatProperty zoomLevel = Property.floatProp(3.0f);
/** Whether zoom is currently active (read by the FOV mixin). */
public static boolean isZooming = false;
public ZoomFeature() {
super(
new ModSpec("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;
}
}