/* * 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 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 textLines = Screen.getTooltipFromItem(mc, stack); if (textLines == null || textLines.isEmpty()) return; java.util.List 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 lines, java.util.Optional 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 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 lines, java.util.Optional 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 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(); } } }