fix(render): чиним EndCrystal под новый MC 26.2 API, AutoLeave race и InvisESP alpha

This commit is contained in:
loki5512344 2026-09-18 18:05:18 +02:00
parent 25060c9fa9
commit 8181f11c5a
9 changed files with 160 additions and 89 deletions

View file

@ -110,7 +110,12 @@ public class AutoLeave extends Module {
private void leave(Minecraft mc) { private void leave(Minecraft mc) {
policy.reset(); policy.reset();
if (mc.getConnection() == null) return; // already disconnected/disconnecting elsewhere
try {
mc.disconnect(null, false); mc.disconnect(null, false);
} catch (IllegalStateException e) {
// A disconnect (e.g. server-initiated) was already in progress this same tick; nothing left to do.
}
} }
/** /**

View file

@ -181,6 +181,42 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
ViewObstructionFadeContext.push(state instanceof ViewObstructionFadeState fadeState ? fadeState : null); ViewObstructionFadeContext.push(state instanceof ViewObstructionFadeState fadeState ? fadeState : null);
} }
/**
* InvisESP alpha is applied only around the body's own submitModel call
* (not around the whole submit()), so gear layers (armor, held items,
* elytra) rendered afterwards stay fully opaque, matching vanilla
* behaviour for invisible players.
*/
@com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation(
method = "submit",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/SubmitNodeCollector;submitModel(Lnet/minecraft/client/model/Model;Ljava/lang/Object;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/rendertype/RenderType;IIILnet/minecraft/client/renderer/texture/TextureAtlasSprite;ILnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)V")
)
private void lovisual$fadeBodyOnlyForInvisReveal(
net.minecraft.client.renderer.SubmitNodeCollector collector,
net.minecraft.client.model.Model model,
Object state2,
com.mojang.blaze3d.vertex.PoseStack poseStack,
RenderType renderType,
int light,
int overlay,
int color,
net.minecraft.client.renderer.texture.TextureAtlasSprite sprite,
int outlineColor,
net.minecraft.client.renderer.feature.ModelFeatureRenderer.CrumblingOverlay crumbling,
com.llamalad7.mixinextras.injector.wrapoperation.Operation<Void> original) {
if (!(state2 instanceof LivingEntityRenderState livingState) || !SeeInvisiblesReveal.isRevealedTranslucent(livingState)) {
original.call(collector, model, state2, poseStack, renderType, light, overlay, color, sprite, outlineColor, crumbling);
return;
}
float priorAlpha = ViewObstructionFadeContext.isActive() ? ViewObstructionFadeContext.alpha() : 1.0f;
ViewObstructionFadeContext.push(true, priorAlpha * SeeInvisiblesReveal.revealAlpha());
try {
original.call(collector, model, state2, poseStack, renderType, light, overlay, color, sprite, outlineColor, crumbling);
} finally {
ViewObstructionFadeContext.pop();
}
}
@Inject(method = "submit", at = @At("RETURN")) @Inject(method = "submit", at = @At("RETURN"))
private void lovisual$popViewObstructionFadeContext(S state, com.mojang.blaze3d.vertex.PoseStack matrixStack, net.minecraft.client.renderer.SubmitNodeCollector orderedRenderCommandQueue, net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState, CallbackInfo ci) { private void lovisual$popViewObstructionFadeContext(S state, com.mojang.blaze3d.vertex.PoseStack matrixStack, net.minecraft.client.renderer.SubmitNodeCollector orderedRenderCommandQueue, net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState, CallbackInfo ci) {
ViewObstructionFadeContext.pop(); ViewObstructionFadeContext.pop();

View file

@ -0,0 +1,40 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins.render.entity;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal;
import net.minecraft.client.model.object.crystal.EndCrystalModel;
import net.minecraft.client.renderer.entity.state.EndCrystalRenderState;
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;
/**
* Скрытие частей модели энд-кристалла по настройкам {@link EndCrystal}.
* {@code outerGlass}, {@code innerGlass} и {@code cube} — вложенные друг в
* друга {@link net.minecraft.client.model.geom.ModelPart}, поэтому вместо
* {@code visible} (которое обрезало бы и дочерние части) используется
* {@code skipDraw} — он прячет только собственную геометрию части, не
* затрагивая рендер детей.
*/
@Mixin(EndCrystalModel.class)
public abstract class EndCrystalModelMixin {
@Inject(method = "setupAnim", at = @At("TAIL"))
private void lovisual$applyPartVisibility(EndCrystalRenderState state, CallbackInfo ci) {
EndCrystalModel self = (EndCrystalModel) (Object) this;
EndCrystal module = Modules.get(EndCrystal.class);
if (module == null || !module.isEnabled()) return;
self.outerGlass.skipDraw = !module.outerGlassVisible.get();
self.innerGlass.skipDraw = !module.innerGlassVisible.get();
self.cube.skipDraw = !module.cubeVisible.get();
}
}

View file

@ -0,0 +1,50 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins.render.entity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal;
import net.minecraft.client.model.Model;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.object.crystal.EndCrystalModel;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Тонировка модели энд-кристалла по настройкам {@link EndCrystal}.
* {@code Model#renderToBuffer} — единственная точка, где весь меш (все части
* разом) отправляется на отрисовку с одним общим ARGB-цветом, поэтому тут же
* подменяется итоговый {@code color} для {@link EndCrystalModel}, если у
* модуля выбран Custom-режим окраски.
*/
@Mixin(Model.class)
public abstract class EndCrystalModelTintMixin {
@WrapOperation(
method = "renderToBuffer",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/geom/ModelPart;render(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;III)V")
)
private void lovisual$tintEndCrystal(ModelPart root, PoseStack poseStack, VertexConsumer buffer, int light,
int overlay, int color, Operation<Void> original) {
if ((Object) this instanceof EndCrystalModel) {
EndCrystal module = Modules.get(EndCrystal.class);
if (module != null && module.isEnabled()) {
int tint = module.resolveTintArgb();
if (tint != -1) {
original.call(root, poseStack, buffer, light, overlay, tint);
return;
}
}
}
original.call(root, poseStack, buffer, light, overlay, color);
}
}

View file

@ -1,81 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins.render.entity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.renderer.entity.EndCrystalRenderer;
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;
/**
* Тонировка стекла/куба энд-кристалла и скрытие отдельных частей модели по
* настройкам {@link EndCrystal}. Ванильный {@code render(...)} зовёт
* {@code this.glass.render(...)} дважды подряд (внешний куб, затем повёрнутый
* вложенный) и {@code this.cube.render(...)} один раз — различаем эти три
* вызова счётчиком {@link #lovisual$glassCallOrdinal}, сбрасываемым в начале
* каждого рендера кристалла.
*/
@Mixin(EndCrystalRenderer.class)
public abstract class EndCrystalRendererMixin {
@Shadow
private ModelPart glass;
@Shadow
private ModelPart cube;
@Unique
private int lovisual$glassCallOrdinal;
@Inject(
method = "render(Lnet/minecraft/world/entity/boss/enderdragon/EndCrystal;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At("HEAD")
)
private void lovisual$resetOrdinal(Object entity, float entityYaw, float partialTicks, PoseStack poseStack,
Object buffer, int packedLight, CallbackInfo ci) {
lovisual$glassCallOrdinal = 0;
}
@WrapOperation(
method = "render(Lnet/minecraft/world/entity/boss/enderdragon/EndCrystal;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/geom/ModelPart;render(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;II)V")
)
private void lovisual$tintOrHideModelPart(ModelPart instance, PoseStack poseStack, VertexConsumer vertexConsumer,
int light, int overlay, Operation<Void> original) {
EndCrystal module = Modules.get(EndCrystal.class);
if (module == null || !module.isEnabled()) {
original.call(instance, poseStack, vertexConsumer, light, overlay);
return;
}
boolean visible;
if (instance == this.cube) {
visible = module.cubeVisible.get();
} else {
lovisual$glassCallOrdinal++;
visible = lovisual$glassCallOrdinal == 1 ? module.outerGlassVisible.get() : module.innerGlassVisible.get();
}
if (!visible) return;
int tint = module.resolveTintArgb();
if (tint == -1) {
original.call(instance, poseStack, vertexConsumer, light, overlay);
} else {
instance.render(poseStack, vertexConsumer, light, overlay, tint);
}
}
}

View file

@ -11,42 +11,44 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
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.Coerce;
/** /**
* Vanilla bug: FireworkParticles$Starter.createParticle assumes ParticleEngine.createParticle never returns null. * Vanilla bug: FireworkParticles$Starter.createParticle assumes ParticleEngine.createParticle never returns null.
* Our ParticleManagerMixin returns null when a ParticleSpawnEvent is cancelled (e.g. FpsOptimizer.disable_particles). * Our ParticleManagerMixin returns null when a ParticleSpawnEvent is cancelled (e.g. FpsOptimizer.disable_particles).
* This causes NPE: sparkParticle is null -> setTrail() crashes. * This causes NPE: sparkParticle is null -> setTrail() crashes.
* Fix: guard every SparkParticle setter to be null-safe. Use Object for private inner type. * Fix: guard every SparkParticle setter to be null-safe. SparkParticle is a private inner class, so we use Object
* with @Coerce to avoid InvalidInjectionException (expected SparkParticle vs Object).
*/ */
@Mixin(targets = "net.minecraft.client.particle.FireworkParticles$Starter") @Mixin(targets = "net.minecraft.client.particle.FireworkParticles$Starter")
public abstract class FireworkParticlesStarterMixin { public abstract class FireworkParticlesStarterMixin {
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setTrail(Z)V")) @WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setTrail(Z)V"))
private void lovisual$guardSetTrail(Object instance, boolean trail, Operation<Void> original) { private void lovisual$guardSetTrail(@Coerce Object instance, boolean trail, Operation<Void> original) {
if (instance == null) return; if (instance == null) return;
original.call(instance, trail); original.call(instance, trail);
} }
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setTwinkle(Z)V")) @WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setTwinkle(Z)V"))
private void lovisual$guardSetTwinkle(Object instance, boolean twinkle, Operation<Void> original) { private void lovisual$guardSetTwinkle(@Coerce Object instance, boolean twinkle, Operation<Void> original) {
if (instance == null) return; if (instance == null) return;
original.call(instance, twinkle); original.call(instance, twinkle);
} }
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setAlpha(F)V")) @WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setAlpha(F)V"))
private void lovisual$guardSetAlpha(Object instance, float alpha, Operation<Void> original) { private void lovisual$guardSetAlpha(@Coerce Object instance, float alpha, Operation<Void> original) {
if (instance == null) return; if (instance == null) return;
original.call(instance, alpha); original.call(instance, alpha);
} }
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setColor(I)V")) @WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setColor(I)V"))
private void lovisual$guardSetColor(Object instance, int color, Operation<Void> original) { private void lovisual$guardSetColor(@Coerce Object instance, int color, Operation<Void> original) {
if (instance == null) return; if (instance == null) return;
original.call(instance, color); original.call(instance, color);
} }
@WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setFadeColor(I)V")) @WrapOperation(method = "createParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/FireworkParticles$SparkParticle;setFadeColor(I)V"))
private void lovisual$guardSetFadeColor(Object instance, int fadeColor, Operation<Void> original) { private void lovisual$guardSetFadeColor(@Coerce Object instance, int fadeColor, Operation<Void> original) {
if (instance == null) return; if (instance == null) return;
original.call(instance, fadeColor); original.call(instance, fadeColor);
} }

View file

@ -25,6 +25,11 @@ public enum ViewObstructionFadeContext {
)); ));
} }
/** Pushes an explicit fade state, used when combining multiple independent fade sources. */
public static void push(boolean active, float alpha) {
STACK.get().addLast(active ? new State(true, alpha) : State.INACTIVE);
}
public static void pop() { public static void pop() {
ArrayDeque<State> stack = STACK.get(); ArrayDeque<State> stack = STACK.get();
if (!stack.isEmpty()) { if (!stack.isEmpty()) {

View file

@ -39,6 +39,19 @@ public enum SeeInvisiblesReveal {
state.isInvisible = false; state.isInvisible = false;
} }
/** @return true if the entity was revealed this frame and should render below full opacity. */
public static boolean isRevealedTranslucent(LivingEntityRenderState state) {
InvisEsp module = Modules.get(InvisEsp.class);
return module != null && module.isEnabled() && module.isTranslucentReveal()
&& state instanceof SeeInvisiblesState seeInvis && seeInvis.lovisual$isSeeInvisRevealed();
}
/** @return the configured InvisESP reveal opacity, or fully opaque if the module is missing. */
public static float revealAlpha() {
InvisEsp module = Modules.get(InvisEsp.class);
return module == null ? 1.0f : module.getOpacity();
}
public static RenderType renderTypeFor(RenderType original, Identifier texture) { public static RenderType renderTypeFor(RenderType original, Identifier texture) {
InvisEsp module = Modules.get(InvisEsp.class); InvisEsp module = Modules.get(InvisEsp.class);
if (module == null || !module.isEnabled() || IrisRuntime.isRenderingShadowPass()) { if (module == null || !module.isEnabled() || IrisRuntime.isRenderingShadowPass()) {

View file

@ -140,7 +140,8 @@
"network.game.PlayerInfoMixin", "network.game.PlayerInfoMixin",
"network.packet.ClientCommonPacketListenerImplMixin", "network.packet.ClientCommonPacketListenerImplMixin",
"network.packet.ServerboundUseItemPacketMixin", "network.packet.ServerboundUseItemPacketMixin",
"render.entity.EndCrystalRendererMixin", "render.entity.EndCrystalModelMixin",
"render.entity.EndCrystalModelTintMixin",
"render.entity.EntityRenderDispatcherMixin", "render.entity.EntityRenderDispatcherMixin",
"render.entity.ItemEntityRenderStateMixin", "render.entity.ItemEntityRenderStateMixin",
"render.entity.ItemEntityRendererMixin", "render.entity.ItemEntityRendererMixin",