This commit is contained in:
loki5512344 2026-07-30 11:12:56 +02:00
commit 8084ac50b3
1629 changed files with 238620 additions and 0 deletions

View file

@ -0,0 +1,123 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
import dev.loki.lovisual.features.module.modules.visuals.FovControl;
import dev.loki.lovisual.util.pvp.CooldownRegistry;
import dev.loki.lovisual.util.pvp.PvpTargetState;
import dev.loki.lovisual.util.pvp.opponents.OpponentCooldownManager;
@Mixin(AbstractClientPlayer.class)
public class AbstractClientPlayerMixin {
@Unique
private boolean combatant$wasUsing = false;
@Inject(method = "getFieldOfViewModifier", at = @At("HEAD"), cancellable = true)
private void combatant$overrideFovMultiplier(boolean changingFov, float fovEffectScale,
CallbackInfoReturnable<Float> cir) {
FovControl module = Modules.get(FovControl.class);
if (module == null || !module.isEnabled()) return;
AbstractClientPlayer player = (AbstractClientPlayer) (Object) this;
float f = 1.0f;
if (player.getAbilities().flying && !module.disableFlyingFov()) {
f *= 1.1f;
}
boolean skipSpeed = module.disableSprintFov() && player.isSprinting();
if (module.disableSpeedFov() && player.hasEffect(MobEffects.SPEED)) skipSpeed = true;
if (module.disableSlownessFov() && player.hasEffect(MobEffects.SLOWNESS)) skipSpeed = true;
if (!skipSpeed) {
float walkSpeed = player.getAbilities().getWalkingSpeed();
if (walkSpeed != 0.0f) {
float attr = (float) player.getAttributeValue(Attributes.MOVEMENT_SPEED);
float speedFactor = attr / walkSpeed;
f *= (speedFactor + 1.0f) / 2.0f;
}
}
if (player.isUsingItem()) {
if (player.getUseItem().is(Items.BOW)) {
if (!module.disableBowFov()) {
float use = player.getTicksUsingItem() / 20.0f;
use = Math.min(use, 1.0f);
f *= 1.0f - (Mth.sqrt(use) * 0.15f);
}
} else if (changingFov && player.isScoping()) {
if (!module.disableSpyglassFov()) {
cir.setReturnValue(0.1f);
return;
}
}
}
float mult = Mth.lerp(fovEffectScale, 1.0f, f);
float expand = Math.max(0.0f, module.getExpandScale());
float shrink = Math.max(0.0f, module.getShrinkScale());
if (mult > 1.0f) {
mult = 1.0f + (mult - 1.0f) * expand;
} else if (mult < 1.0f) {
mult = 1.0f - (1.0f - mult) * shrink;
}
cir.setReturnValue(mult);
}
@Inject(method = "tick", at = @At("TAIL"))
private void combatant$trackEat(CallbackInfo ci) {
AbstractClientPlayer p = (AbstractClientPlayer) (Object) this;
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || p == mc.player) {
combatant$wasUsing = p.isUsingItem();
return;
}
boolean using = p.isUsingItem();
PvpCooldowns mod = Modules.get(PvpCooldowns.class);
if (mod == null || !mod.isSystemEnabled() || !PvpTargetState.isTargetInPvp(p.getUUID())) {
combatant$wasUsing = using;
return;
}
if (using && !combatant$wasUsing) {
ItemStack stack = p.getUseItem();
if (!stack.isEmpty() && CooldownRegistry.isTracked(stack.getItem())) {
OpponentCooldownManager.recordUse(p.getUUID(), stack.getItem());
}
}
combatant$wasUsing = using;
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.InventoryMove;
@Mixin(AbstractContainerScreen.class)
public abstract class AbstractContainerScreenMixin {
@Inject(method = "onClose", at = @At("HEAD"), cancellable = true)
private void combatant$inventoryMove$deferClose(CallbackInfo ci) {
InventoryMove module = Modules.get(InventoryMove.class);
if (module == null || !module.isEnabled()) return;
if (module.shouldCancelHandledScreenClose((AbstractContainerScreen<?>) (Object) this)) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,38 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractSelectionList;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
@Mixin(AbstractSelectionList.class)
public abstract class AbstractSelectionListMixin {
@Inject(method = "enableScissor(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V", at = @At("HEAD"))
private void combatant$pushScissor(GuiGraphicsExtractor context, CallbackInfo ci) {
AbstractSelectionList<?> self = (AbstractSelectionList<?>) (Object) this;
BetterButtons.pushScissorContext(self.getX(), self.getY(), self.getRight(), self.getBottom());
}
@Inject(
method = "extractWidgetRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;disableScissor()V")
)
private void combatant$popScissor(GuiGraphicsExtractor context, int mouseX, int mouseY, float deltaTicks, CallbackInfo ci) {
BetterButtons.popScissorContext();
}
}

View file

@ -0,0 +1,49 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractSliderButton;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
import dev.loki.lovisual.mixins.accessors.SliderWidgetAccessor;
@Mixin(AbstractSliderButton.class)
public abstract class AbstractSliderButtonMixin {
@Inject(method = "extractWidgetRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$renderWidget(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
BetterButtons buttons = BetterButtons.get();
if (buttons == null || !buttons.useUiButtons()) return;
Minecraft mc = Minecraft.getInstance();
if (mc != null && ClientScreen.current() instanceof MerchantScreen) return;
AbstractSliderButton self = (AbstractSliderButton) (Object) this;
AbstractWidget widget = self;
boolean hovered = widget.isMouseOver(mouseX, mouseY);
boolean dragging = ((SliderWidgetAccessor) self).combatant$isDragging();
float hover = BetterButtons.updateHover(widget, hovered || dragging);
double value = ((SliderWidgetAccessor) self).combatant$getValue();
BetterButtons.enqueueSlider(widget, (float) value, dragging, hover);
if (hovered) {
BetterButtons.captureTooltip(widget, mouseX, mouseY);
}
ci.cancel();
}
}

View file

@ -0,0 +1,87 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.components.AbstractWidget;
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.CallbackInfoReturnable;
import dev.loki.lovisual.mixininterface.IRoundedHitbox;
@Mixin(AbstractWidget.class)
public abstract class AbstractWidgetMixin implements IRoundedHitbox {
@Unique
private float combatant$roundedRadius = 0.0f;
@Unique
private boolean combatant$roundedEnabled = false;
@Override
public void combatant$setRoundedHitbox(float radius) {
combatant$roundedRadius = Math.max(0.0f, radius);
combatant$roundedEnabled = combatant$roundedRadius > 0.0f;
}
@Override
public void combatant$clearRoundedHitbox() {
combatant$roundedRadius = 0.0f;
combatant$roundedEnabled = false;
}
@Override
public float combatant$getRoundedHitbox() {
return combatant$roundedRadius;
}
@Override
public boolean combatant$useRoundedHitbox() {
return combatant$roundedEnabled;
}
@Inject(method = "isMouseOver(DD)Z", at = @At("HEAD"), cancellable = true)
private void combatant$isMouseOverRounded(double mouseX, double mouseY, CallbackInfoReturnable<Boolean> cir) {
if (!combatant$roundedEnabled) return;
AbstractWidget self = (AbstractWidget) (Object) this;
float x = self.getX();
float y = self.getY();
float w = self.getWidth();
float h = self.getHeight();
if (mouseX < x || mouseY < y || mouseX > x + w || mouseY > y + h) {
cir.setReturnValue(false);
return;
}
float r = Math.min(combatant$roundedRadius, Math.min(w, h) * 0.5f);
if (r <= 0.0f) {
cir.setReturnValue(true);
return;
}
float px = (float) (mouseX - x);
float py = (float) (mouseY - y);
if ((px >= r && px <= w - r) || (py >= r && py <= h - r)) {
cir.setReturnValue(true);
return;
}
float cx = (px < r) ? r : (w - r);
float cy = (py < r) ? r : (h - r);
float dx = px - cx;
float dy = py - cy;
cir.setReturnValue((dx * dx + dy * dy) <= (r * r));
}
}

View file

@ -0,0 +1,57 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.fog.FogData;
import net.minecraft.client.renderer.fog.environment.AtmosphericFogEnvironment;
import net.minecraft.world.level.Level;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(value = AtmosphericFogEnvironment.class, priority = 1200)
public class AtmosphericFogEnvironmentMixin {
@Inject(method = "setupFog", at = @At("HEAD"), cancellable = true)
private void combatant$noDimensionFog(FogData data,
Camera camera,
ClientLevel world,
float viewDistance,
DeltaTracker tickCounter,
CallbackInfo ci) {
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogControlEnabled()) return;
if (module.isFogModifyEnabled()) {
module.applyFogOverride(data);
ci.cancel();
return;
}
if (world == null) return;
boolean disable =
(world.dimension() == Level.OVERWORLD && module.disableOverworldFog())
|| (world.dimension() == Level.NETHER && module.disableNetherFog())
|| (world.dimension() == Level.END && module.disableEndFog());
if (!disable) return;
float far = Math.max(viewDistance, 16.0f);
data.environmentalStart = far;
data.environmentalEnd = far;
data.skyEnd = far;
data.cloudEnd = far;
ci.cancel();
}
}

View file

@ -0,0 +1,73 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.SubmitNodeCollection;
import net.minecraft.util.Mth;
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.ModifyVariable;
import dev.loki.lovisual.render.ViewObstructionFadeContext;
import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget;
import java.util.Arrays;
@Mixin(SubmitNodeCollection.class)
public abstract class BatchingRenderCommandQueueMixin {
@Unique
private static final int COMBATANT_VIEW_FADE_DITHER_MARK = 0x00010001;
@Unique
private static final int COMBATANT_VIEW_FADE_A2C_MARK = 0x00010100;
@Unique
private static int combatant$markViewObstructionFade(int argb, float alpha, boolean msaaActive) {
int rgb = (argb & 0x00FEFEFE) | (msaaActive ? COMBATANT_VIEW_FADE_A2C_MARK : COMBATANT_VIEW_FADE_DITHER_MARK);
int alphaByte = Math.round(255.0f * Mth.clamp(alpha, 0.0f, 1.0f));
return rgb | ((alphaByte & 0xFF) << 24);
}
@Unique
private static boolean combatant$shouldApplyViewObstructionFade() {
return ViewObstructionFadeContext.isActive() && ViewObstructionFadeContext.alpha() < 0.99f;
}
@Unique
private static int combatant$applyFadeToArgb(int color) {
int baseColor = color == -1 ? 0xFFFFFFFF : ((color & 0x00FFFFFF) | 0xFF000000);
return combatant$markViewObstructionFade(baseColor, ViewObstructionFadeContext.alpha(), MsaaWorldTarget.isActive());
}
@ModifyVariable(method = "submitModel", at = @At("HEAD"), argsOnly = true, index = 7)
private int combatant$applyViewObstructionFadeAlpha(int tintedColor) {
if (!combatant$shouldApplyViewObstructionFade()) {
return tintedColor;
}
return combatant$applyFadeToArgb(tintedColor);
}
@ModifyVariable(method = "submitItem", at = @At("HEAD"), argsOnly = true, index = 6)
private int[] combatant$applyViewObstructionFadeAlphaToItemTints(int[] tints) {
if (!combatant$shouldApplyViewObstructionFade()) {
return tints;
}
int sentinel = combatant$applyFadeToArgb(-1);
if (tints == null || tints.length == 0) {
return new int[]{sentinel};
}
int[] faded = Arrays.copyOf(tints, tints.length);
faded[0] = combatant$applyFadeToArgb(faded[0]);
for (int i = 1; i < faded.length; i++) {
faded[i] = combatant$applyFadeToArgb(faded[i]);
}
return faded;
}
}

View file

@ -0,0 +1,41 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
* Portions derived from ThunderHack Recode, copyright (c) 2023-2024 Pan4ur & 06ED.
* Upstream: https://github.com/Pan4ur/ThunderHack-Recode
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockCollisions;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.EventCollision;
@Mixin(value = BlockCollisions.class, priority = 800)
public abstract class BlockCollisionSpliteratorMixin {
@Redirect(
method = "computeNext",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/level/BlockGetter;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;"
)
)
private BlockState combatant$collisionHook(BlockGetter instance, BlockPos blockPos) {
BlockState state = instance.getBlockState(blockPos);
if (!Events.BUS.hasListeners(EventCollision.class)) {
return state;
}
EventCollision event = new EventCollision(state, blockPos);
Events.BUS.post(event);
return event.getState();
}
}

View file

@ -0,0 +1,43 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.world.level.block.Block;
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;
import dev.loki.lovisual.util.player.EnvironmentMovementController;
@Mixin(Block.class)
public abstract class BlockMixin {
@Inject(
method = "getFriction",
at = @At("HEAD"),
cancellable = true
)
private void nostun$overrideSlipperiness(CallbackInfoReturnable<Float> cir) {
Float override = EnvironmentMovementController.getSlipperinessOverride((Block) (Object) this);
if (override != null) {
cir.setReturnValue(override);
}
}
@Inject(
method = "getSpeedFactor",
at = @At("HEAD"),
cancellable = true
)
private void nostun$overrideVelocityMultiplier(CallbackInfoReturnable<Float> cir) {
Float override = EnvironmentMovementController.getVelocityMultiplierOverride((Block) (Object) this);
if (override != null) {
cir.setReturnValue(override);
}
}
}

View file

@ -0,0 +1,82 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.CombatProtocolBossbarEvent;
import dev.loki.lovisual.mixins.accessors.BossHealthOverlayAccessor;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.BossHealthOverlay;
import net.minecraft.client.gui.components.LerpingBossEvent;
import net.minecraft.network.chat.Component;
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 java.util.ArrayList;
import java.util.List;
@Mixin(BossHealthOverlay.class)
public class BossHealthOverlayMixin {
@Inject(method = "extractRenderState", at = @At("HEAD"))
private void onRender(GuiGraphicsExtractor ctx, CallbackInfo ci) {
if (Events.BUS.hasListeners(CombatProtocolBossbarEvent.class)) {
List<String> names = new ArrayList<>();
for (LerpingBossEvent bar : ((BossHealthOverlayAccessor) this).getBars().values()) {
if (bar == null) continue;
Component name = bar.getName();
if (name != null && !name.getString().isBlank()) {
names.add(name.getString());
}
}
if (!names.isEmpty()) {
Events.BUS.post(new CombatProtocolBossbarEvent(names));
}
}
// PvP bossbar detection disabled: bossbar no longer reliable.
/*
BossBarHud hud = (BossBarHud) (Object) this;
Map<UUID, ClientBossBar> bars =
((BossHealthOverlayAccessor) hud).getBars();
boolean pvpActive = false;
// проверяем только боссбары
for (ClientBossBar bar : bars.values()) {
String name = bar.getName().getString();
if (name.startsWith("Режим PVP -")) {
pvpActive = true;
break;
}
}
// === только тут меняем состояние PvP ===
if (pvpActive && !CooldownsState.MANAGER.isInPvp()) {
//System.out.println("[PvPDetector] → ENTER PVP");
CooldownsState.MANAGER.enterPvp();
}
if (!pvpActive && CooldownsState.MANAGER.isInPvp()) {
//System.out.println("[PvPDetector] → EXIT PVP");
CooldownsState.MANAGER.exitPvp();
}
*/
}
}

View file

@ -0,0 +1,41 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
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 dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
@Mixin(targets = "com.mojang.blaze3d.opengl.DirectStateAccess$Emulated")
public abstract class BufferManagerDefaultBufferManagerMixin {
@Inject(method = "bindFrameBufferTextures", at = @At("HEAD"), cancellable = true)
private void combatant$setupFramebuffer(int framebuffer,
int[] colorAttachments,
int[] colorMipLevels,
int depthAttachment,
int depthMipLevel,
int bindTarget,
CallbackInfo ci) {
if (colorAttachments == null || colorAttachments.length != 1) {
return;
}
int colorAttachment = colorAttachments[0];
int mipLevel = colorMipLevels != null && colorMipLevels.length > 0 ? colorMipLevels[0] : depthMipLevel;
if (CombatantRenderSystem.rhi().msaa().setupFramebuffer(framebuffer, colorAttachment, depthAttachment, mipLevel, bindTarget)) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,41 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
@Mixin(Button.Plain.class)
public abstract class ButtonWidgetTextMixin {
@Inject(method = "extractContents(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$drawIcon(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
BetterButtons buttons = BetterButtons.get();
if (buttons == null || !buttons.useUiButtons()) return;
Minecraft mc = Minecraft.getInstance();
if (mc != null && ClientScreen.current() instanceof MerchantScreen) return;
AbstractWidget widget = (AbstractWidget) (Object) this;
boolean hovered = widget.isMouseOver(mouseX, mouseY);
float hover = BetterButtons.updateHover(widget, hovered, mouseX, mouseY);
BetterButtons.enqueueButton(widget, widget.getMessage(), hover, widget.isActive(), widget.isFocused());
if (hovered) {
BetterButtons.captureTooltip(widget, mouseX, mouseY);
}
ci.cancel();
}
}

View file

@ -0,0 +1,192 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Camera;
import net.minecraft.client.CameraType;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.world.level.material.FogType;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.CameraClip;
import dev.loki.lovisual.features.module.modules.visuals.FovControl;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.features.module.modules.visuals.Zoom;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Shadow
private boolean initialized;
@Shadow
private Level level;
@Shadow
private Entity entity;
@Unique
private static float combatant$getGameFov() {
Minecraft mc = Minecraft.getInstance();
if (mc == null) return 70.0f;
return mc.options.fov().get();
}
@Unique
private static float combatant$getFovEffectScale() {
Minecraft mc = Minecraft.getInstance();
if (mc == null) return 1.0f;
return mc.options.fovEffectScale().get().floatValue();
}
@Shadow
protected abstract void move(float surge, float heave, float sway);
@Inject(method = "getMaxZoom(F)F", at = @At("HEAD"), cancellable = true)
private void combatant$disableCameraCollision(float desiredDistance, CallbackInfoReturnable<Float> cir) {
CameraClip mod = Modules.get(CameraClip.class);
if (mod != null && mod.isEnabled() && isThirdPerson()) {
// Skip collision shortening and apply distance multiplier
float dist = desiredDistance * mod.distanceMultiplier();
cir.setReturnValue(dist);
}
}
@Inject(method = "update", at = @At("TAIL"))
private void combatant$applyCameraOffsets(DeltaTracker tickCounter, CallbackInfo ci) {
CameraClip mod = Modules.get(CameraClip.class);
if (mod == null || !mod.isEnabled() || !isThirdPerson()) return;
float back = mod.backOffset();
float up = mod.upOffset();
float right = mod.rightOffset();
if (back != 0.0f || up != 0.0f || right != 0.0f) {
// moveBy uses (surge, heave, sway); negative surge moves backward
move(-back, up, right);
}
}
@Inject(method = "calculateFov", at = @At("RETURN"), cancellable = true)
private void combatant$adjustCalculatedFov(float tickDelta, CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(combatant$adjustFovValue(cir.getReturnValue()));
}
@Unique
private float combatant$adjustFovValue(float fov) {
FovControl module = Modules.get(FovControl.class);
Zoom zoom = Modules.get(Zoom.class);
boolean hasFovControl = module != null && module.isEnabled();
boolean hasZoom = zoom != null && zoom.isEnabled() && zoom.shouldApplyZoom();
if (!hasFovControl && !hasZoom) return fov;
if (hasFovControl && module.useCustomFov()) {
float base = combatant$getGameFov();
float custom = module.getCustomFov();
if (base > 0.01f) {
fov = fov * (custom / base);
} else {
fov = custom;
}
}
if (hasFovControl && module.disableFluidFov()) {
FogType type = ((Camera) (Object) this).getFluidInCamera();
if (type == FogType.WATER || type == FogType.LAVA) {
float effectScale = combatant$getFovEffectScale();
float factor = Mth.lerp(effectScale, 1.0f, 0.85714287f);
if (factor > 0.0001f) {
fov /= factor;
}
}
}
if (hasZoom) {
float divisor = zoom.getZoomDivisor();
if (divisor > 1.0f) {
fov /= divisor;
}
}
return fov;
}
@Unique
private boolean isThirdPerson() {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.options == null) return false;
CameraType p = mc.options.getCameraType();
return p == CameraType.THIRD_PERSON_BACK || p == CameraType.THIRD_PERSON_FRONT;
}
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Shadow
protected abstract void setPosition(Vec3 pos);
@Shadow
public abstract Matrix4f getViewRotationMatrix(Matrix4f matrix);
@Invoker("createProjectionMatrixForCulling")
protected abstract Matrix4f combatant$createProjectionMatrixForCulling();
@Invoker("prepareCullFrustum")
protected abstract void combatant$prepareCullFrustum(Matrix4fc viewMatrix, Matrix4f projectionMatrix, Vec3 cameraPos);
@Inject(method = "update", at = @At("TAIL"))
private void freecam$override(
DeltaTracker tickCounter,
CallbackInfo ci
) {
Freecam fc = Modules.get(Freecam.class);
if (fc == null || !fc.isEnabled() || fc.camEntity == null) return;
this.initialized = true;
if (Minecraft.getInstance().level != null) {
this.level = Minecraft.getInstance().level;
}
this.entity = fc.camEntity;
this.setRotation(fc.camYaw, fc.camPitch);
float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true);
Vec3 p = fc.getCameraPos(tickDelta);
fc.syncCameraEntity(fc.camPos, fc.camPosPrev);
this.setPosition(p);
this.combatant$prepareCullFrustum(this.getViewRotationMatrix(new Matrix4f()), this.combatant$createProjectionMatrixForCulling(), p);
}
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void combatant$freecamExtractRenderState(CameraRenderState renderState, float tickDelta, CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc == null || !fc.isEnabled() || fc.camEntity == null) return;
renderState.smartCull = false;
}
}

View file

@ -0,0 +1,87 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.ChatComponent;
import net.minecraft.client.gui.components.ChatComponent.DisplayMode;
import net.minecraft.client.multiplayer.chat.GuiMessageSource;
import net.minecraft.client.multiplayer.chat.GuiMessageTag;
import net.minecraft.client.multiplayer.chat.GuiMessage;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MessageSignature;
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 dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.features.gui.chat.BetterChatStoreManager;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.misc.MessageFilter;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(ChatComponent.class)
public class ChatComponentMixin {
@Inject(
method = "addMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/MessageSignature;Lnet/minecraft/client/multiplayer/chat/GuiMessageSource;Lnet/minecraft/client/multiplayer/chat/GuiMessageTag;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$filterChatFull(Component message,
MessageSignature signatureData,
GuiMessageSource source,
GuiMessageTag indicator,
CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
MessageFilter messageFilter = Modules.get(MessageFilter.class);
if (messageFilter != null && messageFilter.shouldHideChat(message)) {
ci.cancel();
return;
}
if (!CommandOutput.isLoVisualMessage(message)) {
BetterChatStoreManager.addMessage(message);
}
}
@Inject(method = "logChatMessage(Lnet/minecraft/client/multiplayer/chat/GuiMessage;)V", at = @At("HEAD"), cancellable = true)
private void combatant$commands$suppressClientLog(GuiMessage message, CallbackInfo ci) {
if (message != null && CommandOutput.isLoVisualMessage(message.content())) {
ci.cancel();
}
}
@Inject(method = "clearMessages(Z)V", at = @At("TAIL"))
private void combatant$betterChat$clear(boolean clearHistory, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
if (!BetterChat.isActive()) return;
// Never wipe BetterChat store on vanilla clear; just persist to disk.
BetterChatStoreManager.flushActive();
}
@Inject(method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/gui/Font;IIILnet/minecraft/client/gui/components/ChatComponent$DisplayMode;Z)V",
at = @At("HEAD"), cancellable = true)
private void combatant$betterChat$hideVanilla(GuiGraphicsExtractor context,
Font textRenderer,
int currentTick,
int mouseX,
int mouseY,
DisplayMode displayMode,
boolean inChat,
CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
if (!BetterChat.isActive()) return;
BetterChat cfg = BetterChat.get();
if (cfg != null && cfg.hideVanilla()) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,102 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.ActiveTextCollector;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.ChatComponent;
import net.minecraft.client.gui.components.ChatComponent.DisplayMode;
import net.minecraft.client.gui.components.CommandSuggestions;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.input.KeyEvent;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(ChatScreen.class)
public class ChatScreenMixin {
@Inject(method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$betterChat$render(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
if (BetterChat.isActive()) {
BetterChat cfg = BetterChat.get();
if (cfg != null && cfg.hideVanilla()) {
// Stash suggestion context; BetterChatRenderer will draw it in correct Z-order.
BetterChatRenderer.setSuggestionContext((ChatScreen) (Object) this, context, mouseX, mouseY);
ci.cancel();
}
}
}
@Redirect(
method = "mouseClicked(Lnet/minecraft/client/input/MouseButtonEvent;Z)Z",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/components/ChatComponent;captureClickableText(Lnet/minecraft/client/gui/ActiveTextCollector;IILnet/minecraft/client/gui/components/ChatComponent$DisplayMode;)V"
)
)
private void combatant$betterChat$disableVanillaChatClickHitbox(ChatComponent chatHud,
ActiveTextCollector textConsumer,
int windowHeight,
int currentTick,
DisplayMode displayMode) {
if (combatant$betterChat$shouldHideVanillaChat()) {
return;
}
chatHud.captureClickableText(textConsumer, windowHeight, currentTick, displayMode);
}
@Inject(method = "mouseScrolled(DDDD)Z", at = @At("HEAD"), cancellable = true)
private void combatant$betterChat$disableVanillaChatScroll(double mouseX,
double mouseY,
double horizontalAmount,
double verticalAmount,
CallbackInfoReturnable<Boolean> cir) {
if (combatant$betterChat$shouldHideVanillaChat()) {
cir.setReturnValue(true);
}
}
@Inject(method = "keyPressed(Lnet/minecraft/client/input/KeyEvent;)Z", at = @At("HEAD"))
private void combatant$betterChat$allowEscape(KeyEvent input, CallbackInfoReturnable<Boolean> cir) {
if (!RuntimeGate.canRunHud()) return;
if (!BetterChat.isActive()) return;
if (!input.isEscape()) return;
CommandSuggestions suggestor = ((ChatScreenSuggestorAccessor) this).getCombatant$suggestor();
if (suggestor != null) {
suggestor.setAllowSuggestions(false);
suggestor.hide();
}
}
@Inject(method = "handleChatInput(Ljava/lang/String;Z)V", at = @At("HEAD"), cancellable = true, require = 0)
private void combatant$commands$sendMessage(String message, boolean addToHistory, CallbackInfo ci) {
if (!RuntimeGate.canRunClientLogic()) return;
if (CommandManager.handle(message)) {
ci.cancel();
}
}
@Unique
private boolean combatant$betterChat$shouldHideVanillaChat() {
if (!RuntimeGate.canRunHud()) return false;
if (!BetterChat.isActive()) return false;
BetterChat cfg = BetterChat.get();
return cfg != null && cfg.hideVanilla();
}
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.ClientClockManager;
import net.minecraft.core.Holder;
import net.minecraft.world.clock.WorldClock;
import net.minecraft.world.clock.WorldClocks;
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;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(ClientClockManager.class)
public abstract class ClientClockManagerMixin {
@Inject(method = "getTotalTicks", at = @At("HEAD"), cancellable = true)
private void combatant$overrideRenderClock(Holder<WorldClock> clock, CallbackInfoReturnable<Long> cir) {
if (clock == null || !clock.is(WorldClocks.OVERWORLD)) return;
WorldTweaks worldTweaks = Modules.get(WorldTweaks.class);
if (worldTweaks == null || !worldTweaks.shouldOverrideTime()) return;
cir.setReturnValue(worldTweaks.getRenderClockTicks());
}
}

View file

@ -0,0 +1,44 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.Flight;
@Mixin(ClientCommonPacketListenerImpl.class)
public abstract class ClientCommonPacketListenerImplMixin {
@Final
@Shadow
protected Connection connection;
@Inject(method = "send(Lnet/minecraft/network/protocol/Packet;)V", at = @At("HEAD"), cancellable = true)
private void combatant$flightSendPacket(Packet<?> packet, CallbackInfo ci) {
if (!((Object) this instanceof ClientPacketListener)) return;
if (!(packet instanceof ServerboundMovePlayerPacket move)) return;
Flight flight = Modules.get(Flight.class);
if (flight == null || !flight.isEnabled()) return;
ServerboundMovePlayerPacket replacement = flight.onSendMovePacket(move);
if (replacement != null && replacement != move) {
ci.cancel();
connection.send(replacement);
}
}
}

View file

@ -0,0 +1,72 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ItemParticleOption;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.util.block.BlockObservationHub;
@Mixin(ClientLevel.class)
public abstract class ClientLevelMixin {
@Inject(method = "setServerVerifiedBlockState", at = @At("TAIL"))
private void combatant$recordBlockUpdate(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, CallbackInfo ci) {
BlockObservationHub.observeWorldUpdate(pos, state);
}
@Inject(
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$noRenderEatParticles(
ParticleOptions effect,
boolean force,
boolean canSpawnOnMinimal,
double x, double y, double z,
double vx, double vy, double vz,
CallbackInfo ci
) {
NoRender nr = Modules.get(NoRender.class);
if (nr == null || !nr.isEnabled()) return;
// частицы еды / питья
if (nr.off("eat_particles")
&& effect instanceof ItemParticleOption) {
ci.cancel();
}
if (nr.off("hit_particles")) {
var type = effect.getType();
if (type == ParticleTypes.DAMAGE_INDICATOR
|| type == ParticleTypes.CRIT
|| type == ParticleTypes.ENCHANTED_HIT) {
ci.cancel();
}
}
if (nr.off("sweep_particles")) {
var type = effect.getType();
if (type == ParticleTypes.SWEEP_ATTACK) {
ci.cancel();
}
}
}
}

View file

@ -0,0 +1,430 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.*;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Items;
import net.minecraft.world.phys.Vec3;
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;
import dev.loki.lovisual.config.values.ItemCooldownRulesValue;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.PvpChatEvent;
import dev.loki.lovisual.events.impl.PvpTabEvent;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
import dev.loki.lovisual.features.module.modules.misc.NoSound;
import dev.loki.lovisual.features.module.modules.movement.Flight;
import dev.loki.lovisual.features.module.modules.visuals.SoundESP;
import dev.loki.lovisual.features.module.modules.visuals.TotemFX;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
import dev.loki.lovisual.features.relations.StaffTracker;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.util.logging.ServerDumpUtil;
import dev.loki.lovisual.util.player.NetworkStatsUtil;
import dev.loki.lovisual.util.player.effect.StatusEffectHeuristics;
import dev.loki.lovisual.util.pvp.client.CooldownsState;
import dev.loki.lovisual.util.pvp.opponents.OpponentCooldownManager;
import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Mixin(ClientPacketListener.class)
public abstract class ClientPacketListenerMixin {
// "TPS: 20.0" или "TPS: *20.0"
@Unique
private static final Pattern TPS_PATTERN =
Pattern.compile("TPS:\\s*\\*?(\\d+(?:\\.\\d+)?)");
// "Пинг: 57"
@Unique
private static final Pattern PING_PATTERN =
Pattern.compile("Пинг:\\s*(\\d+)");
@Inject(method = "handlePlayerAbilities", at = @At("HEAD"), cancellable = true)
private void combatant$flightAbilities(ClientboundPlayerAbilitiesPacket packet, CallbackInfo ci) {
Flight flight = Modules.get(Flight.class);
if (flight == null || !flight.isEnabled()) return;
if (flight.onReceiveAbilities(packet)) {
ci.cancel();
}
}
@Inject(method = "handleTabListCustomisation", at = @At("TAIL"))
private void combatant$captureTab(ClientboundTabListPacket packet, CallbackInfo ci) {
String header = packet.header() == null ? "" : packet.header().getString();
String footer = packet.footer() == null ? "" : packet.footer().getString();
ServerDumpUtil.dumpTabText("HEADER", packet.header());
ServerDumpUtil.dumpTabText("FOOTER", packet.footer());
if (Events.BUS.hasListeners(PvpTabEvent.class)) {
Events.BUS.post(new PvpTabEvent(header, footer));
}
}
@Inject(method = "handleSystemChat", at = @At("HEAD"))
private void onChat(ClientboundSystemChatPacket packet, CallbackInfo ci) {
String msg = packet.content().getString();
// PvP state listeners via events; legacy direct setters disabled.
if (msg != null && !msg.isBlank() && Events.BUS.hasListeners(PvpChatEvent.class)) {
Events.BUS.post(new PvpChatEvent(msg, PvpChatEvent.Source.GAME_MESSAGE));
}
/*
if (msg.contains("Вы вошли в режим PVP")) {
CooldownsState.MANAGER.enterPvp();
} else if (msg.contains("PVP окончено")) {
CooldownsState.MANAGER.exitPvp();
}
*/
}
@Inject(method = "handleSoundEvent", at = @At("HEAD"))
private void combatant$onChorus(ClientboundSoundPacket packet, CallbackInfo ci) {
// Opponent cooldown tracking disabled (legacy system).
/*
if (!CooldownsState.shouldTrackOpponents()) return;
if (packet.getSound().value() != SoundEvents.ITEM_CHORUS_FRUIT_TELEPORT) return;
MinecraftClient mc = MinecraftClient.getInstance();
if (mc.world == null || mc.player == null) return;
Vec3d pos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
if (!SoundEntityResolver.isUnambiguousOpponent(pos, 1.6)) return;
PlayerEntity p = SoundEntityResolver.findClosestOpponent(pos, 1.6);
if (p == null) return;
OpponentCooldownManager.start(p.getUuid(), Items.CHORUS_FRUIT);
*/
}
@Inject(method = "handlePlayerChat", at = @At("HEAD"))
private void combatant$pvpChatMessage(ClientboundPlayerChatPacket packet, CallbackInfo ci) {
String msg = packet == null ? null : packet.body().content();
if (msg == null || msg.isBlank()) return;
if (Events.BUS.hasListeners(PvpChatEvent.class)) {
Events.BUS.post(new PvpChatEvent(msg, PvpChatEvent.Source.CHAT_MESSAGE));
}
}
@Inject(method = "handleRemoveEntities", at = @At("TAIL"))
private void combatant$onEntitiesDestroy(ClientboundRemoveEntitiesPacket packet, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
ClientLevel world = mc.level;
if (world == null) return;
for (int id : packet.getEntityIds()) {
Entity e = world.getEntity(id);
if (!(e instanceof Player player)) continue;
StaffTracker.onEntityDisappear(player.getUUID());
}
}
@Inject(method = "handleAddEntity", at = @At("TAIL"))
private void combatant$onEntitySpawn(ClientboundAddEntityPacket packet, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
ClientLevel world = mc.level;
if (world == null) return;
if (packet.getType() != net.minecraft.world.entity.EntityTypes.PLAYER) return;
Entity entity = world.getEntity(packet.getId());
if (!(entity instanceof Player player)) return;
StaffTracker.onEntityAppear(player.getUUID(), player.getGameProfile().name());
}
@Inject(method = "handlePlayerInfoUpdate", at = @At("TAIL"))
private void combatant$onPlayerList(ClientboundPlayerInfoUpdatePacket packet, CallbackInfo ci) {
var actions = packet.actions();
boolean listedAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED)
|| actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER);
boolean gmAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE)
|| actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER);
for (var entry : packet.entries()) {
var uuid = entry.profileId();
String name = entry.profile() != null ? entry.profile().name() : null;
Boolean listed = listedAffects ? entry.listed() : null;
var gm = gmAffects ? entry.gameMode() : null;
// ВАЖНО: берём ПОЛНЫЙ Text из TAB
var displayName = entry.displayName();
if (displayName == null && name != null) {
displayName = net.minecraft.network.chat.Component.literal(name);
}
StaffTracker.onTabUpdate(uuid, name, displayName, listed, gm);
}
}
@Inject(method = "handlePlayerInfoRemove", at = @At("TAIL"))
private void combatant$onPlayerRemove(ClientboundPlayerInfoRemovePacket packet, CallbackInfo ci) {
for (UUID id : packet.profileIds()) {
// убрали из TAB
StaffTracker.onTabUpdate(id, null, null, false, null);
TotemPopCounter.onPlayerLogout(id);
// ВАЖНО — фикс VANISH после кика
StaffTracker.onEntityDisappear(id);
}
}
@Inject(method = "handleSetTime", at = @At("HEAD"))
private void combatant$captureServerTime(ClientboundSetTimePacket packet, CallbackInfo ci) {
WorldTweaks.setServerTimeOfDay(packet.gameTime());
}
@Inject(method = "handleTabListCustomisation", at = @At("TAIL"))
private void combatant$onTab(ClientboundTabListPacket packet, CallbackInfo ci) {
parse(packet.header());
}
@Unique
private void parse(Component text) {
if (text == null) return;
String raw = text.getString();
if (raw.isEmpty()) return;
Matcher tps = TPS_PATTERN.matcher(raw);
if (tps.find()) {
try {
NetworkStatsUtil.updateTabTps(Float.parseFloat(tps.group(1)));
} catch (NumberFormatException ignored) {
}
}
Matcher ping = PING_PATTERN.matcher(raw);
if (ping.find()) {
try {
NetworkStatsUtil.updateTabPing(Integer.parseInt(ping.group(1)));
} catch (NumberFormatException ignored) {
}
}
}
@Inject(method = "handleContainerSetSlot", at = @At("HEAD"))
private void combatant$debugSlotUpdate(ClientboundContainerSetSlotPacket packet, CallbackInfo ci) {
// PvP cooldown confirmation disabled (legacy system).
/*
int syncId = packet.getSyncId();
int slot = packet.getSlot();
ItemStack stack = packet.getStack();
//System.out.println("[Combatant][DEBUG] Packet slot update: syncId=" + syncId + " slot=" + slot + " item=" + stack.getItem() + " x" + stack.getCount());
var pending = CooldownsState.PENDING.consumeIfConfirmed(slot, stack.getCount());
if (pending != null) {
//System.out.println("[Combatant][DEBUG] Confirmed use of " + pending.item() + " (count decreased from " + pending.count() + " to " + stack.getCount() + ")");
CooldownsState.MANAGER.commitConfirmedUse(pending.item(), pending.startedAtMs());
}
*/
}
@Inject(method = "handleContainerContent", at = @At("HEAD"))
private void combatant$debugFullInventory(ClientboundContainerSetContentPacket packet, CallbackInfo ci) {
// PvP cooldown confirmation disabled (legacy system).
/*
int syncId = packet.syncId();
List<ItemStack> contents = packet.contents();
//System.out.println("[Combatant][DEBUG] Full inventory syncId=" + syncId + ", size=" + contents.size());
for (int i = 0; i < contents.size(); i++) {
ItemStack stack = contents.get(i);
if (stack.isEmpty()) continue;
var pending = CooldownsState.PENDING.consumeIfConfirmed(i, stack.getCount());
if (pending != null) {
//System.out.println("[Combatant][DEBUG] Confirmed (FULL INV) " + pending.item() + " slot=" + i + " count=" + stack.getCount());
CooldownsState.MANAGER.commitConfirmedUse(pending.item(), pending.startedAtMs());
}
}
*/
}
@Inject(method = "handleSoundEvent", at = @At("HEAD"), cancellable = true)
private void combatant$filterSound(ClientboundSoundPacket packet, CallbackInfo ci) {
NoSound module = Modules.get(NoSound.class);
if (module == null || !module.isEnabled()) return;
if (packet == null || packet.getSound() == null) return;
String id = packet.getSound().value().location().toString().toLowerCase();
if (module.shouldMute(id)) {
ci.cancel();
}
}
@Inject(method = "handleSoundEntityEvent", at = @At("HEAD"), cancellable = true)
private void combatant$filterSoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) {
NoSound module = Modules.get(NoSound.class);
if (module == null || !module.isEnabled()) return;
if (packet == null || packet.getSound() == null) return;
String id = packet.getSound().value().location().toString().toLowerCase();
if (module.shouldMute(id)) {
ci.cancel();
}
}
@Inject(method = "handleParticleEvent", at = @At("HEAD"))
private void combatant$heuristicParticle(ClientboundLevelParticlesPacket packet, CallbackInfo ci) {
var p = packet.getParticle();
// Log raw particles once per hook for diagnostics
DebugLog.info("Particle packet type=%s pos=%.2f %.2f %.2f count=%d",
p.getType().toString(),
packet.getX(), packet.getY(), packet.getZ(),
packet.getCount());
StatusEffectHeuristics.handle(p, packet.getX(), packet.getY(), packet.getZ());
}
@Inject(method = "handleSoundEvent", at = @At("HEAD"))
private void combatant$onPearl(ClientboundSoundPacket packet, CallbackInfo ci) {
// Opponent cooldown tracking disabled (legacy system).
/*
if (!CooldownsState.shouldTrackOpponents()) return;
if (packet.getSound().value() != SoundEvents.ENTITY_ENDER_PEARL_THROW) return;
MinecraftClient mc = MinecraftClient.getInstance();
if (mc.world == null || mc.player == null) return;
Vec3d pos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
if (!SoundEntityResolver.isUnambiguousOpponent(pos, 1.6)) return;
PlayerEntity p = SoundEntityResolver.findClosestOpponent(pos, 1.6);
if (p == null) return;
OpponentCooldownManager.start(p.getUuid(), Items.ENDER_PEARL);
*/
}
@Inject(method = "handleSoundEvent", at = @At("HEAD"))
private void combatant$onPlaySound(ClientboundSoundPacket packet, CallbackInfo ci) {
SoundESP esp = Modules.get(SoundESP.class);
if (esp == null || !esp.isEnabled()) return;
if (packet == null || packet.getSound() == null) return;
Vec3 pos = new Vec3(packet.getX(), packet.getY(), packet.getZ());
esp.handleSound(packet.getSound().value().location(), pos);
}
@Inject(method = "handleSoundEntityEvent", at = @At("HEAD"))
private void combatant$onPlaySoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) {
SoundESP esp = Modules.get(SoundESP.class);
if (esp == null || !esp.isEnabled()) return;
if (packet == null || packet.getSound() == null) return;
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) return;
Entity entity = mc.level.getEntity(packet.getId());
if (entity == null) return;
esp.handleSound(packet.getSound().value().location(), entity.position());
}
@Inject(method = "handleUpdateMobEffect", at = @At("HEAD"))
private void combatant$logStatusEffect(ClientboundUpdateMobEffectPacket packet, CallbackInfo ci) {
DebugLog.server("Packet: add/refresh effect eid=%d effect=%s amp=%d dur=%d particles=%s icon=%s ambient=%s keepFading=%s",
packet.getEntityId(),
packet.getEffect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown"),
packet.getEffectAmplifier(),
packet.getEffectDurationTicks(),
packet.isEffectVisible(),
packet.effectShowsIcon(),
packet.isEffectAmbient(),
packet.shouldBlend()
);
}
@Inject(method = "handleRemoveMobEffect", at = @At("HEAD"))
private void combatant$logRemove(ClientboundRemoveMobEffectPacket packet, CallbackInfo ci) {
DebugLog.server("Packet: remove effect eid=%d (%s) effect=%s",
packet.entityId(),
resolveName(packet.entityId()),
packet.effect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown"));
}
@Unique
private String resolveName(int entityId) {
var mc = Minecraft.getInstance();
if (mc == null || mc.level == null) return "unknown";
Entity e = mc.level.getEntity(entityId);
if (e == null) return "unknown";
if (e instanceof Player p) return p.getName().getString();
return e.getName().getString();
}
@Inject(method = "handleSoundEvent", at = @At("HEAD"))
private void combatant$onTotemSound(ClientboundSoundPacket packet, CallbackInfo ci) {
// Opponent cooldown tracking disabled (legacy system).
/*
if (!CooldownsState.shouldTrackOpponents()) return;
if (packet.getSound().value() != SoundEvents.ITEM_TOTEM_USE) return;
MinecraftClient mc = MinecraftClient.getInstance();
if (mc.world == null || mc.player == null) return;
Vec3d pos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
if (!SoundEntityResolver.isUnambiguousOpponent(pos, 1.6)) return;
PlayerEntity p = SoundEntityResolver.findClosestOpponent(pos, 1.6);
if (p == null) return;
OpponentCooldownManager.start(p.getUuid(), Items.TOTEM_OF_UNDYING);
*/
}
@Inject(method = "handleEntityEvent", at = @At("HEAD"))
private void combatant$onTotemStatus(ClientboundEntityEventPacket packet, CallbackInfo ci) {
if (packet.getEventId() == 35) { // 35 = сработал тотем бессмертия
ClientPacketListener handler = (ClientPacketListener) (Object) this;
Minecraft client = Minecraft.getInstance();
Entity entity = packet.getEntity(handler.getLevel());
if (entity == null) return;
if (entity.equals(client.player)) {
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
if (cooldowns != null) {
cooldowns.tryStartLocalCooldown(Items.TOTEM_OF_UNDYING, ItemCooldownRulesValue.Trigger.TOTEM_POP);
}
TotemFX fx = Modules.get(TotemFX.class);
if (fx != null) {
fx.onTotemPop();
}
return;
}
if (entity instanceof Player player) {
TotemPopCounter.recordPop(player);
if (CooldownsState.shouldTrackOpponents()) {
OpponentCooldownManager.recordUse(player.getUUID(), Items.TOTEM_OF_UNDYING);
}
}
}
}
}

View file

@ -0,0 +1,114 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.systems.RenderPass;
import com.mojang.blaze3d.systems.RenderPassDescriptor;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTextureView;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector4fc;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.rhi.scissor.GlobalScissorState;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.function.Supplier;
@Mixin(CommandEncoder.class)
public abstract class CommandEncoderMixin {
@Unique
private static String combatant$label(Supplier<String> label) {
try {
return label != null ? label.get() : "<null-label>";
} catch (Throwable t) {
return "<label-threw-" + t.getClass().getSimpleName() + ">";
}
}
@Unique
private static @Nullable GpuTextureView combatant$firstColorView(
List<RenderPassDescriptor.Attachment<Optional<Vector4fc>>> colorAttachments
) {
if (colorAttachments == null) {
return null;
}
for (RenderPassDescriptor.Attachment<Optional<Vector4fc>> attachment : colorAttachments) {
if (attachment != null) {
return attachment.textureView();
}
}
return null;
}
@Unique
private static @Nullable GpuTextureView combatant$depthView(
@Nullable RenderPassDescriptor.Attachment<OptionalDouble> depthAttachment
) {
return depthAttachment == null ? null : depthAttachment.textureView();
}
@Unique
private static boolean combatant$isVulkanBackend() {
try {
if (RenderSystem.getDevice() == null || RenderSystem.getDevice().getDeviceInfo() == null) return false;
String backendName = RenderSystem.getDevice().getDeviceInfo().backendName();
return backendName != null && backendName.toLowerCase(java.util.Locale.ROOT).contains("vulkan");
} catch (Throwable ignored) {
return false;
}
}
@Inject(method = "createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPass;", at = @At("RETURN"))
private void combatant$onCreateRenderPass(
RenderPassDescriptor descriptor,
CallbackInfoReturnable<RenderPass> cir
) {
RenderPass pass = cir.getReturnValue();
if (pass == null || descriptor == null) {
return;
}
try {
GlobalScissorState.applyTo(pass, descriptor.renderArea);
} catch (Throwable t) {
DebugLog.error("[Scissor] render pass scissor hook failed for %s", t, combatant$label(descriptor.label()));
}
try {
CombatantRenderSystem.rhi().shapeClip().beginRenderPass(
combatant$label(descriptor.label()),
combatant$firstColorView(descriptor.colorAttachments()),
combatant$depthView(descriptor.depthAttachment())
);
} catch (Throwable t) {
DebugLog.error("[ShapeClip] beginRenderPass hook failed for %s", t, combatant$label(descriptor.label()));
}
}
@Inject(method = "submitRenderPass", at = @At("HEAD"))
private void combatant$resetRhiRenderPassState(CallbackInfo ci) {
if (combatant$isVulkanBackend()) {
return;
}
try {
CombatantRenderSystem.rhi().pipelineState().resetRenderPassState();
} catch (Throwable t) {
DebugLog.error("[ShapeClip/GL] render pass reset hook failed", t);
}
}
}

View file

@ -0,0 +1,102 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import io.netty.channel.ChannelPipeline;
import net.minecraft.network.Connection;
import net.minecraft.network.PacketListener;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundBundlePacket;
import net.minecraft.server.RunningOnDifferentThreadException;
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;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.PacketEvent;
import dev.loki.lovisual.util.network.BlinkManager;
import dev.loki.lovisual.util.proxy.ProxyNettyInstaller;
@Mixin(Connection.class)
public class ConnectionMixin {
@Shadow
private static <T extends PacketListener> void genericsFtw(Packet<T> packet, PacketListener listener) {
throw new AssertionError();
}
@Inject(method = "genericsFtw", at = @At("HEAD"), cancellable = true)
private static <T extends PacketListener> void combatant$onHandlePacket(Packet<T> packet, PacketListener listener, CallbackInfo ci) {
if (BlinkManager.isSilentlyHandlingPackets()) return;
if (!Events.BUS.hasListeners(PacketEvent.Receive.class)) return;
if (packet instanceof ClientboundBundlePacket packs) {
ci.cancel();
for (Packet<?> p : packs.subPackets()) {
try {
combatant$handlePacketUnchecked(p, listener);
} catch (RunningOnDifferentThreadException ignored) {
}
}
} else {
PacketEvent.Receive event = new PacketEvent.Receive(packet);
Events.BUS.post(event);
if (event.isCancelled()) {
ci.cancel();
}
}
}
@Unique
@SuppressWarnings({"rawtypes", "unchecked"})
private static void combatant$handlePacketUnchecked(Packet<?> packet, PacketListener listener) {
ConnectionMixin.genericsFtw((Packet) packet, listener);
}
@Inject(method = "genericsFtw", at = @At("TAIL"))
private static <T extends PacketListener> void combatant$onHandlePacketPost(Packet<T> packet, PacketListener listener, CallbackInfo ci) {
if (BlinkManager.isSilentlyHandlingPackets()) return;
if (!Events.BUS.hasListeners(PacketEvent.ReceivePost.class)) return;
if (packet instanceof ClientboundBundlePacket packs) {
for (Packet<?> p : packs.subPackets()) {
PacketEvent.ReceivePost event = new PacketEvent.ReceivePost(p);
Events.BUS.post(event);
}
} else {
PacketEvent.ReceivePost event = new PacketEvent.ReceivePost(packet);
Events.BUS.post(event);
}
}
@Inject(method = "send(Lnet/minecraft/network/protocol/Packet;)V", at = @At("HEAD"), cancellable = true)
private void combatant$onSendPacketPre(Packet<?> packet, CallbackInfo ci) {
if (BlinkManager.isSilentlyHandlingPackets()) return;
if (!Events.BUS.hasListeners(PacketEvent.Send.class)) return;
PacketEvent.Send event = new PacketEvent.Send(packet);
Events.BUS.post(event);
if (event.isCancelled()) {
ci.cancel();
}
}
@Inject(method = "send(Lnet/minecraft/network/protocol/Packet;)V", at = @At("RETURN"))
private void combatant$onSendPacketPost(Packet<?> packet, CallbackInfo ci) {
if (BlinkManager.isSilentlyHandlingPackets()) return;
if (!Events.BUS.hasListeners(PacketEvent.SendPost.class)) return;
PacketEvent.SendPost event = new PacketEvent.SendPost(packet);
Events.BUS.post(event);
}
@Inject(method = "configurePacketHandler", at = @At("HEAD"))
private void combatant$installProxyHandler(ChannelPipeline pipeline, CallbackInfo ci) {
ProxyNettyInstaller.install(pipeline);
}
}

View file

@ -0,0 +1,40 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.contextualbar.ContextualBar;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(ContextualBar.class)
public interface ContextualBarMixin {
@Inject(method = "extractExperienceLevel", at = @At("HEAD"), cancellable = true)
private static void combatant$drawExperienceLevel(GuiGraphicsExtractor context,
Font textRenderer,
int level,
CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
ci.cancel();
}
}

View file

@ -0,0 +1,55 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.CycleButton;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
import net.minecraft.resources.Identifier;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
import dev.loki.lovisual.mixins.accessors.CyclingButtonWidgetAccessor;
@Mixin(CycleButton.class)
public abstract class CycleButtonMixin<T> {
@Inject(method = "extractContents(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$drawIcon(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
BetterButtons buttons = BetterButtons.get();
if (buttons == null || !buttons.useUiButtons()) return;
Minecraft mc = Minecraft.getInstance();
if (mc != null && ClientScreen.current() instanceof MerchantScreen) return;
CycleButton<T> self = (CycleButton<T>) (Object) this;
AbstractWidget widget = self;
boolean hovered = widget.isMouseOver(mouseX, mouseY);
float hover = BetterButtons.updateHover(widget, hovered, mouseX, mouseY);
CyclingButtonWidgetAccessor<T> accessor = (CyclingButtonWidgetAccessor<T>) self;
Identifier icon = accessor.combatant$getIcon() != null
? accessor.combatant$getIcon().apply(self, self.getValue())
: null;
boolean showLabel = accessor.combatant$getLabelType() != CycleButton.DisplayState.HIDE;
BetterButtons.enqueueCyclingButton(self, icon, showLabel, hover, widget.isActive(), widget.isFocused());
if (hovered) {
BetterButtons.captureTooltip(widget, mouseX, mouseY);
}
ci.cancel();
}
}

View file

@ -0,0 +1,145 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.components.DebugScreenOverlay;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import dev.loki.lovisual.config.MainConfig;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.debug.RenderThread2DDebugRenderer;
import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.FontDebugStats;
import dev.loki.lovisual.runtime.RuntimeGate;
import dev.loki.lovisual.util.logging.DebugMode;
import dev.loki.lovisual.util.wav.WavDebugStats;
import java.util.ArrayList;
import java.util.List;
@Mixin(DebugScreenOverlay.class)
public abstract class DebugHudMixin {
@ModifyArg(
method = "extractRenderState",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/components/DebugScreenOverlay;extractLines(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Ljava/util/List;Z)V",
ordinal = 0
),
index = 1
)
private List<String> combatant$addEngineInfo(List<String> list) {
if (RuntimeGate.isPanic() || list == null) return list;
DebugMode debugMode = MainConfig.get().getDebugMode();
if (debugMode != DebugMode.ALL && debugMode != DebugMode.RENDER_THREAD) return list;
int samples = MainConfig.get().getMsaa3dSamples();
boolean supported = CombatantRenderSystem.rhi().msaa().supported();
MsaaWorldTarget.DebugStatus msaaStatus = MsaaWorldTarget.debugStatus();
String msaa;
if (samples <= 1) {
msaa = "msaa: off";
} else if (supported) {
msaa = String.format("msaa: cfg=%dx target=%dx %s %dx%d alloc=%s active=%s resolve=%s",
samples,
msaaStatus.targetSamples(),
msaaStatus.state(),
msaaStatus.width(),
msaaStatus.height(),
msaaStatus.allocated() ? "yes" : "no",
msaaStatus.active() ? "yes" : "no",
msaaStatus.lastResolveOk() ? "ok" : "no"
);
} else {
msaa = "msaa: unavailable";
}
List<String> out = new ArrayList<>(list.size() + 20);
out.add("§cCombatant Engine");
out.add("status:§aok");
out.add(msaa);
RenderThread2DDebugRenderer.Snapshot debug2d = RenderThread2DDebugRenderer.snapshot();
out.add(String.format("2d rt gui: frame=%d gui=%dx%d rects=%d err=%s",
debug2d.guiStateFrame(),
debug2d.guiWidth(),
debug2d.guiHeight(),
debug2d.guiStateRects(),
debug2d.guiStateError() == null || debug2d.guiStateError().isBlank() ? "none" : debug2d.guiStateError()
));
out.add(String.format("2d rt imm: frame=%d fb=%dx%d draws=%d/%d v=%d i=%d err=%s",
debug2d.immediateFrame(),
debug2d.framebufferWidth(),
debug2d.framebufferHeight(),
debug2d.immediateDraws(),
debug2d.immediateBatches(),
debug2d.immediateVertices(),
debug2d.immediateIndices(),
debug2d.immediateError() == null || debug2d.immediateError().isBlank() ? "none" : debug2d.immediateError()
));
Renderer2D.BatchStats batchStats = Renderer2D.getBatchStats();
out.add(String.format("ui batch: %s, last %d draws / %d batches",
batchStats.isActive() ? "active" : "idle",
batchStats.getLastDraws(),
batchStats.getLastOrder()
));
out.add(String.format("ui batch frame: %d draws / %d batches",
batchStats.getFrameDraws(),
batchStats.getFrameBatches()
));
out.add(String.format("ui batch geom: v=%d, i=%d, pool=%d",
batchStats.getLastVertices(),
batchStats.getLastIndices(),
batchStats.getPoolTotal()
));
out.add(String.format("ui batch frame geom: v=%d, i=%d",
batchStats.getFrameVertices(),
batchStats.getFrameIndices()
));
if (batchStats.getLastError() != null && !batchStats.getLastError().isBlank()) {
out.add("ui batch warn: " + batchStats.getLastError());
}
out.add(String.format("text msdf: ok=%d, fb=%d, attempts=%d",
FontDebugStats.getMsdfSuccess(),
FontDebugStats.getMsdfFallbacks(),
FontDebugStats.getMsdfAttempts()
));
if (!FontDebugStats.getLastMsdfFallback().isBlank()) {
String reason = FontDebugStats.getLastMsdfFallbackReason();
out.add("text msdf last fb: " + FontDebugStats.getLastMsdfFallback()
+ (reason.isBlank() ? "" : " (" + reason + ")"));
}
out.add(String.format("wav(al): buffers %d/%d, sources %d/%d",
WavDebugStats.getBuffersAlive(),
WavDebugStats.getBuffersCreated(),
WavDebugStats.getSourcesAlive(),
WavDebugStats.getSourcesCreated()
));
out.add(String.format("wav(al): allocs=%d (buf=%d, src=%d)",
WavDebugStats.getBuffersCreated() + WavDebugStats.getSourcesCreated(),
WavDebugStats.getBuffersCreated(),
WavDebugStats.getSourcesCreated()
));
out.add(String.format("wav(al): uploads=%d, bytes=%.2f MB",
WavDebugStats.getBufferUploads(),
WavDebugStats.getBufferBytes() / (1024.0 * 1024.0)
));
out.add("");
out.addAll(list);
return out;
}
}

View file

@ -0,0 +1,42 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.DynamicUniformStorage;
import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.util.logging.DebugLog;
@Mixin(DynamicUniformStorage.class)
public abstract class DynamicUniformStorageMixin {
@Final
@Shadow
private String label;
@Redirect(
method = {"writeUniform", "writeUniforms"},
at = @At(
value = "INVOKE",
target = "Lorg/slf4j/Logger;info(Ljava/lang/String;[Ljava/lang/Object;)V"
)
)
private void combatant$redirectResizeLog(Logger logger, String format, Object[] args) {
if (label != null && label.startsWith("Combatant")) {
String message = "Resizing " + args[0]
+ ", capacity limit of " + args[1]
+ " reached during a single frame. New capacity will be " + args[2] + ".";
DebugLog.renderThread("%s", message);
return;
}
logger.info(format, args);
}
}

View file

@ -0,0 +1,189 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.PlayerMoveEvent;
import dev.loki.lovisual.events.impl.PlayerStepEvent;
import dev.loki.lovisual.events.impl.PlayerStepSuccessEvent;
import dev.loki.lovisual.events.impl.PlayerVelocityStrafe;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.NoPush;
import dev.loki.lovisual.features.module.modules.movement.NoStun;
import dev.loki.lovisual.mixininterface.IEntity;
import dev.loki.lovisual.mixins.accessors.EntityAccessor;
import dev.loki.lovisual.mixins.accessors.EntityInvoker;
import dev.loki.lovisual.render.helpers.TrailPoint;
import dev.loki.lovisual.util.aiming.RotationManager;
import java.util.ArrayList;
import java.util.List;
@Mixin(Entity.class)
public abstract class EntityMixin implements IEntity {
@Unique
private final List<TrailPoint> trails = new ArrayList<>();
@Shadow
public double xOld;
@Shadow
public double yOld;
@Shadow
public double zOld;
@Unique
private MoverType combatant$lastMoveType = MoverType.SELF;
@Unique
private Vec3 combatant$stepBeforePos = Vec3.ZERO;
@Unique
private Vec3 combatant$stepRequestedMovement = Vec3.ZERO;
@Unique
@Override
public Vec3 get$InstantRenderPos() {
return new Vec3(xOld, yOld, zOld);
}
@Unique
@Override
public List<TrailPoint> combatant$getTrails() {
return trails;
}
@Inject(method = "move", at = @At("HEAD"))
private void combatant$cacheMoveType(MoverType type, Vec3 movement, CallbackInfo ci) {
combatant$lastMoveType = type;
Entity self = (Entity) (Object) this;
if (self instanceof LocalPlayer) {
combatant$stepBeforePos = self.position();
combatant$stepRequestedMovement = movement;
}
}
@ModifyVariable(method = "move", at = @At("HEAD"), argsOnly = true, index = 2)
private Vec3 combatant$onMove(Vec3 movement) {
Entity self = (Entity) (Object) this;
if (!(self instanceof LocalPlayer)) return movement;
if (!Events.BUS.hasListeners(PlayerMoveEvent.class)) return movement;
PlayerMoveEvent event = new PlayerMoveEvent(combatant$lastMoveType, movement);
Events.BUS.post(event);
Vec3 out = event.getMovement();
return out != null ? out : movement;
}
@Inject(method = "move", at = @At("RETURN"))
private void combatant$onMoveReturn(MoverType type, Vec3 movement, CallbackInfo ci) {
Entity self = (Entity) (Object) this;
if (!(self instanceof LocalPlayer)) return;
if (!Events.BUS.hasListeners(PlayerStepSuccessEvent.class)) return;
Vec3 before = combatant$stepBeforePos;
Vec3 after = self.position();
Vec3 requested = combatant$stepRequestedMovement;
double yDelta = after.y - before.y;
double horizontalRequested = Math.hypot(requested.x, requested.z);
if (yDelta <= 0.5 || horizontalRequested <= 1.0E-5) return;
Events.BUS.post(new PlayerStepSuccessEvent(after.subtract(before)));
}
@ModifyReturnValue(method = "maxUpStep", at = @At("RETURN"))
private float combatant$modifyStepHeight(float original) {
Entity self = (Entity) (Object) this;
if (!(self instanceof LocalPlayer)) return original;
if (!Events.BUS.hasListeners(PlayerStepEvent.class)) return original;
PlayerStepEvent event = new PlayerStepEvent(original);
Events.BUS.post(event);
return event.getHeight();
}
@Redirect(
method = "moveRelative",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/Entity;getInputVector(Lnet/minecraft/world/phys/Vec3;FF)Lnet/minecraft/world/phys/Vec3;"
)
)
private Vec3 combatant$velocityStrafe(Vec3 movementInput, float speed, float yaw) {
Entity self = (Entity) (Object) this;
Vec3 vanilla = EntityInvoker.combatant$movementInputToVelocity(movementInput, speed, yaw);
if (!(self instanceof LocalPlayer)) return vanilla;
if (!Events.BUS.hasListeners(PlayerVelocityStrafe.class)) return vanilla;
PlayerVelocityStrafe event = new PlayerVelocityStrafe(movementInput, speed, yaw, vanilla);
Events.BUS.post(event);
return event.getVelocity();
}
@ModifyVariable(
method = "calculateViewVector(FF)Lnet/minecraft/world/phys/Vec3;",
at = @At("HEAD"),
ordinal = 0,
argsOnly = true
)
private float combatant$rotationVectorPitch(float pitch) {
if (!((Object) this instanceof LocalPlayer)) {
return pitch;
}
return RotationManager.INSTANCE.getCurrentRotation() != null
? RotationManager.INSTANCE.getCurrentRotation().pitch()
: pitch;
}
@Redirect(
method = "push(Lnet/minecraft/world/entity/Entity;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/Entity;push(DDD)V"
)
)
private void combatant$cancelEntityPush(Entity instance, double x, double y, double z) {
NoPush noPush = Modules.get(NoPush.class);
if (noPush == null || !noPush.off("players")) {
instance.push(x, y, z);
}
}
@Inject(method = "makeStuckInBlock", at = @At("HEAD"), cancellable = true)
private void nostun$cancelSlowMovement(BlockState state, Vec3 multiplier, CallbackInfo ci) {
Entity self = (Entity) (Object) this;
if (!(self instanceof LocalPlayer)) return;
NoStun ns = Modules.get(NoStun.class);
if (ns == null || !ns.isEnabled()) return;
Vec3 override = ns.getSlowMovementOverride(state, multiplier);
if (override != null && self instanceof EntityAccessor accessor) {
self.resetFallDistance();
accessor.combatant$setMovementMultiplier(override);
ci.cancel();
return;
}
if (ns.shouldCancelSlowMovement(state)) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.entity.layers.EquipmentLayerRenderer;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.resources.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.render.ViewObstructionFadeContext;
import dev.loki.lovisual.render.iris.IrisRuntime;
@Mixin(EquipmentLayerRenderer.class)
public abstract class EquipmentLayerRendererMixin {
@Redirect(
method = "renderLayers(Lnet/minecraft/client/resources/model/EquipmentClientInfo$LayerType;Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/client/model/Model;Ljava/lang/Object;Lnet/minecraft/world/item/ItemStack;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;ILnet/minecraft/resources/Identifier;II)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/rendertype/RenderTypes;armorCutoutNoCull(Lnet/minecraft/resources/Identifier;)Lnet/minecraft/client/renderer/rendertype/RenderType;")
)
private RenderType combatant$useTranslucentArmorWhenViewFaded(Identifier texture) {
if (IrisRuntime.isRenderingShadowPass()) {
return RenderTypes.armorCutoutNoCull(texture);
}
if (ViewObstructionFadeContext.isActive() && ViewObstructionFadeContext.alpha() < 0.99f) {
return RenderTypes.armorTranslucent(texture);
}
return RenderTypes.armorCutoutNoCull(texture);
}
}

View file

@ -0,0 +1,51 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.contextualbar.ExperienceBar;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(ExperienceBar.class)
public abstract class ExperienceBarMixin {
@Inject(method = "extractBackground", at = @At("HEAD"), cancellable = true)
private void combatant$renderXpBar(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
// We render XP/Locator ourselves from GuiMixin to avoid vanilla bar state.
ci.cancel();
}
@Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true)
private void combatant$renderXpAddons(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
ci.cancel();
}
}

View file

@ -0,0 +1,74 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.Nullable;
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 dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.FireworkEvent;
import dev.loki.lovisual.util.aiming.RotationManager;
import dev.loki.lovisual.util.aiming.data.Rotation;
@Mixin(FireworkRocketEntity.class)
public abstract class FireworkRocketEntityMixin {
@Shadow
@Nullable
private LivingEntity attachedToEntity;
@Unique
private static Rotation resolveMoveRotation(LocalPlayer player) {
return RotationManager.INSTANCE.getMovementRotation();
}
@WrapOperation(
method = "tick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getLookAngle()Lnet/minecraft/world/phys/Vec3;"
)
)
private Vec3 combatant$fireworkMoveRotation(LivingEntity instance, Operation<Vec3> original) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || attachedToEntity != mc.player) {
return original.call(instance);
}
return resolveMoveRotation(mc.player).directionVector();
}
@WrapOperation(
method = "tick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getDeltaMovement()Lnet/minecraft/world/phys/Vec3;",
ordinal = 0
)
)
private Vec3 combatant$fireworkVelocityHook(LivingEntity instance, Operation<Vec3> original) {
Minecraft mc = Minecraft.getInstance();
Vec3 velocity = original.call(instance);
if (mc == null || attachedToEntity != mc.player || !Events.BUS.hasListeners(FireworkEvent.class)) {
return velocity;
}
FireworkEvent event = new FireworkEvent(velocity);
Events.BUS.post(event);
return event.getVector();
}
}

View file

@ -0,0 +1,55 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
* Portions derived from ThunderHack Recode, copyright (c) 2023-2024 Pan4ur & 06ED.
* Upstream: https://github.com/Pan4ur/ThunderHack-Recode
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.material.FlowingFluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.NoPush;
import static net.minecraft.world.level.material.FlowingFluid.FALLING;
@Mixin(FlowingFluid.class)
public abstract class FlowingFluidMixin {
@Shadow
protected abstract boolean isSolidFace(BlockGetter world, BlockPos pos, Direction direction);
@Inject(method = "getFlow", at = @At("HEAD"), cancellable = true)
private void combatant$getVelocity(BlockGetter world, BlockPos pos, FluidState state, CallbackInfoReturnable<Vec3> cir) {
NoPush np = Modules.get(NoPush.class);
if (np == null || !np.liquidsEnabled()) return;
double d = 0.0;
double e = 0.0;
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
Vec3 vec3d = new Vec3(d, 0.0, e);
if (state.getValue(FALLING)) {
for (Direction direction2 : Direction.Plane.HORIZONTAL) {
mutable.setWithOffset(pos, direction2);
if (!this.isSolidFace(world, mutable, direction2) && !this.isSolidFace(world, mutable.above(), direction2)) {
continue;
}
vec3d = vec3d.normalize().add(0.0, -6.0, 0.0);
break;
}
}
cir.setReturnValue(vec3d.normalize());
}
}

View file

@ -0,0 +1,83 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.fog.FogRenderer;
import net.minecraft.world.level.Level;
import org.joml.Vector4f;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(value = FogRenderer.class, priority = 1200)
public class FogRendererMixin {
@ModifyVariable(
method = "setupFog(Lnet/minecraft/client/Camera;ILnet/minecraft/client/DeltaTracker;FLnet/minecraft/client/multiplayer/ClientLevel;)Lnet/minecraft/client/renderer/fog/FogData;",
at = @At("HEAD"),
argsOnly = true,
index = 2)
private int combatant$disableDistanceFog(int value) {
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogControlEnabled()) return value;
Minecraft mc = Minecraft.getInstance();
ClientLevel world = mc != null ? mc.level : null;
if (world == null) return value;
int out = value;
if (module.disableDistanceFog()) {
int blocks = Math.max(16, module.getDistanceBlocks());
int chunks = (blocks + 15) / 16;
out = Math.max(out, chunks);
}
if (module.disableWeatherFog()
&& world.dimension() == Level.OVERWORLD
&& (world.getRainLevel(1.0f) > 0.0f
|| world.getThunderLevel(1.0f) > 0.0f)) {
int blocks = Math.max(16, module.getWeatherDistanceBlocks());
int chunks = (blocks + 15) / 16;
out = Math.max(out, chunks);
}
return out;
}
@Inject(
method = "computeFogColor(Lnet/minecraft/client/Camera;FLnet/minecraft/client/multiplayer/ClientLevel;IFLorg/joml/Vector4f;)V",
at = @At("RETURN"),
cancellable = true
)
private void combatant$modifyFogColor(Camera camera,
float tickDelta,
ClientLevel world,
int viewDistance,
float skyDarkness,
Vector4f color, CallbackInfo ci) {
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogModifyEnabled()) return;
int argb = module.getFogColorArgb();
float r = ((argb >> 16) & 0xFF) / 255f;
float g = ((argb >> 8) & 0xFF) / 255f;
float b = (argb & 0xFF) / 255f;
float a = 1.0f;
if (color != null) {
a = color.w;
color.set(r, g, b, a);
}
}
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.google.gson.JsonParseException;
import net.minecraft.client.gui.font.FontManager;
import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(FontManager.class)
public abstract class FontManagerMixin {
@Redirect(
method = "loadResourceStack",
at = @At(
value = "INVOKE",
target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;[Ljava/lang/Object;)V"
)
)
private static void combatant$maybeSuppressFontLoadWarn(
Logger logger,
String format,
Object[] args
) {
if (args != null && args.length > 0) {
Object last = args[args.length - 1];
if (last instanceof JsonParseException jsonParseException) {
String message = jsonParseException.getMessage();
if (message != null && message.contains("No key providers")) {
return;
}
}
}
logger.warn(format, args);
}
}

View file

@ -0,0 +1,811 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.loki.lovisual.features.module.modules.visuals.*;
import dev.loki.lovisual.render.engine.postprocess.PostProcessPass;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import net.minecraft.client.renderer.SubmitNodeStorage;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.client.renderer.feature.FeatureRenderDispatcher;
import net.minecraft.client.renderer.state.GameRenderState;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.util.Mth;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.level.material.FogType;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Final;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
import dev.loki.lovisual.addon.AddonRenderPipelineManager;
import dev.loki.lovisual.api.v0.render.LoVisualRenderStage;
import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.WorldPhase;
import dev.loki.lovisual.features.module.modules.visuals.*;
import dev.loki.lovisual.mixins.accessors.GameRendererAccessor;
import dev.loki.lovisual.mixins.accessors.LocalPlayerAccessor;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.core.CombatantWorldMatrices;
import dev.loki.lovisual.render.engine.core.RenderPhase;
import dev.loki.lovisual.render.engine.core.RenderPhaseScope;
import dev.loki.lovisual.render.engine.depth.WorldSceneDepth;
// import dev.loki.lovisual.render.engine.debug.RenderThread2DDebugRenderer; // disabled: debug probes are manual-only
import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget;
import dev.loki.lovisual.config.MainConfig;
import dev.loki.lovisual.render.engine.pipeline.CombatantRenderPipelines;
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
import dev.loki.lovisual.render.engine.postprocess.MenuBackgroundRenderer;
import dev.loki.lovisual.render.engine.profiler.ProfilerPhase;
import dev.loki.lovisual.render.engine.profiler.RenderProfiler3D;
import dev.loki.lovisual.render.engine.profiler.TracyGpuProfiler;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.engine.visuals.CombatantVisuals;
import dev.loki.lovisual.render.iris.IrisCombatantFrameHooks;
import dev.loki.lovisual.render.iris.IrisFinalizedSceneRenderer;
import dev.loki.lovisual.render.iris.IrisCompatibilityGuards;
import dev.loki.lovisual.render.iris.IrisRuntime;
import dev.loki.lovisual.util.logging.DebugLog;
@Mixin(GameRenderer.class)
public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer {
@Unique
private static final boolean COMBATANT_DEBUG_PRIMITIVE = false;
@Unique
private static final boolean COMBATANT_DEBUG_CLIP_SPACE = false;
@Unique
private static final long COMBATANT_DEBUG_INTERVAL_MS = 2000L;
@Unique
private static long COMBATANT_DEBUG_LAST_MS;
@Unique
private final PoseStack combatant$matrices = new PoseStack();
@Unique
private final PoseStack combatant$postMatrices = new PoseStack();
@Shadow
@Final
private Minecraft minecraft;
@Shadow
@Final
private Camera mainCamera;
@Shadow
@Final
private GameRenderState gameRenderState;
@Shadow
private float spinningEffectTime;
@Shadow
private float spinningEffectSpeed;
@Unique
private Renderer3D combatant$renderer;
@Unique
private Renderer3D combatant$depthRenderer;
@Unique
private Renderer3D combatant$postRenderer;
@Unique
private Renderer3D combatant$postDepthRenderer;
@Unique
private static void combatant$drawDebugCube(Renderer3D rdr,
double x1, double y1, double z1,
double x2, double y2, double z2) {
rdr.line(x1, y1, z1, x2, y1, z1, 255, 0, 0, 255);
rdr.line(x1, y2, z1, x2, y2, z1, 255, 0, 0, 255);
rdr.line(x1, y1, z2, x2, y1, z2, 255, 0, 0, 255);
rdr.line(x1, y2, z2, x2, y2, z2, 255, 0, 0, 255);
rdr.line(x1, y1, z1, x1, y2, z1, 0, 255, 0, 255);
rdr.line(x2, y1, z1, x2, y2, z1, 0, 255, 0, 255);
rdr.line(x1, y1, z2, x1, y2, z2, 0, 255, 0, 255);
rdr.line(x2, y1, z2, x2, y2, z2, 0, 255, 0, 255);
rdr.line(x1, y1, z1, x1, y1, z2, 0, 0, 255, 255);
rdr.line(x2, y1, z1, x2, y1, z2, 0, 0, 255, 255);
rdr.line(x1, y2, z1, x1, y2, z2, 0, 0, 255, 255);
rdr.line(x2, y2, z1, x2, y2, z2, 0, 0, 255, 255);
}
@Unique
private static boolean isWorldTranslucent(FogType type) {
return type == FogType.WATER
|| type == FogType.LAVA
|| type == FogType.POWDER_SNOW;
}
@Shadow
protected abstract void bobView(CameraRenderState cameraRenderState, PoseStack matrices);
@Shadow
protected abstract void bobHurt(CameraRenderState cameraRenderState, PoseStack matrices);
@Unique
private static boolean combatant$needsResolvedMainDepth() {
ReimaginedVisual module = Modules.get(ReimaginedVisual.class);
if (module == null) return false;
if (module.needsResolvedMainDepthCapture()) return true;
return MainConfig.get().getMsaa3dSamples() > 1 && module.isActive();
}
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void combatant$skipTickWithoutPlayer(CallbackInfo ci) {
if (minecraft != null && minecraft.level != null && minecraft.player == null) {
ci.cancel();
}
}
@Inject(method = "extract(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("HEAD"))
private void combatant$beginDeferred2DExtract(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {
Renderer2D.beginDeferredExtractFrame();
}
@Inject(method = "extract(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("TAIL"))
private void combatant$endDeferred2DExtract(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {
Renderer2D.endDeferredExtractFrame();
}
@Redirect(
method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/GameRenderer;bobView(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;)V"
)
)
private void combatant$viewModel$replaceHandBobbing(GameRenderer instance,
CameraRenderState cameraRenderState,
PoseStack matrices,
CameraRenderState originalCameraRenderState,
float tickDelta,
Matrix4fc positionMatrix) {
ViewModel viewModel = Modules.get(ViewModel.class);
if (viewModel != null && viewModel.smoothRunLoweringEnabled()) {
if (minecraft != null && minecraft.getCameraEntity() instanceof AbstractClientPlayer player) {
float movement = player.avatarState().getInterpolatedBob(tickDelta);
float yOffset = -Mth.clamp(movement, 0.0f, 1.0f) * viewModel.getSmoothRunLoweringAmount();
if (Math.abs(yOffset) > 0.0001f) {
matrices.translate(0.0f, yOffset, 0.0f);
}
}
return;
}
((GameRendererAccessor) instance).invokeBobView(cameraRenderState, matrices);
}
@Inject(method = "extractCamera", at = @At("TAIL"))
private void combatant$modifyAspect(DeltaTracker tickCounter, float tickDelta, float fov, CallbackInfo ci) {
AspectRatio ar = Modules.get(AspectRatio.class);
if (ar == null || !ar.isEnabled()) {
return;
}
float mul = ar.getMultiplier();
if (mul == 1.0f) {
return;
}
Matrix4f m = gameRenderState.levelRenderState.cameraRenderState.projectionMatrix;
if (m == null) return;
// horizontal aspect stretch
m.m00(m.m00() * mul);
}
@Inject(method = "extractCamera", at = @At("TAIL"))
private void combatant$captureProjectionFor2DOverlays(DeltaTracker tickCounter, float tickDelta, float fov, CallbackInfo ci) {
if (gameRenderState == null || gameRenderState.levelRenderState == null) return;
CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState;
if (cameraRenderState == null || cameraRenderState.viewRotationMatrix == null || cameraRenderState.projectionMatrix == null) return;
PoseStack effectMatrices = new PoseStack();
Matrix4f renderProjection = combatant$buildWorldProjection(fov, effectMatrices, tickDelta);
AspectRatio.applyToProjection(renderProjection);
Matrix4f screenProjection = new Matrix4f(cameraRenderState.projectionMatrix);
CombatantWorldMatrices.capture(
cameraRenderState.viewRotationMatrix,
renderProjection,
screenProjection,
cameraRenderState.pos != null ? cameraRenderState.pos : mainCamera.position()
);
}
@Inject(method = "mainRenderTarget", at = @At("HEAD"), cancellable = true)
private void combatant$overrideWorldFramebuffer(CallbackInfoReturnable<com.mojang.blaze3d.pipeline.RenderTarget> cir) {
com.mojang.blaze3d.pipeline.RenderTarget msaa = MsaaWorldTarget.getFramebufferOverride();
if (msaa != null) {
cir.setReturnValue(msaa);
}
}
@Unique
private void combatant$renderWorldEngine(DeltaTracker tickCounter) {
try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:world");
TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:world")) {
float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true);
float fov = mainCamera.getFov();
Matrix4f capturedPosition = CombatantWorldMatrices.positionMatrix();
Matrix4f capturedProjection = CombatantWorldMatrices.renderProjectionMatrix();
boolean useCapturedMatrices = capturedPosition != null && capturedProjection != null;
Matrix4f position = useCapturedMatrices
? capturedPosition
: new Matrix4f().rotation(mainCamera.rotation().conjugate(new org.joml.Quaternionf()));
RenderState.rendering3D = true;
RenderState.tickDelta = tickDelta;
Vec3 capturedCameraPos = CombatantWorldMatrices.cameraPosition();
RenderState.cameraPos = capturedCameraPos != null ? capturedCameraPos : mainCamera.position();
RenderState.cameraRotation.set(mainCamera.rotation());
RenderState.cameraYaw = mainCamera.yRot();
RenderState.cameraPitch = mainCamera.xRot();
RenderState.cameraLook = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize();
RenderState.cameraSubmersion = mainCamera.getFluidInCamera();
RenderState.worldTranslucent = isWorldTranslucent(RenderState.cameraSubmersion);
RenderState.cameraLook = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize();
RenderState.cameraSubmersion = mainCamera.getFluidInCamera();
RenderState.worldTranslucent = isWorldTranslucent(RenderState.cameraSubmersion);
RenderState.frustum = mainCamera != null
? mainCamera.getCapturedFrustum()
: null;
if (COMBATANT_DEBUG_CLIP_SPACE) {
var mv = com.mojang.blaze3d.systems.RenderSystem.getModelViewStack();
mv.pushMatrix();
mv.identity();
MeshRenderer.setProjection(new Matrix4f().identity());
RenderState.rendering3D = false;
if (combatant$renderer == null) {
combatant$renderer = new Renderer3D(
CombatantRenderPipelines.WORLD_COLORED_LINES,
CombatantRenderPipelines.WORLD_COLORED
);
}
combatant$renderer.begin();
// Clip-space square + diagonals (NDC)
combatant$renderer.line(-0.8, -0.8, 0.0, 0.8, -0.8, 0.0, 255, 0, 0, 255);
combatant$renderer.line(0.8, -0.8, 0.0, 0.8, 0.8, 0.0, 0, 255, 0, 255);
combatant$renderer.line(0.8, 0.8, 0.0, -0.8, 0.8, 0.0, 0, 0, 255, 255);
combatant$renderer.line(-0.8, 0.8, 0.0, -0.8, -0.8, 0.0, 255, 255, 0, 255);
combatant$renderer.line(-0.8, -0.8, 0.0, 0.8, 0.8, 0.0, 255, 0, 255, 255);
combatant$renderer.line(0.8, -0.8, 0.0, -0.8, 0.8, 0.0, 0, 255, 255, 255);
combatant$renderer.render(new PoseStack());
mv.popMatrix();
RenderState.rendering3D = false;
return;
}
if (combatant$renderer == null) {
combatant$renderer = new Renderer3D(
CombatantRenderPipelines.WORLD_COLORED_LINES,
CombatantRenderPipelines.WORLD_COLORED
);
}
if (combatant$depthRenderer == null) {
combatant$depthRenderer = new Renderer3D(
CombatantRenderPipelines.WORLD_COLORED_LINES_DEPTH,
CombatantRenderPipelines.WORLD_COLORED_DEPTH
);
}
var mv = com.mojang.blaze3d.systems.RenderSystem.getModelViewStack();
mv.pushMatrix();
if (IrisCombatantFrameHooks.isRenderingAfterIrisFinalization()) {
mv.identity();
}
mv.mul(position);
combatant$matrices.pushPose();
Matrix4f projection = useCapturedMatrices
? capturedProjection
: combatant$buildWorldProjection(fov, combatant$matrices, tickDelta);
MeshRenderer.setProjection(projection);
CombatantRenderSystem.beginFrame(
tickDelta,
tickCounter.getGameTimeDeltaTicks(),
tickCounter.getRealtimeDeltaTicks(),
projection,
new Matrix4f(mv)
);
try (RenderPhaseScope combatant$beforeTranslucentPhase = CombatantRenderSystem.phase(RenderPhase.WORLD_BEFORE_TRANSLUCENT)) {
RenderState.worldProjection.set(projection);
RenderState.frustum = new Frustum(position, projection);
RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z);
if (!useCapturedMatrices) {
Matrix4f inverseBob = new Matrix4f(combatant$matrices.last().pose()).invert();
mv.mul(inverseBob);
}
combatant$renderer.begin();
combatant$depthRenderer.begin();
if (COMBATANT_DEBUG_PRIMITIVE) {
Vec3 camPos = mainCamera.position();
float yaw = mainCamera.yRot() * ((float) Math.PI / 180.0f);
float pitch = mainCamera.xRot() * ((float) Math.PI / 180.0f);
Vec3 forward = new Vec3(
-Mth.sin(yaw) * Mth.cos(pitch),
-Mth.sin(pitch),
Mth.cos(yaw) * Mth.cos(pitch)
);
Vec3 center = camPos.add(forward.scale(3.0));
double r = 0.5;
double x1 = center.x - r, x2 = center.x + r;
double y1 = center.y - r, y2 = center.y + r;
double z1 = center.z - r, z2 = center.z + r;
double ox1 = -1.0, oy1 = -1.0, oz1 = -1.0;
double ox2 = 1.0, oy2 = 1.0, oz2 = 1.0;
// Wire cube (no depth) and depth variant
Renderer3D[] targets = new Renderer3D[]{combatant$renderer, combatant$depthRenderer};
for (Renderer3D rdr : targets) {
combatant$drawDebugCube(rdr, x1, y1, z1, x2, y2, z2);
combatant$drawDebugCube(rdr, ox1, oy1, oz1, ox2, oy2, oz2);
}
long now = System.currentTimeMillis();
if (now - COMBATANT_DEBUG_LAST_MS >= COMBATANT_DEBUG_INTERVAL_MS) {
COMBATANT_DEBUG_LAST_MS = now;
Matrix4f mvMat = com.mojang.blaze3d.systems.RenderSystem.getModelViewMatrixCopy();
DebugLog.renderThread(
"[Combatant] Debug diag: cam=" + camPos
+ " proj=[" + projection.m00() + "," + projection.m11() + "," + projection.m22() + "," + projection.m33() + "]"
+ " mv=[" + mvMat.m30() + "," + mvMat.m31() + "," + mvMat.m32() + "]"
);
}
}
try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:modules_before_translucent");
RenderProfiler3D.Section ignored = RenderProfiler3D.section("modules_before_translucent")) {
ModuleManager.renderWorldEngine(WorldPhase.BEFORE_TRANSLUCENT, combatant$renderer, combatant$depthRenderer, tickDelta);
AddonRenderPipelineManager.render3D(LoVisualRenderStage.WORLD_BEFORE_TRANSLUCENT,
WorldPhase.BEFORE_TRANSLUCENT, combatant$renderer, combatant$depthRenderer, combatant$matrices, tickDelta);
}
try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:flush_before_translucent");
TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone("3d:flush_before_translucent");
RenderProfiler3D.Section ignored = RenderProfiler3D.section("flush_before_translucent")) {
combatant$renderer.render(combatant$matrices);
combatant$depthRenderer.render(combatant$matrices);
}
}
// Start a new batch for END_MAIN so overlays render above grass.
combatant$renderer.begin();
combatant$depthRenderer.begin();
try (RenderPhaseScope combatant$afterTranslucentPhase = CombatantRenderSystem.phase(RenderPhase.WORLD_AFTER_TRANSLUCENT)) {
try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:modules_end_main");
RenderProfiler3D.Section ignored = RenderProfiler3D.section("modules_end_main")) {
ModuleManager.renderWorldEngine(WorldPhase.END_MAIN, combatant$renderer, combatant$depthRenderer, tickDelta);
AddonRenderPipelineManager.render3D(LoVisualRenderStage.WORLD_END_MAIN,
WorldPhase.END_MAIN, combatant$renderer, combatant$depthRenderer, combatant$matrices, tickDelta);
}
try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:flush_end_main");
TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone("3d:flush_end_main");
RenderProfiler3D.Section ignored = RenderProfiler3D.section("flush_end_main")) {
combatant$renderer.render(combatant$matrices);
combatant$depthRenderer.render(combatant$matrices);
}
}
combatant$matrices.popPose();
mv.popMatrix();
RenderState.rendering3D = false;
}
}
@Inject(method = "renderLevel", at = @At("HEAD"))
private void combatant$beginWorldMsaa(DeltaTracker tickCounter, CallbackInfo ci) {
IrisCombatantFrameHooks.beginRenderLevel(tickCounter);
CombatantWorldMatrices.reset();
int samples = MainConfig.get().getMsaa3dSamples();
MsaaWorldTarget.begin(minecraft, samples, combatant$needsResolvedMainDepth());
}
@Inject(method = "renderLevel", at = @At("RETURN"))
private void combatant$endIrisFrameHooks(DeltaTracker tickCounter, CallbackInfo ci) {
IrisCombatantFrameHooks.endRenderLevel(tickCounter);
}
@WrapOperation(
method = "renderLevel",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/ProjectionMatrixBuffer;getBuffer(Lorg/joml/Matrix4f;)Lcom/mojang/blaze3d/buffers/GpuBufferSlice;"
)
)
private GpuBufferSlice combatant$captureFullWorldMatrices(ProjectionMatrixBuffer instance,
Matrix4f renderProjectionMatrix,
Operation<GpuBufferSlice> original) {
if (renderProjectionMatrix != null
&& gameRenderState != null
&& gameRenderState.levelRenderState != null
&& gameRenderState.levelRenderState.cameraRenderState != null
&& gameRenderState.levelRenderState.cameraRenderState.viewRotationMatrix != null
&& gameRenderState.levelRenderState.cameraRenderState.projectionMatrix != null) {
CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState;
CombatantWorldMatrices.capture(
cameraRenderState.viewRotationMatrix,
renderProjectionMatrix,
cameraRenderState.projectionMatrix,
cameraRenderState.pos != null ? cameraRenderState.pos : mainCamera.position()
);
}
return original.call(instance, renderProjectionMatrix);
}
@Inject(
method = "renderLevel",
at = @At(
value = "INVOKE_STRING",
target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V",
args = "ldc=hand",
shift = At.Shift.AFTER
)
)
private void combatant$postProcess(DeltaTracker tickCounter, CallbackInfo ci) {
if (IrisRuntime.isShaderpackRendererActive()
&& IrisCompatibilityGuards.deferIrisFinalizationForSecondHandScene()) {
return;
}
combatant$renderPreHandPostProcess(tickCounter);
}
@Unique
private void combatant$renderPreHandPostProcess(DeltaTracker tickCounter) {
ProfilerPhase.begin("3d:post_pre_hand");
try (TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:post_pre_hand")) {
if (minecraft != null) {
boolean needsResolvedDepth = !IrisRuntime.isShaderpackRendererActive()
&& combatant$needsResolvedMainDepth();
boolean capturedMsaaDepth = needsResolvedDepth
&& MsaaWorldTarget.isActive()
&& WorldSceneDepth.captureResolvedMain(MsaaWorldTarget.getMsaaFramebuffer());
MsaaWorldTarget.resolveToMain(minecraft);
if (needsResolvedDepth && !capturedMsaaDepth) {
WorldSceneDepth.captureResolvedMain(minecraft.gameRenderer.mainRenderTarget());
}
CombatantVisuals.renderWorldBase(minecraft, tickCounter.getGameTimeDeltaPartialTick(true));
}
try (RenderPhaseScope combatant$postPreHandPhase = CombatantRenderSystem.phase(RenderPhase.WORLD_POST_PRE_HAND)) {
PostProcessManager.renderAll(PostProcessPass.Phase.PRE_HAND,
tickCounter.getGameTimeDeltaPartialTick(true));
combatant$renderCombatantWorldAfterPreHandPostProcess(tickCounter);
}
}
ProfilerPhase.end("3d:post_pre_hand");
}
@Unique
private void combatant$renderCombatantWorldAfterPreHandPostProcess(DeltaTracker tickCounter) {
if (minecraft == null) {
combatant$renderWorldEngine(tickCounter);
combatant$renderPostProcessWorld(tickCounter);
return;
}
MsaaWorldTarget.begin(minecraft, MainConfig.get().getMsaa3dSamples(), false, true);
try {
combatant$renderWorldEngine(tickCounter);
combatant$renderPostProcessWorld(tickCounter);
} finally {
MsaaWorldTarget.resolveToMain(minecraft);
}
}
@Override
public void combatant$renderAfterIrisFinalization(DeltaTracker tickCounter) {
combatant$renderPreHandPostProcess(tickCounter);
}
@Inject(
method = "renderLevel",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher;renderAllFeatures(Lnet/minecraft/client/renderer/SubmitNodeStorage;)V",
shift = At.Shift.AFTER
)
)
private void combatant$postProcessAfterHand(DeltaTracker tickCounter, CallbackInfo ci) {
ProfilerPhase.begin("3d:post_after_hand");
try (TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:post_after_hand")) {
if (minecraft != null) {
MsaaWorldTarget.resolveToMain(minecraft);
}
try (RenderPhaseScope combatant$postHandPhase = CombatantRenderSystem.phase(RenderPhase.WORLD_POST_HAND)) {
PostProcessManager.renderAll(PostProcessPass.Phase.POST_HAND,
tickCounter.getGameTimeDeltaPartialTick(true));
}
}
ProfilerPhase.end("3d:post_after_hand");
}
@Unique
private void combatant$renderPostProcessWorld(DeltaTracker tickCounter) {
try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:world_post");
TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:world_post")) {
float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true);
float fov = mainCamera.getFov();
Matrix4f capturedPosition = CombatantWorldMatrices.positionMatrix();
Matrix4f capturedProjection = CombatantWorldMatrices.renderProjectionMatrix();
boolean useCapturedMatrices = capturedPosition != null && capturedProjection != null;
Matrix4f position = useCapturedMatrices
? capturedPosition
: new Matrix4f().rotation(mainCamera.rotation().conjugate(new org.joml.Quaternionf()));
RenderState.rendering3D = true;
RenderState.tickDelta = tickDelta;
Vec3 capturedCameraPos = CombatantWorldMatrices.cameraPosition();
RenderState.cameraPos = capturedCameraPos != null ? capturedCameraPos : mainCamera.position();
RenderState.cameraRotation.set(mainCamera.rotation());
RenderState.cameraYaw = mainCamera.yRot();
RenderState.cameraPitch = mainCamera.xRot();
if (combatant$postRenderer == null) {
combatant$postRenderer = new Renderer3D(
CombatantRenderPipelines.WORLD_COLORED_LINES,
CombatantRenderPipelines.WORLD_COLORED
);
}
if (combatant$postDepthRenderer == null) {
combatant$postDepthRenderer = new Renderer3D(
CombatantRenderPipelines.WORLD_COLORED_LINES_DEPTH,
CombatantRenderPipelines.WORLD_COLORED_DEPTH
);
}
var mv = com.mojang.blaze3d.systems.RenderSystem.getModelViewStack();
mv.pushMatrix();
if (IrisCombatantFrameHooks.isRenderingAfterIrisFinalization()) {
mv.identity();
}
mv.mul(position);
combatant$postMatrices.pushPose();
Matrix4f projection = useCapturedMatrices
? capturedProjection
: combatant$buildWorldProjection(fov, combatant$postMatrices, tickDelta);
MeshRenderer.setProjection(projection);
CombatantRenderSystem.beginFrame(
tickDelta,
tickCounter.getGameTimeDeltaTicks(),
tickCounter.getRealtimeDeltaTicks(),
projection,
new Matrix4f(mv)
);
try (RenderPhaseScope combatant$postWorldPhase = CombatantRenderSystem.phase(RenderPhase.WORLD_POST_PRE_HAND)) {
RenderState.worldProjection.set(projection);
RenderState.frustum = new Frustum(position, projection);
RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z);
if (!useCapturedMatrices) {
Matrix4f inverseBob = new Matrix4f(combatant$postMatrices.last().pose()).invert();
mv.mul(inverseBob);
}
combatant$postRenderer.begin();
combatant$postDepthRenderer.begin();
try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:post_modules");
RenderProfiler3D.Section ignored = RenderProfiler3D.section("modules")) {
ModuleManager.renderWorldEngine(WorldPhase.AFTER_POST_PROCESS, combatant$postRenderer, combatant$postDepthRenderer, tickDelta);
AddonRenderPipelineManager.render3D(LoVisualRenderStage.WORLD_POST_PROCESS,
WorldPhase.AFTER_POST_PROCESS, combatant$postRenderer, combatant$postDepthRenderer, combatant$postMatrices, tickDelta);
}
try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:post_flush");
TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone("3d:post_flush");
RenderProfiler3D.Section ignored = RenderProfiler3D.section("flush")) {
combatant$postRenderer.render(combatant$postMatrices);
combatant$postDepthRenderer.render(combatant$postMatrices);
}
}
combatant$postMatrices.popPose();
mv.popMatrix();
RenderState.rendering3D = false;
}
}
@Unique
private Matrix4f combatant$buildWorldProjection(float fov, PoseStack viewEffectMatrices, float tickDelta) {
CameraRenderState cameraRenderState = new CameraRenderState();
mainCamera.extractRenderState(cameraRenderState, fov);
Matrix4f projection = new Matrix4f(cameraRenderState.projectionMatrix);
combatant$applyViewEffectMatrices(viewEffectMatrices, tickDelta);
projection.mul(viewEffectMatrices.last().pose());
combatant$applyNauseaProjection(projection, tickDelta);
return projection;
}
@Unique
private void combatant$applyViewEffectMatrices(PoseStack matrices, float cameraTickProgress) {
NoRender noRender = Modules.get(NoRender.class);
CameraRenderState cameraRenderState = new CameraRenderState();
mainCamera.extractRenderState(cameraRenderState, mainCamera.getFov());
if (noRender == null || !noRender.off("camera_shake")) {
bobHurt(cameraRenderState, matrices);
}
boolean freecamEnabled = Modules.get(Freecam.class) != null && Modules.get(Freecam.class).isEnabled();
if (minecraft.options.bobView().get()
&& !freecamEnabled
&& (noRender == null || !noRender.off("view_bob"))) {
bobView(cameraRenderState, matrices);
}
}
@Unique
private void combatant$applyNauseaProjection(Matrix4f projection, float tickDelta) {
LocalPlayer player = minecraft.player;
if (player == null) return;
float distortionScale = minecraft.options.screenEffectScale().get().floatValue();
if (distortionScale <= 0.0f) return;
float nauseaIntensity = 0.0f;
if (player instanceof LocalPlayerAccessor accessor) {
nauseaIntensity = Mth.lerp(
tickDelta,
accessor.combatant$getLastNauseaIntensity(),
accessor.combatant$getNauseaIntensity()
);
}
float nauseaEffect = player.getEffectBlendFactor(MobEffects.NAUSEA, tickDelta);
float strength = Math.max(nauseaIntensity, nauseaEffect) * (distortionScale * distortionScale);
if (strength <= 0.0f) return;
float scale = 5.0f / (strength * strength + 5.0f) - strength * 0.04f;
scale *= scale;
Vector3f axis = new Vector3f(0.0f, Mth.SQRT_OF_TWO / 2.0f, Mth.SQRT_OF_TWO / 2.0f);
float angle = (this.spinningEffectTime + tickDelta * this.spinningEffectSpeed) * Mth.DEG_TO_RAD;
projection.rotate(angle, axis);
projection.scale(1.0f / scale, 1.0f, 1.0f);
projection.rotate(-angle, axis);
}
@Inject(method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", at = @At("HEAD"), cancellable = true)
private void freecam$hand(CameraRenderState cameraRenderState, float tickDelta, Matrix4fc positionMatrix, CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && !fc.renderHand()) {
ci.cancel();
}
}
@WrapOperation(
method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher;renderAllFeatures(Lnet/minecraft/client/renderer/SubmitNodeStorage;)V"
)
)
private void combatant$renderChamsHandPreparedFrame(FeatureRenderDispatcher dispatcher,
SubmitNodeStorage storage,
Operation<Void> original,
CameraRenderState cameraRenderState,
float tickDelta,
Matrix4fc positionMatrix) {
Chams module = Modules.get(Chams.class);
if (module != null && module.renderPreparedHandScene(dispatcher, storage)) {
return;
}
original.call(dispatcher, storage);
}
@Inject(method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("TAIL"))
private void combatant$flushQueuedButtons(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || ClientScreen.current() == null) return;
if (!BetterButtons.hasPending()) {
BetterButtons.renderTooltip();
return;
}
BetterButtons.flush();
BetterButtons.renderTooltip();
}
@Inject(
method = "render(Lnet/minecraft/client/DeltaTracker;Z)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V",
shift = At.Shift.BEFORE
)
)
private void combatant$captureWorldForHudGlass(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {
MenuBackgroundRenderer.drainDeferred(minecraft);
Renderer2D.captureWorldGlassSource();
if (AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.SCREEN_BEFORE_VANILLA_GUI)) {
float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true);
Renderer2D.COLOR.begin();
AddonRenderPipelineManager.render2D(LoVisualRenderStage.SCREEN_BEFORE_VANILLA_GUI,
null, Renderer2D.COLOR, TextRenderer.get(), null, tickDelta);
Renderer2D.COLOR.render();
}
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI);
combatant$drainHudLayersBelowScreenGui();
}
/**
* Gui.extractRenderState() extracts vanilla HUD first and the current Screen after it.
* Normal gameplay keeps Combatant HUD spliced into that HUD order through HudDeferredGuiElement
* markers. When a real Screen is open, however, the screen must remain the top GUI owner:
* HUD/world overlaysare drained before GuiRenderer.render(),
* preserving their internal order while keeping container/inventory GUI above them.
* Explicit SCREEN_TOP/AFTER_VANILLA_GUI overlays (Dynamic Island, ClickGui top layer, etc.) are
* intentionally not drained here.
*/
@Unique
private static void combatant$drainHudLayersBelowScreenGui() {
if (ClientScreen.current() == null) {
return;
}
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_FIRST);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_MISC_OVERLAYS);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_BOSS_BAR);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_DEMO_TIMER);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_CHAT);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_SUBTITLES);
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_LAST);
}
@Inject(
method = "render(Lnet/minecraft/client/DeltaTracker;Z)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V",
shift = At.Shift.AFTER
)
)
private void combatant$debug2dImmediateAfterGui(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) {
Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.AFTER_VANILLA_GUI);
if (AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.SCREEN_AFTER_VANILLA_GUI)) {
float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true);
Renderer2D.COLOR.begin();
AddonRenderPipelineManager.render2D(LoVisualRenderStage.SCREEN_AFTER_VANILLA_GUI,
null, Renderer2D.COLOR, TextRenderer.get(), null, tickDelta);
Renderer2D.COLOR.render();
}
// Debug 2D probe intentionally disabled after projection validation.
// RenderThread2DDebugRenderer.renderImmediateAfterGui(tickCounter);
}
}

View file

@ -0,0 +1,44 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.state.level.CameraRenderState;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
@Mixin(GameRenderer.class)
public abstract class GameRendererViewEffectsMixin {
@Inject(method = "bobHurt", at = @At("HEAD"), cancellable = true)
private void disableHurtCamera(CameraRenderState cameraRenderState, PoseStack matrices, CallbackInfo ci) {
NoRender noRender = Modules.get(NoRender.class);
if (noRender != null && noRender.off("camera_shake")) {
ci.cancel();
}
}
@Inject(method = "bobView", at = @At("HEAD"), cancellable = true)
private void disableViewBobbing(CameraRenderState cameraRenderState, PoseStack matrices, CallbackInfo ci) {
if (Modules.get(Freecam.class) != null && Modules.get(Freecam.class).isEnabled()) {
ci.cancel();
return;
}
NoRender noRender = Modules.get(NoRender.class);
if (noRender != null && noRender.off("view_bob")) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,25 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.opengl.DirectStateAccess;
import com.mojang.blaze3d.opengl.FrameBufferCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
import dev.loki.lovisual.mixininterface.IGlBackendInfo;
@Mixin(targets = "com.mojang.blaze3d.opengl.GlDevice")
public abstract class GlDeviceBackendMixin implements IGlBackendInfo {
@Override
@Invoker("directStateAccess")
public abstract DirectStateAccess combatant$directStateAccess();
@Override
@Invoker("frameBufferCache")
public abstract FrameBufferCache combatant$frameBufferCache();
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.opengl.GlStateManager;
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;
import dev.loki.lovisual.render.engine.profiler.GlSyncTracker;
@Mixin(GlStateManager.class)
public class GlStateManagerMixin {
@Inject(method = "_glFenceSync", at = @At("RETURN"))
private static void combatant$onFenceSync(int condition, int flags, CallbackInfoReturnable<Long> cir) {
GlSyncTracker.onFence(cir.getReturnValue());
}
@Inject(method = "_glClientWaitSync", at = @At("HEAD"))
private static void combatant$onClientWaitSync(long sync, int flags, long timeout, CallbackInfoReturnable<Integer> cir) {
GlSyncTracker.onWaitStart(sync);
}
@Inject(method = "_glClientWaitSync", at = @At("RETURN"))
private static void combatant$onClientWaitSyncReturn(long sync, int flags, long timeout, CallbackInfoReturnable<Integer> cir) {
GlSyncTracker.onWaitEnd();
}
}

View file

@ -0,0 +1,58 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.opengl.GlTexture;
import org.spongepowered.asm.mixin.Final;
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;
import dev.loki.lovisual.mixininterface.IMsaaTexture;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
@Mixin(GlTexture.class)
public abstract class GlTextureMixin implements IMsaaTexture {
@Final
@Shadow
protected int id;
@Unique
private int combatant$samples = 1;
@Override
public void combatant$setSamples(int samples) {
combatant$samples = samples;
}
@Override
public int combatant$getSamples() {
return combatant$samples;
}
@Override
public boolean combatant$isMsaa() {
return combatant$samples > 1;
}
@Override
public int combatant$getGlId() {
return id;
}
@Inject(method = "destroyImmediately", at = @At("HEAD"))
private void combatant$onFree(CallbackInfo ci) {
CombatantRenderSystem.rhi().msaa().unregisterTexture(id);
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.systems.GpuDevice;
import com.mojang.blaze3d.textures.GpuTexture;
import org.spongepowered.asm.mixin.Mixin;
import dev.loki.lovisual.mixininterface.IGpuDevice;
import dev.loki.lovisual.mixininterface.IMsaaDevice;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.rhi.scissor.GlobalScissorState;
@Mixin(GpuDevice.class)
public abstract class GpuDeviceMixin implements IGpuDevice, IMsaaDevice {
@Override
public void combatant$pushScissor(int x, int y, int width, int height) {
GlobalScissorState.push(x, y, width, height);
}
@Override
public void combatant$popScissor() {
GlobalScissorState.pop();
}
@Override
public GpuTexture combatant$createMsaaTexture(String label, int usage, GpuFormat format, int width, int height, int samples) {
return CombatantRenderSystem.rhi().msaa().createTexture(label, usage, format, width, height, samples);
}
}

View file

@ -0,0 +1,54 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.systems.GpuSurface;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.textures.GpuTextureView;
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 dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.profiler.TracyGpuProfiler;
import dev.loki.lovisual.render.engine.profiler.TracyProfiler;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.util.FastFps;
@Mixin(GpuSurface.class)
public abstract class GpuSurfaceMixin {
/**
* This is the last safe point before Minecraft copies the main render target into the
* window surface. Combatant queued text/UI/RHI submission must be flushed here, not in
* present(), otherwise those draws happen after the surface blit and are not visible in
* the frame that produced them.
*/
@Inject(method = "blitFromTexture", at = @At("HEAD"))
private void combatant$beforeSurfaceBlit(CommandEncoder encoder, GpuTextureView textureView, CallbackInfo info) {
CombatantRenderSystem.endRenderSubmission();
}
@Inject(method = "present", at = @At("HEAD"))
private void combatant$onPresentHead(CallbackInfo info) {
FastFps.onFrame();
}
@Inject(method = "present", at = @At("TAIL"))
private void combatant$onPresentTail(CallbackInfo info) {
CombatantRenderSystem.onFramePresented();
if (TracyGpuProfiler.isEnabled()) {
TracyGpuProfiler.onFrameEnd();
}
if (TracyProfiler.isEnabled()) {
Renderer2D.BatchStats stats = Renderer2D.getBatchStats();
TracyProfiler.plotUiBatch(stats.getFrameDraws(), stats.getFrameVertices());
}
Renderer2D.getBatchStats().onFrameStart();
Renderer2D.invalidateWorldGlassSource();
}
}

View file

@ -0,0 +1,311 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.joml.Matrix3x2fStack;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.gui.hud.CooldownRender;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterTooltips;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
import dev.loki.lovisual.mixininterface.IGuiGraphics;
import dev.loki.lovisual.render.engine.core.ViewportContext;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.HudDeferredGuiElement;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.renderer.state.gui.GuiRenderState;
import dev.loki.lovisual.util.pvp.CooldownRegistry;
import dev.loki.lovisual.util.pvp.ItemCooldownSnapshot;
import dev.loki.lovisual.util.pvp.PvpState;
import dev.loki.lovisual.util.pvp.client.CooldownsState;
@Mixin(GuiGraphicsExtractor.class)
public abstract class GuiGraphicsMixin implements IGuiGraphics {
@Final
@Shadow
private Matrix3x2fStack pose;
@Final
@Shadow
public GuiRenderState guiRenderState;
@Shadow
public abstract int guiWidth();
@Shadow
public abstract int guiHeight();
@Override
public void combatant$runUnscaled(Runnable task) {
Minecraft mc = Minecraft.getInstance();
double scale = mc.getWindow().getGuiScale();
double uiScale = ViewportContext.getUiScale();
float ratio = (float) (uiScale / scale);
pose.scale(ratio, ratio);
try {
task.run();
} finally {
pose.scale((float) (scale / uiScale), (float) (scale / uiScale));
}
}
@Override
public void combatant$withTransform(float tx, float ty, float angleRad, Runnable task) {
pose.pushMatrix();
try {
pose.translate(tx, ty);
pose.rotate(angleRad);
task.run();
} finally {
pose.popMatrix();
}
}
@Override
public void combatant$addDeferredHudMarker(Renderer2D.Deferred2DLayer layer) {
if (layer == null || guiRenderState == null) return;
int width = Math.max(1, guiWidth());
int height = Math.max(1, guiHeight());
guiRenderState.addGlyphToCurrentLayer(new HudDeferredGuiElement(layer, new ScreenRectangle(0, 0, width, height)));
}
@Override
public void combatant$drawItemBar(ItemStack stack, int x, int y) {
combatant$invokeDrawItemBar(stack, x, y);
}
@Override
public void combatant$drawCooldownProgress(ItemStack stack, int x, int y) {
combatant$invokeDrawCooldownProgress(stack, x, y);
}
@Invoker("itemBar")
public abstract void combatant$invokeDrawItemBar(ItemStack stack, int x, int y);
@Invoker("itemCooldown")
public abstract void combatant$invokeDrawCooldownProgress(ItemStack stack, int x, int y);
@Accessor("deferredTooltip")
public abstract Runnable combatant$getTooltipDrawer();
@Accessor("deferredTooltip")
public abstract void combatant$setTooltipDrawer(Runnable drawer);
@Inject(method = "extractDeferredElements", at = @At("HEAD"))
private void combatant$suppressVanillaTooltip(int mouseX, int mouseY, float tickDelta, CallbackInfo ci) {
if (!BetterTooltips.hasTooltip()) return;
if (combatant$getTooltipDrawer() == null) return;
combatant$setTooltipDrawer(null);
}
@Inject(
method = "setTooltipForNextFrame(Lnet/minecraft/client/gui/Font;Ljava/util/List;Lnet/minecraft/client/gui/screens/inventory/tooltip/ClientTooltipPositioner;IIZ)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$drawCustomTooltip(net.minecraft.client.gui.Font tr,
java.util.List<FormattedCharSequence> lines,
ClientTooltipPositioner positioner,
int x, int y, boolean focused,
CallbackInfo ci) {
BetterTooltips tooltips = BetterTooltips.get();
if (tooltips == null || !tooltips.useCustomGuiTooltips()) return;
if (lines == null || lines.isEmpty()) return;
BetterTooltips.setDrawContext((GuiGraphicsExtractor) (Object) this);
BetterTooltips.captureTooltipOrdered(lines, positioner, x, y);
ci.cancel();
}
@Inject(
method = "setTooltipForNextFrame(Lnet/minecraft/client/gui/Font;Lnet/minecraft/world/item/ItemStack;II)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$drawCustomItemTooltip(net.minecraft.client.gui.Font tr,
ItemStack stack,
int x, int y,
CallbackInfo ci) {
BetterTooltips tooltips = BetterTooltips.get();
if (tooltips == null || !tooltips.useCustomItemTooltips()) return;
if (stack == null || stack.isEmpty()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
java.util.List<net.minecraft.network.chat.Component> textLines = Screen.getTooltipFromItem(mc, stack);
if (textLines == null || textLines.isEmpty()) return;
java.util.List<FormattedCharSequence> ordered = new java.util.ArrayList<>();
for (net.minecraft.network.chat.Component t : textLines) {
ordered.addAll(tr.split(t, 200));
}
BetterTooltips.captureItemTooltipOrdered(
ordered,
net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE,
x, y,
stack,
(GuiGraphicsExtractor) (Object) this
);
ci.cancel();
}
@Inject(
method = "setTooltipForNextFrame(Lnet/minecraft/client/gui/Font;Ljava/util/List;Ljava/util/Optional;IILnet/minecraft/resources/Identifier;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$drawCustomTooltipWithData(net.minecraft.client.gui.Font tr,
java.util.List<net.minecraft.network.chat.Component> lines,
java.util.Optional<net.minecraft.world.inventory.tooltip.TooltipComponent> data,
int x, int y,
net.minecraft.resources.Identifier style,
CallbackInfo ci) {
BetterTooltips tooltips = BetterTooltips.get();
if (tooltips == null || !tooltips.useCustomItemTooltips()) return;
if (lines == null || lines.isEmpty()) return;
java.util.List<FormattedCharSequence> ordered = new java.util.ArrayList<>();
for (net.minecraft.network.chat.Component t : lines) {
ordered.addAll(tr.split(t, 200));
}
BetterTooltips.setDrawContext((GuiGraphicsExtractor) (Object) this);
ItemStack stack = BetterTooltips.consumeLastTooltipStack();
if (stack != null && !stack.isEmpty()) {
BetterTooltips.captureItemTooltipOrdered(
ordered,
net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE,
x, y,
stack,
(GuiGraphicsExtractor) (Object) this
);
} else {
BetterTooltips.captureTooltipOrdered(ordered, net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE, x, y);
}
ci.cancel();
}
@Inject(
method = "setTooltipForNextFrame(Lnet/minecraft/client/gui/Font;Ljava/util/List;Ljava/util/Optional;II)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$drawCustomTooltipWithDataNoStyle(net.minecraft.client.gui.Font tr,
java.util.List<net.minecraft.network.chat.Component> lines,
java.util.Optional<net.minecraft.world.inventory.tooltip.TooltipComponent> data,
int x, int y,
CallbackInfo ci) {
BetterTooltips tooltips = BetterTooltips.get();
if (tooltips == null || !tooltips.useCustomItemTooltips()) return;
if (lines == null || lines.isEmpty()) return;
java.util.List<FormattedCharSequence> ordered = new java.util.ArrayList<>();
for (net.minecraft.network.chat.Component t : lines) {
ordered.addAll(tr.split(t, 200));
}
BetterTooltips.setDrawContext((GuiGraphicsExtractor) (Object) this);
ItemStack stack = BetterTooltips.consumeLastTooltipStack();
if (stack != null && !stack.isEmpty()) {
BetterTooltips.captureItemTooltipOrdered(
ordered,
net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE,
x, y,
stack,
(GuiGraphicsExtractor) (Object) this
);
} else {
BetterTooltips.captureTooltipOrdered(ordered, net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE, x, y);
}
ci.cancel();
}
@Inject(method = "itemCooldown", at = @At("HEAD"), cancellable = true)
private void combatant$renderCustomPvpCooldown(ItemStack stack, int x, int y, CallbackInfo ci) {
if (stack.isEmpty()) return;
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
if (cooldowns == null || !cooldowns.isSystemEnabled()) return;
if (stack.is(Items.ENDER_PEARL) || stack.is(Items.CHORUS_FRUIT)) {
if (!PvpState.isActive()) return;
Float secondsLeft = PvpState.getSecondsLeft();
if (secondsLeft == null || secondsLeft <= 0f) return;
Float maxSeconds = PvpState.getMaxSeconds();
float total = (maxSeconds == null || maxSeconds <= 0f) ? secondsLeft : maxSeconds;
float progress = Mth.clamp(secondsLeft / total, 0f, 1f);
Minecraft mc = Minecraft.getInstance();
GuiGraphicsExtractor ctx = (GuiGraphicsExtractor) (Object) this;
float secondsLeftValue = secondsLeft;
float displaySeconds = secondsLeftValue >= 10f
? (float) ((int) secondsLeftValue)
: Math.round(secondsLeftValue * 10f) / 10f;
int top = y + Mth.floor(16.0F * (1.0F - progress));
int bottom = top + Mth.ceil(16.0F * progress);
int color = 0x80FF5656;
ctx.fill(RenderPipelines.GUI, x, top, x + 16, bottom, color);
CooldownRender.renderTime(
ctx, progress, x, y, displaySeconds
);
if (ClientScreen.current() == null) {
ci.cancel();
}
return;
}
if (!cooldowns.shouldRenderSlots()) return;
if (!CooldownRegistry.isTracked(stack.getItem())) return;
ItemCooldownSnapshot snapshot = CooldownsState.MANAGER.snapshot(stack.getItem());
if (!snapshot.visible()) return;
Minecraft mc = Minecraft.getInstance();
GuiGraphicsExtractor ctx = (GuiGraphicsExtractor) (Object) this;
float progress = snapshot.cooling() ? snapshot.cooldownProgress() : snapshot.usesProgress();
if (progress <= 0f) return;
float secondsLeft = snapshot.cooling() ? snapshot.cooldownRemainingMs() / 1000.0f : snapshot.useWindowRemainingMs() / 1000.0f;
float displaySeconds = snapshot.cooling()
? (secondsLeft >= 10 ? (float) ((int) secondsLeft) : Math.round(secondsLeft * 10f) / 10f)
: 0f;
int top = y + Mth.floor(16.0F * (1.0F - progress));
int bottom = top + Mth.ceil(16.0F * progress);
int color = snapshot.cooling()
? (CooldownsState.MANAGER.isInPvp() ? 0x80FFA500 : 0x8000A0FF)
: 0x706D9DFF;
ctx.fill(RenderPipelines.GUI, x, top, x + 16, bottom, color);
if (snapshot.cooling()) {
CooldownRender.renderTime(ctx, progress, x, y, displaySeconds);
}
if (ClientScreen.current() == null) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,431 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.Hud;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.Holder;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.scores.Objective;
import org.spongepowered.asm.mixin.Final;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.PvpOverlayEvent;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
import dev.loki.lovisual.features.gui.hud.draggable.impl.Potions;
import dev.loki.lovisual.features.gui.hud.draggable.impl.Scoreboard;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.SwapTooltip;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomHealthBar;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomHotbar;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.misc.MessageFilter;
import dev.loki.lovisual.features.module.modules.visuals.Crosshair;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.mixininterface.IGuiGraphics;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.core.RenderPhase;
import dev.loki.lovisual.render.engine.core.RenderPhaseScope;
import dev.loki.lovisual.render.engine.core.ViewportContext;
// import dev.loki.lovisual.render.engine.debug.RenderThread2DDebugRenderer; // disabled: debug probes are manual-only
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.runtime.RuntimeGate;
import dev.loki.lovisual.util.item.IllegalItemUtil;
import dev.loki.lovisual.util.item.RarityColorUtil;
import dev.loki.lovisual.util.item.TopEnchantUtil;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.util.text.TextJsonUtil;
@Mixin(Hud.class)
public abstract class GuiMixin {
@Shadow
private int toolHighlightTimer;
@Shadow
private net.minecraft.world.item.ItemStack lastToolHighlight;
@Shadow
@Final
private Minecraft minecraft;
@Inject(method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", at = @At("TAIL"))
private void combatant$debug2dGuiStateProbe(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) {
// RenderThread2DDebugRenderer.extractGuiStateProbe(ctx);
}
@Unique
private static void combatant$renderCustomBarOnly(GuiGraphicsExtractor ctx,
DeltaTracker tickCounter,
Minecraft mc,
CustomBar bar) {
boolean useJump = CustomBar.shouldUseJumpBar(mc);
boolean useLocator = !useJump && CustomBar.shouldUseLocator(mc, bar);
boolean drawXp = !useJump && !useLocator;
CombatantRenderSystem.ensureFrameContext();
try (RenderPhaseScope ignored = CombatantRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_bar")) {
Renderer2D.withDeferredLayer(COMBATANT$VANILLA_HUD_REPLACEMENT_LAYER, () -> {
ViewportContext.beginUnscaled(ctx);
Renderer2D.COLOR.begin();
if (drawXp) {
CustomBar.renderXpBarBackground(Renderer2D.COLOR, mc);
}
if (useJump) {
CustomBar.renderJumpBarBackground(Renderer2D.COLOR, mc);
}
if (useLocator) {
CustomBar.renderLocatorBackground(Renderer2D.COLOR, mc);
}
Renderer2D.COLOR.render();
ViewportContext.end(ctx);
if (drawXp) {
CustomBar.renderXpBarText(ctx, mc);
}
});
combatant$appendVanillaHudReplacementMarker(ctx);
}
}
@Unique
private static final Renderer2D.Deferred2DLayer COMBATANT$VANILLA_HUD_REPLACEMENT_LAYER =
Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS;
@Unique
private static void combatant$appendVanillaHudReplacementMarker(GuiGraphicsExtractor ctx) {
if (!(ctx instanceof IGuiGraphics graphics)) return;
ctx.nextStratum();
graphics.combatant$addDeferredHudMarker(COMBATANT$VANILLA_HUD_REPLACEMENT_LAYER);
ctx.nextStratum();
}
@Unique
private static int argbFrom(float[] rgba) {
if (rgba == null || rgba.length < 3) return 0xFFFFFFFF;
float r = rgba[0], g = rgba[1], b = rgba[2];
float a = rgba.length > 3 ? rgba[3] : 1f;
int ai = (int) (a * 255) & 0xFF;
int ri = (int) (r * 255) & 0xFF;
int gi = (int) (g * 255) & 0xFF;
int bi = (int) (b * 255) & 0xFF;
return (ai << 24) | (ri << 16) | (gi << 8) | bi;
}
@Inject(
method = "displayScoreboardSidebar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/scores/Objective;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$hideScoreboardSidebar(GuiGraphicsExtractor context, Objective objective, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
NoRender noRender = Modules.get(NoRender.class);
if ((noRender != null && noRender.hideScoreboard()) || Scoreboard.shouldReplaceVanilla()) {
ci.cancel();
}
}
@Inject(
method = "setOverlayMessage(Lnet/minecraft/network/chat/Component;Z)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$filterOverlayMessage(net.minecraft.network.chat.Component message, boolean tinted, CallbackInfo ci) {
if (DebugLog.serverOnly()) {
if (message == null) {
// DebugLog.server("HUD overlay message: tinted=%s raw=null", tinted);
} else {
String raw = message.getString();
String json = TextJsonUtil.toJson(message);
// DebugLog.server("HUD overlay message: tinted=%s raw=\"%s\" json=%s", tinted, raw, json);
}
}
if (Events.BUS.hasListeners(PvpOverlayEvent.class)) {
Events.BUS.post(new PvpOverlayEvent(message, tinted));
}
if (RuntimeGate.isPanic()) return;
MessageFilter messageFilter = Modules.get(MessageFilter.class);
if (messageFilter != null && messageFilter.shouldHideHud(message)) {
ci.cancel();
}
}
@Inject(
method = "extractEffects(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$hideVanillaPotions(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
if (!DraggableHudElementRegistry.isEnabled(Potions.class)) return;
if (mc.player.getActiveEffects().isEmpty()) return;
ci.cancel();
}
@Inject(
method = "extractCrosshair(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$replaceCrosshair(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
Crosshair crosshair = Modules.get(Crosshair.class);
if (crosshair != null && crosshair.shouldHideVanilla()) {
ci.cancel();
}
}
@Inject(
method = "extractVignette(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/entity/Entity;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$killVignette(GuiGraphicsExtractor context, Entity entity, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
// Completely remove vanilla vignette post-processing
ci.cancel();
}
@Inject(
method = "extractItemHotbar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$customHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomHotbar hotbar = CustomHotbar.get();
CustomBar bar = CustomBar.get();
if (hotbar == null && bar == null) return;
boolean hotbarEnabled = hotbar != null && hotbar.isHudHotbarEnabled();
boolean barEnabled = bar != null && bar.isHudBarEnabled() && bar.isHotbarXpBarEnabled();
if (!hotbarEnabled && !barEnabled) return;
if (!hotbarEnabled) return;
boolean useJump = barEnabled && CustomBar.shouldUseJumpBar(mc);
boolean useLocator = barEnabled && !useJump && CustomBar.shouldUseLocator(mc, bar);
boolean drawXp = barEnabled && !useJump && !useLocator;
CombatantRenderSystem.ensureFrameContext();
try (RenderPhaseScope ignored = CombatantRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_hotbar")) {
Renderer2D.withDeferredLayer(COMBATANT$VANILLA_HUD_REPLACEMENT_LAYER, () -> {
ViewportContext.beginUnscaled(ctx);
Renderer2D.COLOR.begin();
if (hotbarEnabled) {
CustomHotbar.renderBackground(Renderer2D.COLOR, mc);
}
if (drawXp) {
CustomBar.renderXpBarBackground(Renderer2D.COLOR, mc);
}
if (barEnabled && useJump) {
CustomBar.renderJumpBarBackground(Renderer2D.COLOR, mc);
}
if (barEnabled && useLocator) {
CustomBar.renderLocatorBackground(Renderer2D.COLOR, mc);
}
Renderer2D.COLOR.render();
ViewportContext.end(ctx);
if (hotbarEnabled) {
CustomHotbar.renderItems(ctx, tickCounter, mc);
}
});
combatant$appendVanillaHudReplacementMarker(ctx);
if (drawXp) {
CustomBar.renderXpBarText(ctx, mc);
}
if (hotbarEnabled) {
ci.cancel();
}
}
}
@Inject(
method = "extractItemHotbar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V",
at = @At("TAIL")
)
private void combatant$customBarAfterVanillaHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomHotbar hotbar = CustomHotbar.get();
if (hotbar != null && hotbar.isHudHotbarEnabled()) return;
CustomBar bar = CustomBar.get();
if (bar == null || !bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
combatant$renderCustomBarOnly(ctx, tickCounter, mc, bar);
}
@Inject(
method = "extractPortalOverlay(Lnet/minecraft/client/gui/GuiGraphicsExtractor;F)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$disableVanillaPortalOverlay(GuiGraphicsExtractor context, float nauseaStrength, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
ci.cancel();
}
@Inject(
method = "extractSleepOverlay(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$disableVanillaSleepOverlay(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
ci.cancel();
}
@Inject(
method = "extractHearts(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/entity/player/Player;IIIIFIIIZ)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$renderHealthBar(GuiGraphicsExtractor context,
Player player,
int x, int y, int lines,
int regeneratingHeartIndex, float maxHealth,
int lastHealth, int health,
int absorption, boolean blinking,
CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
if (!(player instanceof LocalPlayer clientPlayer)) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
if (!CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, maxHealth)) return;
CombatantRenderSystem.ensureFrameContext();
try (RenderPhaseScope ignored = CombatantRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_health")) {
Renderer2D.withDeferredLayer(COMBATANT$VANILLA_HUD_REPLACEMENT_LAYER, () -> {
ViewportContext.beginUnscaled(context);
Renderer2D.COLOR.begin();
CustomHealthBar.HealthTextInfo textInfo =
CustomHealthBar.renderHealthBarBackground(
Renderer2D.COLOR,
mc,
clientPlayer,
x,
y,
maxHealth
);
CustomHealthBar.renderHealthBarText(textInfo);
Renderer2D.COLOR.render();
ViewportContext.end(context);
});
combatant$appendVanillaHudReplacementMarker(context);
ci.cancel();
}
}
@Inject(method = "extractSelectedItemName", at = @At("HEAD"), cancellable = true)
private void combatant$colorizeHeldItemTooltip(GuiGraphicsExtractor context, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
SwapTooltip swapTooltip = SwapTooltip.get();
if (swapTooltip == null || !swapTooltip.isEnabled()) {
return;
}
if (this.toolHighlightTimer <= 0 || this.lastToolHighlight == null || this.lastToolHighlight.isEmpty()) {
SwapTooltip.clear();
return;
}
MutableComponent name = Component.empty().append(this.lastToolHighlight.getHoverName());
if (this.lastToolHighlight.has(net.minecraft.core.component.DataComponents.CUSTOM_NAME)) {
name.withStyle(net.minecraft.ChatFormatting.ITALIC);
}
int illegalColor = IllegalItemUtil.illegalColor();
int topColor = TopEnchantUtil.topColor();
int rarityColor = argbFrom(RarityColorUtil.INSTANCE.getRarityColor(this.lastToolHighlight));
boolean hasIllegal = IllegalItemUtil.isIllegal(this.lastToolHighlight);
boolean hasTop = !hasIllegal && TopEnchantUtil.hasTopEnchant(this.lastToolHighlight);
int nameColor = hasIllegal ? illegalColor : (hasTop ? topColor : rarityColor);
int alpha = (int) ((float) this.toolHighlightTimer * 256.0F / 10.0F);
if (alpha > 255) alpha = 255;
if (alpha <= 0) {
SwapTooltip.clear();
return;
}
float fade = alpha / 255f;
int argb = 0xFF000000 | (nameColor & 0x00FFFFFF);
SwapTooltip.capture(
name.getString(),
argb,
fade,
this.minecraft.gameMode.canHurtPlayer()
);
ci.cancel();
}
@Redirect(
method = "extractPlayerHealth(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;getAbsorptionAmount()F"
)
)
private float combatant$ignoreAbsorptionForLayout(Player player) {
if (!RuntimeGate.canRunHud()) {
return player.getAbsorptionAmount();
}
Minecraft mc = Minecraft.getInstance();
if (mc != null && player instanceof LocalPlayer clientPlayer) {
float actualMaxHealth = (float) player.getAttributeValue(Attributes.MAX_HEALTH);
if (CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, actualMaxHealth)) {
return 0.0f;
}
}
return player.getAbsorptionAmount();
}
@Redirect(
method = "extractPlayerHealth(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;getAttributeValue(Lnet/minecraft/core/Holder;)D"
)
)
private double combatant$clampMaxHealthForLayout(Player player, Holder<Attribute> attribute) {
if (!RuntimeGate.canRunHud()) {
return player.getAttributes().getValue(attribute);
}
if (attribute == Attributes.MAX_HEALTH) {
Minecraft mc = Minecraft.getInstance();
if (mc != null && player instanceof LocalPlayer clientPlayer) {
float actualMaxHealth = (float) player.getAttributes().getValue(attribute);
if (CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, actualMaxHealth)) {
return 20.0;
}
}
}
return player.getAttributes().getValue(attribute);
}
}

View file

@ -0,0 +1,263 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.ProjectionType;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.ui.HudDeferredGuiElement;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.render.GuiRenderer;
import net.minecraft.client.renderer.Projection;
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
import net.minecraft.client.renderer.state.WindowRenderState;
import net.minecraft.client.renderer.state.gui.GuiElementRenderState;
import net.minecraft.client.renderer.state.gui.GuiRenderState;
import org.joml.Matrix4f;
import org.spongepowered.asm.mixin.Final;
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;
import java.util.List;
import java.util.function.Supplier;
@Mixin(GuiRenderer.class)
public abstract class GuiRendererMixin {
@Shadow
@Final
private List<?> draws;
@Shadow
private int firstDrawIndexAfterBlur;
@Shadow
@Final
private Projection guiProjection;
@Shadow
@Final
private ProjectionMatrixBuffer guiProjectionMatrixBuffer;
@Unique
private final ObjectArrayList<QueuedHudMarker> combatant$hudMarkers = new ObjectArrayList<>(16);
@Unique
private GuiRenderState.TraverseRange combatant$currentTraverseRange;
@Shadow
private void executeDrawRange(Supplier<String> supplier,
RenderTarget renderTarget,
GpuBufferSlice gpuBufferSlice,
int start,
int end) {
throw new AssertionError();
}
@Inject(method = "render", at = @At("HEAD"))
private void combatant$clearHudMarkersAtFrameStart(CallbackInfo ci) {
combatant$hudMarkers.clear();
}
@Inject(method = "render", at = @At("RETURN"))
private void combatant$clearHudMarkersAtFrameEnd(CallbackInfo ci) {
combatant$hudMarkers.clear();
}
@Inject(method = "addElementsToMeshes", at = @At("HEAD"))
private void combatant$enterTraverseRange(GuiRenderState.TraverseRange range, CallbackInfo ci) {
combatant$currentTraverseRange = range;
}
@Inject(method = "addElementsToMeshes", at = @At("RETURN"))
private void combatant$exitTraverseRange(GuiRenderState.TraverseRange range, CallbackInfo ci) {
combatant$currentTraverseRange = null;
}
@Inject(method = "addElementToMesh", at = @At("HEAD"), cancellable = true)
private void combatant$captureHudDeferredMarker(GuiElementRenderState element, CallbackInfo ci) {
if (!(element instanceof HudDeferredGuiElement marker)) return;
int drawIndex = draws != null ? draws.size() : 0;
GuiRenderState.TraverseRange range = combatant$currentTraverseRange != null
? combatant$currentTraverseRange
: GuiRenderState.TraverseRange.BEFORE_BLUR;
combatant$hudMarkers.add(new QueuedHudMarker(drawIndex, range, marker.layer()));
ci.cancel();
}
@Inject(method = "draw", at = @At("HEAD"), cancellable = true)
private void combatant$drawWithDeferredHudMarkers(CallbackInfo ci) {
if (combatant$hudMarkers.isEmpty()) return;
if (draws == null || draws.isEmpty()) {
combatant$drainHudMarkersUpTo(Integer.MAX_VALUE);
ci.cancel();
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.gameRenderer == null) {
combatant$drainHudMarkersUpTo(Integer.MAX_VALUE);
ci.cancel();
return;
}
WindowRenderState window = mc.gameRenderer.gameRenderState().windowRenderState;
if (window == null || window.guiScale == 0) {
combatant$drainHudMarkersUpTo(Integer.MAX_VALUE);
ci.cancel();
return;
}
guiProjection.setupOrtho(
1000.0F,
11000.0F,
(float) window.width / (float) window.guiScale,
(float) window.height / (float) window.guiScale,
true
);
RenderSystem.setProjectionMatrix(guiProjectionMatrixBuffer.getBuffer(guiProjection), ProjectionType.ORTHOGRAPHIC);
RenderTarget target = mc.gameRenderer.mainRenderTarget();
if (target == null) {
combatant$drainHudMarkersUpTo(Integer.MAX_VALUE);
ci.cancel();
return;
}
GpuBufferSlice transform = RenderSystem.getDynamicUniforms()
.writeTransform(new Matrix4f().setTranslation(0.0F, 0.0F, -11000.0F));
int beforeBlurEnd = Math.min(firstDrawIndexAfterBlur, draws.size());
if (beforeBlurEnd > 0) {
combatant$executeDrawRangeWithHudMarkers(
() -> "gui before blur",
target,
transform,
0,
beforeBlurEnd,
GuiRenderState.TraverseRange.BEFORE_BLUR
);
} else {
combatant$drainHudMarkersInRange(GuiRenderState.TraverseRange.BEFORE_BLUR, Integer.MAX_VALUE);
}
boolean hasAfterBlurMarkers = combatant$hasPendingHudMarkersInRange(GuiRenderState.TraverseRange.AFTER_BLUR);
if (draws.size() <= firstDrawIndexAfterBlur && !hasAfterBlurMarkers) {
combatant$drainHudMarkersUpTo(Integer.MAX_VALUE);
ci.cancel();
return;
}
RenderSystem.getDevice().createCommandEncoder().clearDepthTexture(target.getDepthTexture(), 0.0D);
mc.gameRenderer.processBlurEffect();
combatant$executeDrawRangeWithHudMarkers(
() -> "gui after blur",
target,
transform,
firstDrawIndexAfterBlur,
draws.size(),
GuiRenderState.TraverseRange.AFTER_BLUR
);
combatant$drainHudMarkersUpTo(Integer.MAX_VALUE);
ci.cancel();
}
@Unique
private void combatant$executeDrawRangeWithHudMarkers(Supplier<String> label,
RenderTarget target,
GpuBufferSlice transform,
int start,
int end,
GuiRenderState.TraverseRange range) {
int cursor = start;
for (int i = 0, size = combatant$hudMarkers.size(); i < size; i++) {
QueuedHudMarker marker = combatant$hudMarkers.get(i);
if (marker == null || marker.drained || marker.range != range) continue;
int markerIndex = marker.drawIndex;
if (markerIndex < start) {
combatant$drainHudMarker(marker);
continue;
}
if (markerIndex > end) continue;
if (cursor < markerIndex) {
executeDrawRange(label, target, transform, cursor, markerIndex);
}
combatant$drainHudMarker(marker);
cursor = Math.max(cursor, markerIndex);
}
if (cursor < end) {
executeDrawRange(label, target, transform, cursor, end);
}
}
@Unique
private boolean combatant$hasPendingHudMarkersInRange(GuiRenderState.TraverseRange range) {
for (int i = 0, size = combatant$hudMarkers.size(); i < size; i++) {
QueuedHudMarker marker = combatant$hudMarkers.get(i);
if (marker != null && !marker.drained && marker.range == range) {
return true;
}
}
return false;
}
@Unique
private void combatant$drainHudMarkersInRange(GuiRenderState.TraverseRange range, int drawIndexInclusive) {
for (int i = 0, size = combatant$hudMarkers.size(); i < size; i++) {
QueuedHudMarker marker = combatant$hudMarkers.get(i);
if (marker == null || marker.drained || marker.range != range) continue;
if (marker.drawIndex <= drawIndexInclusive) {
combatant$drainHudMarker(marker);
}
}
}
@Unique
private void combatant$drainHudMarkersUpTo(int drawIndexInclusive) {
for (int i = 0, size = combatant$hudMarkers.size(); i < size; i++) {
QueuedHudMarker marker = combatant$hudMarkers.get(i);
if (marker == null || marker.drained) continue;
if (marker.drawIndex <= drawIndexInclusive) {
combatant$drainHudMarker(marker);
}
}
}
@Unique
private void combatant$drainHudMarker(QueuedHudMarker marker) {
if (marker == null || marker.drained) return;
marker.drained = true;
Renderer2D.drainDeferred2D(marker.layer);
}
@Unique
private static final class QueuedHudMarker {
private final int drawIndex;
private final GuiRenderState.TraverseRange range;
private final Renderer2D.Deferred2DLayer layer;
private boolean drained;
private QueuedHudMarker(int drawIndex,
GuiRenderState.TraverseRange range,
Renderer2D.Deferred2DLayer layer) {
this.drawIndex = drawIndex;
this.range = range;
this.layer = layer;
}
}
}

View file

@ -0,0 +1,102 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.sugar.Local;
import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
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;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiEditorScreen;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiPickerScreen;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiRenderer;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiScreen;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.DynamicIsland;
import dev.loki.lovisual.features.module.modules.misc.ClickGui;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.core.RenderPhase;
import dev.loki.lovisual.render.engine.core.RenderPhaseScope;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
@Mixin(Gui.class)
public abstract class GuiScreenMixin {
@Unique
private static void combatant$extractCustomBarHeads(GuiGraphicsExtractor drawContext, DeltaTracker tickCounter) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
if (ClientScreen.current() != null) return;
CustomBar bar = CustomBar.get();
if (bar == null || !bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
if (CustomBar.shouldUseJumpBar(mc) || !CustomBar.shouldUseLocator(mc, bar)) return;
CombatantRenderSystem.ensureFrameContext();
try (RenderPhaseScope ignored = CombatantRenderSystem.phase(RenderPhase.SCREEN_TOP, "2d:hud:custom_bar_heads")) {
Renderer2D.withDeferredLayer(Renderer2D.Deferred2DLayer.AFTER_VANILLA_GUI, () -> {
CustomBar.renderLocatorHeads(drawContext, tickCounter, mc);
Renderer2D.COLOR.render();
});
}
}
@Inject(method = "setScreen", at = @At("HEAD"))
private void combatant$closeClickGuiOnScreen(Screen screen, CallbackInfo ci) {
if (!ModuleManager.isEnabled("clickgui")) {
return;
}
if (ClickGuiRenderer.waitingForKey) {
return;
}
if (screen instanceof ClickGuiScreen
|| screen instanceof ClickGuiPickerScreen
|| screen instanceof ClickGuiEditorScreen) {
return;
}
if (screen != null) {
ClickGui.setSuppressScreenClose(true);
try {
ModuleManager.setEnabled("clickgui", false);
} finally {
ClickGui.setSuppressScreenClose(false);
}
}
}
@Inject(
method = "extractRenderState",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;applyCursor(Lcom/mojang/blaze3d/platform/Window;)V"
)
)
private void combatant$extractTopLayer(DeltaTracker tickCounter, boolean renderLevel, boolean renderScreens, CallbackInfo ci,
@Local(ordinal = 0) GuiGraphicsExtractor drawContext) {
if (drawContext == null) {
return;
}
drawContext.nextStratum();
combatant$extractCustomBarHeads(drawContext, tickCounter);
DynamicIsland.renderScreenOverlay(drawContext, tickCounter.getGameTimeDeltaTicks());
ClickGuiRenderer.renderTopLayer(drawContext, tickCounter.getGameTimeDeltaTicks());
}
}

View file

@ -0,0 +1,37 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.HoneyBlock;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.util.player.EnvironmentMovementController;
@Mixin(HoneyBlock.class)
public class HoneyBlockMixin {
@Redirect(
method = "doSlideMovement",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/Entity;setDeltaMovement(Lnet/minecraft/world/phys/Vec3;)V"
)
)
private void nostun$preserveHorizontalSlide(Entity entity, Vec3 velocity) {
if (!EnvironmentMovementController.shouldPreserveHoneySlide(entity)) {
entity.setDeltaMovement(velocity);
return;
}
Vec3 current = entity.getDeltaMovement();
entity.setDeltaMovement(new Vec3(current.x, velocity.y, current.z));
}
}

View file

@ -0,0 +1,34 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.player.ClientInput;
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;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.Sprint;
@Mixin(ClientInput.class)
public abstract class InputMixin {
@Inject(method = "hasForwardImpulse", at = @At("HEAD"), cancellable = true)
private void allowAllDirections(CallbackInfoReturnable<Boolean> cir) {
Sprint module = Modules.get(Sprint.class);
if (module == null || !module.shouldAllowDirectionalSprint()) return;
ClientInput input = (ClientInput) (Object) this;
if (input.keyPresses.forward()
|| input.keyPresses.backward()
|| input.keyPresses.left()
|| input.keyPresses.right()) {
cir.setReturnValue(true);
}
}
}

View file

@ -0,0 +1,84 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.ItemCooldowns;
import org.spongepowered.asm.mixin.Final;
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;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.TPSSync;
import dev.loki.lovisual.features.module.modules.movement.Timer;
import dev.loki.lovisual.mixins.accessors.ItemCooldownEntryAccessor;
import java.util.Iterator;
import java.util.Map;
@Mixin(ItemCooldowns.class)
public abstract class ItemCooldownsMixin {
@Shadow
private int tickCount;
@Shadow
@Final
private Map<Identifier, ?> cooldowns;
@Unique
private float combatant$timerRemainder = 0f;
@Shadow
protected abstract void onCooldownEnded(Identifier groupId);
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void combatant$timerCompensate(CallbackInfo ci) {
float mult = Timer.getTickTimer();
float serverDelta = 1.0f;
TPSSync tps = Modules.get(TPSSync.class);
if (tps != null && tps.isEnabled()) {
serverDelta = tps.getServerTickDelta();
}
float scale = (mult <= 0.0001f) ? 1.0f : (serverDelta / mult);
if (scale > 1.0f) scale = 1.0f;
if (scale < 0.0f) scale = 0.0f;
if (Math.abs(scale - 1.0f) < 0.0001f) {
return;
}
float step = scale;
combatant$timerRemainder += step;
int inc = (int) combatant$timerRemainder;
if (inc <= 0) {
ci.cancel();
return;
}
combatant$timerRemainder -= inc;
tickCount += inc;
if (!cooldowns.isEmpty()) {
@SuppressWarnings("unchecked")
Iterator<Map.Entry<Identifier, Object>> iterator =
((Map<Identifier, Object>) cooldowns).entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Identifier, Object> entry = iterator.next();
Object value = entry.getValue();
if (value instanceof ItemCooldownEntryAccessor accessor && accessor.getEndTick() <= tickCount) {
iterator.remove();
onCooldownEnded(entry.getKey());
}
}
}
ci.cancel();
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.entity.state.ItemEntityRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import dev.loki.lovisual.mixininterface.IItemEntityRenderState;
@Mixin(ItemEntityRenderState.class)
public class ItemEntityRenderStateMixin implements IItemEntityRenderState {
@Unique
private boolean combatant$onGround;
@Override
public boolean combatant$isOnGround() {
return combatant$onGround;
}
@Override
public void combatant$setOnGround(boolean onGround) {
combatant$onGround = onGround;
}
}

View file

@ -0,0 +1,76 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.ItemEntityRenderer;
import net.minecraft.client.renderer.entity.state.ItemEntityRenderState;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.phys.AABB;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
import dev.loki.lovisual.mixininterface.IItemEntityRenderState;
@Mixin(ItemEntityRenderer.class)
public abstract class ItemEntityRendererMixin extends EntityRenderer<ItemEntity, ItemEntityRenderState> {
@Shadow
@Final
private RandomSource random;
protected ItemEntityRendererMixin(EntityRendererProvider.Context ctx) {
super(ctx);
}
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void combatant$captureOnGround(ItemEntity itemEntity, ItemEntityRenderState state, float tickDelta, CallbackInfo ci) {
((IItemEntityRenderState) state).combatant$setOnGround(itemEntity.onGround());
}
@Inject(method = "submit", at = @At("HEAD"), cancellable = true)
private void combatant$renderItemPhysics(
ItemEntityRenderState state,
PoseStack matrices,
SubmitNodeCollector queue,
CameraRenderState cameraState,
CallbackInfo ci
) {
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isItemPhysicsEnabled() || state.item.isEmpty()) {
return;
}
matrices.pushPose();
AABB box = state.item.getModelBoundingBox();
float baseY = -((float) box.minY) + 0.0625F;
matrices.translate(0.0F, baseY, 0.0F);
if (((IItemEntityRenderState) state).combatant$isOnGround()) {
matrices.mulPose(Axis.XP.rotationDegrees(90.0F));
} else {
float rotation = ItemEntity.getSpin(state.ageInTicks, state.bobOffset);
matrices.mulPose(Axis.XP.rotationDegrees(rotation * 300.0F));
}
ItemEntityRenderer.submitMultipleFromCount(matrices, queue, state.lightCoords, state, this.random, box);
matrices.popPose();
super.submit(state, matrices, queue, cameraState);
ci.cancel();
}
}

View file

@ -0,0 +1,66 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.feature.ItemFeatureRenderer;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.resources.model.geometry.BakedQuad;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ItemFeatureRenderer.class)
public abstract class ItemFeatureRendererMixin {
@Unique
private static final int COMBATANT_VIEW_FADE_DITHER_MARK = 0x00010001;
@Unique
private static final int COMBATANT_VIEW_FADE_A2C_MARK = 0x00010100;
@Unique
private static boolean combatant$isViewFadeMarked(int argb) {
int alpha = (argb >>> 24) & 0xFF;
if (alpha >= 255) {
return false;
}
int rgbMarker = argb & 0x00010101;
return rgbMarker == COMBATANT_VIEW_FADE_DITHER_MARK || rgbMarker == COMBATANT_VIEW_FADE_A2C_MARK;
}
@Unique
private static boolean combatant$hasViewFadeTint(int[] tints) {
return tints != null && tints.length > 0 && combatant$isViewFadeMarked(tints[0]);
}
@ModifyReturnValue(
method = "getLayerColorSafe([ILnet/minecraft/client/resources/model/geometry/BakedQuad$MaterialInfo;)I",
at = @At("RETURN")
)
private static int combatant$applyViewFadeToUntintedItemQuads(int original,
int[] tints,
BakedQuad.MaterialInfo materialInfo) {
if (original != -1 || !combatant$hasViewFadeTint(tints)) {
return original;
}
return tints[0];
}
@ModifyExpressionValue(
method = "prepareMainSubmit",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/geometry/BakedQuad$MaterialInfo;itemRenderType()Lnet/minecraft/client/renderer/rendertype/RenderType;")
)
private RenderType combatant$useTranslucentRenderTypeForViewFadeItem(RenderType original,
@Local(argsOnly = true) ItemFeatureRenderer.Submit submit,
@Local(ordinal = 0) BakedQuad.MaterialInfo materialInfo) {
// Vanilla item translucent sheets render into ITEM_ENTITY_TARGET. For entity fade this can
// corrupt the following world translucent composite, so item layers keep their own type.
return original;
}
}

View file

@ -0,0 +1,144 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.vertex.PoseStack;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.ItemInHandRenderer;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.HumanoidArm;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.ViewModel;
@Environment(EnvType.CLIENT)
@Mixin(ItemInHandRenderer.class)
public abstract class ItemInHandRendererMixin {
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void combatant$skipTickWithoutPlayer(CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (mc.level != null && mc.player == null) {
ci.cancel();
}
}
@Inject(
method = "submitArmWithItem",
at = @At("HEAD")
)
private void combatant$applyOffsets(
AbstractClientPlayer player,
float tickDelta,
float pitch,
InteractionHand hand,
float swingProgress,
ItemStack item,
float equipProgress,
PoseStack matrices,
SubmitNodeCollector queue,
int light,
CallbackInfo ci
) {
ViewModel viewModel = Modules.get(ViewModel.class);
if (viewModel == null || !viewModel.isEnabled()) return;
matrices.translate(
viewModel.swingX.get(),
viewModel.swingY.get(),
viewModel.swingZ.get() + (isInLiquid(player) ? viewModel.liquidOffsetZ.get() : 0.0f)
);
}
@ModifyVariable(
method = "submitArmWithItem",
at = @At("HEAD"),
argsOnly = true,
index = 7)
private float combatant$scaleEquipProgress(float equipProgress) {
ViewModel viewModel = Modules.get(ViewModel.class);
if (viewModel == null || !viewModel.isEnabled()) return equipProgress;
return equipProgress * viewModel.equipLowering.get();
}
@Unique
private boolean isInLiquid(AbstractClientPlayer player) {
return player != null && (player.isInWater() || player.isInLava());
}
@Inject(
method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemDisplayContext;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/item/ItemStackRenderState;submit(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;III)V"
)
)
private void combatant$scaleMiniItems(
LivingEntity entity,
ItemStack stack,
ItemDisplayContext ctx,
PoseStack ms,
SubmitNodeCollector queue,
int light,
CallbackInfo ci
) {
ViewModel viewModel = Modules.get(ViewModel.class);
if (viewModel == null || !viewModel.isEnabled()) return;
if (!ctx.firstPerson()) return;
if (viewModel.shouldScale(stack)) {
viewModel.applyMini(ms, stack);
}
}
@Inject(
method = "swingArm",
at = @At("HEAD"),
cancellable = true
)
private void combatant$customSwing(
float swingProgress,
PoseStack ms,
int direction,
HumanoidArm arm,
CallbackInfo ci
) {
ViewModel viewModel = Modules.get(ViewModel.class);
if (viewModel == null || !viewModel.isEnabled()) return;
if (viewModel.mc.player == null) return;
ItemStack stack = viewModel.mc.player.getMainHandItem();
if (!viewModel.shouldSwing(stack))
return;
if (viewModel.shouldBypassRotationTransform(stack))
return;
// Cancel vanilla swing entirely
ci.cancel();
viewModel.applySwingAnimation(swingProgress, ms, arm);
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.util.aiming.RotationManager;
@Mixin(Item.class)
public class ItemMixin {
@Redirect(
method = "getPlayerPOVHitResult",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;calculateViewVector(FF)Lnet/minecraft/world/phys/Vec3;"
)
)
private static Vec3 combatant$raycastRotation(Player player, float pitch, float yaw) {
return RotationManager.INSTANCE.getMovementRotation().directionVector();
}
}

View file

@ -0,0 +1,107 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import org.jetbrains.annotations.Nullable;
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.CallbackInfoReturnable;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterTooltips;
import dev.loki.lovisual.util.item.IllegalItemUtil;
import dev.loki.lovisual.util.item.RarityColorUtil;
import dev.loki.lovisual.util.item.TopEnchantUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Mixin(ItemStack.class)
public class ItemStackMixin {
@Unique
private static Component recolor(Component text, int argb) {
int rgb = argb & 0x00FFFFFF;
return text.copy().setStyle(text.getStyle().withColor(rgb));
}
@Unique
private static int argbFrom(float[] rgba) {
if (rgba == null || rgba.length < 3) return 0xFFFFFFFF;
float r = rgba[0], g = rgba[1], b = rgba[2];
float a = rgba.length > 3 ? rgba[3] : 1f;
int ai = (int) (a * 255) & 0xFF;
int ri = (int) (r * 255) & 0xFF;
int gi = (int) (g * 255) & 0xFF;
int bi = (int) (b * 255) & 0xFF;
return (ai << 24) | (ri << 16) | (gi << 8) | bi;
}
@Inject(method = "getTooltipLines", at = @At("RETURN"), cancellable = true)
private void combatant$colorizeTooltip(Item.TooltipContext tooltipContext, @Nullable Player player, TooltipFlag type, CallbackInfoReturnable<List<Component>> cir) {
BetterTooltips tooltips = BetterTooltips.get();
if (tooltips == null || !tooltips.isItemInfoColorizeEnabled()) return;
ItemStack self = (ItemStack) (Object) this;
BetterTooltips.setLastTooltipStack(self);
List<Component> original = cir.getReturnValue();
if (original == null || original.isEmpty()) return;
List<Component> modified = new ArrayList<>(original);
int illegalColor = IllegalItemUtil.illegalColor();
int topColor = TopEnchantUtil.topColor();
int rarityColor = argbFrom(RarityColorUtil.INSTANCE.getRarityColor(self));
Set<String> illegalLabels = new HashSet<>();
Set<String> topLabels = new HashSet<>();
IllegalItemUtil.collectEnchants(self, (label, overMax) -> {
if (overMax) illegalLabels.add(label.trim());
});
TopEnchantUtil.collectTopEnchants(self, (label, isTop) -> {
if (isTop) topLabels.add(label.trim());
});
boolean hasIllegal = !illegalLabels.isEmpty();
boolean hasTop = !topLabels.isEmpty();
if (!modified.isEmpty()) {
int nameColor = hasIllegal ? illegalColor : (hasTop ? topColor : rarityColor);
modified.set(0, recolor(modified.get(0), nameColor));
}
for (int i = 1; i < modified.size(); i++) {
Component t = modified.get(i);
String trimmed = t.getString().trim();
if (illegalLabels.contains(trimmed)) {
modified.set(i, recolor(t, illegalColor));
continue;
}
if (topLabels.contains(trimmed)) {
modified.set(i, recolor(t, topColor));
continue;
}
if (hasIllegal && t.getStyle().getColor() == null) {
modified.set(i, recolor(t, illegalColor));
}
}
cir.setReturnValue(modified);
}
}

View file

@ -0,0 +1,115 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.features.theme.Theme;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.features.theme.Themes;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.core.ViewportContext;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
import dev.loki.lovisual.util.proxy.ProxyBackend;
import dev.loki.lovisual.util.proxy.ProxyEntry;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.util.Mth;
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(Screen.class)
public abstract class JoinMultiplayerScreenMixin {
@Unique
private static final float COMBATANT_PROXY_BUTTON_X = 8f;
@Unique
private static final float COMBATANT_PROXY_BUTTON_Y = 8f;
@Unique
private static final float COMBATANT_PROXY_BUTTON_W = 24f;
@Unique
private static final float COMBATANT_PROXY_BUTTON_H = 24f;
@Unique
private float combatant$proxyHover;
@Unique
private float combatant$proxyPress;
@Inject(method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("RETURN"))
private void combatant$renderProxyEntry(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
Screen self = (Screen) (Object) this;
if (!(self instanceof JoinMultiplayerScreen)) return;
float dt = AnimationUtility.deltaTime();
boolean hovered = combatant$inside(mouseX, mouseY, COMBATANT_PROXY_BUTTON_X, COMBATANT_PROXY_BUTTON_Y,
COMBATANT_PROXY_BUTTON_W, COMBATANT_PROXY_BUTTON_H);
combatant$proxyHover = combatant$animate(combatant$proxyHover, hovered ? 1f : 0f, dt, 11f);
combatant$proxyPress = combatant$animate(combatant$proxyPress, 0f, dt, 8f);
ViewportContext.beginScaled(context);
Renderer2D.COLOR.begin();
try {
combatant$drawProxyButton();
} finally {
Renderer2D.COLOR.render();
ViewportContext.end(context);
}
}
@Unique
private void combatant$drawProxyButton() {
float hover = AnimationUtility.easeOutCubic(combatant$proxyHover);
float press = AnimationUtility.easeOutCubic(combatant$proxyPress);
Themes.Theme theme = Theme.theme();
boolean enabled = ProxyBackend.isEnabled();
ProxyEntry active = ProxyBackend.activeProxy();
boolean configured = active.isConfigured();
float size = COMBATANT_PROXY_BUTTON_W * (1.0f + hover * 0.035f - press * 0.025f);
float x = COMBATANT_PROXY_BUTTON_X + (COMBATANT_PROXY_BUTTON_W - size) * 0.5f;
float y = COMBATANT_PROXY_BUTTON_Y + (COMBATANT_PROXY_BUTTON_H - size) * 0.5f - hover * 0.35f;
float radius = 5.0f;
float centerX = x + size * 0.5f;
float centerY = y + size * 0.5f;
int accent = enabled && configured ? theme.accent() : 0x8A94A3;
int baseTop = enabled && configured ? 0x222A20 : 0x1B2028;
int baseBottom = enabled && configured ? 0x121812 : 0x10141B;
int fillTop = HudRenderUtil.mixColor(withAlpha(baseTop, 226), withAlpha(accent, 96), (enabled && configured ? 0.18f : 0.06f) + hover * 0.12f);
int fillBottom = HudRenderUtil.mixColor(withAlpha(baseBottom, 236), withAlpha(accent, 70), (enabled && configured ? 0.12f : 0.04f) + hover * 0.08f);
int stroke = HudRenderUtil.mixColor(withAlpha(0x46505D, 180), withAlpha(accent, 218), (enabled && configured ? 0.38f : 0.10f) + hover * 0.35f);
int iconColor = enabled && configured
? HudRenderUtil.mixColor(withAlpha(0xFFFFFF, 245), withAlpha(theme.accent(), 255), 0.18f)
: withAlpha(0xC5CCD6, 235);
Renderer2D.COLOR.roundedRectSoftShadow(x, y + 0.8f, size, size, radius,
7.5f, 0.026f + hover * 0.012f, withAlpha(0x060810, 96));
Renderer2D.COLOR.roundedRectGradientQuad(x, y, size, size, radius, 1f,
fillTop, fillTop, fillBottom, fillBottom);
Renderer2D.COLOR.roundedRectStrokeGradient(x + 0.5f, y + 0.5f, size - 1.0f, size - 1.0f,
radius - 0.5f, 1f, 0.55f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.08f), stroke, 90f);
Renderer2D.COLOR.svg("shield-user", centerX - 5.6f, centerY - 5.6f, 11.2f, 11.2f, SvgRenderOptions.overrideColor(iconColor));
}
@Unique
private static float combatant$animate(float current, float target, float dt, float speed) {
float next = AnimationUtility.approach(current, target, dt, speed);
return AnimationUtility.snap(next, target, 0.001f);
}
@Unique
private static boolean combatant$inside(float mx, float my, float x, float y, float w, float h) {
return mx >= x && mx <= x + w && my >= y && my <= y + h;
}
@Unique
private static int withAlpha(int rgb, int alpha) {
return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24);
}
}

View file

@ -0,0 +1,49 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.contextualbar.JumpableVehicleBar;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(JumpableVehicleBar.class)
public abstract class JumpBarMixin {
@Inject(method = "extractBackground", at = @At("HEAD"), cancellable = true)
private void combatant$renderJumpBar(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
ci.cancel();
}
@Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true)
private void combatant$renderJumpAddons(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
ci.cancel();
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.KeyMapping;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.KeybindIsPressedEvent;
@Mixin(KeyMapping.class)
public class KeyMappingMixin {
@Shadow
private boolean isDown;
@Inject(method = "isDown", at = @At("HEAD"), cancellable = true)
private void combatant$keybindIsPressedEvent(CallbackInfoReturnable<Boolean> cir) {
if (!Events.BUS.hasListeners(KeybindIsPressedEvent.class)) {
return;
}
KeybindIsPressedEvent event = new KeybindIsPressedEvent((KeyMapping) (Object) this, isDown);
Events.BUS.post(event);
cir.setReturnValue(event.isPressed());
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.KeyboardInput;
import net.minecraft.world.entity.player.Input;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import dev.loki.lovisual.util.aiming.RotationManager;
import dev.loki.lovisual.util.aiming.RotationUtil;
import dev.loki.lovisual.util.aiming.data.Rotation;
import dev.loki.lovisual.util.aiming.features.MovementCorrection;
@Mixin(KeyboardInput.class)
public abstract class KeyboardInputMixin extends InputMixin {
@ModifyExpressionValue(
method = "tick",
at = @At(
value = "NEW",
target = "(ZZZZZZZ)Lnet/minecraft/world/entity/player/Input;"
)
)
private Input combatant$applyFreeCorrection(Input original) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null) {
return original;
}
var target = RotationManager.INSTANCE.getActiveRotationTarget();
if (target == null
|| target.movementCorrection != MovementCorrection.SILENT
|| !target.freeCorrection) {
return original;
}
Rotation rotation = RotationManager.INSTANCE.getMovementRotation();
return RotationUtil.correctMovementInput(original, mc.player.getYRot(), rotation.yaw());
}
}

View file

@ -0,0 +1,258 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.platform.InputConstants;
import dev.loki.lovisual.features.gui.clickgui.*;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.KeyboardHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Options;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.CharacterEvent;
import net.minecraft.client.input.KeyEvent;
import org.lwjgl.glfw.GLFW;
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;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.KeyInputEvent;
import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.BetterChatSearch;
import dev.loki.lovisual.features.gui.chat.BetterChatStoreManager;
import dev.loki.lovisual.features.gui.clickgui.*;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.runtime.RuntimeGate;
import dev.loki.lovisual.util.NarratorBlocker;
import dev.loki.lovisual.util.input.KeyManager;
import dev.loki.lovisual.util.text.ClipboardUtil;
@Mixin(KeyboardHandler.class)
public class KeyboardMixin {
@Unique
private static void forwardMovementKeys(Options options, KeyEvent input, boolean pressed) {
if (options.keyUp.matches(input)
|| options.keyDown.matches(input)
|| options.keyLeft.matches(input)
|| options.keyRight.matches(input)
|| options.keyJump.matches(input)
|| options.keyShift.matches(input)
|| options.keySprint.matches(input)) {
InputConstants.Key key = InputConstants.getKey(input);
KeyMapping.set(key, pressed);
}
}
@Unique
private static void releaseMovementKeys(Options options) {
options.keyUp.setDown(false);
options.keyDown.setDown(false);
options.keyLeft.setDown(false);
options.keyRight.setDown(false);
options.keyJump.setDown(false);
options.keyShift.setDown(false);
options.keySprint.setDown(false);
}
// ===== CHAR TYPING =====
@Inject(method = "charTyped", at = @At("HEAD"), cancellable = true)
private void combatant$onChar(long window, CharacterEvent input, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (RuntimeGate.canRunHud()
&& ClientScreen.current() instanceof ChatScreen
&& BetterChat.isActive()
&& BetterChatSearch.isActive()) {
BetterChatSearch.append((char) input.codepoint());
ci.cancel();
return;
}
if (!RuntimeGate.canRunClientLogic() || !ModuleManager.isEnabled("clickgui")) return;
char c = (char) input.codepoint();
if (ClickGuiSearch.isActive()) {
ClickGuiRenderer.onCharTyped(c);
ci.cancel();
return;
}
if (ClickGuiRenderer.onCharTyped(c)) {
ci.cancel();
}
}
// ===== KEY DOWN =====
@Inject(method = "keyPress", at = @At("HEAD"), cancellable = true)
private void combatant$onKey(long window, int action, KeyEvent input, CallbackInfo ci) {
KeyManager.handleKeyEvent(input.key(), input.scancode(), action);
Minecraft mc = Minecraft.getInstance();
KeyInputEvent event = new KeyInputEvent(action, input);
Events.BUS.post(event);
if (event.isCancelled()) {
ci.cancel();
return;
}
Screen screen = ClientScreen.current();
boolean narratorHotkeyContext = screen == null
|| !(screen.getFocused() instanceof EditBox)
|| !((EditBox) screen.getFocused()).canConsumeInput();
if (RuntimeGate.canRunClientLogic()
&& NarratorBlocker.isBlocked()
&& narratorHotkeyContext
&& action != GLFW.GLFW_RELEASE
&& input.hasControlDownWithQuirk()
&& input.key() == GLFW.GLFW_KEY_B) {
NarratorBlocker.enforce(mc);
ci.cancel();
return;
}
if (RuntimeGate.canRunClientLogic()
&& ModuleManager.isEnabled("clickgui")
&& (ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen)
) {
if (ClickGuiRenderer.isBlockingModuleKeybinds()) {
releaseMovementKeys(mc.options);
} else {
forwardMovementKeys(mc.options, input, action != GLFW.GLFW_RELEASE);
}
}
if (RuntimeGate.canRunHud()
&& ClientScreen.current() instanceof ChatScreen
&& BetterChat.isActive()
&& BetterChatSearch.isActive()) {
int key = input.key();
if (action == GLFW.GLFW_PRESS || action == GLFW.GLFW_REPEAT) {
boolean ctrlOrCmd = input.hasControlDownWithQuirk();
if (ctrlOrCmd && key == GLFW.GLFW_KEY_V) {
BetterChatSearch.append(ClipboardUtil.get());
ci.cancel();
return;
}
if (ctrlOrCmd && key == GLFW.GLFW_KEY_A) {
BetterChatSearch.clearText();
ci.cancel();
return;
}
if (key == GLFW.GLFW_KEY_BACKSPACE) {
BetterChatSearch.backspace();
ci.cancel();
return;
}
if (key == GLFW.GLFW_KEY_ENTER || key == GLFW.GLFW_KEY_KP_ENTER) {
BetterChatRenderer.resetScroll();
BetterChatSearch.startSearch(BetterChatStoreManager.getActiveStore(mc));
BetterChatSearch.setActive(false);
ci.cancel();
return;
}
if (key == GLFW.GLFW_KEY_ESCAPE) {
BetterChatSearch.deactivate();
ci.cancel();
return;
}
}
}
if (RuntimeGate.canRunHud()
&& ClientScreen.current() instanceof ChatScreen
&& BetterChat.isActive()
&& !BetterChatSearch.isActive()) {
if (action == 1) { // press
int key = input.key();
if (key == 257 || key == 335) { // enter
EditBox field = ((ChatScreenAccessor) ClientScreen.current()).getChatField();
BetterChatRenderer.applyAutoPrefixIfNeeded(field);
}
}
}
if (!RuntimeGate.canRunClientLogic() || !ModuleManager.isEnabled("clickgui")) return;
int key = input.key();
int scancode = input.scancode();
int modifiers = input.modifiers();
// ESC — закрыть GUI
if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_PRESS) {
if (ClickGuiRenderer.onKey(GLFW.GLFW_KEY_ESCAPE, scancode, GLFW.GLFW_PRESS, modifiers)) {
ci.cancel();
return;
}
if (ClientScreen.current() instanceof ClickGuiEditorScreen) {
ClickGuiRenderer.closeGuiEditor();
ci.cancel();
return;
}
ModuleManager.setEnabled("clickgui", false);
ci.cancel();
return;
}
// РџРѕРёСЃРє РІ GUI
if (ClickGuiSearch.isActive()) {
ClickGuiRenderer.onKey(key, scancode, action, modifiers);
ci.cancel();
return;
}
// Обычное событие клавиши
if (ClickGuiRenderer.onKey(key, scancode, action, modifiers)) {
ci.cancel();
}
}
@Inject(method = "keyPress", at = @At("HEAD"))
private void combatant$betterChatCopy(long window, int action, KeyEvent input, CallbackInfo ci) {
if (action != 1) return; // GLFW_PRESS
Minecraft mc = Minecraft.getInstance();
if (!(ClientScreen.current() instanceof ChatScreen)) return;
if (!RuntimeGate.canRunHud()) return;
if (!BetterChat.isActive()) return;
if ((input.modifiers() & GLFW.GLFW_MOD_CONTROL) == 0) return;
if (input.key() == GLFW.GLFW_KEY_C) {
BetterChatRenderer.copySelectionToClipboard();
}
}
@Inject(method = "keyPress", at = @At("HEAD"))
private void combatant$betterChat$clearOnDebug(long window, int action, KeyEvent input, CallbackInfo ci) {
if (action != GLFW.GLFW_PRESS) return;
if (input.key() != GLFW.GLFW_KEY_D) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
if (!RuntimeGate.canRunHud()) return;
// F3 + D clears vanilla chat; mirror it for BetterChat
if (InputConstants.isKeyDown(mc.getWindow(), GLFW.GLFW_KEY_F3)) {
BetterChatStoreManager.clearActive();
BetterChatRenderer.resetScroll();
}
}
}

View file

@ -0,0 +1,51 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.fog.FogData;
import net.minecraft.client.renderer.fog.environment.LavaFogEnvironment;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(value = LavaFogEnvironment.class, priority = 1200)
public class LavaFogModifierMixin {
@Inject(method = "setupFog", at = @At("HEAD"), cancellable = true)
private void combatant$noLavaFog(FogData data,
Camera camera,
ClientLevel world,
float viewDistance,
DeltaTracker tickCounter,
CallbackInfo ci) {
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogControlEnabled()) return;
if (module.isFogModifyEnabled()) {
module.applyFogOverride(data);
ci.cancel();
return;
}
if (!module.disableLavaFog()) return;
float far = Math.max(viewDistance, 16.0f);
data.environmentalStart = far;
data.environmentalEnd = far;
data.renderDistanceStart = far;
data.renderDistanceEnd = far;
data.skyEnd = far;
data.cloudEnd = far;
ci.cancel();
}
}

View file

@ -0,0 +1,58 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.client.renderer.extract.LevelExtractor;
import net.minecraft.client.renderer.state.level.LevelRenderState;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.mixins.accessors.LevelExtractorAccessor;
@Mixin(LevelExtractor.class)
public abstract class LevelExtractorMixin {
@Inject(method = "extractVisibleEntities", at = @At("TAIL"))
private void freecam$addLocalPlayer(
Camera camera,
Frustum frustum,
DeltaTracker tickCounter,
LevelRenderState renderStates,
CallbackInfo ci
) {
Freecam fc = Modules.get(Freecam.class);
Minecraft mc = Minecraft.getInstance();
if (fc == null || !fc.isEnabled()) return;
if (mc.player == null) return;
for (EntityRenderState state : renderStates.entityRenderStates) {
if (state instanceof AvatarRenderState prs && prs.id == mc.player.getId()) {
return;
}
}
EntityRenderState state = ((LevelExtractorAccessor) this).combatant$invokeExtractEntity(
mc.player,
tickCounter.getGameTimeDeltaPartialTick(false)
);
if (state instanceof AvatarRenderState prs) {
renderStates.entityRenderStates.add(prs);
}
}
}

View file

@ -0,0 +1,43 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.world.level.Level;
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;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks.WeatherMode;
@Mixin(Level.class)
public abstract class LevelMixin {
@Inject(method = "getRainLevel", at = @At("HEAD"), cancellable = true)
private void picker$rainGradient(float tickDelta, CallbackInfoReturnable<Float> cir) {
WorldTweaks wt = Modules.get(WorldTweaks.class);
if (wt == null || !wt.isWeatherControlEnabled()) return;
WeatherMode mode = wt.getWeatherMode();
cir.setReturnValue(
(mode == WeatherMode.RAIN || mode == WeatherMode.THUNDER) ? 1.0F : 0.0F
);
}
@Inject(method = "getThunderLevel", at = @At("HEAD"), cancellable = true)
private void picker$thunderGradient(float tickDelta, CallbackInfoReturnable<Float> cir) {
WorldTweaks wt = Modules.get(WorldTweaks.class);
if (wt == null || !wt.isWeatherControlEnabled()) return;
cir.setReturnValue(
wt.getWeatherMode() == WeatherMode.THUNDER ? 1.0F : 0.0F
);
}
}

View file

@ -0,0 +1,282 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.framegraph.FrameGraphBuilder;
import com.mojang.blaze3d.framegraph.FramePass;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.resource.GraphicsResourceAllocator;
import com.mojang.blaze3d.resource.ResourceHandle;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.LevelTargetBundle;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.state.level.LevelRenderState;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.material.FogType;
import org.spongepowered.asm.mixin.Final;
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;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.BlockHighlight;
import dev.loki.lovisual.features.module.modules.visuals.ReimaginedVisual;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
import dev.loki.lovisual.mixins.accessors.GameRendererAccessor;
import dev.loki.lovisual.mixins.accessors.LevelRendererAccessor;
import dev.loki.lovisual.render.engine.core.CombatantWorldMatrices;
import dev.loki.lovisual.render.engine.depth.PreTranslucentDepth;
import dev.loki.lovisual.render.engine.depth.WorldSceneDepth;
import dev.loki.lovisual.render.iris.IrisRuntime;
import dev.loki.lovisual.render.sky.CustomSkyboxRenderer;
@Mixin(LevelRenderer.class)
public abstract class LevelRendererMixin {
@Shadow
@Final
private LevelTargetBundle targets;
@WrapOperation(
method = "addSkyPass(Lcom/mojang/blaze3d/framegraph/FrameGraphBuilder;Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lcom/mojang/blaze3d/buffers/GpuBufferSlice;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/GameRenderer;mainRenderTarget()Lcom/mojang/blaze3d/pipeline/RenderTarget;"
)
)
private RenderTarget combatant$stableSkyRendererTarget(GameRenderer instance, Operation<RenderTarget> original) {
return ((GameRendererAccessor) instance).combatant$getMainRenderTargetRaw();
}
@Unique
private static void combatant$dependsOn(FramePass pass, ResourceHandle<RenderTarget> handle) {
if (pass != null && handle != null) {
pass.reads(handle);
}
}
@Unique
@SuppressWarnings("unchecked")
private static ResourceHandle<RenderTarget> combatant$asRenderTargetHandle(ResourceHandle<?> handle) {
return (ResourceHandle<RenderTarget>) handle;
}
@Unique
private static RenderTarget combatant$getFramebuffer(ResourceHandle<RenderTarget> handle) {
if (handle == null) {
return null;
}
try {
return handle.get();
} catch (Throwable ignored) {
return null;
}
}
@Unique
private static boolean combatant$needsWorldSceneDepthCapture() {
ReimaginedVisual module = Modules.get(ReimaginedVisual.class);
return module != null && module.needsWorldSceneDepthCapture();
}
@Unique
private static boolean shouldSkipSky(net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState) {
FogType submersion = cameraRenderState.fogType;
if (submersion == FogType.POWDER_SNOW || submersion == FogType.LAVA) {
return true;
}
return cameraRenderState.entityRenderState != null
&& cameraRenderState.entityRenderState.doesMobEffectBlockSky;
}
@Inject(method = "render", at = @At("HEAD"))
private void combatant$captureIrisModelView(
GraphicsResourceAllocator allocator,
DeltaTracker tickCounter,
boolean renderBlockOutline,
net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState,
org.joml.Matrix4fc positionMatrix,
GpuBufferSlice fogBuffer,
org.joml.Vector4f fogColor,
boolean renderSky,
CallbackInfo ci
) {
if (positionMatrix == null) {
return;
}
if (CombatantWorldMatrices.isValid()) {
CombatantWorldMatrices.capturePosition(
positionMatrix,
cameraRenderState != null && cameraRenderState.pos != null
? cameraRenderState.pos
: null
);
return;
}
if (cameraRenderState != null && cameraRenderState.projectionMatrix != null) {
CombatantWorldMatrices.capture(
positionMatrix,
cameraRenderState.projectionMatrix,
cameraRenderState.projectionMatrix,
cameraRenderState.pos
);
}
}
@Inject(
method = "submitBlockOutline",
at = @At("HEAD"),
cancellable = true
)
private void combatant$disableVanillaOutline(PoseStack matrices,
SubmitNodeCollector consumers,
LevelRenderState renderState,
CallbackInfo ci) {
BlockHighlight blockHighlight = Modules.get(BlockHighlight.class);
if (blockHighlight != null && blockHighlight.isOutlineActive()) {
ci.cancel();
}
}
@Inject(
method = "render",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/LevelRenderer;addMainPass(Lcom/mojang/blaze3d/framegraph/FrameGraphBuilder;Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher$PreparedFrame;Lcom/mojang/blaze3d/buffers/GpuBufferSlice;Lnet/minecraft/client/renderer/state/level/LevelRenderState;Lnet/minecraft/util/profiling/ProfilerFiller;Lnet/minecraft/client/renderer/chunk/ChunkSectionsToRender;)V",
shift = At.Shift.AFTER
)
)
private void combatant$captureWorldSceneDepth(
GraphicsResourceAllocator allocator,
DeltaTracker tickCounter,
boolean renderBlockOutline,
net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState,
org.joml.Matrix4fc positionMatrix,
GpuBufferSlice fogBuffer,
org.joml.Vector4f fogColor,
boolean renderSky,
CallbackInfo ci,
@Local FrameGraphBuilder frameGraphBuilder
) {
if (IrisRuntime.isShaderpackRendererActive()) {
return;
}
if (frameGraphBuilder == null || targets == null || targets.main == null) {
return;
}
if (!combatant$needsWorldSceneDepthCapture()) {
WorldSceneDepth.reset();
return;
}
ResourceHandle<RenderTarget> main = targets.main;
ResourceHandle<RenderTarget> translucent = targets.translucent;
ResourceHandle<RenderTarget> itemEntity = targets.itemEntity;
ResourceHandle<RenderTarget> particles = targets.particles;
ResourceHandle<RenderTarget> weather = targets.weather;
FramePass pass = frameGraphBuilder.addPass("combatant_scene_depth_capture");
combatant$dependsOn(pass, main);
combatant$dependsOn(pass, translucent);
combatant$dependsOn(pass, itemEntity);
combatant$dependsOn(pass, particles);
combatant$dependsOn(pass, weather);
pass.disableCulling();
pass.executes(() -> {
RenderTarget mainFramebuffer = combatant$getFramebuffer(main);
Minecraft client = Minecraft.getInstance();
RenderTarget fallbackFramebuffer = client != null ? client.gameRenderer.mainRenderTarget() : null;
int width = mainFramebuffer != null ? mainFramebuffer.width : fallbackFramebuffer != null ? fallbackFramebuffer.width : 1;
int height = mainFramebuffer != null ? mainFramebuffer.height : fallbackFramebuffer != null ? fallbackFramebuffer.height : 1;
WorldSceneDepth.beginFrame(width, height);
WorldSceneDepth.captureMain(mainFramebuffer);
WorldSceneDepth.captureTranslucent(combatant$getFramebuffer(translucent));
WorldSceneDepth.captureItemEntity(combatant$getFramebuffer(itemEntity));
WorldSceneDepth.captureParticles(combatant$getFramebuffer(particles));
WorldSceneDepth.captureWeather(combatant$getFramebuffer(weather));
});
}
@Inject(method = "addSkyPass", at = @At("HEAD"), cancellable = true)
private void combatant$renderSky(FrameGraphBuilder frameGraphBuilder, net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState, GpuBufferSlice fog, CallbackInfo ci) {
if (IrisRuntime.isModLoaded()) return;
if (shouldSkipSky(cameraRenderState)) return;
LevelRendererAccessor accessor = (LevelRendererAccessor) this;
SkyRenderState sky = accessor.combatant$getWorldRenderState().skyRenderState;
if (sky == null || sky.skybox != DimensionType.Skybox.OVERWORLD) return;
WorldTweaks worldTweaks = Modules.get(WorldTweaks.class);
if (worldTweaks != null) {
worldTweaks.applySkyOverrides(sky);
}
ReimaginedVisual module = Modules.get(ReimaginedVisual.class);
if (module == null || !module.isEnabled()) return;
Minecraft client = Minecraft.getInstance();
net.minecraft.client.Camera camera = client != null && client.gameRenderer != null
? client.gameRenderer.mainCamera()
: null;
if (camera == null) return;
FramePass pass = frameGraphBuilder.addPass("combatant_sky");
targets.main = pass.readsAndWrites(targets.main);
pass.executes(() -> CustomSkyboxRenderer.render(camera, fog, sky, accessor.combatant$getSkyRendering()));
ci.cancel();
}
@Inject(
method = "lambda$addMainPass$0",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/chunk/ChunkSectionsToRender;renderGroup(Lnet/minecraft/client/renderer/chunk/ChunkSectionLayerGroup;Lcom/mojang/blaze3d/textures/GpuSampler;)V",
ordinal = 1,
shift = At.Shift.BEFORE
),
remap = false
)
private void combatant$capturePreWaterScene(
GpuBufferSlice fogBuffer,
LevelRenderState worldRenderState,
net.minecraft.util.profiling.ProfilerFiller profiler,
net.minecraft.client.renderer.chunk.ChunkSectionsToRender chunkSections,
com.mojang.blaze3d.resource.ResourceHandle<?> entityOutlineHandle,
net.minecraft.client.renderer.feature.FeatureRenderDispatcher.PreparedFrame preparedFrame,
com.mojang.blaze3d.resource.ResourceHandle<?> translucentFramebufferHandle,
com.mojang.blaze3d.resource.ResourceHandle<?> mainFramebufferHandle,
com.mojang.blaze3d.resource.ResourceHandle<?> itemEntityFramebufferHandle,
com.mojang.blaze3d.resource.ResourceHandle<?> particlesFramebufferHandle,
CallbackInfo ci
) {
if (IrisRuntime.isShaderpackRendererActive()) {
return;
}
RenderTarget mainFramebuffer = combatant$getFramebuffer(combatant$asRenderTargetHandle(mainFramebufferHandle));
if (mainFramebuffer == null) {
Minecraft mc = Minecraft.getInstance();
mainFramebuffer = mc != null && mc.gameRenderer != null ? mc.gameRenderer.mainRenderTarget() : null;
}
PreTranslucentDepth.captureFrom(mainFramebuffer);
}
}

View file

@ -0,0 +1,37 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.audio.DeviceList;
import com.mojang.blaze3d.audio.Library;
import org.jetbrains.annotations.Nullable;
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 dev.loki.lovisual.util.wav.CustomSoundEngine;
/**
* Hooks into Minecraft SoundEngine lifecycle to reset our WAV caches
* when audio is (re)initialized or closed.
*/
@Mixin(Library.class)
public class LibraryMixin {
@Inject(method = "init", at = @At("RETURN"))
private void combatant$resetOnInit(@Nullable String deviceSpecifier, DeviceList deviceList, boolean directionalAudio, CallbackInfo ci) {
CustomSoundEngine.get().reset();
}
@Inject(method = "cleanup", at = @At("HEAD"))
private void combatant$resetOnClose(CallbackInfo ci) {
CustomSoundEngine.get().reset();
}
}

View file

@ -0,0 +1,43 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.Lightmap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.LightmapEvent;
import dev.loki.lovisual.render.engine.light.LightmapState;
@Mixin(Lightmap.class)
public abstract class LightmapMixin {
@ModifyArg(
method = "render",
at = @At(
value = "INVOKE",
target = "Lcom/mojang/blaze3d/buffers/Std140Builder;putFloat(F)Lcom/mojang/blaze3d/buffers/Std140Builder;",
ordinal = 5
),
index = 0
)
private float combatant$ambientLightEvent(float original) {
LightmapEvent event = new LightmapEvent(original);
Events.BUS.post(event);
float value = event.getAmbientLight();
float base = original;
float previousOverride = LightmapState.getOverrideAmbient();
if (previousOverride >= 0.0f && value >= previousOverride - 1.0e-4f) {
base = LightmapState.getBaseAmbient();
}
LightmapState.setAmbient(base, value);
return value;
}
}

View file

@ -0,0 +1,34 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.LightmapRenderStateExtractor;
import net.minecraft.client.renderer.state.LightmapRenderState;
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 dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.LightmapModifyEvent;
import dev.loki.lovisual.render.engine.light.LightmapState;
@Mixin(LightmapRenderStateExtractor.class)
public abstract class LightmapRenderStateExtractorMixin {
@Inject(method = "extract", at = @At("TAIL"))
private void combatant$modifyLightmapState(LightmapRenderState state, float tickDelta, CallbackInfo ci) {
if (state == null) return;
LightmapModifyEvent event = new LightmapModifyEvent(state);
Events.BUS.post(event);
if (event.isModified()) {
state.needsUpdate = true;
LightmapState.setAmbient(event.originalBrightness(), state.brightness);
}
}
}

View file

@ -0,0 +1,463 @@
/*
* This file is part of the Combatant Client distribution.
* Combatant modifications copyright (c) 2026 pivosos2007.
*
* Portions of this file are based on LiquidBounce
* (https://github.com/CCBlueX/LiquidBounce).
* Copyright (c) 2015-2026 CCBlueX.
*
* LiquidBounce portions are licensed under GPLv3-or-later.
* Combatant modifications are licensed under GPLv3.
* See THIRD_PARTY_NOTICES.md for details.
*/
package dev.loki.lovisual.mixins;
/*
* Parts of jump rotation correction are adapted from LiquidBounce
* (https://github.com/CCBlueX/LiquidBounce).
* Original copyright (c) CCBlueX.
*/
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.util.Mth;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.config.values.ItemCooldownRulesValue;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.PlayerJumpEvent;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
import dev.loki.lovisual.features.module.modules.combat.TPSSync;
import dev.loki.lovisual.features.module.modules.movement.ElytraFly;
import dev.loki.lovisual.features.module.modules.movement.NoStun;
import dev.loki.lovisual.features.module.modules.movement.Timer;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
import dev.loki.lovisual.util.aiming.RotationManager;
import dev.loki.lovisual.util.aiming.data.Rotation;
import dev.loki.lovisual.util.aiming.features.MovementCorrection;
import dev.loki.lovisual.util.player.inventory.InventorySwap;
import dev.loki.lovisual.util.pvp.client.CooldownsState;
import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter;
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
@Unique
private float combatant$itemUseRemainder = 0f;
@Unique
private float combatant$effectTickRemainder = 0f;
@Shadow
protected int useItemRemaining;
@Unique
private PlayerJumpEvent combatant$playerJumpEvent;
@Shadow
protected abstract float getJumpPower();
@Inject(method = "getAttributeValue", at = @At("RETURN"))
private void combatant$noStunSlownessMovementSpeed(Holder<Attribute> attribute, CallbackInfoReturnable<Double> cir) {
if (!attribute.equals(Attributes.MOVEMENT_SPEED)) return;
return;
}
@ModifyReturnValue(method = "getMainHandItem", at = @At("RETURN"))
private ItemStack combatant$applyInventorySwapLeaseForMainHand(ItemStack original) {
Minecraft mc = Minecraft.getInstance();
if ((Object) this == mc.player) {
InventorySwap.INSTANCE.isHotbarLeased();
}
return original;
}
/**
* Optional local cooldown synthesis for servers that lock consumables without sending vanilla cooldown packets.
* This never cancels item use; it only starts a visible local timer when explicitly configured.
*/
@Inject(method = "updatingUsingItem", at = @At("HEAD"))
private void combatant$onFinishEating(CallbackInfo ci) {
LivingEntity self = (LivingEntity) (Object) this;
Minecraft mc = Minecraft.getInstance();
if (self != mc.player) return;
ItemStack stack = self.getUseItem();
if (stack == null || stack.isEmpty()) return;
if (self.getUseItemRemainingTicks() != 1) return;
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
if (cooldowns == null) return;
Item item = stack.getItem();
cooldowns.tryStartLocalCooldown(item, ItemCooldownRulesValue.Trigger.CONSUME_FINISH);
}
@Inject(method = "updatingUsingItem", at = @At("HEAD"), cancellable = true)
private void combatant$fixItemUseSpeed(CallbackInfo ci) {
LivingEntity self = (LivingEntity) (Object) this;
if (!(self instanceof Player player)) {
combatant$itemUseRemainder = 0f;
return;
}
if (!player.level().isClientSide()) return;
if (!player.isUsingItem()) {
combatant$itemUseRemainder = 0f;
return;
}
float scale = combatant$clientTickScale();
if (scale >= 0.999f) {
combatant$itemUseRemainder = 0f;
return;
}
combatant$itemUseRemainder += scale;
if (combatant$itemUseRemainder < 1.0f) {
ci.cancel();
return;
}
combatant$itemUseRemainder -= 1.0f;
}
@Redirect(
method = "tickEffects",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/effect/MobEffectInstance;tickClient()V"
)
)
private void combatant$scaleClientEffects(MobEffectInstance instance) {
LivingEntity self = (LivingEntity) (Object) this;
if (!self.level().isClientSide()) {
instance.tickClient();
return;
}
Minecraft mc = Minecraft.getInstance();
if (!(self instanceof Player player) || mc == null || player != mc.player) {
instance.tickClient();
return;
}
float scale = combatant$clientTickScale();
if (scale >= 0.999f) {
combatant$effectTickRemainder = 0f;
instance.tickClient();
return;
}
combatant$effectTickRemainder += scale;
if (combatant$effectTickRemainder < 1.0f) {
return;
}
combatant$effectTickRemainder -= 1.0f;
instance.tickClient();
}
@Unique
private float combatant$clientTickScale() {
float mult = Timer.getTickTimer();
if (mult <= 0.0001f) return 1.0f;
float serverDelta = 1.0f;
TPSSync tps = Modules.get(TPSSync.class);
if (tps != null && tps.isEnabled()) {
serverDelta = tps.getServerTickDelta();
}
float scale = serverDelta / mult;
if (scale > 1.0f) return 1.0f;
return Math.max(scale, 0.0f);
}
/**
* Local totem cooldowns are handled from the confirmed entity-status packet when explicitly configured.
*/
@Inject(method = "checkTotemDeathProtection", at = @At("RETURN"))
private void combatant$onTotemUse(DamageSource source, CallbackInfoReturnable<Boolean> cir) {
// no local use blocking or prediction here
}
/**
* Сброс кулдаунов при смерти.
*/
@Inject(method = "die", at = @At("HEAD"))
private void combatant$onDeath(DamageSource source, CallbackInfo ci) {
LivingEntity self = (LivingEntity) (Object) this;
if (self instanceof Player player) {
TotemPopCounter.onPlayerDeath(player.getUUID());
}
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
if (cooldowns == null || cooldowns.isSystemEnabled()) {
CooldownsState.MANAGER.clear();
}
}
@Inject(
method = "getBlockSpeedFactor",
at = @At("HEAD"),
cancellable = true
)
private void weathercontrol$dontDampRiptide(CallbackInfoReturnable<Float> cir) {
LivingEntity self = (LivingEntity) (Object) this;
if (!(self instanceof Player player)) return;
if (!player.isAutoSpinAttack()) return;
Level world = player.level();
if (world == null) return;
if (WorldTweaks.isServerRaining(world)) {
cir.setReturnValue(1.0F);
}
}
@Inject(method = "aiStep", at = @At("HEAD"))
private void preventCrash(CallbackInfo ci) {
LivingEntity self = (LivingEntity) (Object) this;
if (!(self instanceof Player p)) return;
ElytraFly elytraFly = Modules.get(ElytraFly.class);
if (elytraFly == null || !elytraFly.isEnabled()) return;
if (!elytraFly.noCrash.get()) return;
if (!p.isFallFlying()) return;
Vec3 vel = p.getDeltaMovement();
if (vel.horizontalDistance() < 1) return;
AABB next = p.getBoundingBox().move(vel);
if (!p.level().noCollision(next)) {
p.setDeltaMovement(0, 0, 0);
}
}
@ModifyExpressionValue(
method = "updateFallFlyingMovement",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getLookAngle()Lnet/minecraft/world/phys/Vec3;"
)
)
private Vec3 combatant$modifyGlidingRotationVector(Vec3 original) {
LivingEntity self = (LivingEntity) (Object) this;
Minecraft mc = Minecraft.getInstance();
if (!(self instanceof LocalPlayer player) || mc == null || player != mc.player) {
return original;
}
Rotation rotation = RotationManager.INSTANCE.getMovementRotation();
var rotationTarget = RotationManager.INSTANCE.getActiveRotationTarget();
if (rotationTarget == null
|| rotationTarget.movementCorrection == MovementCorrection.OFF
|| rotationTarget.movementCorrection == MovementCorrection.STRICT) {
return original;
}
return rotation.directionVector();
}
@ModifyExpressionValue(
method = "updateFallFlyingMovement",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getXRot()F"
)
)
private float combatant$modifyGlidingPitch(float original) {
LivingEntity self = (LivingEntity) (Object) this;
Minecraft mc = Minecraft.getInstance();
if (!(self instanceof LocalPlayer player) || mc == null || player != mc.player) {
return original;
}
Rotation rotation = RotationManager.INSTANCE.getMovementRotation();
var rotationTarget = RotationManager.INSTANCE.getActiveRotationTarget();
if (rotationTarget == null || rotationTarget.movementCorrection == MovementCorrection.OFF) {
return original;
}
return rotation.pitch();
}
@Inject(method = "hasEffect", at = @At("HEAD"), cancellable = true)
private void killHasEffect(Holder<MobEffect> effect,
CallbackInfoReturnable<Boolean> cir) {
NoRender noRender = Modules.get(NoRender.class);
if (noRender == null || !noRender.off("bad_effects")) return;
if (isBad(effect)) {
cir.setReturnValue(false);
}
}
@Inject(method = "getEffect", at = @At("HEAD"), cancellable = true)
private void killGetEffect(Holder<MobEffect> effect,
CallbackInfoReturnable<MobEffectInstance> cir) {
NoRender noRender = Modules.get(NoRender.class);
if (noRender == null || !noRender.off("bad_effects")) return;
if (isBad(effect)) {
cir.setReturnValue(null);
}
}
@Inject(method = "jumpFromGround", at = @At("HEAD"), cancellable = true)
private void combatant$onJumpHead(CallbackInfo ci) {
LivingEntity self = (LivingEntity) (Object) this;
if (!(self instanceof LocalPlayer player)) {
combatant$playerJumpEvent = null;
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc == null || player != mc.player) {
combatant$playerJumpEvent = null;
return;
}
PlayerJumpEvent event = new PlayerJumpEvent(self.getYRot(), getJumpPower());
Events.BUS.post(event);
if (event.isCancelled()) {
combatant$playerJumpEvent = null;
ci.cancel();
return;
}
combatant$playerJumpEvent = event;
}
@ModifyExpressionValue(
method = "jumpFromGround",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getJumpPower()F"
)
)
private float combatant$modifyJumpMotion(float original) {
return combatant$playerJumpEvent != null ? combatant$playerJumpEvent.getMotion() : original;
}
@ModifyExpressionValue(
method = "jumpFromGround",
at = @At(
value = "NEW",
target = "(DDD)Lnet/minecraft/world/phys/Vec3;"
)
)
private Vec3 combatant$fixJumpRotation(Vec3 original) {
LivingEntity self = (LivingEntity) (Object) this;
Minecraft mc = Minecraft.getInstance();
if (!(self instanceof LocalPlayer player) || mc == null || player != mc.player) {
return original;
}
Rotation rotation = RotationManager.INSTANCE.getMovementRotation();
var rotationTarget = RotationManager.INSTANCE.getActiveRotationTarget();
if (rotationTarget == null || rotationTarget.movementCorrection == MovementCorrection.OFF) {
return original;
}
float yaw = rotation.yaw() * Mth.DEG_TO_RAD;
return new Vec3(-Mth.sin(yaw) * 0.2F, 0.0, Mth.cos(yaw) * 0.2F);
}
@Redirect(
method = "jumpFromGround",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getYRot()F"
)
)
private float combatant$omniSprintJumpYaw(LivingEntity instance) {
return combatant$playerJumpEvent != null ? combatant$playerJumpEvent.getYaw() : instance.getYRot();
}
@Inject(method = "jumpFromGround", at = @At("RETURN"))
private void combatant$onJumpReturn(CallbackInfo ci) {
combatant$playerJumpEvent = null;
}
@Unique
private boolean isBad(Holder<MobEffect> entry) {
MobEffect eff = entry.value();
return eff == MobEffects.BLINDNESS.value()
|| eff == MobEffects.DARKNESS.value()
|| eff == MobEffects.NAUSEA.value();
}
@Inject(
method = "getBlockSpeedFactor",
at = @At("RETURN"),
cancellable = true
)
private void nostun$soulSandVelocity(CallbackInfoReturnable<Float> cir) {
LivingEntity self = (LivingEntity) (Object) this;
if (!(self instanceof Player player)) return;
Minecraft client = Minecraft.getInstance();
if (client == null || player != client.player) return;
NoStun ns = Modules.get(NoStun.class);
if (ns == null || !ns.isEnabled()) return;
if (ns.isVulcan297()) {
BlockPos pos = player.blockPosition();
BlockState state = player.level().getBlockState(pos);
if (state.is(Blocks.SOUL_SAND)) {
cir.setReturnValue(ns.getSoulSandVelocityMultiplier(cir.getReturnValue()));
}
return;
}
// общий тумблер env
if (!ns.isFunctionEnabled(NoStun.fnEnvBlocks())) return;
// конкретный тумблер песка душ
if (!ns.isEnvBlockEnabled(NoStun.envSoulSand())) return;
BlockPos pos = player.blockPosition();
BlockState state = player.level().getBlockState(pos);
if (!state.is(Blocks.SOUL_SAND)) return;
float vanilla = cir.getReturnValue();
if (vanilla < 1.0F) {
cir.setReturnValue(1.0F);
}
}
}

View file

@ -0,0 +1,55 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import dev.loki.lovisual.render.ViewObstructionFadeState;
@Mixin(LivingEntityRenderState.class)
public abstract class LivingEntityRenderStateMixin implements ViewObstructionFadeState {
@Unique
private boolean combatant$viewObstructionFadeActive;
@Unique
private float combatant$viewObstructionFadeAlpha = 1.0f;
@Unique
private boolean combatant$seeInvisibleFadeActive;
@Override
public boolean combatant$isViewObstructionFadeActive() {
return combatant$viewObstructionFadeActive;
}
@Override
public float combatant$getViewObstructionFadeAlpha() {
return combatant$viewObstructionFadeAlpha;
}
@Override
public void combatant$setViewObstructionFadeActive(boolean active) {
this.combatant$viewObstructionFadeActive = active;
}
@Override
public void combatant$setViewObstructionFadeAlpha(float alpha) {
this.combatant$viewObstructionFadeAlpha = alpha;
}
@Override
public boolean combatant$isSeeInvisibleFadeActive() {
return combatant$seeInvisibleFadeActive;
}
@Override
public void combatant$setSeeInvisibleFadeActive(boolean active) {
this.combatant$seeInvisibleFadeActive = active;
}
}

View file

@ -0,0 +1,196 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NameTags;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.features.module.modules.visuals.SeeInvisibles;
import dev.loki.lovisual.render.ViewObstructionFadeContext;
import dev.loki.lovisual.render.ViewObstructionFadeState;
import dev.loki.lovisual.render.iris.IrisRuntime;
import dev.loki.lovisual.util.aiming.RotationManager;
import dev.loki.lovisual.util.aiming.data.Rotation;
@Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> extends EntityRenderer<T, S> {
protected LivingEntityRendererMixin(EntityRendererProvider.Context ctx) {
super(ctx);
}
@Shadow
public abstract Identifier getTextureLocation(S state);
@Inject(
method = "shouldShowName(Lnet/minecraft/world/entity/LivingEntity;D)Z",
at = @At("HEAD"),
cancellable = true
)
private void combatant$hideVanillaLabels(T entity, double distanceSq, CallbackInfoReturnable<Boolean> cir) {
if (entity instanceof Player && Modules.get(NameTags.class) != null && Modules.get(NameTags.class).isEnabled()) {
cir.setReturnValue(false);
}
}
@Inject(
method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V",
at = @At("TAIL")
)
private void combatant$updateViewObstructionFadeState(T entity, S state, float tickProgress, CallbackInfo ci) {
if (!(state instanceof ViewObstructionFadeState fadeState)) {
return;
}
NoRender noRender = Modules.get(NoRender.class);
boolean obstructionFadeActive = noRender != null && noRender.shouldFadeEntity(entity);
float obstructionFadeAlpha = obstructionFadeActive ? noRender.getEntityFadeAlpha(entity) : 1.0f;
obstructionFadeActive = obstructionFadeActive && obstructionFadeAlpha < 0.99f;
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class);
boolean seeInvisibleActive = seeInvisibles != null && seeInvisibles.shouldRenderInvisiblePlayer(entity);
float seeInvisibleAlpha = seeInvisibleActive ? seeInvisibles.getInvisiblePlayerAlpha01() : 1.0f;
boolean seeInvisibleFadeActive = seeInvisibleActive && seeInvisibleAlpha < 0.99f;
if (seeInvisibleActive) {
state.isInvisibleToPlayer = false;
}
boolean active = obstructionFadeActive || seeInvisibleFadeActive;
float alpha = obstructionFadeAlpha;
if (seeInvisibleFadeActive) {
alpha = Math.min(alpha, seeInvisibleAlpha);
}
fadeState.combatant$setViewObstructionFadeActive(active);
fadeState.combatant$setViewObstructionFadeAlpha(active ? alpha : 1.0f);
fadeState.combatant$setSeeInvisibleFadeActive(seeInvisibleFadeActive);
}
@ModifyExpressionValue(
method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Mth;rotLerp(FFF)F")
)
private float combatant$renderRotationYaw(float original, @Local(argsOnly = true) T entity, @Local(argsOnly = true) float tickProgress) {
if (!(entity instanceof LocalPlayer player)) {
return original;
}
Minecraft mc = Minecraft.getInstance();
if (mc.player != player || ClientScreen.current() instanceof AbstractContainerScreen<?>) {
return original;
}
Rotation current = RotationManager.INSTANCE.getCurrentRotation();
Rotation previous = RotationManager.INSTANCE.getPreviousRotation();
if (current == null) {
return original;
}
float prevYaw = previous != null ? previous.yaw() : current.yaw();
if (prevYaw == player.getYRot() && current.yaw() == player.getYRot()) {
return original;
}
return Mth.rotLerp(tickProgress, prevYaw, current.yaw());
}
@ModifyExpressionValue(
method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getXRot(F)F")
)
private float combatant$renderRotationPitch(float original, @Local(argsOnly = true) T entity, @Local(argsOnly = true) float tickProgress) {
if (!(entity instanceof LocalPlayer player)) {
return original;
}
Minecraft mc = Minecraft.getInstance();
if (mc.player != player || ClientScreen.current() instanceof AbstractContainerScreen<?>) {
return original;
}
Rotation current = RotationManager.INSTANCE.getCurrentRotation();
Rotation previous = RotationManager.INSTANCE.getPreviousRotation();
if (current == null) {
return original;
}
float prevPitch = previous != null ? previous.pitch() : current.pitch();
if (prevPitch == player.getXRot() && current.pitch() == player.getXRot()) {
return original;
}
return Mth.lerp(tickProgress, prevPitch, current.pitch());
}
@ModifyExpressionValue(
method = "submit",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/LivingEntityRenderer;getRenderType(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;ZZZ)Lnet/minecraft/client/renderer/rendertype/RenderType;")
)
private RenderType combatant$useTranslucentRenderTypeForViewFade(RenderType original, @Local(argsOnly = true) S state) {
if (original == null || original.hasBlending()) {
return original;
}
if (IrisRuntime.isRenderingShadowPass()) {
return original;
}
if (!(state instanceof ViewObstructionFadeState fadeState)
|| !fadeState.combatant$isViewObstructionFadeActive()
|| fadeState.combatant$getViewObstructionFadeAlpha() >= 0.99f) {
return original;
}
return RenderTypes.entityTranslucent(getTextureLocation(state), true);
}
@ModifyConstant(
method = "submit",
constant = @Constant(intValue = 654311423)
)
private int combatant$seeInvisibles$overrideInvisiblePlayerAlpha(int vanillaTint, S state) {
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class);
if (seeInvisibles != null
&& state instanceof ViewObstructionFadeState fadeState
&& fadeState.combatant$isSeeInvisibleFadeActive()
&& seeInvisibles.shouldRenderInvisiblePlayer(state.entityType, state.isInvisible)) {
return seeInvisibles.getInvisiblePlayerTintArgb();
}
return vanillaTint;
}
@Inject(method = "submit", at = @At("HEAD"))
private void combatant$pushViewObstructionFadeContext(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.push(state instanceof ViewObstructionFadeState fadeState ? fadeState : null);
}
@Inject(method = "submit", at = @At("RETURN"))
private void combatant$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();
}
}

View file

@ -0,0 +1,124 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.fabricmc.loader.api.FabricLoader;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.List;
import java.util.Set;
public final class LoVisualMixinPlugin implements IMixinConfigPlugin {
// Sodium 26.2 uses Mojang RenderPipeline for terrain on both GL and Vulkan backends.
private static final boolean STUB_SODIUM = false;
private static final boolean STUB_IRIS = false;
private static final Set<String> DISABLED_SODIUM_SHADOW_GRAPH_MIXINS = Set.of();
private static final Set<String> OPTIONAL_SODIUM_MIXINS = Set.of(
"dev.loki.lovisual.mixins.sodium.SodiumBlockRendererMixin",
"dev.loki.lovisual.mixins.sodium.SodiumBlockVisibilityMixin",
"dev.loki.lovisual.mixins.sodium.SodiumChunkMeshFormatsMixin",
"dev.loki.lovisual.mixins.sodium.SodiumChunkBuilderMeshingTaskMixin",
"dev.loki.lovisual.mixins.sodium.SodiumChunkVertexMixin",
"dev.loki.lovisual.mixins.sodium.SodiumShaderChunkRendererMixin",
"dev.loki.lovisual.mixins.sodium.SodiumVertexConsumerTrackerMixin"
);
private static final Set<String> OPTIONAL_IRIS_MIXINS = Set.of(
"dev.loki.lovisual.mixins.iris.IrisCommonUniformsMixin",
"dev.loki.lovisual.mixins.iris.IrisGameRendererInteropMixin",
"dev.loki.lovisual.mixins.iris.IrisHandRendererAccessor",
"dev.loki.lovisual.mixins.iris.IrisHandRendererMixin",
"dev.loki.lovisual.mixins.iris.IrisIncludeProcessorMixin",
"dev.loki.lovisual.mixins.iris.IrisShaderPackLoadMixin",
"dev.loki.lovisual.mixins.iris.IrisRenderingPipelineFinalizeMixin",
"dev.loki.lovisual.mixins.iris.IrisVanillaHandInteropMixin"
);
private static final Set<String> DISABLED_WITH_IRIS_MIXINS = Set.of(
"dev.loki.lovisual.mixins.sodium.SodiumBlockRendererMixin",
"dev.loki.lovisual.mixins.sodium.SodiumChunkMeshFormatsMixin",
"dev.loki.lovisual.mixins.sodium.SodiumChunkBuilderMeshingTaskMixin",
"dev.loki.lovisual.mixins.sodium.SodiumChunkVertexMixin",
"dev.loki.lovisual.mixins.sodium.SodiumShaderChunkRendererMixin"
);
private static final Set<String> OPTIONAL_XAERO_MINIMAP_MIXINS = Set.of(
"dev.loki.lovisual.mixins.xaero.minimap.AbstractWaypointRenderProviderMixin"
);
private static final Set<String> OPTIONAL_XAERO_WORLDMAP_MIXINS = Set.of(
"dev.loki.lovisual.mixins.xaero.worldmap.WorldMapWaypointAccessor",
"dev.loki.lovisual.mixins.xaero.worldmap.WaypointRenderProviderMixin"
);
private static final Set<String> OPTIONAL_MORECULLING_MIXINS = Set.of(
"dev.loki.lovisual.mixins.moreculling.CullingUtilsMixin"
);
private static volatile boolean loggedSodiumDecision;
@Override
public void onLoad(String mixinPackage) {
}
@Override
public String getRefMapperConfig() {
return null;
}
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
if (STUB_SODIUM && OPTIONAL_SODIUM_MIXINS.contains(mixinClassName)) {
return false;
}
if (STUB_IRIS && OPTIONAL_IRIS_MIXINS.contains(mixinClassName)) {
return false;
}
if (DISABLED_SODIUM_SHADOW_GRAPH_MIXINS.contains(mixinClassName)) {
return false;
}
if (OPTIONAL_XAERO_MINIMAP_MIXINS.contains(mixinClassName)) {
return FabricLoader.getInstance().isModLoaded("xaerominimap");
}
if (OPTIONAL_XAERO_WORLDMAP_MIXINS.contains(mixinClassName)) {
return FabricLoader.getInstance().isModLoaded("xaeroworldmap");
}
if (OPTIONAL_MORECULLING_MIXINS.contains(mixinClassName)) {
return FabricLoader.getInstance().isModLoaded("moreculling");
}
if (OPTIONAL_IRIS_MIXINS.contains(mixinClassName)) {
return FabricLoader.getInstance().isModLoaded("iris");
}
if (DISABLED_WITH_IRIS_MIXINS.contains(mixinClassName)) {
return !FabricLoader.getInstance().isModLoaded("iris");
}
if (!OPTIONAL_SODIUM_MIXINS.contains(mixinClassName)) {
return true;
}
boolean sodiumLoaded = FabricLoader.getInstance().isModLoaded("sodium");
if (!loggedSodiumDecision) {
loggedSodiumDecision = true;
DebugLog.warn("[SodiumCompat] optional mixins %s", sodiumLoaded ? "enabled" : "disabled");
}
return sodiumLoaded;
}
@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> getMixins() {
return null;
}
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
}

View file

@ -0,0 +1,904 @@
/*
* This file is part of the Combatant Client distribution.
* Combatant modifications copyright (c) 2026 pivosos2007.
*
* Portions of this file are based on LiquidBounce
* (https://github.com/CCBlueX/LiquidBounce).
* Copyright (c) 2015-2026 CCBlueX.
*
* LiquidBounce portions are licensed under GPLv3-or-later.
* Combatant modifications are licensed under GPLv3.
* See THIRD_PARTY_NOTICES.md for details.
*/
package dev.loki.lovisual.mixins;
/*
* Parts of sprint control, silent rotations, and movement-input correction
* are adapted from LiquidBounce (https://github.com/CCBlueX/LiquidBounce).
* Original copyright (c) CCBlueX.
*/
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import dev.loki.lovisual.events.impl.*;
import dev.loki.lovisual.features.module.modules.movement.*;
import dev.loki.lovisual.util.combat.SprintController;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.player.ClientInput;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PortalProcessor;
import net.minecraft.world.entity.player.Input;
import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.*;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.*;
import dev.loki.lovisual.features.module.modules.player.XCarry;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.mixininterface.ILocalPlayer;
import dev.loki.lovisual.mixins.accessors.EntityAccessor;
import dev.loki.lovisual.mixins.accessors.InputAccessor;
import dev.loki.lovisual.mixins.accessors.LocalPlayerAccessor;
import dev.loki.lovisual.runtime.RuntimeGate;
import dev.loki.lovisual.util.aiming.RotationManager;
import dev.loki.lovisual.util.aiming.data.Rotation;
import java.util.ArrayList;
import java.util.List;
@Mixin(LocalPlayer.class)
public abstract class LocalPlayerMixin implements ILocalPlayer {
@Shadow
public ClientInput input;
@Unique
private float combatant$lastYaw;
@Unique
private float combatant$lastPitch;
@Unique
private double combatant$rotationPrevX;
@Unique
private double combatant$rotationPrevZ;
@Unique
private float combatant$rotationPrevBodyYaw;
@Unique
private boolean combatant$rotationBodyYawInitialized;
@Unique
private float freecam$lastHealth = Float.NaN;
@Unique
private boolean frozen = false;
@Unique
private Vec3 savedVelocity = Vec3.ZERO;
@Unique
private double savedX, savedY, savedZ;
@Unique
private float savedBodyYaw;
@Unique
private float savedHeadYaw;
@Unique
private Runnable combatant$eventPostAction;
@Unique
private EventSync combatant$activeSyncEvent;
@Unique
private boolean combatant$postUpdateLock = false;
@Unique
private Input lastInput;
@Unique
private boolean pendingSpoof = false;
@Unique
private int boostTicks = 0;
@Unique
private double boostX = 0, boostZ = 0;
@Unique
private boolean wasGround = true;
@Unique
private static boolean combatant$sameInput(Input a, Input b) {
if (a == b) return true;
if (a == null || b == null) return false;
return a.forward() == b.forward()
&& a.backward() == b.backward()
&& a.left() == b.left()
&& a.right() == b.right()
&& a.jump() == b.jump()
&& a.shift() == b.shift()
&& a.sprint() == b.sprint();
}
@Unique
private static float combatant$impulse(boolean positive, boolean negative) {
if (positive == negative) {
return 0.0f;
}
return positive ? 1.0f : -1.0f;
}
@Unique
private static float combatant$simulateBodyYaw(
float yaw,
float prevBodyYaw,
double prevX,
double prevZ,
double currentX,
double currentZ,
float handSwingProgress
) {
double motionX = currentX - prevX;
double motionZ = currentZ - prevZ;
float motionSquared = (float) (motionX * motionX + motionZ * motionZ);
float bodyYaw = prevBodyYaw;
if (motionSquared > 0.0025000002F) {
float movementYaw = (float) Mth.atan2(motionZ, motionX) * Mth.RAD_TO_DEG - 90.0F;
float yawDiff = Math.abs(Mth.wrapDegrees(yaw) - movementYaw);
bodyYaw = (yawDiff > 95.0F && yawDiff < 265.0F) ? movementYaw - 180.0F : movementYaw;
}
if (handSwingProgress - 0.2F > 0.0F) {
bodyYaw = yaw;
}
float deltaYaw = Mth.wrapDegrees(bodyYaw - prevBodyYaw);
bodyYaw = prevBodyYaw + deltaYaw * 0.3F;
float yawOffsetDiff = Mth.wrapDegrees(yaw - bodyYaw);
float maxHeadRotation = 52.0F;
if (Math.abs(yawOffsetDiff) > maxHeadRotation) {
bodyYaw += yawOffsetDiff - Math.signum(yawOffsetDiff) * maxHeadRotation;
}
return bodyYaw;
}
@Inject(method = "tick", at = @At("HEAD"))
private void combatant$cacheLastRotations(CallbackInfo ci) {
LocalPlayer self = (LocalPlayer) (Object) this;
combatant$lastYaw = self.getYRot();
combatant$lastPitch = self.getXRot();
RotationManager.INSTANCE.runTickLifecycle();
if (!combatant$rotationBodyYawInitialized) {
combatant$rotationPrevX = self.getX();
combatant$rotationPrevZ = self.getZ();
combatant$rotationPrevBodyYaw = self.getVisualRotationYInDegrees();
combatant$rotationBodyYawInitialized = true;
}
}
@Inject(method = "handlePortalTransitionEffect", at = @At("HEAD"), cancellable = true)
private void combatant$disablePortalNausea(boolean inPortal, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
LocalPlayer self = (LocalPlayer) (Object) this;
PortalProcessor portalManager = null;
if (self instanceof EntityAccessor accessor) {
portalManager = accessor.combatant$getPortalManager();
}
if (portalManager != null && portalManager.isInsidePortalThisTick()) {
if (self instanceof LocalPlayerAccessor accessor) {
accessor.combatant$setNauseaIntensity(0.0f);
accessor.combatant$setLastNauseaIntensity(0.0f);
}
ci.cancel();
}
}
@Override
public float combatant$getLastYaw() {
return combatant$lastYaw;
}
@Override
public float combatant$getLastPitch() {
return combatant$lastPitch;
}
@Override
public void combatant$setLastYaw(float yaw) {
this.combatant$lastYaw = yaw;
}
@Override
public void combatant$setLastPitch(float pitch) {
this.combatant$lastPitch = pitch;
}
@ModifyExpressionValue(
method = "aiStep",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;canStartSprinting()Z"
)
)
private boolean combatant$sprintMovementStart(boolean original) {
return combatant$postSprintControl(original, SprintControlEvent.Source.MOVEMENT_TICK);
}
@ModifyExpressionValue(
method = "aiStep",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Input;sprint()Z"
)
)
private boolean combatant$sprintMovementInput(boolean original) {
return combatant$postSprintControl(original, SprintControlEvent.Source.MOVEMENT_TICK);
}
@ModifyExpressionValue(
method = "sendIsSprintingIfNeeded",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;isSprinting()Z"
)
)
private boolean combatant$sprintNetwork(boolean original) {
if (SprintController.INSTANCE.isSprintBlocked()) {
return false;
}
return original;
}
@Unique
private boolean combatant$postSprintControl(boolean original, SprintControlEvent.Source source) {
LocalPlayer self = (LocalPlayer) (Object) this;
if (self.input == null) {
return original;
}
if (!Events.BUS.hasListeners(SprintControlEvent.class)) {
return original;
}
Vec2 movement = self.input.getMoveVector();
SprintControlEvent event = new SprintControlEvent(
movement.y,
movement.x,
original,
source
);
Events.BUS.post(event);
return event.shouldSprint();
}
@Inject(method = "modifyInput", at = @At("RETURN"), cancellable = true)
private void boostWhenUsing(Vec2 input, CallbackInfoReturnable<Vec2> cir) {
NoStun ns = Modules.get(NoStun.class);
if (ns == null || !ns.isEnabled()) return;
if (!ns.isFunctionEnabled(NoStun.fnUseSpeed())) return;
LocalPlayer player = (LocalPlayer) (Object) this;
if (player.isUsingItem() && !player.isPassenger()) {
Vec2 base = cir.getReturnValue();
// 0.0 -> vanilla (no change), 1.0 -> normal walking speed.
// In vanilla, most item-use slowdowns are ~0.2x, so we approximate by lerping 1..5.
float t = ns.getEatSpeed01();
float mult = 1.0f + t * 4.0f;
if (mult > 1.001f) {
cir.setReturnValue(base.scale(mult));
}
}
}
@Inject(method = "aiStep", at = @At("HEAD"))
private void combatant$elytraFlyVanilla(CallbackInfo ci) {
LocalPlayer player = (LocalPlayer) (Object) this;
ElytraFly elytraFly = Modules.get(ElytraFly.class);
if (elytraFly == null || !elytraFly.shouldApplyVanillaHook(player)) {
return;
}
Vec3 velocity = player.getDeltaMovement();
Vec3 look = player.getViewVector(1.0f);
float pitch = player.getXRot();
velocity = velocity.scale(elytraFly.getVanillaDragFactor());
velocity = velocity.add(look.scale(elytraFly.getVanillaForwardBoostFactor()));
double lift = -Math.sin(Math.toRadians(pitch)) * elytraFly.getVanillaPitchLiftFactor();
player.setDeltaMovement(velocity.add(0.0, lift, 0.0));
}
@Shadow
protected abstract void sendPosition();
@Inject(method = "applyInput", at = @At("HEAD"), cancellable = true)
private void freecam$blockWASD(CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && fc.isCameraInput()) {
ci.cancel();
}
}
@Inject(
method = "aiStep",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/ClientInput;tick()V",
shift = At.Shift.AFTER
)
)
private void freecam$afterInputTick(CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc == null || !fc.isEnabled() || !fc.isCameraInput()) return;
((InputAccessor) this.input).setMovementVector(Vec2.ZERO);
boolean sneak = false;
boolean sprint = this.input.keyPresses.sprint();
this.input.keyPresses = new Input(
false, false, false, false,
false,
sneak,
sprint
);
}
@Inject(
method = "aiStep",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/ClientInput;tick()V",
shift = At.Shift.AFTER
)
)
private void combatant$afterInputTickSprint(CallbackInfo ci) {
if (this.input == null) return;
Input original = this.input.keyPresses;
if (original == null) return;
Input input = original;
if (Events.BUS.hasListeners(MovementInputEvent.class)) {
MovementInputEvent event = new MovementInputEvent(input);
Events.BUS.post(event);
Input eventInput = event.toPlayerInput();
if (!combatant$sameInput(input, eventInput)) {
input = eventInput;
}
}
Input transformedInput = combatant$transformDirection(input);
if (!combatant$sameInput(input, transformedInput)) {
input = transformedInput;
}
if (Events.BUS.hasListeners(SprintControlEvent.class)) {
float forward = combatant$impulse(input.forward(), input.backward());
float sideways = combatant$impulse(input.left(), input.right());
SprintControlEvent event = new SprintControlEvent(
forward,
sideways,
input.sprint(),
SprintControlEvent.Source.INPUT
);
Events.BUS.post(event);
if (event.shouldSprint() != input.sprint()) {
input = new Input(
input.forward(),
input.backward(),
input.left(),
input.right(),
input.jump(),
input.shift(),
event.shouldSprint()
);
}
}
if (!combatant$sameInput(original, input)) {
combatant$applyPlayerInput(input);
}
}
@Unique
private void combatant$applyPlayerInput(Input input) {
if (this.input == null || input == null) {
return;
}
InputAccessor accessor = (InputAccessor) this.input;
accessor.setPlayerInput(input);
float forward = combatant$impulse(input.forward(), input.backward());
float sideways = combatant$impulse(input.left(), input.right());
accessor.setMovementVector(new Vec2(sideways, forward).normalized());
}
@Unique
private Input combatant$transformDirection(Input input) {
// Free correction now runs at KeyboardInput.tick(), matching the input pipeline.
// Keep this post-input hook inert to avoid double-transforming movement input.
return input;
}
@Redirect(
method = "aiStep",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Input;shift()Z",
ordinal = 0
)
)
private boolean freecam$noFlyDown0(Input in) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && fc.isCameraInput()) {
LocalPlayer self = (LocalPlayer) (Object) this;
if (self.getAbilities().flying) return false;
}
return in.shift();
}
@Redirect(
method = "aiStep",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Input;shift()Z",
ordinal = 1
),
require = 0
)
private boolean freecam$noFlyDown1(Input in) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && fc.isCameraInput()) {
LocalPlayer self = (LocalPlayer) (Object) this;
if (self.getAbilities().flying) return false;
}
return in.shift();
}
@Inject(method = "hurtTo", at = @At("HEAD"))
private void freecam$onUpdateHealth(float newHealth, CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc == null || !fc.isEnabled() || !fc.disableOnDamage()) {
freecam$lastHealth = newHealth;
return;
}
if (!Float.isNaN(freecam$lastHealth) && newHealth < freecam$lastHealth) {
fc.toggle();
}
freecam$lastHealth = newHealth;
}
@Unique
private boolean shouldFreezeNow() {
boolean freezeModule = Modules.enabled(Freeze.class);
Freecam freecam = Modules.get(Freecam.class);
boolean freecamFreeze = freecam != null && freecam.freezePlayer() && freecam.isCameraInput();
return freezeModule || freecamFreeze;
}
@Inject(method = "sendPosition", at = @At("HEAD"), cancellable = true)
private void combatant$eventSync(CallbackInfo ci) {
if (!Events.BUS.hasListeners(EventSync.class)) return;
combatant$eventPostAction = null;
LocalPlayer self = (LocalPlayer) (Object) this;
EventSync event = new EventSync(self.getYRot(), self.getXRot());
Events.BUS.post(event);
combatant$activeSyncEvent = event;
combatant$eventPostAction = event.getPostAction();
if (event.isCancelled()) {
combatant$activeSyncEvent = null;
combatant$eventPostAction = null;
ci.cancel();
}
}
@Inject(method = "sendPosition", at = @At("RETURN"))
private void combatant$eventPostSync(CallbackInfo ci) {
if (Events.BUS.hasListeners(EventPostSync.class)) {
Events.BUS.post(new EventPostSync());
}
if (combatant$eventPostAction != null) {
combatant$eventPostAction.run();
combatant$eventPostAction = null;
}
combatant$activeSyncEvent = null;
}
@ModifyExpressionValue(
method = "sendPosition",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;getYRot()F"
)
)
private float combatant$eventSyncYaw(float original) {
combatant$updateVisualBodyYaw();
EventSync event = combatant$activeSyncEvent;
if (event != null && event.hasRotationOverride()) return event.getYaw();
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
return rotation != null ? rotation.yaw() : original;
}
@ModifyExpressionValue(
method = "sendPosition",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;getXRot()F"
)
)
private float combatant$eventSyncPitch(float original) {
EventSync event = combatant$activeSyncEvent;
if (event != null && event.hasRotationOverride()) return Mth.clamp(event.getPitch(), -90.0f, 90.0f);
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original;
}
@ModifyExpressionValue(
method = "tick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;getYRot()F"
)
)
private float combatant$visualTickYaw(float original) {
combatant$updateVisualBodyYaw();
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
return rotation != null ? rotation.yaw() : original;
}
@ModifyExpressionValue(
method = "tick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;getXRot()F"
)
)
private float combatant$visualTickPitch(float original) {
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original;
}
@Inject(
method = "tick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;sendPosition()V",
ordinal = 0,
shift = At.Shift.AFTER
),
cancellable = true
)
private void combatant$postPlayerUpdate(CallbackInfo ci) {
if (!Events.BUS.hasListeners(PostPlayerUpdateEvent.class)) return;
if (combatant$postUpdateLock) return;
PostPlayerUpdateEvent event = new PostPlayerUpdateEvent();
Events.BUS.post(event);
if (!event.isCancelled()) return;
ci.cancel();
int iterations = event.getIterations();
if (iterations <= 0) return;
LocalPlayer self = (LocalPlayer) (Object) this;
for (int i = 0; i < iterations; i++) {
combatant$postUpdateLock = true;
self.tick();
combatant$postUpdateLock = false;
sendPosition();
}
}
@Inject(method = "sendPosition", at = @At("HEAD"), cancellable = true)
private void cancelPackets(CallbackInfo ci) {
if (Modules.enabled(Freeze.class)) {
ci.cancel();
}
}
@Inject(method = "aiStep", at = @At("HEAD"), cancellable = true)
private void combatant$freezeTickMovement(CallbackInfo ci) {
LocalPlayer p = (LocalPlayer) (Object) this;
if (!shouldFreezeNow()) {
if (frozen) {
p.setDeltaMovement(savedVelocity);
frozen = false;
}
return;
}
if (!frozen) {
frozen = true;
savedX = p.getX();
savedY = p.getY();
savedZ = p.getZ();
savedVelocity = p.getDeltaMovement();
if (Modules.get(Freecam.class) == null || !Modules.get(Freecam.class).isEnabled()) {
savedBodyYaw = p.yBodyRot;
savedHeadYaw = p.yHeadRot;
}
}
p.setDeltaMovement(Vec3.ZERO);
p.absSnapTo(savedX, savedY, savedZ);
if (Modules.get(Freecam.class) == null || !Modules.get(Freecam.class).isEnabled()) {
p.yBodyRot = savedBodyYaw;
p.yHeadRot = savedHeadYaw;
}
ci.cancel();
}
@Inject(method = "aiStep", at = @At("HEAD"))
private void combatant$noStunTickMovement(CallbackInfo ci) {
NoStun ns = Modules.get(NoStun.class);
if (ns == null || !ns.isEnabled()) return;
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && fc.isCameraInput()) {
return;
}
LocalPlayer player = (LocalPlayer) (Object) this;
if (ns.isFunctionEnabled(NoStun.fnNoHurtStun())) {
if (player.hurtTime == 0) {
lastInput = player.input.keyPresses;
} else if (lastInput != null) {
player.input.keyPresses = lastInput;
}
}
}
@Unique
private void combatant$updateVisualBodyYaw() {
LocalPlayer self = (LocalPlayer) (Object) this;
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
if (rotation == null) {
combatant$rotationPrevX = self.getX();
combatant$rotationPrevZ = self.getZ();
combatant$rotationPrevBodyYaw = self.getVisualRotationYInDegrees();
return;
}
float newBodyYaw = combatant$simulateBodyYaw(
rotation.yaw(),
combatant$rotationPrevBodyYaw,
combatant$rotationPrevX,
combatant$rotationPrevZ,
self.getX(),
self.getZ(),
self.attackAnim
);
self.setYBodyRot(newBodyYaw);
combatant$rotationPrevBodyYaw = newBodyYaw;
combatant$rotationPrevX = self.getX();
combatant$rotationPrevZ = self.getZ();
}
@Inject(method = "tick", at = @At("HEAD"))
private void detectPearlImpact(CallbackInfo ci) {
NoPush noPush = Modules.get(NoPush.class);
if (noPush == null || !noPush.off("pearl_phase"))
return;
LocalPlayer p = (LocalPlayer) (Object) this;
List<ThrownEnderpearl> pearls = new ArrayList<>();
p.level().getEntities(
EntityTypeTest.forClass(ThrownEnderpearl.class),
p.getBoundingBox().inflate(1.5),
e -> true,
pearls
);
// перл соприкасается → сейчас будет телепорт → готовим спуф
if (!pearls.isEmpty()) {
pendingSpoof = true;
}
}
@Inject(method = "sendPosition", at = @At("HEAD"))
private void sendSpoofAfterPearl(CallbackInfo ci) {
NoPush noPush = Modules.get(NoPush.class);
if (noPush == null || !noPush.off("pearl_phase"))
return;
if (!pendingSpoof) return;
LocalPlayer p = (LocalPlayer) (Object) this;
// Avoid spoof when we only bonk a ceiling (causes freezes while looking up).
if (p.verticalCollision && !p.horizontalCollision) {
pendingSpoof = false;
return;
}
pendingSpoof = false;
Vec3 pos = p.position();
p.connection.send(new ServerboundMovePlayerPacket.Pos(
pos.x,
pos.y + 0.0313, // легитная фаза
pos.z,
p.onGround(),
false // horizontalCollision
));
}
@Inject(method = "aiStep", at = @At("TAIL"))
private void onCollideBoost(CallbackInfo ci) {
Speed speed = Modules.get(Speed.class);
if (speed == null || !speed.enabled("entity_boost"))
return;
LocalPlayer p = (LocalPlayer) (Object) this;
if (boostTicks > 0) {
boostTicks--;
Vec3 vel = p.getDeltaMovement();
double vx = vel.x + boostX;
double vz = vel.z + boostZ;
double max = 0.48;
double h = Math.hypot(vx, vz);
if (h > max) {
double s = max / h;
vx *= s;
vz *= s;
}
p.setDeltaMovement(vx, vel.y, vz);
return;
}
List<Entity> nearby = p.level().getEntities(p, p.getBoundingBox().inflate(0.15), e -> e instanceof LivingEntity);
if (nearby.isEmpty()) return;
Vec3 vel = p.getDeltaMovement();
double dirX = vel.x;
double dirZ = vel.z;
if (dirX == 0 && dirZ == 0) return;
double len = Math.sqrt(dirX * dirX + dirZ * dirZ);
if (len < 1e-5) return;
dirX /= len;
dirZ /= len;
double boost = speed.entityBoostStrength.get();
boostX = (dirX * boost) / 3.0;
boostZ = (dirZ * boost) / 3.0;
boostTicks = 3;
}
@Inject(method = "aiStep", at = @At("TAIL"))
private void omniLongJump(CallbackInfo ci) {
Speed speed = Modules.get(Speed.class);
if (speed == null || !speed.enabled("jump_boost")) return;
LocalPlayer p = (LocalPlayer) (Object) this;
boolean onGround = p.onGround();
if (p.isFallFlying()) {
wasGround = onGround;
return;
}
if (wasGround && !onGround && p.isSprinting() && !p.isInWater()) {
Vec2 move = p.input.getMoveVector();
float mx = move.x;
float mz = move.y;
if (mx == 0 && mz == 0) {
wasGround = onGround;
return;
}
float norm = Mth.sqrt(mx * mx + mz * mz);
mx /= norm;
mz /= norm;
float yaw = p.getYRot();
float rad = (float) Math.toRadians(yaw);
double dirX = mz * -Mth.sin(rad) + mx * Mth.cos(rad);
double dirZ = mz * Mth.cos(rad) + mx * Mth.sin(rad);
Vec3 vel = p.getDeltaMovement();
double ui = speed.jumpAcceleration.get();
double internal = ui * 0.832;
double strength = 0.33 * internal;
double vx = dirX * strength;
double vz = dirZ * strength;
double max = 0.42;
double h = Math.hypot(vx, vz);
if (h > max) {
double scale = max / h;
vx *= scale;
vz *= scale;
}
p.setDeltaMovement(vx, vel.y, vz);
}
wasGround = onGround;
}
@Inject(method = "modifyInput", at = @At("RETURN"), cancellable = true)
private void applyLegitSpeed(Vec2 input, CallbackInfoReturnable<Vec2> cir) {
Speed speed = Modules.get(Speed.class);
if (speed == null || !speed.isEnabled()) return;
if (!speed.enabled("legit_speed")) return;
Vec2 base = cir.getReturnValue();
float mult = speed.legitMultiplier.get();
cir.setReturnValue(base.scale(mult));
}
@Inject(method = "closeContainer", at = @At("HEAD"), cancellable = true)
private void combatant$inventoryMove$deferCloseHandledScreen(CallbackInfo ci) {
InventoryMove module = Modules.get(InventoryMove.class);
if (module == null || !module.isEnabled()) return;
if (module.shouldCancelCloseHandledScreen()) {
ci.cancel();
}
}
@Inject(method = "closeContainer", at = @At("HEAD"), cancellable = true)
private void combatant$keepInventoryOpen(CallbackInfo ci) {
if (!Modules.enabled(XCarry.class)) return;
// Cancel only when closing own inventory screen (PlayerScreenHandler) to retain crafting/cursor items
var screen = ClientScreen.current(net.minecraft.client.Minecraft.getInstance());
if (screen instanceof net.minecraft.client.gui.screens.inventory.AbstractContainerScreen<?> handled
&& handled.getMenu() instanceof InventoryMenu) {
((LocalPlayer) (Object) this).clientSideCloseContainer(); // client-side close without sending packet
ci.cancel(); // skip packet CloseHandledScreen
}
}
@Inject(method = "moveTowardsClosestSpace", at = @At("HEAD"), cancellable = true)
private void combatant$noPushOutOfBlocks(double x, double z, CallbackInfo ci) {
if (!Events.BUS.hasListeners(EventPushOutOfBlocks.class)) return;
EventPushOutOfBlocks event = new EventPushOutOfBlocks(x, z);
Events.BUS.post(event);
if (event.isCancelled()) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,52 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.contextualbar.LocatorBar;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(LocatorBar.class)
public abstract class LocatorBarMixin {
@Inject(method = "extractBackground", at = @At("HEAD"), cancellable = true)
private void combatant$renderLocatorBar(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
// Always mute vanilla locator rendering; we draw everything ourselves.
ci.cancel();
}
@Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true)
private void combatant$renderLocatorAddons(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) {
if (!RuntimeGate.canRunHud()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.player == null) return;
CustomBar bar = CustomBar.get();
if (bar == null) return;
if (!bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return;
// Always mute vanilla locator addons (markers/arrows).
ci.cancel();
}
}

View file

@ -0,0 +1,423 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.systems.GpuSurface;
import dev.loki.lovisual.features.module.modules.combat.*;
import net.minecraft.client.FramerateLimiter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.main.GameConfig;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Final;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.CrosshairTargetUpdateEvent;
import dev.loki.lovisual.features.gui.chat.BetterChatStoreManager;
import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.*;
import dev.loki.lovisual.features.module.modules.movement.Timer;
import dev.loki.lovisual.features.module.modules.player.NoDelay;
import dev.loki.lovisual.features.module.modules.player.NoInteract;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.features.relations.StaffTracker;
import dev.loki.lovisual.render.helpers.TickDelta;
import dev.loki.lovisual.util.combat.AntiBotTracker;
import dev.loki.lovisual.util.session.SessionChanger;
import dev.loki.lovisual.util.session.MinecraftGameConfigHolder;
@Mixin(Minecraft.class)
public class MinecraftMixin implements MinecraftGameConfigHolder {
@Shadow
@Final
private GpuSurface windowSurface;
@Unique
private GameConfig combatant$gameConfig;
@Inject(method = "<init>", at = @At("RETURN"))
private void combatant$captureGameConfig(GameConfig config, CallbackInfo ci) {
this.combatant$gameConfig = config;
}
@Override
public GameConfig combatant$getGameConfig() {
return combatant$gameConfig;
}
@Unique
private static BlockHitResult combatant$miss(Vec3 pos) {
return new BlockHitResult(
pos,
Direction.UP,
BlockPos.ZERO,
false
);
}
@Unique
private static void combatant$guardSingleplayerSaveWithAltUsername() {
SessionChanger.restoreSingleplayerJoinSessionForDisconnect();
}
@Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true)
private void fastThrow(CallbackInfo ci) {
NoDelay noDelay = Modules.get(NoDelay.class);
if (noDelay != null && noDelay.handleFastUse()) {
ci.cancel();
return;
}
AutoBow autoBow = Modules.get(AutoBow.class);
if (autoBow != null && autoBow.shouldCancelVanillaDoItemUse()) {
ci.cancel();
}
}
@Inject(
method = "handleKeybinds",
at = @At("HEAD")
)
private void spearassist$blockAttackInput(CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null) return;
SpearAssist assist = Modules.get(SpearAssist.class);
if (assist == null || !assist.isEnabled() || !assist.respectCooldownEnabled()) {
return;
}
ItemStack stack = mc.player.getMainHandItem();
if (!SpearAssist.isSpear(stack)) return;
float cooldown = mc.player.getAttackStrengthScale(0.0F);
if (cooldown < 0.99F) {
// ВАЖНО: гасим сам ввод
mc.options.keyAttack.setDown(false);
}
}
@Inject(method = "handleKeybinds", at = @At("TAIL"))
private void combatant$autobow$finishInputCycle(CallbackInfo ci) {
AutoBow autoBow = Modules.get(AutoBow.class);
if (autoBow != null && autoBow.isEnabled()) {
autoBow.onInputCycleHandled();
}
}
@Inject(method = "shouldEntityAppearGlowing", at = @At("HEAD"), cancellable = true)
private void combatant$disablePvpGlow(Entity entity, CallbackInfoReturnable<Boolean> cir) {
if (!(entity instanceof Player)) return;
PvpCooldowns mod = Modules.get(PvpCooldowns.class);
if (mod == null || !mod.shouldHideTargetGlow()) return;
cir.setReturnValue(false);
}
// === КУРСОР ДЛЯ CLICKGUI ===
@Inject(method = "continueAttack", at = @At("HEAD"), cancellable = true)
private void onHandleBlockBreaking(boolean breaking, CallbackInfo ci) {
Minecraft client = Minecraft.getInstance();
LocalPlayer player = client.player;
if (player == null) return;
// Hitbox должен работать даже при ломании блоков: не блокируем vanilla логику
Hitbox hitbox = Modules.get(Hitbox.class);
if (hitbox != null && hitbox.isEnabled()) {
return;
}
boolean attackKeyPressed = client.options.keyAttack.isDown();
AutoAttack autoAttack = Modules.get(AutoAttack.class);
boolean blockByAutoAttack = autoAttack != null
&& autoAttack.isEnabled()
&& !autoAttack.isAutoMode()
&& autoAttack.shouldBlockBreaking();
boolean shouldBlockBreaking = blockByAutoAttack;
if (shouldBlockBreaking && attackKeyPressed) {
// блокируем ломание
ci.cancel();
if (client.gameMode != null)
client.gameMode.stopDestroyBlock();
// имитация удара
}
}
@Inject(method = "startAttack", at = @At("HEAD"), cancellable = true)
private void onDoAttack(CallbackInfoReturnable<Boolean> cir) {
AttributeSwap attributeSwap = Modules.get(AttributeSwap.class);
if (attributeSwap != null && attributeSwap.isEnabled() && attributeSwap.handleManualAttack()) {
cir.setReturnValue(true);
cir.cancel();
return;
}
AutoAttack autoAttack = Modules.get(AutoAttack.class);
if (autoAttack != null && autoAttack.isEnabled()) {
cir.setReturnValue(false);
cir.cancel();
}
}
@Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;ZZ)V", at = @At("HEAD"))
private void combatant$betterChat$saveOnDisconnect(net.minecraft.client.gui.screens.Screen screen, boolean transferring, boolean bl, CallbackInfo ci) {
combatant$guardSingleplayerSaveWithAltUsername();
try {
BetterChatStoreManager.flushAll();
} catch (Throwable ignored) {
}
}
@Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;ZZ)V", at = @At("TAIL"))
private void combatant$sessionRestoreAfterDisconnect(net.minecraft.client.gui.screens.Screen screen, boolean transferring, boolean bl, CallbackInfo ci) {
SessionChanger.restoreDeferredSessionAfterDisconnect();
}
@Inject(method = "pick", at = @At("HEAD"), cancellable = true)
private void onUpdateCrosshairTarget(float tickDelta, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || mc.level == null) return;
var fc = Modules.get(Freecam.class);
// =========================================================
// FREECAM
// =========================================================
if (fc != null && fc.isEnabled()) {
// позиция и направление FREECAM
Vec3 start = new Vec3(
Mth.lerp(tickDelta, fc.camPosPrev.x, fc.camPos.x),
Mth.lerp(tickDelta, fc.camPosPrev.y, fc.camPos.y),
Mth.lerp(tickDelta, fc.camPosPrev.z, fc.camPos.z)
);
float yawRad = -fc.camYaw * ((float) Math.PI / 180F);
float pitchRad = -fc.camPitch * ((float) Math.PI / 180F);
Vec3 dir = new Vec3(
Mth.sin(yawRad) * Mth.cos(pitchRad),
Mth.sin(pitchRad),
Mth.cos(yawRad) * Mth.cos(pitchRad)
);
// ===============================
// FREECAM + NO INTERACT
// ===============================
if (!fc.allowInteract()) {
mc.hitResult = combatant$miss(start);
mc.crosshairPickEntity = null;
ci.cancel();
return;
}
// ===============================
// FREECAM + INTERACT
// ===============================
// Reach (FREECAM)
Reach reachModule = Modules.get(Reach.class);
if (reachModule != null && reachModule.isEnabled()) {
EntityHitResult hit = reachModule.raycastEntities(
mc.player, start, dir
);
if (hit != null && hit.getEntity() instanceof LivingEntity target && target.isAlive()) {
mc.hitResult = hit;
mc.crosshairPickEntity = target;
ci.cancel();
return;
}
}
// Blocks (FREECAM)
double reach = mc.player.blockInteractionRange();
Vec3 end = start.add(dir.scale(reach));
BlockHitResult bhr = mc.level.clip(new net.minecraft.world.level.ClipContext(
start, end,
net.minecraft.world.level.ClipContext.Block.OUTLINE,
net.minecraft.world.level.ClipContext.Fluid.NONE,
mc.player
));
mc.hitResult = bhr;
mc.crosshairPickEntity = null;
ci.cancel();
return;
}
Reach reachModule = Modules.get(Reach.class);
if (reachModule != null && reachModule.isEnabled()) {
EntityHitResult entityHit = reachModule.raycastEntities(mc.player);
if (entityHit != null && entityHit.getEntity() instanceof LivingEntity target && target.isAlive()) {
mc.hitResult = entityHit;
mc.crosshairPickEntity = target;
ci.cancel();
}
}
}
@Inject(method = "pick", at = @At("RETURN"))
private void combatant$dispatchCrosshairTargetEvent(float tickDelta, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || mc.level == null) return;
CrosshairTargetUpdateEvent event = new CrosshairTargetUpdateEvent(
tickDelta,
mc.hitResult,
mc.crosshairPickEntity
);
Events.BUS.post(event);
mc.hitResult = event.getHitResult();
mc.crosshairPickEntity = event.getTargetedEntity();
}
@Inject(method = "pick", at = @At("RETURN"))
private void noInteract$afterRaycast(float tickDelta, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || mc.level == null) return;
NoInteract noInteract = Modules.get(NoInteract.class);
if (noInteract == null || !noInteract.shouldBlockBlockInteraction()) return;
var hit = mc.hitResult;
if (hit instanceof EntityHitResult) return;
Vec3 camPos = mc.player.getEyePosition(tickDelta);
mc.hitResult = new BlockHitResult(
camPos,
Direction.UP,
BlockPos.ZERO,
false
);
mc.crosshairPickEntity = null;
}
@Inject(method = "disconnect", at = @At("HEAD"))
private void freecam$onDisconnect(CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled()) {
fc.toggle();
}
}
@Inject(method = "setLevel", at = @At("HEAD"))
private void freecam$onJoinWorld(ClientLevel world, CallbackInfo ci) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled()) {
fc.toggle();
}
}
@Inject(method = "setLevel", at = @At("TAIL"))
private void combatant$captureSingleplayerJoinSession(ClientLevel world, CallbackInfo ci) {
if (world == null) {
SessionChanger.clearSingleplayerJoinSession();
} else {
SessionChanger.captureSingleplayerJoinSession();
}
}
@Inject(method = "clearDownloadedResourcePacks", at = @At("HEAD"))
private void combatant$onDisconnected(CallbackInfo ci) {
StaffTracker.resetAll();
AntiBotTracker.INSTANCE.reset();
}
@Inject(
method = "renderFrame",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/DeltaTracker$Timer;advanceRealTime(J)V",
shift = At.Shift.AFTER
)
)
private void combatant$frameModulesAfterRenderTick(boolean tick, CallbackInfo ci) {
ModuleManager.frameAll(TickDelta.frameDeltaTicks());
}
@Inject(
method = "renderFrame",
at = @At(
value = "INVOKE_STRING",
target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V",
args = "ldc=frameLimiter",
shift = At.Shift.AFTER
)
)
private void combatant$paceImmediateNoVsync(boolean tick, CallbackInfo ci) {
Minecraft mc = (Minecraft) (Object) this;
if (mc == null || mc.options == null || mc.getWindow() == null || windowSurface == null) {
return;
}
if (Boolean.TRUE.equals(mc.options.enableVsync().get())) {
return;
}
if (windowSurface.currentConfiguration().isEmpty()
|| windowSurface.currentConfiguration().get().presentMode() != GpuSurface.PresentMode.IMMEDIATE) {
return;
}
int configuredLimit = mc.gameRenderer.gameRenderState().framerateLimit;
if (configuredLimit < 260) {
return;
}
int refreshRate = mc.getWindow().getRefreshRate();
int targetFps = combatant$immediatePacingTarget(refreshRate);
FramerateLimiter.limitDisplayFPS(targetFps);
}
@Unique
private static int combatant$immediatePacingTarget(int refreshRate) {
int base = refreshRate > 0 ? refreshRate : 120;
return Mth.clamp(base * 2, 120, 360);
}
@Inject(method = "getTickTargetMillis", at = @At("RETURN"), cancellable = true)
private void combatant$applyTimer(float millis, CallbackInfoReturnable<Float> cir) {
float mult = Timer.getTickTimer();
if (mult <= 0.0001f) return;
if (Math.abs(mult - 1.0f) < 0.0001f) return;
float base = cir.getReturnValue();
cir.setReturnValue(base / mult);
}
@Inject(method = "isLevelRunningNormally", at = @At("HEAD"), cancellable = true)
private void combatant$skipPlayerDependentTicksWithoutPlayer(CallbackInfoReturnable<Boolean> cir) {
Minecraft mc = Minecraft.getInstance();
if (mc.level != null && mc.player == null) {
cir.setReturnValue(false);
}
}
}

View file

@ -0,0 +1,229 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import dev.loki.lovisual.features.gui.mainmenu.LoVisualProxyManagerScreen;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.MouseHandler;
import net.minecraft.client.input.MouseButtonInfo;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.util.Mth;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiEditorScreen;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiPickerScreen;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiRenderer;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiScreen;
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementRegistry;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.DynamicIsland;
import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.features.module.modules.visuals.Zoom;
import dev.loki.lovisual.runtime.RuntimeGate;
import dev.loki.lovisual.util.input.KeyManager;
import dev.loki.lovisual.util.logging.DebugLog;
@Mixin(MouseHandler.class)
public class MouseMixin {
@Unique
private double lastX;
@Unique
private double lastY;
@Unique
private static final float COMBATANT_PROXY_BUTTON_X = 8f;
@Unique
private static final float COMBATANT_PROXY_BUTTON_Y = 8f;
@Unique
private static final float COMBATANT_PROXY_BUTTON_W = 24f;
@Unique
private static final float COMBATANT_PROXY_BUTTON_H = 24f;
// ===== Mouse scroll for ClickGUI =====
@Inject(method = "onScroll", at = @At("HEAD"), cancellable = true)
public void onMouseScroll(long window, double horizontal, double vertical, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (RuntimeGate.canRunClientLogic()
&& ModuleManager.isEnabled("clickgui")
&& !(ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen)) {
ClickGuiRenderer.onMouseScroll(lastX, lastY, vertical);
ci.cancel();
return;
}
if (RuntimeGate.canRunHud()
&& ClientScreen.current(net.minecraft.client.Minecraft.getInstance()) instanceof net.minecraft.client.gui.screens.ChatScreen) {
if (BetterChatRenderer.onScroll(vertical)) {
ci.cancel();
}
}
}
@Inject(method = "onButton", at = @At("HEAD"), cancellable = true)
private void betterChat$mouseButton(long window, MouseButtonInfo input, int action, CallbackInfo ci) {
KeyManager.handleMouseButtonEvent(input.button(), action);
net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance();
if (RuntimeGate.canRunHud() && mc != null) {
boolean pressed = action == 1;
int button = input.button();
if (pressed && button == 0 && ClientScreen.current() instanceof JoinMultiplayerScreen screen) {
double scale = Math.max(1.0, mc.getWindow().getGuiScale());
float mx = (float) (mc.mouseHandler.xpos() / scale);
float my = (float) (mc.mouseHandler.ypos() / scale);
if (combatant$proxyButtonHit(mx, my)) {
ClientScreen.show(mc, new LoVisualProxyManagerScreen(screen));
ci.cancel();
return;
}
}
if (ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen) {
double mx = mc.mouseHandler.xpos();
double my = mc.mouseHandler.ypos();
if (BetterChatRenderer.onMouseButton(mx, my, button, pressed)) {
ci.cancel();
return;
}
}
if ((ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen
|| DynamicIsland.shouldRenderInScreenOverlay(ClientScreen.current()))
&& StaticHudElementRegistry.handleMouseButton(button, pressed)) {
ci.cancel();
}
}
}
@Unique
private static boolean combatant$proxyButtonHit(float mx, float my) {
return mx >= COMBATANT_PROXY_BUTTON_X
&& mx <= COMBATANT_PROXY_BUTTON_X + COMBATANT_PROXY_BUTTON_W
&& my >= COMBATANT_PROXY_BUTTON_Y
&& my <= COMBATANT_PROXY_BUTTON_Y + COMBATANT_PROXY_BUTTON_H;
}
// ===== Mouse button for ClickGUI =====
@Inject(method = "onButton", at = @At("HEAD"), cancellable = true)
private void clickGuiMouseButton(long window, MouseButtonInfo input, int action, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
if (RuntimeGate.canRunClientLogic()
&& ModuleManager.isEnabled("clickgui")
&& !(ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen)) {
// action == 1 — это "нажато" (см. оригинальный код: boolean bl = action == 1;)
boolean pressed = (action == 1);
// Кнопка теперь внутри MouseInput
int button = input.button();
DebugLog.info("[CLICK] button=%d action=%d x=%.2f y=%.2f",
button, action, lastX, lastY);
ClickGuiRenderer.onMouseButton(lastX, lastY, button, pressed);
ci.cancel();
}
}
// ===== Mouse move for ClickGUI =====
@Inject(method = "onMove", at = @At("HEAD"), cancellable = true)
private void clickGuiCursorMove(long window, double x, double y, CallbackInfo ci) {
Minecraft mc = Minecraft.getInstance();
// Always track last mouse position
lastX = x;
lastY = y;
if (RuntimeGate.canRunClientLogic()
&& ModuleManager.isEnabled("clickgui")
&& !(ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen)) {
ClickGuiRenderer.onMouseMove(x, y);
ci.cancel();
return;
}
if (RuntimeGate.canRunHud()
&& ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen) {
BetterChatRenderer.onMouseMove(x, y);
}
}
@Redirect(
method = "turnPlayer",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/player/LocalPlayer;turn(DD)V"
)
)
private void freecam$look(LocalPlayer player, double dx, double dy) {
Freecam fc = Modules.get(Freecam.class);
if (!RuntimeGate.canRunClientLogic() || fc == null || !fc.isEnabled()) {
player.turn(dx, dy);
return;
}
if (!fc.isCameraInput()) {
player.turn(dx, dy);
return;
}
Minecraft mc = Minecraft.getInstance();
double sens = mc.options.sensitivity().get();
double f = sens * 0.6 + 0.2;
double scale = f * f * f * 8.0;
float yawDelta = (float) (dx * scale * 0.15F);
float pitchDelta = (float) (dy * scale * 0.15F);
fc.camYaw += yawDelta;
fc.camPitch += pitchDelta;
fc.camPitch = Mth.clamp(fc.camPitch, -90.0F, 90.0F);
}
@ModifyExpressionValue(
method = "turnPlayer",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/OptionInstance;get()Ljava/lang/Object;",
ordinal = 0
)
)
private Object combatant$zoomSensitivity(Object original) {
Zoom zoom = Modules.get(Zoom.class);
if (!RuntimeGate.canRunClientLogic() || zoom == null || !zoom.isEnabled() || !zoom.shouldApplyZoom())
return original;
if (!(original instanceof Double originalValue)) return original;
float divisor = zoom.getZoomDivisor();
if (divisor <= 1.0f) return original;
float percent = zoom.getSensitivityPercent() / 100.0f;
double factor = Mth.lerp(percent, 1.0f, divisor);
if (factor <= 0.0001) return original;
return originalValue / factor;
}
}

View file

@ -0,0 +1,185 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.config.values.ItemCooldownRulesValue;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.AttackEntityEvent;
import dev.loki.lovisual.events.impl.EventBreakBlock;
import dev.loki.lovisual.features.gui.hud.draggable.impl.TargetHud;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.AttributeSwap;
import dev.loki.lovisual.features.module.modules.combat.Criticals;
import dev.loki.lovisual.features.module.modules.combat.Hitbox;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
import dev.loki.lovisual.features.module.modules.misc.HitSounds;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.features.module.modules.visuals.HitEffect;
import dev.loki.lovisual.mixins.accessors.WorldAccessor;
import dev.loki.lovisual.util.player.inventory.InventorySwap;
import dev.loki.lovisual.util.target.TargetManager;
@Mixin(MultiPlayerGameMode.class)
public class MultiPlayerGameModeMixin {
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
private void combatant$skipTickWithoutPlayer(CallbackInfo ci) {
if (Minecraft.getInstance().player == null) {
ci.cancel();
}
}
@Inject(method = "ensureHasSentCarriedItem", at = @At("HEAD"), cancellable = true)
private void combatant$skipCarriedItemSyncWithoutPlayer(CallbackInfo ci) {
if (Minecraft.getInstance().player == null) {
ci.cancel();
}
}
@ModifyExpressionValue(
method = "ensureHasSentCarriedItem",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Inventory;getSelectedSlot()I"
)
)
private int combatant$hookSilentSelectedSlot(int original) {
return InventorySwap.INSTANCE.effectiveSelectedSlot();
}
@Inject(method = "destroyBlock", at = @At("RETURN"))
private void combatant$breakBlockEvent(BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
if (!cir.getReturnValue()) return;
if (!Events.BUS.hasListeners(EventBreakBlock.class)) return;
Events.BUS.post(new EventBreakBlock(pos));
}
@Inject(method = "useItem", at = @At("HEAD"), cancellable = true)
private void onInteractItemHead(Player player, InteractionHand hand, CallbackInfoReturnable<InteractionResult> cir) {
ItemStack stack = player.getItemInHand(hand);
if (stack.isEmpty()) return;
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
if (cooldowns == null) return;
// No hard-coded PvP item blocking here anymore. This path is opt-in and item-list based.
if (cooldowns.shouldBlockItemUse(stack.getItem())) {
cir.setReturnValue(InteractionResult.FAIL);
}
}
@Inject(method = "useItem", at = @At("RETURN"))
private void onInteractItemReturn(Player player, InteractionHand hand, CallbackInfoReturnable<InteractionResult> cir) {
InteractionResult result = cir.getReturnValue();
if (!result.consumesAction()) return;
ItemStack stack = player.getItemInHand(hand);
if (stack.isEmpty()) return;
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
if (cooldowns == null) return;
cooldowns.tryStartLocalCooldown(stack.getItem(), ItemCooldownRulesValue.Trigger.INTERACT_ACCEPT);
}
@Inject(method = "useItemOn", at = @At("HEAD"))
private void onInteractBlockHead(LocalPlayer player, InteractionHand hand, BlockHitResult hit, CallbackInfoReturnable<InteractionResult> cir) {
// no PvP cooldown block on interactBlock
}
@Inject(method = "useItemOn", at = @At("RETURN"))
private void onInteractBlockReturn(LocalPlayer player, InteractionHand hand, BlockHitResult hit, CallbackInfoReturnable<InteractionResult> cir) {
// no PvP cooldown prediction on interactBlock
}
@Inject(method = "useItemOn", at = @At("HEAD"), cancellable = true)
private void freecam$block(LocalPlayer player, InteractionHand hand, BlockHitResult hit,
CallbackInfoReturnable<InteractionResult> cir) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && !fc.allowInteract()) {
cir.setReturnValue(InteractionResult.PASS);
cir.cancel();
}
}
@Inject(method = "useItem", at = @At("HEAD"), cancellable = true)
private void freecam$item(Player player, InteractionHand hand, CallbackInfoReturnable<InteractionResult> cir) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && !fc.allowInteract()) {
cir.setReturnValue(InteractionResult.PASS);
cir.cancel();
}
}
@Inject(method = "interact", at = @At("HEAD"), cancellable = true)
private void freecam$entity(Player player, Entity entity, EntityHitResult hit, InteractionHand hand,
CallbackInfoReturnable<InteractionResult> cir) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled() && !fc.allowInteract()) {
cir.setReturnValue(InteractionResult.PASS);
cir.cancel();
}
}
@Inject(method = "attack(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;)V",
at = @At("HEAD"))
private void onAttackEntity(Player player, Entity target, CallbackInfo ci) {
if (!((WorldAccessor) player.level()).combatant$isClient()) return;
if (player instanceof LocalPlayer clientPlayer) {
Criticals criticals = Modules.get(Criticals.class);
if (criticals != null && criticals.isEnabled()) {
criticals.beforeAttack(Minecraft.getInstance(), clientPlayer, target);
}
}
if (Events.BUS.hasListeners(AttackEntityEvent.class)) {
Events.BUS.post(new AttackEntityEvent(player, target));
}
Hitbox hitbox = Modules.get(Hitbox.class);
if (hitbox != null) hitbox.markHit(target);
HitSounds hitSounds = Modules.get(HitSounds.class);
if (hitSounds != null) {
hitSounds.handleHit(target);
}
HitEffect hitEffect = Modules.get(HitEffect.class);
if (hitEffect != null) {
hitEffect.handleHit(target);
}
TargetHud.notifyHit(target);
TargetManager.onAttack(target);
}
@Inject(method = "attack(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;)V",
at = @At("RETURN"))
private void combatant$attributeSwapAfterClientAttack(Player player, Entity target, CallbackInfo ci) {
if (player != Minecraft.getInstance().player) return;
AttributeSwap.tryBreakShieldAfterClientAttack(target);
}
}

View file

@ -0,0 +1,104 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.GameNarrator;
import net.minecraft.client.Minecraft;
import net.minecraft.client.NarratorStatus;
import net.minecraft.network.chat.Component;
import org.spongepowered.asm.mixin.Final;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.util.NarratorBlocker;
@Mixin(GameNarrator.class)
public class NarratorManagerMixin {
@Shadow
@Final
private Minecraft minecraft;
@Inject(method = "sayChatQueued(Lnet/minecraft/network/chat/Component;)V", at = @At("HEAD"), cancellable = true)
private void combatant$muteChat(Component message, CallbackInfo ci) {
if (shouldMute()) {
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
}
@Inject(method = "saySystemChatQueued(Lnet/minecraft/network/chat/Component;)V", at = @At("HEAD"), cancellable = true)
private void combatant$muteNarrator(Component message, CallbackInfo ci) {
if (shouldMute()) {
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
}
@Inject(method = "saySystemQueued(Lnet/minecraft/network/chat/Component;)V", at = @At("HEAD"), cancellable = true)
private void combatant$muteSystem(Component message, CallbackInfo ci) {
if (shouldMute()) {
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
}
@Inject(method = "saySystemNow(Lnet/minecraft/network/chat/Component;)V", at = @At("HEAD"), cancellable = true)
private void combatant$muteImmediateText(Component message, CallbackInfo ci) {
if (shouldMute()) {
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
}
@Inject(method = "saySystemNow(Ljava/lang/String;)V", at = @At("HEAD"), cancellable = true)
private void combatant$muteImmediateString(String message, CallbackInfo ci) {
if (shouldMute()) {
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
}
@Inject(method = "narrateMessage(Ljava/lang/String;Z)V", at = @At("HEAD"), cancellable = true)
private void combatant$muteSay(String message, boolean interrupt, CallbackInfo ci) {
if (shouldMute()) {
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
}
@Inject(method = "updateNarratorStatus", at = @At("HEAD"), cancellable = true)
private void combatant$blockToggle(NarratorStatus mode, CallbackInfo ci) {
if (!shouldMute()) return;
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
@Inject(method = "checkStatus", at = @At("HEAD"), cancellable = true)
private void combatant$skipNarratorLibraryCheck(boolean narratorEnabled, CallbackInfo ci) {
if (!shouldMute()) return;
NarratorBlocker.enforce(this.minecraft);
ci.cancel();
}
@Inject(method = "isActive", at = @At("HEAD"), cancellable = true)
private void combatant$reportInactive(CallbackInfoReturnable<Boolean> cir) {
if (!shouldMute()) return;
cir.setReturnValue(false);
}
@Unique
private boolean shouldMute() {
return NarratorBlocker.isBlocked();
}
}

View file

@ -0,0 +1,45 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Minecraft;
import net.minecraft.client.NarratorStatus;
import net.minecraft.client.OptionInstance;
import net.minecraft.client.Options;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.util.NarratorBlocker;
import java.io.File;
@Mixin(Options.class)
public abstract class OptionsNarratorMixin {
@Inject(method = "<init>(Lnet/minecraft/client/Minecraft;Ljava/io/File;)V", at = @At("RETURN"))
private void combatant$enforceNarratorDefaults(Minecraft client, File optionsFile, CallbackInfo ci) {
NarratorBlocker.enforce((Options) (Object) this);
}
@Inject(method = "save", at = @At("HEAD"))
private void combatant$enforceNarratorBeforeWrite(CallbackInfo ci) {
NarratorBlocker.enforce((Options) (Object) this);
}
@Inject(method = "narrator", at = @At("HEAD"))
private void combatant$enforceNarratorGetter(CallbackInfoReturnable<OptionInstance<NarratorStatus>> cir) {
NarratorBlocker.enforce((Options) (Object) this);
}
@Inject(method = "narratorHotkey", at = @At("HEAD"))
private void combatant$enforceNarratorHotkeyGetter(CallbackInfoReturnable<OptionInstance<Boolean>> cir) {
NarratorBlocker.enforce((Options) (Object) this);
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.particle.ParticleEngine;
import net.minecraft.core.particles.ParticleOptions;
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;
import dev.loki.lovisual.util.player.effect.StatusEffectHeuristics;
@Mixin(ParticleEngine.class)
public class ParticleManagerMixin {
@Inject(method = "createParticle(Lnet/minecraft/core/particles/ParticleOptions;DDDDDD)Lnet/minecraft/client/particle/Particle;", at = @At("HEAD"))
private void combatant$heuristicParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ, CallbackInfoReturnable<?> cir) {
// dev.loki.lovisual.util.logging.DebugLog.info("ParticleManager add type=%s pos=%.2f %.2f %.2f", parameters.getType().toString(), x, y, z);
StatusEffectHeuristics.handle(parameters, x, y, z);
}
}

View file

@ -0,0 +1,80 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.screens.PauseScreen;
import org.jetbrains.annotations.Nullable;
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;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns;
@Mixin(PauseScreen.class)
public abstract class PauseScreenMixin {
@Shadow
@Nullable
private Button disconnectButton;
@Unique
private Button combatant$lastExitButton;
@Unique
private boolean combatant$disconnectBlocked;
@Unique
private boolean combatant$previousExitActive = true;
@Inject(method = "init", at = @At("TAIL"))
private void combatant$blockDisconnectOnInit(CallbackInfo ci) {
combatant$syncDisconnectButton();
}
@Inject(method = "extractRenderState", at = @At("HEAD"))
private void combatant$blockDisconnectOnRender(GuiGraphicsExtractor context, int mouseX, int mouseY, float deltaTicks, CallbackInfo ci) {
combatant$syncDisconnectButton();
}
@Unique
private void combatant$syncDisconnectButton() {
Button button = disconnectButton;
if (button == null) {
combatant$lastExitButton = null;
combatant$disconnectBlocked = false;
return;
}
if (button != combatant$lastExitButton) {
combatant$lastExitButton = button;
combatant$disconnectBlocked = false;
combatant$previousExitActive = button.active;
}
PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class);
boolean shouldBlock = cooldowns != null && cooldowns.shouldBlockDisconnect();
if (shouldBlock) {
if (!combatant$disconnectBlocked) {
combatant$previousExitActive = button.active;
}
combatant$disconnectBlocked = true;
button.active = false;
button.setTooltip(Tooltip.create(cooldowns.getDisconnectBlockTooltip()));
return;
}
if (combatant$disconnectBlocked) {
button.active = combatant$previousExitActive;
button.setTooltip(null);
combatant$disconnectBlocked = false;
}
}
}

View file

@ -0,0 +1,164 @@
/*
* This file is part of the Combatant Client distribution.
* Combatant modifications copyright (c) 2026 pivosos2007.
*
* Portions of this file are based on LiquidBounce
* (https://github.com/CCBlueX/LiquidBounce).
* Copyright (c) 2015-2026 CCBlueX.
*
* LiquidBounce portions are licensed under GPLv3-or-later.
* Combatant modifications are licensed under GPLv3.
* See THIRD_PARTY_NOTICES.md for details.
*/
package dev.loki.lovisual.mixins;
/*
* Parts of attack rotation and keep-sprint related hooks are adapted from
* LiquidBounce (https://github.com/CCBlueX/LiquidBounce).
* Original copyright (c) CCBlueX.
*/
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemCooldowns;
import net.minecraft.world.item.Items;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.events.impl.PlayerSafeWalkEvent;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.player.NoDelay;
import dev.loki.lovisual.features.module.modules.combat.TPSSync;
import dev.loki.lovisual.features.module.modules.movement.NoStun;
import dev.loki.lovisual.features.module.modules.movement.Timer;
import dev.loki.lovisual.mixininterface.IPlayerAttackCooldown;
import dev.loki.lovisual.util.aiming.RotationManager;
@Mixin(Player.class)
public abstract class PlayerEntityMixin implements IPlayerAttackCooldown {
@Shadow
@Final
private ItemCooldowns cooldowns;
@Inject(method = "isStayingOnGroundSurface", at = @At("RETURN"), cancellable = true)
private void combatant$safeWalk(CallbackInfoReturnable<Boolean> cir) {
if (cir.getReturnValue()) return;
if ((Object) this != Minecraft.getInstance().player) return;
if (!Events.BUS.hasListeners(PlayerSafeWalkEvent.class)) return;
PlayerSafeWalkEvent event = new PlayerSafeWalkEvent();
Events.BUS.post(event);
if (event.isSafeWalk()) {
cir.setReturnValue(true);
}
}
@Inject(method = "isMobilityRestricted", at = @At("HEAD"), cancellable = true)
private void combatant$noStunBlindnessSprint(CallbackInfoReturnable<Boolean> cir) {
NoStun noStun = Modules.get(NoStun.class);
if (noStun != null && noStun.shouldIgnoreBlindness((Player) (Object) this)) {
cir.setReturnValue(false);
}
}
@ModifyExpressionValue(
method = "causeExtraKnockback",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;getYRot()F"
)
)
private float combatant$fixKnockbackYaw(float original) {
if ((Object) this != Minecraft.getInstance().player) {
return original;
}
return RotationManager.INSTANCE.getMovementRotation().yaw();
}
@ModifyExpressionValue(
method = "doSweepAttack",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;getYRot()F"
)
)
private float combatant$fixSweepYaw(float original) {
if ((Object) this != Minecraft.getInstance().player) {
return original;
}
return RotationManager.INSTANCE.getMovementRotation().yaw();
}
@ModifyExpressionValue(
method = "travel",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;getLookAngle()Lnet/minecraft/world/phys/Vec3;"
)
)
private Vec3 combatant$fixTravelRotationVector(Vec3 original) {
if ((Object) this != Minecraft.getInstance().player) {
return original;
}
return RotationManager.INSTANCE.getMovementRotation().directionVector();
}
@Inject(method = "tick", at = @At("HEAD"))
private void handleShieldCooldown(CallbackInfo ci) {
NoDelay noDelay = Modules.get(NoDelay.class);
if (noDelay == null || !noDelay.shouldDisableShieldCooldown()) return;
cooldowns.removeCooldown(BuiltInRegistries.ITEM.getKey(Items.SHIELD));
}
/**
* Синхронизация attack cooldown с серверным TPS.
* НЕ трогает ItemCooldownManager.
* НЕ влияет на shield.
*/
@ModifyReturnValue(
method = "getCurrentItemAttackStrengthDelay",
at = @At("RETURN")
)
private float combatant$tpsSyncAttackCooldown(float original) {
float result = original;
TPSSync tpsSync = Modules.get(TPSSync.class);
if (tpsSync != null && tpsSync.isEnabled()) {
float serverDelta = tpsSync.getServerTickDelta();
if (serverDelta > 0.0f && serverDelta < 1.0f) {
// замедляем заполнение cooldown’а под TPS
result = result / serverDelta;
}
}
float timerMult = Timer.getTickTimer();
if (timerMult > 0.0001f && Math.abs(timerMult - 1.0f) > 0.0001f) {
result = result * timerMult;
}
return result;
}
@Override
public float combatant$getAttackCooldownProgress(float tickDelta) {
Player self = (Player) (Object) this;
return self.getAttackStrengthScale(tickDelta);
}
@Override
public boolean combatant$isAttackCharged(float tickDelta) {
return combatant$getAttackCooldownProgress(tickDelta) >= 1.0f;
}
}

View file

@ -0,0 +1,110 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.authlib.GameProfile;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentContents;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.contents.PlainTextContents;
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.CallbackInfoReturnable;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.CustomTabList;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.misc.BetterMinecraft;
import dev.loki.lovisual.features.relations.CategoryService;
import dev.loki.lovisual.features.relations.CategoryType;
@Mixin(PlayerInfo.class)
public abstract class PlayerInfoMixin {
@Shadow
public abstract GameProfile getProfile();
@Inject(method = "getTabListDisplayName", at = @At("RETURN"), cancellable = true)
private void injectColorIntoTab(CallbackInfoReturnable<Component> cir) {
if (CustomTabList.shouldReplaceVanilla()) return;
BetterMinecraft module = Modules.get(BetterMinecraft.class);
if (module == null || !module.isTablistRelationsEnabled()) return;
Component original = cir.getReturnValue();
if (original == null) return;
GameProfile profile = getProfile();
if (profile == null) return;
String name = profile.name();
if (name == null) return;
// category (friend/enemy/staff/default)
CategoryType type = CategoryService.get(name);
if (type == CategoryType.DEFAULT)
return;
int color = CategoryService.getColor(name);
MutableComponent modified = recolorNameOnly(original, name, color);
cir.setReturnValue(modified);
}
@Unique
private MutableComponent recolorNameOnly(Component text, String name, int color) {
ComponentContents content = text.getContents();
MutableComponent result;
if (content instanceof PlainTextContents plain) {
String raw = plain.text();
String rawLower = raw.toLowerCase();
String nameLower = name.toLowerCase();
int idx = rawLower.indexOf(nameLower);
if (idx != -1) {
// raw = before + name + after
String before = raw.substring(0, idx);
String mid = raw.substring(idx, idx + name.length());
String after = raw.substring(idx + name.length());
result = Component.empty();
if (!before.isEmpty())
result.append(Component.literal(before).withStyle(text.getStyle()));
result.append(
Component.literal(mid)
.withStyle(style -> style.withColor(color))
);
if (!after.isEmpty())
result.append(Component.literal(after).withStyle(text.getStyle()));
} else {
result = Component.literal(raw).withStyle(text.getStyle());
}
} else {
result = MutableComponent.create(content).withStyle(text.getStyle());
}
for (Component child : text.getSiblings()) {
result.append(recolorNameOnly(child, name, color));
}
return result;
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import dev.loki.lovisual.util.player.inventory.InventorySwap;
@Mixin(Inventory.class)
public abstract class PlayerInventoryMixin {
@Shadow
@Final
public Player player;
@Shadow
public abstract ItemStack getItem(int slot);
@ModifyReturnValue(method = "getSelectedSlot", at = @At("RETURN"))
private int combatant$silentSelectedSlot(int original) {
Minecraft mc = Minecraft.getInstance();
return player == mc.player ? InventorySwap.INSTANCE.effectiveSelectedSlot() : original;
}
@ModifyReturnValue(method = "getSelectedItem", at = @At("RETURN"))
private ItemStack combatant$silentSelectedStack(ItemStack original) {
Minecraft mc = Minecraft.getInstance();
if (player == mc.player && InventorySwap.INSTANCE.isHotbarLeased()) {
return getItem(InventorySwap.INSTANCE.effectiveSelectedSlot());
}
return original;
}
}

View file

@ -0,0 +1,38 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.CustomTabList;
import dev.loki.lovisual.runtime.RuntimeGate;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.PlayerTabOverlay;
import net.minecraft.world.scores.Objective;
import net.minecraft.world.scores.Scoreboard;
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;
@Mixin(PlayerTabOverlay.class)
public abstract class PlayerTabOverlayMixin {
@Inject(
method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;ILnet/minecraft/world/scores/Scoreboard;Lnet/minecraft/world/scores/Objective;)V",
at = @At("HEAD"),
cancellable = true
)
private void combatant$extractCustomTabList(GuiGraphicsExtractor context,
int width,
Scoreboard scoreboard,
Objective objective,
CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
if (!CustomTabList.shouldReplaceVanilla()) return;
CustomTabList.renderVanillaTabStratum(context, width);
ci.cancel();
}
}

View file

@ -0,0 +1,51 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.fog.FogData;
import net.minecraft.client.renderer.fog.environment.PowderedSnowFogEnvironment;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
@Mixin(value = PowderedSnowFogEnvironment.class, priority = 1200)
public class PowderSnowFogModifierMixin {
@Inject(method = "setupFog", at = @At("HEAD"), cancellable = true)
private void combatant$noPowderFog(FogData data,
Camera camera,
ClientLevel world,
float viewDistance,
DeltaTracker tickCounter,
CallbackInfo ci) {
WorldTweaks module = Modules.get(WorldTweaks.class);
if (module == null || !module.isFogControlEnabled()) return;
if (module.isFogModifyEnabled()) {
module.applyFogOverride(data);
ci.cancel();
return;
}
if (!module.disablePowderSnowFog()) return;
float far = Math.max(viewDistance, 16.0f);
data.environmentalStart = far;
data.environmentalEnd = far;
data.renderDistanceStart = far;
data.renderDistanceEnd = far;
data.skyEnd = far;
data.cloudEnd = far;
ci.cancel();
}
}

View file

@ -0,0 +1,84 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.combat.Hitbox;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Mixin(ProjectileUtil.class)
public class ProjectileUtilMixin {
// Intercept getOtherEntities in ProjectileUtil.raycast:
// - If Hitbox is disabled/panic -> keep vanilla list.
// - If enabled -> drop dead LivingEntity and those Hitbox wants to ignore.
@Redirect(
method = "getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/level/Level;getEntities(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;)Ljava/util/List;"
)
)
private static List<Entity> combatant$filterEntities(Level world, Entity source, AABB box, Predicate<Entity> predicate) {
List<Entity> original = world.getEntities(source, box, predicate);
if (original == null || original.isEmpty()) return original;
Hitbox hb = Modules.get(Hitbox.class);
if (hb == null || !hb.isEnabled()) return original;
List<Entity> filtered = new ArrayList<>(original.size());
for (Entity e : original) {
if (e instanceof LivingEntity living && !living.isAlive()) continue;
if (hb.shouldIgnore(e)) continue;
filtered.add(e);
}
return filtered;
}
// Intercept getBoundingBox: add padding only for live targets allowed by Hitbox.
@Redirect(
method = "getEntityHitResult(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/AABB;Ljava/util/function/Predicate;D)Lnet/minecraft/world/phys/EntityHitResult;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/Entity;getBoundingBox()Lnet/minecraft/world/phys/AABB;"
)
)
private static AABB combatant$padBoundingBox(Entity target,
Entity source,
Vec3 start,
Vec3 end,
AABB box,
Predicate<Entity> predicate,
double maxDistance) {
Hitbox hb = Modules.get(Hitbox.class);
AABB bb = target.getBoundingBox();
if (hb == null || !hb.shouldExpandFor(target)) {
return bb;
}
double baseDistanceSq = Hitbox.boxDistanceSq(bb, start);
if (baseDistanceSq > maxDistance) {
return bb;
}
return bb.inflate(hb.getPadding());
}
}

View file

@ -0,0 +1,36 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen;
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.misc.BetterMinecraft;
@Mixin(AbstractRecipeBookScreen.class)
public abstract class RecipeBookScreenMixin {
@Final
@Shadow
private RecipeBookComponent<?> recipeBookComponent;
@Inject(method = "initButton", at = @At("HEAD"), cancellable = true)
private void combatant$disableRecipeBook(CallbackInfo ci) {
BetterMinecraft module = Modules.get(BetterMinecraft.class);
if (module != null && module.isHideRecipeBookEnabled()) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderPass;
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 dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.util.logging.DebugLog;
@Mixin(RenderPass.class)
public abstract class RenderPassMixin {
@Inject(method = "setPipeline", at = @At("RETURN"))
private void combatant$applyRhiPipelineState(RenderPipeline pipeline, CallbackInfo ci) {
try {
CombatantRenderSystem.rhi().pipelineState().applyPipelineState(pipeline);
} catch (Throwable t) {
DebugLog.error("[ShapeClip] pipeline state hook failed for " + pipeline, t);
}
}
}

View file

@ -0,0 +1,49 @@
/*
* This file is part of the Combatant Client distribution.
* Combatant modifications copyright (c) 2026 pivosos2007.
*
* Portions of this file are based on, adapted from, or implemented
* with reference to Meteor Client
* (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*
* Licensed under the GNU General Public License v3.0.
* See THIRD_PARTY_NOTICES.md for details.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import dev.loki.lovisual.mixininterface.IRenderPipeline;
import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract;
@Mixin(RenderPipeline.class)
public abstract class RenderPipelineMixin implements IRenderPipeline {
@Unique
private boolean combatant$lineSmooth;
@Unique
private ShapeClipRenderPassContract combatant$shapeClipContract = ShapeClipRenderPassContract.NONE;
@Override
public void combatant$setLineSmooth(boolean lineSmooth) {
this.combatant$lineSmooth = lineSmooth;
}
@Override
public boolean combatant$getLineSmooth() {
return combatant$lineSmooth;
}
@Override
public void combatant$setShapeClipContract(ShapeClipRenderPassContract contract) {
this.combatant$shapeClipContract = contract == null ? ShapeClipRenderPassContract.NONE : contract;
}
@Override
public ShapeClipRenderPassContract combatant$getShapeClipContract() {
return combatant$shapeClipContract;
}
}

View file

@ -0,0 +1,85 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ScreenEffectRenderer;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.mixins.accessors.InGameOverlayRendererAccessor;
@Mixin(ScreenEffectRenderer.class)
public abstract class ScreenEffectRendererMixin {
@Redirect(
method = "submit",
at = @At(
value = "INVOKE",
target =
"Lnet/minecraft/client/renderer/ScreenEffectRenderer;submitFire(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;)V"
)
)
private static void combatant$maybeRenderFireOverlay(
PoseStack matrices,
SubmitNodeCollector vertexConsumers,
TextureAtlasSprite sprite
) {
Minecraft client = Minecraft.getInstance();
Player player = client.player;
if (player == null) return;
NoRender noRender = Modules.get(NoRender.class);
if (noRender != null && noRender.off("fire_overlay")) {
if (!noRender.fireOnlyWhenResistant()) {
return;
}
if (player.hasEffect(MobEffects.FIRE_RESISTANCE)) {
return;
}
}
if (player.hasEffect(MobEffects.FIRE_RESISTANCE)) {
return;
}
InGameOverlayRendererAccessor.callRenderFireOverlay(matrices, vertexConsumers, sprite);
}
@Redirect(
method = "submit",
at = @At(
value = "INVOKE",
target =
"Lnet/minecraft/client/renderer/ScreenEffectRenderer;submitBlockSprite(Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V"
)
)
private static void combatant$maybeRenderBlockOverlay(
TextureAtlasSprite sprite,
PoseStack matrices,
SubmitNodeCollector vertexConsumers,
int color
) {
NoRender noRender = Modules.get(NoRender.class);
if (noRender != null && noRender.off("block_overlay")) {
return;
}
InGameOverlayRendererAccessor.callRenderInWallOverlay(sprite, matrices, vertexConsumers, color);
}
}

View file

@ -0,0 +1,160 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.resources.Identifier;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.config.MainConfig;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.core.RenderPhase;
import dev.loki.lovisual.render.engine.core.RenderPhaseScope;
import dev.loki.lovisual.render.engine.postprocess.MenuBackgroundRenderer;
import dev.loki.lovisual.runtime.RuntimeGate;
import dev.loki.lovisual.util.NarratorBlocker;
@Mixin(Screen.class)
public abstract class ScreenMixin {
@Unique
private RenderPhaseScope combatant$screenPhaseScope;
@Inject(method = "defaultHandleGameClickEvent", at = @At("HEAD"), cancellable = true)
private static void combatant$runClientChatCommand(ClickEvent event,
Minecraft client,
Screen screen,
CallbackInfo ci) {
if (!(event instanceof ClickEvent.RunCommand run)) return;
String command = run.command();
if (command == null || !command.startsWith("@")) return;
if (CommandManager.handle(command)) {
ci.cancel();
}
}
@Inject(method = "extractMenuBackgroundTexture", at = @At("HEAD"), cancellable = true)
private static void combatant$replaceMenuBackground(GuiGraphicsExtractor context,
Identifier texture,
int x,
int y,
float u,
float v,
int width,
int height,
CallbackInfo ci) {
if (texture == null || context == null) return;
if (RuntimeGate.isPanic()) return;
if (!Screen.MENU_BACKGROUND.equals(texture)) return;
if (x != 0 || y != 0) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
if (width != context.guiWidth() || height != context.guiHeight()) return;
MainConfig cfg = MainConfig.get();
if (cfg == null) return;
String mode = cfg.getMenuBackgroundMode();
if (mode == null || mode.equalsIgnoreCase("off")) return;
boolean aurora = mode.equalsIgnoreCase("aurora");
MenuBackgroundRenderer.render(mc, aurora);
ci.cancel();
}
@Inject(method = "shouldRunNarration", at = @At("HEAD"), cancellable = true)
private void combatant$disableNarratorUi(CallbackInfoReturnable<Boolean> cir) {
if (!NarratorBlocker.isBlocked()) return;
cir.setReturnValue(false);
}
@Inject(method = "extractBackground(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$replaceScreenBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.level != null) return;
MainConfig cfg = MainConfig.get();
String mode = (cfg != null) ? cfg.getMenuBackgroundMode() : "off";
if (mode == null || mode.equalsIgnoreCase("off")) return;
boolean aurora = mode.equalsIgnoreCase("aurora");
MenuBackgroundRenderer.render(mc, aurora);
ci.cancel();
}
@Inject(method = "extractBackground(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$noRender$cancelDarkening(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
NoRender module = Modules.get(NoRender.class);
Minecraft mc = Minecraft.getInstance();
if (module != null && mc != null && mc.level != null && module.screenDarkeningDisabled()) {
ci.cancel();
}
}
@Inject(method = "extractTransparentBackground(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V", at = @At("HEAD"), cancellable = true)
private void combatant$noRender$cancelInGameBackground(GuiGraphicsExtractor context, CallbackInfo ci) {
NoRender module = Modules.get(NoRender.class);
Minecraft mc = Minecraft.getInstance();
if (module != null && mc != null && mc.level != null && module.screenDarkeningDisabled()) {
ci.cancel();
}
}
@Inject(method = "extractPanorama", at = @At("HEAD"), cancellable = true, require = 0)
private void combatant$replaceTitlePanorama(GuiGraphicsExtractor context, float delta, CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
Screen self = (Screen) (Object) this;
if (!(self instanceof TitleScreen)) return;
MainConfig cfg = MainConfig.get();
String mode = (cfg != null) ? cfg.getMenuBackgroundMode() : "off";
if (mode == null || mode.equalsIgnoreCase("off")) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
boolean aurora = mode.equalsIgnoreCase("aurora");
MenuBackgroundRenderer.render(mc, aurora);
ci.cancel();
}
@Inject(method = "extractRenderStateWithTooltipAndSubtitles(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"))
private void combatant$beginButtonQueue(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
BetterButtons.beginFrame(ctx);
}
@Inject(method = "extractRenderStateWithTooltipAndSubtitles(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"))
private void combatant$phaseScreenHead(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
Screen self = (Screen) (Object) this;
String label = "2d:screen:" + self.getClass().getSimpleName();
CombatantRenderSystem.ensureFrameContext();
combatant$screenPhaseScope = CombatantRenderSystem.phase(RenderPhase.SCREEN, label);
}
@Inject(method = "extractRenderStateWithTooltipAndSubtitles(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("RETURN"))
private void combatant$phaseScreenTail(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
Screen self = (Screen) (Object) this;
String label = "2d:screen:" + self.getClass().getSimpleName();
if (combatant$screenPhaseScope != null) {
combatant$screenPhaseScope.close();
combatant$screenPhaseScope = null;
}
}
}

View file

@ -0,0 +1,45 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.network.protocol.game.ServerboundUseItemPacket;
import net.minecraft.world.InteractionHand;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.util.aiming.RotationManager;
import dev.loki.lovisual.util.aiming.data.Rotation;
@Mixin(ServerboundUseItemPacket.class)
public class ServerboundUseItemPacketMixin {
@Mutable
@Shadow
@Final
private float yRot;
@Mutable
@Shadow
@Final
private float xRot;
@Inject(method = "<init>(Lnet/minecraft/world/InteractionHand;IFF)V", at = @At("RETURN"))
private void combatant$applyRotation(InteractionHand hand, int sequence, float yaw, float pitch, CallbackInfo ci) {
Rotation rotation = RotationManager.INSTANCE.getCurrentRotation();
if (rotation == null) {
return;
}
this.yRot = rotation.yaw();
this.xRot = rotation.pitch();
}
}

View file

@ -0,0 +1,236 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.google.common.collect.ImmutableMap;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.shaders.ShaderType;
import net.minecraft.client.renderer.PostChainConfig;
import net.minecraft.client.renderer.ShaderManager;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.profiling.ProfilerFiller;
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;
import dev.loki.lovisual.mixins.accessors.ShaderLoaderDefinitionsAccessor;
import dev.loki.lovisual.mixins.accessors.ShaderSourceKeyAccessor;
import dev.loki.lovisual.render.engine.shader.CombatantShaderSources;
import dev.loki.lovisual.render.iris.IrisCompatibilityGuards;
import dev.loki.lovisual.util.logging.DebugLog;
import dev.loki.lovisual.util.resources.RenderResourceReadiness;
import dev.loki.lovisual.util.resources.ResourceReloadHooks;
import java.lang.reflect.Constructor;
import java.util.LinkedHashMap;
import java.util.Map;
@Mixin(ShaderManager.class)
public abstract class ShaderLoaderMixin {
@Unique
private static final Identifier COMBATANT_ENTITY_VERTEX = Identifier.fromNamespaceAndPath("lovisual", "shaders/core/entity_dither.vsh");
@Unique
private static final Identifier COMBATANT_ENTITY_FRAGMENT = Identifier.fromNamespaceAndPath("lovisual", "shaders/core/entity_dither.fsh");
@Unique
private static final Identifier VANILLA_ENTITY_ID = Identifier.withDefaultNamespace("core/entity");
@Unique
private static boolean combatant$loggedEntityOverride;
@Unique
private static boolean combatant$loggedExtendedShaderSources;
@Unique
private static boolean combatant$loggedShaderLoadError;
@Unique
private static Constructor<?> combatant$shaderSourceKeyConstructor;
@Unique
private static int combatant$shaderLoadFailCount;
@WrapOperation(
method = "prepare",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/renderer/ShaderManager;loadShader(Lnet/minecraft/resources/Identifier;Lnet/minecraft/server/packs/resources/Resource;Lcom/mojang/blaze3d/shaders/ShaderType;Ljava/util/Map;Lcom/google/common/collect/ImmutableMap$Builder;)V")
)
private static void combatant$wrapLoadShader(Identifier id, Resource resource, ShaderType type,
Map<?, ?> allResources, ImmutableMap.Builder<?, ?> builder,
Operation<Void> original) {
try {
original.call(id, resource, type, allResources, builder);
} catch (Throwable t) {
combatant$shaderLoadFailCount++;
if (!combatant$loggedShaderLoadError) {
combatant$loggedShaderLoadError = true;
DebugLog.error("[Shader] Failed to load shader", t);
}
DebugLog.warn("[Shader] Skipping shader {}: {}", id, t.getMessage());
}
}
@Unique
private static boolean combatant$addExtendedShaderSources(ResourceManager resourceManager, Map<Object, String> shaderSources) {
Map<Identifier, Resource> resources = resourceManager.listResources("shaders", CombatantShaderSources::isCombatantExtendedSource);
if (resources.isEmpty()) {
return false;
}
int added = 0;
for (Map.Entry<Identifier, Resource> entry : resources.entrySet()) {
Identifier resourceId = entry.getKey();
ShaderType type = CombatantShaderSources.typeByLocation(resourceId);
if (type == null) continue;
String source = CombatantShaderSources.load(resourceManager, resourceId, entry.getValue());
shaderSources.put(combatant$newShaderSourceKey(resourceId, type), source);
added++;
Identifier canonicalId = CombatantShaderSources.canonicalShaderId(resourceId);
if (!canonicalId.equals(resourceId)) {
shaderSources.put(combatant$newShaderSourceKey(canonicalId, type), source);
added++;
}
}
if (added > 0 && !combatant$loggedExtendedShaderSources) {
combatant$loggedExtendedShaderSources = true;
DebugLog.renderThread("[ShaderSource] registered Combatant .vert/.frag sources in ShaderManager: " + added + " keys");
}
return added > 0;
}
@Unique
private static boolean combatant$allowVanillaEntityShaderOverride() {
if (IrisCompatibilityGuards.suppressCombatantTerrainShaderOverrides()) {
return false;
}
return true;
}
@Unique
private static boolean combatant$applyEntityOverride(ResourceManager resourceManager, Map<Object, String> shaderSources) {
String entityFragmentSrc = combatant$loadShaderSource(resourceManager, COMBATANT_ENTITY_FRAGMENT);
String entityVertexSrc = combatant$loadShaderSource(resourceManager, COMBATANT_ENTITY_VERTEX);
if (((entityFragmentSrc == null || entityFragmentSrc.isEmpty()) || (entityVertexSrc == null || entityVertexSrc.isEmpty())) && !combatant$loggedEntityOverride) {
combatant$loggedEntityOverride = true;
DebugLog.warn("[ShaderOverride] entity fade override skipped: combatant entity_dither shader missing");
}
if ((entityFragmentSrc == null || entityFragmentSrc.isEmpty()) || (entityVertexSrc == null || entityVertexSrc.isEmpty())) {
return false;
}
boolean entityFragmentChanged = false;
boolean entityVertexChanged = false;
for (Map.Entry<Object, String> entry : shaderSources.entrySet()) {
Object key = entry.getKey();
if (entityVertexSrc != null && !entityVertexSrc.isEmpty() && combatant$isEntityVertexKey(key)) {
entry.setValue(entityVertexSrc);
entityVertexChanged = true;
continue;
}
if (entityFragmentSrc != null && !entityFragmentSrc.isEmpty() && combatant$isEntityFragmentKey(key)) {
entry.setValue(entityFragmentSrc);
entityFragmentChanged = true;
}
}
if ((!entityFragmentChanged || !entityVertexChanged) && !combatant$loggedEntityOverride) {
combatant$loggedEntityOverride = true;
DebugLog.warn("[ShaderOverride] entity fade override skipped: entity shader key not found");
}
if (entityFragmentChanged && entityVertexChanged && !combatant$loggedEntityOverride) {
combatant$loggedEntityOverride = true;
DebugLog.renderThread("[ShaderOverride] override applied: core/entity -> combatant entity_dither");
}
return entityFragmentChanged || entityVertexChanged;
}
@Unique
private static boolean combatant$isEntityFragmentKey(Object key) {
if (!(key instanceof ShaderSourceKeyAccessor accessor)) return false;
return VANILLA_ENTITY_ID.equals(accessor.combatant$getId()) && accessor.combatant$getType() == ShaderType.FRAGMENT;
}
@Unique
private static boolean combatant$isEntityVertexKey(Object key) {
if (!(key instanceof ShaderSourceKeyAccessor accessor)) return false;
return VANILLA_ENTITY_ID.equals(accessor.combatant$getId()) && accessor.combatant$getType() == ShaderType.VERTEX;
}
@Unique
private static String combatant$loadShaderSource(ResourceManager resourceManager, Identifier id) {
try {
return CombatantShaderSources.load(resourceManager, id);
} catch (Exception e) {
DebugLog.error("[ShaderOverride] read shader override failed: " + id, e);
return null;
}
}
@Unique
private static Object combatant$newShaderSourceKey(Identifier id, ShaderType type) {
try {
if (combatant$shaderSourceKeyConstructor == null) {
Class<?> keyClass = Class.forName("net.minecraft.client.renderer.ShaderManager$ShaderSourceKey");
Constructor<?> constructor = keyClass.getDeclaredConstructor(Identifier.class, ShaderType.class);
constructor.setAccessible(true);
combatant$shaderSourceKeyConstructor = constructor;
}
return combatant$shaderSourceKeyConstructor.newInstance(id, type);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Failed to create ShaderManager shader source key for " + type + " " + id, e);
}
}
@Inject(method = "apply", at = @At("HEAD"))
private void combatant$beforeShaderReload(ShaderManager.Configs definitions,
ResourceManager resourceManager,
ProfilerFiller profiler,
CallbackInfo ci) {
RenderResourceReadiness.markReloading("shader reload");
}
@Inject(method = "apply", at = @At("TAIL"))
private void combatant$afterShaderReload(ShaderManager.Configs definitions,
ResourceManager resourceManager,
ProfilerFiller profiler,
CallbackInfo ci) {
ResourceReloadHooks.onReload(resourceManager);
}
@ModifyReturnValue(method = "prepare", at = @At("RETURN"))
private ShaderManager.Configs combatant$extendShaderSources(ShaderManager.Configs definitions,
ResourceManager resourceManager,
ProfilerFiller profiler) {
ShaderLoaderDefinitionsAccessor accessor = (ShaderLoaderDefinitionsAccessor) (Object) definitions;
Map<?, String> shaderSources = accessor.combatant$getShaderSources();
Map<Object, String> replaced = new LinkedHashMap<>(shaderSources.size() + 64);
replaced.putAll(shaderSources);
boolean changed = combatant$addExtendedShaderSources(resourceManager, replaced);
if (combatant$allowVanillaEntityShaderOverride()) {
changed |= combatant$applyEntityOverride(resourceManager, replaced);
}
if (!changed) {
return definitions;
}
Map<Identifier, PostChainConfig> postChains = accessor.combatant$getPostChains();
@SuppressWarnings({"rawtypes", "unchecked"})
ShaderManager.Configs updated = new ShaderManager.Configs((Map) replaced, postChains);
return updated;
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.pipeline.RenderTarget;
import net.minecraft.client.renderer.SkyRenderer;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget;
@Mixin(SkyRenderer.class)
public abstract class SkyRendererMixin {
@Shadow
@Final
private RenderTarget renderTarget;
@Redirect(
method = {
"renderSkyDisc",
"renderDarkDisc",
"renderSun",
"renderMoon",
"renderStars",
"renderSunriseAndSunset",
"renderEndSky",
"renderEndFlash"
},
at = @At(
value = "FIELD",
target = "Lnet/minecraft/client/renderer/SkyRenderer;renderTarget:Lcom/mojang/blaze3d/pipeline/RenderTarget;",
opcode = Opcodes.GETFIELD
)
)
private RenderTarget combatant$currentWorldTarget(SkyRenderer instance) {
RenderTarget msaa = MsaaWorldTarget.getFramebufferOverride();
return msaa != null ? msaa : renderTarget;
}
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.SlimeBlock;
import net.minecraft.world.level.block.state.BlockState;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.movement.NoStun;
@Mixin(SlimeBlock.class)
public class SlimeBlockMixin {
@Inject(method = "stepOn", at = @At("HEAD"), cancellable = true)
private void nostun$onSteppedOn(Level world, BlockPos pos, BlockState state, Entity entity, CallbackInfo ci) {
NoStun ns = Modules.get(NoStun.class);
if (ns == null || !ns.isEnabled()) return;
if (!ns.isFunctionEnabled(NoStun.fnEnvBlocks())) return;
if (!ns.isEnvBlockEnabled(NoStun.envSlime())) return;
ci.cancel();
}
}

View file

@ -0,0 +1,45 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.SpriteIconButton;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
@Mixin(SpriteIconButton.CenteredIcon.class)
public abstract class SpriteIconButtonCenteredIconMixin {
@Inject(method = "extractContents(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$drawIcon(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
BetterButtons buttons = BetterButtons.get();
if (buttons == null || !buttons.useUiButtons()) return;
Minecraft mc = Minecraft.getInstance();
if (mc != null && ClientScreen.current() instanceof MerchantScreen) return;
SpriteIconButton self = (SpriteIconButton) (Object) this;
AbstractWidget widget = self;
boolean hovered = widget.isMouseOver(mouseX, mouseY);
float hover = BetterButtons.updateHover(widget, hovered, mouseX, mouseY);
BetterButtons.enqueueTextIconButton(self, true, hover, widget.isActive(), widget.isFocused());
if (hovered) {
BetterButtons.captureTooltip(widget, mouseX, mouseY);
}
ci.cancel();
}
}

View file

@ -0,0 +1,45 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.SpriteIconButton;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
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 dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons;
@Mixin(SpriteIconButton.TextAndIcon.class)
public abstract class SpriteIconButtonTextAndIconMixin {
@Inject(method = "extractContents(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("HEAD"), cancellable = true)
private void combatant$drawIcon(GuiGraphicsExtractor ctx, int mouseX, int mouseY, float delta, CallbackInfo ci) {
BetterButtons buttons = BetterButtons.get();
if (buttons == null || !buttons.useUiButtons()) return;
Minecraft mc = Minecraft.getInstance();
if (mc != null && ClientScreen.current() instanceof MerchantScreen) return;
SpriteIconButton self = (SpriteIconButton) (Object) this;
AbstractWidget widget = self;
boolean hovered = widget.isMouseOver(mouseX, mouseY);
float hover = BetterButtons.updateHover(widget, hovered, mouseX, mouseY);
BetterButtons.enqueueTextIconButton(self, false, hover, widget.isActive(), widget.isFocused());
if (hovered) {
BetterButtons.captureTooltip(widget, mouseX, mouseY);
}
ci.cancel();
}
}

View file

@ -0,0 +1,37 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.TitleScreen;
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 dev.loki.lovisual.features.gui.mainmenu.LoVisualMainMenuScreen;
import dev.loki.lovisual.runtime.RuntimeGate;
@Mixin(TitleScreen.class)
public abstract class TitleScreenMixin {
@Inject(method = "init", at = @At("HEAD"), cancellable = true)
private void combatant$replaceTitleScreen(CallbackInfo ci) {
if (RuntimeGate.isPanic()) return;
if (LoVisualMainMenuScreen.shouldUseVanillaTitleScreen()) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null) return;
if (ClientScreen.current() instanceof LoVisualMainMenuScreen) return;
ClientScreen.show(mc, new LoVisualMainMenuScreen());
ci.cancel();
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import net.minecraft.client.gui.components.toasts.Toast;
import net.minecraft.client.gui.components.toasts.ToastManager;
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 dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
@Mixin(ToastManager.class)
public abstract class ToastManagerMixin {
@Inject(method = "addToast(Lnet/minecraft/client/gui/components/toasts/Toast;)V", at = @At("HEAD"), cancellable = true)
private void combatant$noRender$hideToasts(Toast toast, CallbackInfo ci) {
NoRender module = Modules.get(NoRender.class);
if (module != null && module.hideToastHints()) {
ci.cancel();
}
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil;
import net.minecraft.resources.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterTooltips;
@Mixin(TooltipRenderUtil.class)
public class TooltipRenderUtilMixin {
@Redirect(
method = "extractTooltipBackground",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;blitSprite(Lcom/mojang/blaze3d/pipeline/RenderPipeline;Lnet/minecraft/resources/Identifier;IIII)V"
)
)
private static void redirectDrawGuiTexture(
GuiGraphicsExtractor ctx,
RenderPipeline pipeline,
Identifier sprite,
int x, int y, int w, int h
) {
BetterTooltips tooltips = BetterTooltips.get();
if (tooltips == null || !tooltips.isTooltipAlphaEnabled()) {
ctx.blitSprite(pipeline, sprite, x, y, w, h);
return;
}
int argb = tooltips.tooltipColor();
ctx.blitSprite(pipeline, sprite, x, y, w, h, argb);
}
}

View file

@ -0,0 +1,95 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.systems.RenderPassBackend;
import com.mojang.blaze3d.systems.RenderPassDescriptor;
import com.mojang.blaze3d.textures.GpuTextureView;
import com.mojang.blaze3d.vulkan.VulkanCommandEncoder;
import com.mojang.blaze3d.vulkan.VulkanGpuTextureView;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.vulkan.VkClearDepthStencilValue;
import org.lwjgl.vulkan.VkClearValue;
import org.lwjgl.vulkan.VkRenderingAttachmentInfo;
import org.lwjgl.vulkan.VkRenderingInfo;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.core.CombatantRenderSystem;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.clip.VulkanShapeClipBridge;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.msaa.VulkanMsaaResolveBridge;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge;
import static org.lwjgl.vulkan.VK12.*;
@Mixin(VulkanCommandEncoder.class)
public abstract class VulkanCommandEncoderMixin {
@Inject(method = "createRenderPass", at = @At("HEAD"))
private void combatant$beginVulkanRenderPassState(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPassBackend> cir) {
VulkanMsaaResolveBridge.beginRenderPass();
VulkanRenderStateBridge.beginRenderPass(descriptor);
}
@Inject(
method = "createRenderPass",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRDynamicRendering;vkCmdBeginRenderingKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;Lorg/lwjgl/vulkan/VkRenderingInfo;)V", shift = At.Shift.BEFORE)
)
private void combatant$configureAttachments(RenderPassDescriptor descriptor,
CallbackInfoReturnable<RenderPassBackend> cir,
@Local MemoryStack stack,
@Local VkRenderingInfo renderingInfo) {
if (VulkanRenderStateBridge.currentRenderPassNeedsStencilAttachment()) {
GpuTextureView view = VulkanShapeClipBridge.stencilAttachmentFor(descriptor);
if (!(view instanceof VulkanGpuTextureView vkView) || view.isClosed()) {
VulkanRenderStateBridge.markCurrentRenderPassStencilAttachment(false);
} else {
VulkanRenderStateBridge.markCurrentRenderPassStencilAttachment(true);
boolean clear = VulkanShapeClipBridge.consumeStencilClear();
VkRenderingAttachmentInfo stencilAttachment = VkRenderingAttachmentInfo.calloc(stack).sType$Default();
stencilAttachment
.imageView(vkView.vkImageView())
.imageLayout(VK_IMAGE_LAYOUT_GENERAL)
.storeOp(VK_ATTACHMENT_STORE_OP_STORE)
.loadOp(clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD);
if (clear) {
VkClearDepthStencilValue clearValue = VkClearDepthStencilValue.calloc(stack).depth(0.0f).stencil(0);
stencilAttachment.clearValue(VkClearValue.calloc(stack).depthStencil(clearValue));
}
renderingInfo.pStencilAttachment(stencilAttachment);
}
}
try {
VulkanMsaaResolveBridge.configure(descriptor, renderingInfo);
} catch (Throwable t) {
boolean snapshotResolve = VulkanMsaaResolveBridge.snapshotActive();
VulkanMsaaResolveBridge.abortRenderPass();
if (!snapshotResolve) {
VulkanRenderStateBridge.disableMsaaAfterFailure("dynamic rendering resolve configuration", t);
}
throw t instanceof RuntimeException runtime ? runtime
: new IllegalStateException("Failed to configure Vulkan MSAA resolve", t);
}
}
@Inject(method = "submitRenderPass", at = @At("TAIL"))
private void combatant$endVulkanRenderPassState(CallbackInfo ci) {
VulkanMsaaResolveBridge.finishRenderPass();
try {
CombatantRenderSystem.rhi().shapeClip().endRenderPass();
} catch (Throwable ignored) {
// Render pass teardown must never make Vulkan submission fail.
}
VulkanRenderStateBridge.endRenderPass();
}
}

View file

@ -0,0 +1,193 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.shaders.ShaderSource;
import com.mojang.blaze3d.textures.GpuTexture;
import com.mojang.blaze3d.vulkan.VulkanConst;
import com.mojang.blaze3d.vulkan.VulkanDevice;
import com.mojang.blaze3d.vulkan.VulkanInstance;
import com.mojang.blaze3d.vulkan.VulkanPhysicalDevice;
import com.mojang.blaze3d.vulkan.VulkanRenderPipeline;
import com.mojang.blaze3d.vulkan.checkpoints.CheckpointExtension;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.vulkan.VkDevice;
import org.lwjgl.vulkan.VkImageFormatProperties;
import org.lwjgl.vulkan.VkPhysicalDeviceDepthStencilResolvePropertiesKHR;
import org.lwjgl.vulkan.VkPhysicalDeviceLimits;
import org.lwjgl.vulkan.VkPhysicalDeviceProperties2;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static org.lwjgl.vulkan.VK12.*;
@Mixin(VulkanDevice.class)
public abstract class VulkanDeviceMixin {
@Shadow
private ShaderSource defaultShaderSource;
@Unique
private final Map<VulkanRenderStateBridge.PipelineVariantKey, VulkanRenderPipeline> combatant$pipelineVariants = new HashMap<>();
@Invoker("compilePipeline")
protected abstract VulkanRenderPipeline combatant$compilePipelineVariant(RenderPipeline pipeline, ShaderSource source);
@Inject(method = "<init>", at = @At("RETURN"))
private void combatant$captureMsaaCapabilities(ShaderSource shaderSource,
VulkanInstance instance,
VulkanPhysicalDevice physicalDevice,
Set<String> enabledExtensions,
VkDevice device,
long vma,
CheckpointExtension checkpointExtension,
CallbackInfo ci) {
if (physicalDevice == null || physicalDevice.vkPhysicalDeviceProperties() == null) return;
VkPhysicalDeviceLimits limits = physicalDevice.vkPhysicalDeviceProperties().limits();
int framebufferSamples = limits.framebufferColorSampleCounts()
& limits.framebufferDepthSampleCounts()
& limits.framebufferStencilSampleCounts();
try (MemoryStack stack = MemoryStack.stackPush()) {
int textureUsage = GpuTexture.USAGE_COPY_DST
| GpuTexture.USAGE_COPY_SRC
| GpuTexture.USAGE_TEXTURE_BINDING
| GpuTexture.USAGE_RENDER_ATTACHMENT;
framebufferSamples &= combatant$imageSampleCounts(
stack,
physicalDevice,
GpuFormat.RGBA8_UNORM,
VulkanConst.textureUsageToVk(textureUsage, GpuFormat.RGBA8_UNORM)
);
framebufferSamples &= combatant$imageSampleCounts(
stack,
physicalDevice,
GpuFormat.D32_FLOAT,
VulkanConst.textureUsageToVk(textureUsage, GpuFormat.D32_FLOAT)
);
framebufferSamples &= combatant$imageSampleCounts(
stack,
physicalDevice,
GpuFormat.S8_UINT,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
);
VkPhysicalDeviceDepthStencilResolvePropertiesKHR resolve =
VkPhysicalDeviceDepthStencilResolvePropertiesKHR.calloc(stack).sType$Default();
VkPhysicalDeviceProperties2 properties = VkPhysicalDeviceProperties2.calloc(stack)
.sType$Default()
.pNext(resolve);
vkGetPhysicalDeviceProperties2(physicalDevice.vkPhysicalDevice(), properties);
VulkanRenderStateBridge.configureMsaaCapabilities(
framebufferSamples,
resolve.supportedDepthResolveModes(),
resolve.independentResolveNone()
);
} catch (Throwable t) {
VulkanRenderStateBridge.configureMsaaCapabilities(1, 0, false);
DebugLog.warnOnChange(
"msaa.vulkan.capabilities.failed",
t.getClass().getSimpleName() + "|" + t.getMessage(),
"[MSAA/Vulkan] capability query failed; MSAA remains disabled: %s: %s",
t.getClass().getSimpleName(),
t.getMessage()
);
}
}
@Unique
private static int combatant$imageSampleCounts(MemoryStack stack,
VulkanPhysicalDevice physicalDevice,
GpuFormat format,
int usage) {
VkImageFormatProperties properties = VkImageFormatProperties.calloc(stack);
int result = vkGetPhysicalDeviceImageFormatProperties(
physicalDevice.vkPhysicalDevice(),
VulkanConst.toVk(format),
VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL,
usage,
0,
properties
);
return result == VK_SUCCESS ? properties.sampleCounts() : VK_SAMPLE_COUNT_1_BIT;
}
@Inject(method = "getOrCompilePipeline", at = @At("HEAD"), cancellable = true)
private void combatant$getOrCompilePipelineVariant(RenderPipeline pipeline,
CallbackInfoReturnable<VulkanRenderPipeline> cir) {
if (!VulkanRenderStateBridge.needsPipelineVariant(pipeline)) return;
VulkanRenderStateBridge.PipelineVariantKey key = VulkanRenderStateBridge.pipelineVariantKey(pipeline);
VulkanRenderPipeline variant = combatant$pipelineVariants.get(key);
if (variant != null) {
cir.setReturnValue(variant);
return;
}
try {
variant = combatant$compilePipelineVariant(pipeline, defaultShaderSource);
if (variant == null || !variant.isValid()) {
throw new IllegalStateException("Vulkan returned an invalid pipeline variant");
}
combatant$pipelineVariants.put(key, variant);
DebugLog.stencilOnChange(
"vulkan.pipeline.variant.compile",
key.toString(),
"[Vulkan/RHI] compiled pipeline variant %s",
key
);
cir.setReturnValue(variant);
} catch (Throwable t) {
if (key.usesMsaa()) {
VulkanRenderStateBridge.disableMsaaAfterFailure("pipeline variant compile " + key, t);
}
if (key.usesStencil()) {
VulkanRenderStateBridge.disableStencilAfterFailure("pipeline variant compile " + key, t);
}
DebugLog.error("[Vulkan/RHI] pipeline variant compile failed for " + key, t);
// The current dynamic-rendering scope is already active. Falling through to vanilla's
// single-sample/no-stencil pipeline would bind an incompatible VkPipeline and can lose
// the device, so abort on the CPU and only disable the feature for future passes.
throw t instanceof RuntimeException runtime ? runtime
: new IllegalStateException("Failed to compile Vulkan pipeline variant " + key, t);
}
}
@Inject(method = "clearPipelineCache", at = @At("RETURN"))
private void combatant$clearPipelineVariants(CallbackInfo ci) {
if (combatant$pipelineVariants.isEmpty()) return;
for (VulkanRenderPipeline pipeline : combatant$pipelineVariants.values()) {
try {
pipeline.destroy();
} catch (Throwable t) {
DebugLog.warnOnChange(
"vulkan.pipeline.variant.destroy.failed",
t.getClass().getSimpleName() + "|" + t.getMessage(),
"[Vulkan/RHI] failed to destroy pipeline variant: %s: %s",
t.getClass().getSimpleName(),
t.getMessage()
);
}
}
combatant$pipelineVariants.clear();
}
}

View file

@ -0,0 +1,114 @@
/*
* This file is part of the Combatant Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.vulkan.VulkanDevice;
import com.mojang.blaze3d.vulkan.VulkanGpuTexture;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.mixininterface.IMsaaTexture;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge;
import org.lwjgl.vulkan.VkImageSubresourceRange;
import static org.lwjgl.vulkan.VK12.*;
@Mixin(VulkanGpuTexture.class)
public abstract class VulkanGpuTextureMixin implements IMsaaTexture {
@Unique
private int combatant$samples = 1;
/**
* Mojang 26.2 maps render-attachment usage only for color/depth aspects. Pure S8_UINT
* is a valid Vulkan stencil attachment, but without this OR the image is created without
* VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT and using it in pStencilAttachment is invalid.
*/
@Redirect(
method = "<init>",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vulkan/VulkanConst;textureUsageToVk(ILcom/mojang/blaze3d/GpuFormat;)I")
)
private int combatant$stencilAttachmentUsage(int usage, GpuFormat format) {
int vkUsage = com.mojang.blaze3d.vulkan.VulkanConst.textureUsageToVk(usage, format);
if (format != null && format.hasStencilAspect() && (usage & com.mojang.blaze3d.textures.GpuTexture.USAGE_RENDER_ATTACHMENT) != 0) {
vkUsage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
}
return vkUsage;
}
/**
* Mojang picks DEPTH_BIT for every non-color format. For S8_UINT this records an invalid
* initialization barrier and later the image view is invalid too. Keep the patch scoped to
* actual stencil formats; color/depth formats stay vanilla.
*/
@Redirect(
method = "<init>",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VkImageSubresourceRange;aspectMask(I)Lorg/lwjgl/vulkan/VkImageSubresourceRange;")
)
private VkImageSubresourceRange combatant$stencilImageAspect(VkImageSubresourceRange range,
int original,
VulkanDevice device,
int usage,
String label,
GpuFormat format,
int width,
int height,
int depthOrLayers,
int mipLevels) {
if (format != null && format.hasStencilAspect() && !format.hasDepthAspect()) {
return range.aspectMask(VK_IMAGE_ASPECT_STENCIL_BIT);
}
return range.aspectMask(original);
}
@ModifyArg(
method = "<init>",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VkImageCreateInfo;samples(I)Lorg/lwjgl/vulkan/VkImageCreateInfo;"),
index = 0
)
private int combatant$useRequestedMsaaSamples(int original) {
return VulkanRenderStateBridge.requestedTextureSamples();
}
@Inject(method = "<init>", at = @At("RETURN"))
private void combatant$rememberSamples(VulkanDevice device,
int usage,
String label,
GpuFormat format,
int width,
int height,
int depthOrLayers,
int mipLevels,
CallbackInfo ci) {
combatant$samples = VulkanRenderStateBridge.requestedTextureSamples();
}
@Override
public void combatant$setSamples(int samples) {
combatant$samples = VulkanRenderStateBridge.normalizeSamples(samples);
}
@Override
public int combatant$getSamples() {
return combatant$samples;
}
@Override
public boolean combatant$isMsaa() {
return combatant$samples > 1;
}
@Override
public int combatant$getGlId() {
return 0;
}
}

Some files were not shown because too many files have changed in this diff Show more