This commit is contained in:
2026-03-08 16:00:36 +01:00
parent ceeb64282f
commit 301b75c003
19 changed files with 340 additions and 397 deletions
@@ -7,7 +7,7 @@ import net.minecraft.util.hit.HitResult;
import net.minecraft.entity.LivingEntity;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.modules.HealthRenderState;
/**
* Fabric implementation of {@link EntityModule}.
@@ -0,0 +1,20 @@
package org.saturnclient.impl.modules;
import org.saturnclient.common.module.EntityModule;
/**
* Injected interface on {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
*
* Carries the entity's health snapshot and classification into the render
* pipeline so that {@link NametagsMixin} can read them without touching
* the live entity again.
*/
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityModule.EntityType saturn$getEntityType();
void saturn$setEntityType(EntityModule.EntityType type);
}
@@ -0,0 +1,37 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.GameRenderer;
import org.saturnclient.feature.features.ZoomFeature;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Overrides the game's field-of-view when {@link ZoomFeature} is active.
*
* The old code checked both {@code Zoom.isZooming} and
* {@code Zoom.shouldZoom()} separately. The refactored
* {@link ZoomFeature#shouldZoom()} already ORs both conditions
* ({@code enabled.value && isZooming}), so only one check is needed.
*
* No {@link org.saturnclient.common.module.ModuleProvider} is required
* here because zooming is a pure render-time override with no per-tick
* engine queries.
*/
@Mixin(GameRenderer.class)
public class GameRendererMixin {
/**
* After the vanilla FOV has been computed, divide it by the zoom
* level to produce the narrowed field-of-view.
*/
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (ZoomFeature.shouldZoom()) {
float zoomedFov = cir.getReturnValue() / ZoomFeature.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -1,5 +1,6 @@
// code modified from Gamma utils: https://github.com/Sjouwer/gamma-utils
package org.saturnclient.modules.mixins;
package org.saturnclient.impl.modules.mixins;
// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager;
@@ -8,9 +9,22 @@ import org.saturnclient.feature.features.FullbrightFeature;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Overrides the lightmap's gamma computation when
* {@link FullbrightFeature} is active, replacing the vanilla
* brightness with a user-configured value.
*
* The old import was {@code org.saturnclient.feature.features.Fullbright};
* it is now {@link FullbrightFeature}. The injection target and
* strategy are unchanged.
*/
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
/**
* If fullbright is active, substitute the feature's brightness
* value for whatever vanilla calculated.
*/
@ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
private float modifyGamma(float original) {
if (FullbrightFeature.shouldOverrideBrightness()) {
@@ -0,0 +1,59 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.impl.modules.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Implements {@link HealthRenderState} on
* {@link LivingEntityRenderState}, injecting health and entity-type
* storage into every living entity's render-state snapshot.
*
* Previously imported {@code NametagsInterface.EntityType}; the enum
* now lives in {@link EntityModule.EntityType} which is the single
* canonical definition shared by the mixin layer and the feature layer.
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by
* {@link NametagsMixin} at render time.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@Unique
private float saturn$health = 0f;
@Unique
private float saturn$maxHealth = 1f;
@Unique
private EntityModule.EntityType saturn$entityType = EntityModule.EntityType.OTHER;
@Override
public float saturn$getHealth() {
return saturn$health;
}
@Override
public float saturn$getMaxHealth() {
return saturn$maxHealth;
}
@Override
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
@Override
public EntityModule.EntityType saturn$getEntityType() {
return saturn$entityType;
}
@Override
public void saturn$setEntityType(EntityModule.EntityType type) {
saturn$entityType = type;
}
}
@@ -1,4 +1,4 @@
package org.saturnclient.modules.mixins;
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
@@ -8,15 +8,22 @@ import net.minecraft.entity.passive.PassiveEntity;
import net.minecraft.entity.player.PlayerEntity;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.modules.HealthRenderState;
import org.saturnclient.impl.modules.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Captures health and entity-type into the render state each frame so
* that the nametag replacement logic never needs to touch the live entity.
* Captures each living entity's health and type into the render state
* during {@code updateRenderState}, so that nametag rendering can read
* them without touching the live entity again.
*
* Previously imported {@code NametagsInterface.EntityType}; now uses
* {@link EntityModule.EntityType} — the canonical enum shared across
* the whole module system.
*
* The injection target and strategy are unchanged from the original.
*/
@Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
@@ -28,6 +35,7 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
hrs.saturn$setHealth(entity.getHealth(), entity.getMaxHealth());
// Classify entity type for the nametag feature's filtering logic
EntityModule.EntityType type;
if (entity instanceof PlayerEntity)
type = EntityModule.EntityType.PLAYER;
@@ -0,0 +1,94 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
import org.saturnclient.feature.features.NametagsFeature;
import org.saturnclient.impl.modules.EntityModuleFabric;
import org.saturnclient.impl.modules.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Intercepts nametag rendering and replaces the displayed text with
* the string produced by {@link NametagsFeature#getNametagString}.
*
* Key changes from the original:
*
* <ul>
* <li>The old {@code NametagsFabric.EntityStateImpl} bridge class is
* replaced by {@link EntityModuleFabric.RenderStateEntityState},
* which wraps the {@link HealthRenderState} already injected into
* the render state by {@link LivingEntityRenderStateMixin}.</li>
* <li>The feature import changes from {@code Nametags} to
* {@link NametagsFeature}; the two static methods
* ({@code shouldReplaceName} / {@code getNametagString}) are
* unchanged in signature.</li>
* <li>The entity's display name is extracted from the render state's
* {@code nameVisible} / text fields where available, falling back
* to the plain text string passed to this method.</li>
* </ul>
*
* The re-entrancy guard ({@code saturn$rendering}) is kept intact to
* prevent infinite recursion when we call {@code renderLabelIfPresent}
* with the replacement text.
*/
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
/** Guards against the recursive call we make with the replacement text. */
@Unique
private static final ThreadLocal<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
@Shadow
protected abstract <S extends EntityRenderState> void renderLabelIfPresent(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light);
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
private <S extends EntityRenderState> void saturn$replaceNametag(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light, CallbackInfo ci) {
// Skip our own recursive call
if (saturn$rendering.get())
return;
if (!NametagsFeature.shouldReplaceName())
return;
// Only living entities carry health data
if (!(state instanceof LivingEntityRenderState living))
return;
if (!(living instanceof HealthRenderState hrs))
return;
// Build the platform-neutral EntityState from the render-state snapshot
// (health/type were written by LivingEntityRendererMixin at extract time).
String customName = text != null ? text.getString() : null;
EntityModuleFabric.RenderStateEntityState entityState = new EntityModuleFabric.RenderStateEntityState(
customName, hrs);
// Ask the feature for the replacement string
String replacement = NametagsFeature.getNametagString(entityState);
if (replacement == null)
return; // feature decided not to replace this entity
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
}
@@ -0,0 +1,85 @@
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
import org.saturnclient.feature.Feature;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.FeatureManager;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.RenderScopeImpl;
import org.saturnclient.ui.SaturnScreenFabric;
import org.saturnclient.ui.screens.HudEditor;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Drives per-frame rendering for all registered features.
*
* This mixin is intentionally kept thin: it creates a {@link RenderScope},
* iterates the enabled feature list from {@link FeatureManager}, and
* dispatches either {@link HudFeature#renderHud} (for HUD elements) or
* {@link Feature#render} (for world-overlay features like Crosshair).
*
* No feature-specific logic lives here — all decisions about what to
* draw are made inside each feature class.
*
* Changes from the original:
* - Package imports updated to the refactored feature names (no
* more {@code feature.features.*}).
* - The HUD-editor screen check still uses the existing
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
* unchanged infrastructure.
*/
@Mixin(InGameHud.class)
public class RenderMixin {
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
// Skip rendering while the HUD editor is open so the editor
// can draw its own preview layout without double-drawing.
if (textRenderer == null)
return;
if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f
&& f.screen instanceof HudEditor)
return;
RenderScope scope = new RenderScopeImpl(
context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (Feature feature : FeatureManager.ENABLED_MODS) {
if (!feature.isEnabled())
continue;
if (feature instanceof HudFeature hud) {
FeatureLayout dim = hud.getDimensions();
scope.getMatrixStack().push();
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value, 0f);
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
scope.drawRoundedRectangle(0, 0, dim.width, dim.height,
dim.radius.value, dim.bgColor.value);
}
hud.renderHud(scope);
scope.getMatrixStack().pop();
} else {
// Non-HUD features (e.g. CrosshairFeature) use the plain render hook.
feature.render(scope);
}
}
}
}
@@ -1,4 +1,4 @@
package org.saturnclient.modules.mixins;
package org.saturnclient.impl.modules.mixins;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
@@ -9,6 +9,17 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Feeds world-time packets into {@link TpsFeature}'s TPS estimator.
*
* This is the only place in the entire codebase that reads a network
* packet and passes data to a feature. The feature's
* {@link TpsFeature#onTimePacket(long)} method handles all averaging
* logic; the mixin is intentionally reduced to a single forwarding call.
*
* The only change from the original is renaming the import from
* {@code feature.features.Tps} to {@link TpsFeature}.
*/
@Mixin(ClientPlayNetworkHandler.class)
public class TpsMixin {
@@ -1,9 +0,0 @@
package org.saturnclient.modules;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -1,22 +0,0 @@
package org.saturnclient.modules;
import org.saturnclient.common.module.EntityModule;
/**
* Mixed into {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}
* by {@link org.saturnclient.modules.mixins.LivingEntityRenderStateMixin}.
*
* Carries the health snapshot and entity classification that
* {@link org.saturnclient.modules.mixins.NametagsMixin} needs without
* accessing the live entity during rendering.
*/
public interface HealthRenderState {
float saturn$getHealth();
float saturn$getMaxHealth();
void saturn$setHealth(float health, float maxHealth);
EntityModule.EntityType saturn$getEntityType();
void saturn$setEntityType(EntityModule.EntityType type);
}
@@ -1,30 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.render.BackgroundRenderer;
import net.minecraft.entity.Entity;
import org.saturnclient.feature.features.NoFogFeature;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(BackgroundRenderer.class)
public class BackgroundRendererMixin {
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFogFeature.isActive()) {
return Float.MAX_VALUE;
}
return viewDistance;
}
@Redirect(method = "applyFog", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isSpectator()Z"))
private static boolean redirectIsSpectator(Entity entity) {
if (NoFogFeature.isActive() && NoFogFeature.liquids()) {
return true; // pretend to be spectator to bypass liquid fog
}
return entity.isSpectator();
}
}
@@ -1,48 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
import org.saturnclient.feature.features.FreelookFeature;
import org.saturnclient.modules.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
private boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockView focusedBlock, Entity cameraEntity,
boolean isThirdPerson, boolean isFrontFacing,
float tickDelta, CallbackInfo ci) {
if (FreelookFeature.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity overridden = (CameraOverriddenEntity) cameraEntity;
if (firstTime && MinecraftClient.getInstance().player != null) {
overridden.freelook$setCameraPitch(MinecraftClient.getInstance().player.getPitch());
overridden.freelook$setCameraYaw(MinecraftClient.getInstance().player.getYaw());
firstTime = false;
}
this.setRotation(overridden.freelook$getCameraYaw(), overridden.freelook$getCameraPitch());
}
if (!FreelookFeature.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -1,58 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
import org.saturnclient.feature.features.FreelookFeature;
import org.saturnclient.modules.CameraOverriddenEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
// noinspection ConstantValue — reachable at runtime despite IDE warning
if (FreelookFeature.isFreeLooking && (Object) this instanceof ClientPlayerEntity) {
this.cameraPitch = MathHelper.clamp(
this.cameraPitch + (float) (yDelta * 0.15),
-90.0f, 90.0f);
this.cameraYaw += (float) (xDelta * 0.15);
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float p) {
cameraPitch = p;
}
@Override
@Unique
public void freelook$setCameraYaw(float y) {
cameraYaw = y;
}
}
@@ -1,20 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.render.GameRenderer;
import org.saturnclient.feature.features.ZoomFeature;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(GameRenderer.class)
public class GameRendererMixin {
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
private void onGetFov(CallbackInfoReturnable<Float> cir) {
if (ZoomFeature.shouldZoom()) {
cir.setReturnValue(cir.getReturnValue() / ZoomFeature.getZoomLevel());
}
}
}
@@ -1,34 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.modules.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Adds health / entity-type fields to {@link LivingEntityRenderState} so
* that {@link NametagsMixin} can read them without touching the live entity.
*
* The fields are populated each frame by {@link LivingEntityRendererMixin}.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@Unique private float saturn$health = 0f;
@Unique private float saturn$maxHealth = 1f;
@Unique private EntityModule.EntityType saturn$entityType = EntityModule.EntityType.OTHER;
@Override public float saturn$getHealth() { return saturn$health; }
@Override public float saturn$getMaxHealth() { return saturn$maxHealth; }
@Override
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
@Override public EntityModule.EntityType saturn$getEntityType() { return saturn$entityType; }
@Override public void saturn$setEntityType(EntityModule.EntityType type) { saturn$entityType = type; }
}
@@ -1,104 +0,0 @@
package org.saturnclient.modules.mixins;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.feature.features.NametagsFeature;
import org.saturnclient.modules.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
/**
* Guards against infinite recursion: our inject calls
* {@code renderLabelIfPresent} again with the replacement text, so we
* use a thread-local flag to skip re-entry on that second call.
*/
@Unique
private static final ThreadLocal<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
@Shadow
protected abstract <S extends EntityRenderState> void renderLabelIfPresent(
S state, Text text, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light);
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
private <S extends EntityRenderState> void saturn$replaceNametag(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light, CallbackInfo ci) {
if (saturn$rendering.get())
return;
if (!NametagsFeature.shouldReplaceName())
return;
if (!(state instanceof LivingEntityRenderState liv))
return;
String replacement = NametagsFeature.getNametagString(
new RenderStateEntityState(liv));
if (replacement == null)
return;
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement),
matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
// ------------------------------------------------------------------
// Inner adapter — bridges LivingEntityRenderState → EntityModule.EntityState
// Kept here so the mixin package is self-contained; no separate Fabric
// impl class is needed for this thin adapter.
// ------------------------------------------------------------------
@Unique
private static final class RenderStateEntityState implements EntityModule.EntityState {
private final LivingEntityRenderState state;
private final HealthRenderState hrs;
RenderStateEntityState(LivingEntityRenderState state) {
this.state = state;
// LivingEntityRenderStateMixin implements HealthRenderState
this.hrs = (state instanceof HealthRenderState h) ? h : null;
}
@Override
public String getCustomName() {
// The vanilla render state stores the display name as a Text;
// we expose it as a plain string for feature logic.
return state.customName != null ? state.customName.getString() : null;
}
@Override
public float getHealth() {
return hrs != null ? hrs.saturn$getHealth() : 0f;
}
@Override
public float getMaxHealth() {
return hrs != null ? hrs.saturn$getMaxHealth() : 1f;
}
@Override
public EntityModule.EntityType getEntityType() {
return hrs != null ? hrs.saturn$getEntityType() : EntityModule.EntityType.OTHER;
}
}
}
@@ -1,57 +0,0 @@
package org.saturnclient.modules.mixins;
import org.saturnclient.feature.FeatureLayout;
import org.saturnclient.feature.FeatureManager;
import org.saturnclient.feature.HudFeature;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.RenderScopeImpl;
import org.saturnclient.ui.SaturnScreenFabric;
import org.saturnclient.ui.screens.HudEditor;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
@Mixin(InGameHud.class)
public class RenderMixin {
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
if (textRenderer != null &&
!(SaturnClient.client.currentScreen instanceof SaturnScreenFabric f && f.screen instanceof HudEditor)) {
RenderScope renderScope = new RenderScopeImpl(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (org.saturnclient.feature.Feature m : FeatureManager.ENABLED_MODS) {
if (m instanceof HudFeature && m.isEnabled()) {
FeatureLayout dim = ((HudFeature) m).getDimensions();
renderScope.getMatrixStack().push();
renderScope.getMatrixStack().translate(dim.x.value, (float) dim.y.value, 0);
renderScope.getMatrixStack().scale(dim.scale.value, dim.scale.value, 1.0f);
if (dim.renderBackground) {
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value,
dim.bgColor.value);
}
((HudFeature) m).renderHud(renderScope);
renderScope.getMatrixStack().pop();
} else {
m.render(renderScope);
}
}
}
}
}
@@ -4,15 +4,12 @@
"package": "org.saturnclient.modules.mixins",
"compatibilityLevel": "JAVA_21",
"client": [
"CameraMixin",
"EntityMixin",
"RenderMixin",
"LightmapTextureManagerMixin",
"BackgroundRendererMixin",
"GameRendererMixin",
"NametagsMixin",
"LivingEntityRenderStateMixin",
"LightmapTextureManagerMixin",
"LivingEntityRendererMixin",
"LivingEntityRenderStateMixin",
"NametagsMixin",
"RenderMixin",
"TpsMixin"
],
"injectors": {