Custom nametags on every version (replace the name in the render state) #33

Merged
selimaj-dev merged 1 commits from nametags-render-state into master 2026-09-27 05:14:44 +00:00
7 changed files with 29 additions and 122 deletions
Showing only changes of commit 22a7152f0d - Show all commits
+2 -2
View File
@@ -36,9 +36,9 @@ import net.minecraft.client.render.RenderSetup;
Stonecutter rewrites inactive code into its own layout when switching versions (a single inactive line becomes `//code`), so write it that way to begin with, or run a switch and a reset before committing. Stonecutter rewrites inactive code into its own layout when switching versions (a single inactive line becomes `//code`), so write it that way to begin with, or run a switch and a reset before committing.
When a newer version leaves something out that an older one implements, keep the older version's code behind a condition and mark the newer branch with a `TODO`, rather than dropping it for every version. `NametagsMixin` and the panorama in `SaturnScreenFabric.render` are examples. Search for `TODO` to find what newer versions still lack (tracked in #30), and remove the condition once a version gap is fixed. When a newer version leaves something out that an older one implements, keep the older version's code behind a condition and mark the newer branch with a `TODO`, rather than dropping it for every version. The panorama in `SaturnScreenFabric.render` is an example. Search for `TODO` to find what newer versions still lack (tracked in #30), and remove the condition once a version gap is fixed.
A whole class that only exists for some versions (such as `NametagsMixin`, which hooks a method that 1.21.9 changed) can keep an empty class body on the other versions. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments. A whole class that only exists for some versions (such as `MatrixStackMixin`, which only makes sense before 1.21.6's GUI rewrite) can keep an empty class body on the other versions. Before splitting a class like that, look for a hook that works on every version: nametags used to hook label rendering, which 1.21.9 changed, and now replace the name while the render state is built (`LivingEntityRendererMixin`), with no version condition at all. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments.
Inactive code sits inside a `/* … */` comment, so it can't contain `/* … */` comments itself: use `//` comments in code that's only active for some versions. For the same reason, don't nest multi-line version blocks; use `//?} else if <condition> {` for more than two version ranges, as in `PlayerEntityRendererMixin` and `SaturnRenderState`. Inactive code sits inside a `/* … */` comment, so it can't contain `/* … */` comments itself: use `//` comments in code that's only active for some versions. For the same reason, don't nest multi-line version blocks; use `//?} else if <condition> {` for more than two version ranges, as in `PlayerEntityRendererMixin` and `SaturnRenderState`.
@@ -12,10 +12,9 @@ import net.minecraft.entity.LivingEntity;
* Fabric implementation of {@link EntityFeature}. * Fabric implementation of {@link EntityFeature}.
* *
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}. * Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* Entity state for nametag rendering is supplied at render time by * Entity state for nametags is built by LivingEntityRendererMixin, which
* {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which * writes health into the render state and then constructs a
* writes into the render state; {@link NametagsMixin} then constructs * {@link RenderStateEntityState} from that snapshot for NametagsMod.
* a {@link RenderStateEntityState} from the already-extracted snapshot.
*/ */
public class EntityFeatureImpl implements EntityFeature { public class EntityFeatureImpl implements EntityFeature {
@@ -43,7 +42,7 @@ public class EntityFeatureImpl implements EntityFeature {
* Minecraft render-state object) as a platform-neutral * Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}. * {@link EntityState} for use by {@link NametagsFeature}.
* *
* This is constructed inside {@link NametagsMixin} where both the * This is constructed in LivingEntityRendererMixin, where both the
* render-state mixin interface and the entity name are available. * render-state mixin interface and the entity name are available.
*/ */
public static final class RenderStateEntityState implements EntityState { public static final class RenderStateEntityState implements EntityState {
@@ -7,8 +7,8 @@ import org.saturnclient.common.feature.EntityFeature;
* {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}. * {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
* *
* Carries the entity's health snapshot and classification into the render * Carries the entity's health snapshot and classification into the render
* pipeline so that {@link NametagsMixin} can read them without touching * render state, so NametagsMod's replacement nametag (applied in
* the live entity again. * LivingEntityRendererMixin) can use them without touching the live entity again.
*/ */
public interface HealthRenderState { public interface HealthRenderState {
@@ -1,101 +0,0 @@
package org.saturnclient.impl.features.mixins.misc;
import net.minecraft.client.render.entity.EntityRenderer;
import org.spongepowered.asm.mixin.Mixin;
//? if <1.21.9 {
/*import net.minecraft.client.render.VertexConsumerProvider;
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.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.render.LivingEntityRenderStateMixin;
import org.saturnclient.mod.mods.NametagsMod;
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 NametagsMod#getNametagString}.
*
* Key changes from the original:
*
* <ul>
* <li>The old {@code NametagsFabric.EntityStateImpl} bridge class is
* replaced by {@link EntityFeatureImpl.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 NametagsMod}; 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 {
// TODO: 1.21.9 moved label rendering to the render queue (renderLabelIfPresent no longer takes a
// VertexConsumerProvider), so custom nametags only work on older versions. On 1.21.9+ this mixin is
// empty but stays registered, so features.mixins.json is the same for every version.
//? if <1.21.9 {
/*// 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 (!NametagsMod.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;
EntityFeatureImpl.RenderStateEntityState entityState = new EntityFeatureImpl.RenderStateEntityState(
customName, hrs);
// Ask the feature for the replacement string
String replacement = NametagsMod.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);
}
}
*///?}
}
@@ -4,7 +4,6 @@ import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature; import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState; import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.misc.NametagsMixin;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
@@ -19,8 +18,8 @@ import org.spongepowered.asm.mixin.Unique;
* *
* The stored values are written by * The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a * {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by * render state is extracted from a live entity, and read there right away for
* {@link NametagsMixin} at render time. * NametagsMod's replacement nametag.
*/ */
@Mixin(LivingEntityRenderState.class) @Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState { public class LivingEntityRenderStateMixin implements HealthRenderState {
@@ -6,24 +6,25 @@ import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.HostileEntity; import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.passive.PassiveEntity; import net.minecraft.entity.passive.PassiveEntity;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.text.Text;
import org.saturnclient.common.feature.EntityFeature; import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.entity.HealthRenderState; import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.mod.mods.NametagsMod;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** /**
* Captures each living entity's health and type into the render state * Captures each living entity's health and type into the render state during
* during {@code updateRenderState}, so that nametag rendering can read * {@code updateRenderState}, and applies {@link NametagsMod}'s replacement nametag to the state's
* them without touching the live entity again. * display name.
* *
* Previously imported {@code NametagsInterface.EntityType}; now uses * Replacing the name here, while the render state is built, works the same on every version. Hooking
* {@link EntityFeature.EntityType} — the canonical enum shared across * label rendering instead doesn't: 1.21.9 moved it to the render command queue. Player role icons are
* the whole module system. * added after this, in PlayerEntityRendererMixin.
*
* The injection target and strategy are unchanged from the original.
*/ */
@Mixin(LivingEntityRenderer.class) @Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> { public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
@@ -47,5 +48,15 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
type = EntityFeature.EntityType.OTHER; type = EntityFeature.EntityType.OTHER;
hrs.saturn$setEntityType(type); hrs.saturn$setEntityType(type);
// Only entities that already show a label get a replacement one, as when this hooked label
// rendering.
if (NametagsMod.shouldReplaceName() && state.displayName != null) {
String replacement = NametagsMod.getNametagString(
new EntityFeatureImpl.RenderStateEntityState(state.displayName.getString(), hrs));
if (replacement != null) {
state.displayName = Text.literal(replacement);
}
}
} }
} }
-1
View File
@@ -9,7 +9,6 @@
"render.LivingEntityRendererMixin", "render.LivingEntityRendererMixin",
"render.LivingEntityRenderStateMixin", "render.LivingEntityRenderStateMixin",
"render.BackgroundRendererMixin", "render.BackgroundRendererMixin",
"misc.NametagsMixin",
"render.RenderMixin", "render.RenderMixin",
"world.TpsMixin", "world.TpsMixin",
"entity.CameraMixin", "entity.CameraMixin",