diff --git a/common/src/main/java/io/github/axolotlclient/modules/hitboxes/HitboxType.java b/common/src/main/java/io/github/axolotlclient/modules/hitboxes/HitboxType.java new file mode 100644 index 000000000..9210c164f --- /dev/null +++ b/common/src/main/java/io/github/axolotlclient/modules/hitboxes/HitboxType.java @@ -0,0 +1,83 @@ +/* + * Copyright © 2026 DSNS + * + * This file is part of DolphinClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.hitboxes; + +import lombok.Getter; + +/** + * Entity groups the Hitboxes module can toggle and color independently. + * Classification is first-match in the order below so more specific groups + * (self, arrows) win over broader ones (players, projectiles, living). + */ +@Getter +public enum HitboxType { + SELF("hitboxes.self", 0xFF00FFFF), + PLAYERS("hitboxes.players", 0xFFFFFFFF), + ARROWS("hitboxes.arrows", 0xFFFF5555), + PROJECTILES("hitboxes.projectiles", 0xFFFFAA00), + ITEMS("hitboxes.items", 0xFFFFFF55), + HOSTILE("hitboxes.hostile", 0xFFAA0000), + PASSIVE("hitboxes.passive", 0xFF55FF55), + OTHER("hitboxes.other", 0xFFAAAAAA); + + private final String translationKey; + private final int defaultColor; + + HitboxType(String translationKey, int defaultColor) { + this.translationKey = translationKey; + this.defaultColor = defaultColor; + } + + public static HitboxType classify( + boolean self, + boolean player, + boolean arrow, + boolean projectile, + boolean item, + boolean hostile, + boolean living + ) { + if (self) { + return SELF; + } + if (player) { + return PLAYERS; + } + if (arrow) { + return ARROWS; + } + if (projectile) { + return PROJECTILES; + } + if (item) { + return ITEMS; + } + if (hostile) { + return HOSTILE; + } + if (living) { + return PASSIVE; + } + return OTHER; + } +} diff --git a/common/src/main/resources/assets/dolphinclient/lang/en_us.json b/common/src/main/resources/assets/dolphinclient/lang/en_us.json index f8197fc90..5b6126976 100644 --- a/common/src/main/resources/assets/dolphinclient/lang/en_us.json +++ b/common/src/main/resources/assets/dolphinclient/lang/en_us.json @@ -210,6 +210,24 @@ "hitColor": "Hit Color", "hitColor.tooltip": "The Color used for entities when they are being hurt.", "hit_color_on_armor": "Overlay Hit Color on Armor", + "hitboxes": "Hitboxes", + "hitboxes.arrows": "Arrows", + "hitboxes.hostile": "Hostile Mobs", + "hitboxes.items": "Items", + "hitboxes.line_width": "Line Width", + "hitboxes.other": "Other", + "hitboxes.passive": "Passive Mobs", + "hitboxes.players": "Players", + "hitboxes.projectiles": "Projectiles", + "hitboxes.self": "Self", + "hitboxes.show": "Show", + "hitboxes.show.tooltip": "Render hitboxes for this entity type.", + "hitboxes.show_eye_height": "Eye Height Line", + "hitboxes.show_eye_height.tooltip": "Draws the red eye-height debug line on living entities.", + "hitboxes.show_invisible": "Invisible Entities", + "hitboxes.show_invisible.tooltip": "Show hitboxes on entities that are invisible.", + "hitboxes.show_look_vector": "Look Vector", + "hitboxes.show_look_vector.tooltip": "Draws the blue look-direction line from the entity's eyes.", "horizontal": "Horizontal speed only", "hotbarhud": "Hotbar", "hud": "Options", @@ -570,6 +588,7 @@ "hud.entry.usage": "Press Space to enter movement mode. Press Enter to open this module's options.", "hud.entry.usage.move": "Press Escape or Space to exit movement mode, use arrow keys to move this HUD. Press Enter to open this module's options.", "toggle_fullbright": "Toggle Full Bright", + "toggle_hitboxes": "Toggle Hitboxes", "inventoryhud": "Inventory", "key.toggle_hud": "Toggle HUD modules", "coordshud.unknown_biome": "Unknown", diff --git a/common/src/test/java/io/github/axolotlclient/modules/hitboxes/HitboxTypeTest.java b/common/src/test/java/io/github/axolotlclient/modules/hitboxes/HitboxTypeTest.java new file mode 100644 index 000000000..d61ace718 --- /dev/null +++ b/common/src/test/java/io/github/axolotlclient/modules/hitboxes/HitboxTypeTest.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2026 DSNS + * + * This file is part of DolphinClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.hitboxes; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HitboxTypeTest { + + @Test + void classifiesInSpecificToBroadOrder() { + assertEquals(HitboxType.SELF, HitboxType.classify(true, true, false, false, false, false, false)); + assertEquals(HitboxType.PLAYERS, HitboxType.classify(false, true, false, false, false, false, true)); + assertEquals(HitboxType.ARROWS, HitboxType.classify(false, false, true, true, false, false, false)); + assertEquals(HitboxType.PROJECTILES, HitboxType.classify(false, false, false, true, false, false, false)); + assertEquals(HitboxType.ITEMS, HitboxType.classify(false, false, false, false, true, false, false)); + assertEquals(HitboxType.HOSTILE, HitboxType.classify(false, false, false, false, false, true, true)); + assertEquals(HitboxType.PASSIVE, HitboxType.classify(false, false, false, false, false, false, true)); + assertEquals(HitboxType.OTHER, HitboxType.classify(false, false, false, false, false, false, false)); + } + + @Test + void keepsTranslationKeysAndDefaultColorsUnique() { + Set keys = new HashSet<>(); + Set colors = new HashSet<>(); + for (HitboxType type : HitboxType.values()) { + assertTrue(type.getTranslationKey().startsWith("hitboxes.")); + assertTrue(keys.add(type.getTranslationKey()), type.name()); + assertNotEquals(0, type.getDefaultColor() >>> 24, type.name()); + assertTrue(colors.add(type.getDefaultColor()), type.name()); + } + assertEquals(HitboxType.values().length, keys.size()); + assertEquals(HitboxType.values().length, colors.size()); + } + + @Test + void langFileContainsHitboxEntries() throws Exception { + try (var in = HitboxType.class.getClassLoader() + .getResourceAsStream("assets/dolphinclient/lang/en_us.json")) { + assertTrue(in != null, "en_us.json"); + String json = new String(in.readAllBytes()); + assertTrue(json.contains("\"hitboxes\"")); + assertTrue(json.contains("\"hitboxes.show\"")); + assertTrue(json.contains("\"toggle_hitboxes\"")); + for (HitboxType type : HitboxType.values()) { + assertTrue(json.contains("\"" + type.getTranslationKey() + "\""), type.getTranslationKey()); + } + } + } +} diff --git a/docs/hitboxes.png b/docs/hitboxes.png new file mode 100644 index 000000000..1db8696b2 Binary files /dev/null and b/docs/hitboxes.png differ diff --git a/versions/1.8.9/src/main/java/io/github/axolotlclient/DolphinClient.java b/versions/1.8.9/src/main/java/io/github/axolotlclient/DolphinClient.java index 51b00463a..8660e37a0 100644 --- a/versions/1.8.9/src/main/java/io/github/axolotlclient/DolphinClient.java +++ b/versions/1.8.9/src/main/java/io/github/axolotlclient/DolphinClient.java @@ -30,6 +30,7 @@ import io.github.axolotlclient.modules.blur.MotionBlur; import io.github.axolotlclient.modules.hud.HudManager; import io.github.axolotlclient.modules.hud.gui.hud.PackDisplayHud; +import io.github.axolotlclient.modules.hitboxes.Hitboxes; import io.github.axolotlclient.modules.hypixel.HypixelMods; import io.github.axolotlclient.modules.particles.Particles; import io.github.axolotlclient.modules.screenshotUtils.ScreenshotUtils; @@ -52,6 +53,7 @@ private void addBuiltinModules() { registerModule(MenuBlur.getInstance()); registerModule(ScrollableTooltips.getInstance()); + registerModule(Hitboxes.getInstance()); registerModule(Particles.getInstance()); registerModule(ScreenshotUtils.getInstance()); registerModule(UnfocusedFpsLimiter.getInstance()); diff --git a/versions/1.8.9/src/main/java/io/github/axolotlclient/config/ui/MenuCatalog.java b/versions/1.8.9/src/main/java/io/github/axolotlclient/config/ui/MenuCatalog.java index 44d0ab991..1ad4fc4d8 100644 --- a/versions/1.8.9/src/main/java/io/github/axolotlclient/config/ui/MenuCatalog.java +++ b/versions/1.8.9/src/main/java/io/github/axolotlclient/config/ui/MenuCatalog.java @@ -153,7 +153,7 @@ public static List build() { absorbAnimations(modules, category); continue; } - Tab tab = isSettings(name) ? Tab.SETTINGS : Tab.MODS; + Tab tab = isSettings(name) ? Tab.SETTINGS : isHudTab(name) ? Tab.HUD : Tab.MODS; absorb(modules, category, tab); } @@ -193,13 +193,18 @@ private static Module moduleFrom(String nameKey, Tab tab, BooleanOption enabled, /** * Settings keeps a tile per child even when only Screenshots remains * under General, matching the layout from before Authentication was - * removed. Mods still need at least two children before splitting. + * removed. Mods still need at least two children before splitting. HUD + * tiles always stay whole so an overlay's groups render as sections rather + * than as one tile per group. */ private static boolean shouldPromote(Collection children, Tab tab) { int size = children.size(); if (size == 0 || size > 12) { return false; } + if (tab == Tab.HUD) { + return false; + } return tab == Tab.SETTINGS || size >= 2; } @@ -257,6 +262,14 @@ private static boolean isSettings(String name) { return "general".equals(name); } + /** + * Overlays that draw in the world rather than on a HUD widget still belong + * on the HUD tab, so they are routed there instead of Mods. + */ + private static boolean isHudTab(String name) { + return "hitboxes".equals(name); + } + private static OptionCategory find(Collection categories, String name) { for (OptionCategory category : categories) { if (name.equals(category.getName())) { diff --git a/versions/1.8.9/src/main/java/io/github/axolotlclient/mixin/EntityRenderDispatcherMixin.java b/versions/1.8.9/src/main/java/io/github/axolotlclient/mixin/EntityRenderDispatcherMixin.java index afde035cd..38699419d 100644 --- a/versions/1.8.9/src/main/java/io/github/axolotlclient/mixin/EntityRenderDispatcherMixin.java +++ b/versions/1.8.9/src/main/java/io/github/axolotlclient/mixin/EntityRenderDispatcherMixin.java @@ -25,15 +25,45 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import io.github.axolotlclient.modules.freelook.Freelook; +import io.github.axolotlclient.modules.hitboxes.Hitboxes; import net.minecraft.client.render.entity.EntityRenderDispatcher; import net.minecraft.entity.Entity; import org.objectweb.asm.Opcodes; 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(EntityRenderDispatcher.class) public abstract class EntityRenderDispatcherMixin { + @WrapOperation(method = "render(Lnet/minecraft/entity/Entity;DDDFFZ)Z", at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/entity/EntityRenderDispatcher;renderHitboxes:Z", opcode = Opcodes.GETFIELD)) + private boolean dolphinclient$forceHitboxes(EntityRenderDispatcher instance, Operation original, Entity entity) { + Hitboxes hitboxes = Hitboxes.getInstance(); + if (hitboxes.isEnabled()) { + return hitboxes.shouldRender(entity); + } + return original.call(instance); + } + + @WrapOperation(method = "render(Lnet/minecraft/entity/Entity;DDDFFZ)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isInvisible()Z")) + private boolean dolphinclient$showInvisibleHitboxes(Entity entity, Operation original) { + if (Hitboxes.getInstance().shouldRender(entity)) { + return false; + } + return original.call(entity); + } + + @Inject(method = "renderHitbox", at = @At("HEAD"), cancellable = true) + private void dolphinclient$renderHitboxes(Entity entity, double dx, double dy, double dz, float yaw, float tickDelta, CallbackInfo ci) { + Hitboxes hitboxes = Hitboxes.getInstance(); + if (!hitboxes.isEnabled()) { + return; + } + hitboxes.render(entity, dx, dy, dz, tickDelta); + ci.cancel(); + } + @WrapOperation(method = "prepare", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/Entity;yaw:F", opcode = Opcodes.GETFIELD)) public float axolotlclient$freelook$yaw(Entity instance, Operation original) { return Freelook.getInstance().yaw(original.call(instance)); diff --git a/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hitboxes/Hitboxes.java b/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hitboxes/Hitboxes.java new file mode 100644 index 000000000..cad4a4aa5 --- /dev/null +++ b/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hitboxes/Hitboxes.java @@ -0,0 +1,193 @@ +/* + * Copyright © 2026 DSNS + * + * This file is part of DolphinClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.hitboxes; + +import java.util.EnumMap; +import java.util.Map; + +import io.github.axolotlclient.DolphinClient; +import io.github.axolotlclient.DolphinClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Color; +import io.github.axolotlclient.AxolotlClientConfig.impl.options.BooleanOption; +import io.github.axolotlclient.AxolotlClientConfig.impl.options.ColorOption; +import io.github.axolotlclient.AxolotlClientConfig.impl.options.IntegerOption; +import io.github.axolotlclient.bridge.key.AxoKeybinding; +import io.github.axolotlclient.bridge.key.AxoKeys; +import io.github.axolotlclient.modules.AbstractModule; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.render.platform.GlStateManager; +import net.minecraft.client.render.vertex.BufferBuilder; +import net.minecraft.client.render.vertex.DefaultVertexFormat; +import net.minecraft.client.render.vertex.Tesselator; +import net.minecraft.client.render.world.WorldRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.entity.FishingBobberEntity; +import net.minecraft.entity.ItemEntity; +import net.minecraft.entity.living.ArmorStandEntity; +import net.minecraft.entity.living.LivingEntity; +import net.minecraft.entity.living.mob.monster.Monster; +import net.minecraft.entity.living.player.PlayerEntity; +import net.minecraft.entity.projectile.ArrowEntity; +import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.entity.projectile.ThrownEntity; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.lwjgl.opengl.GL11; + +public class Hitboxes extends AbstractModule { + + @Getter + private static final Hitboxes instance = new Hitboxes(); + + private final BooleanOption enabled = new BooleanOption("enabled", false); + private final IntegerOption lineWidth = new IntegerOption("hitboxes.line_width", 2, 1, 7); + private final BooleanOption showLookVector = new BooleanOption("hitboxes.show_look_vector", true); + private final BooleanOption showEyeHeight = new BooleanOption("hitboxes.show_eye_height", true); + private final BooleanOption showInvisible = new BooleanOption("hitboxes.show_invisible", false); + + private final OptionCategory category = OptionCategory.create("hitboxes"); + private final Map types = new EnumMap<>(HitboxType.class); + + @Override + public void init() { + category.add(enabled, lineWidth, showLookVector, showEyeHeight, showInvisible); + + for (HitboxType type : HitboxType.values()) { + OptionCategory group = OptionCategory.create(type.getTranslationKey()); + TypeSettings settings = new TypeSettings( + new BooleanOption("hitboxes.show", true), + new ColorOption("color", new Color(type.getDefaultColor())) + ); + group.add(settings.show, settings.color); + category.add(group); + types.put(type, settings); + } + + DolphinClient.config().addCategory(category); + + AxoKeybinding.create(AxoKeys.KEY_UNKNOWN, "toggle_hitboxes").br$registerOnConsumeClick(() -> { + enabled.toggle(); + DolphinClientCommon.getInstance().saveConfig(); + }); + } + + public boolean isEnabled() { + return enabled.get(); + } + + public boolean shouldRender(Entity entity) { + if (!enabled.get()) { + return false; + } + if (entity.isInvisible() && !showInvisible.get()) { + return false; + } + return types.get(typeOf(entity)).show.get(); + } + + public void render(Entity entity, double dx, double dy, double dz, float tickDelta) { + TypeSettings settings = types.get(typeOf(entity)); + if (!settings.show.get()) { + return; + } + + Color color = settings.color.get(); + int r = color.getRed(); + int g = color.getGreen(); + int b = color.getBlue(); + int a = color.getAlpha(); + + GlStateManager.depthMask(false); + GlStateManager.disableTexture(); + GlStateManager.disableLighting(); + GlStateManager.disableCull(); + if (a < 255) { + GlStateManager.enableBlend(); + GlStateManager.blendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO); + } else { + GlStateManager.disableBlend(); + } + GL11.glLineWidth(lineWidth.get()); + + Box shape = entity.getShape(); + WorldRenderer.renderOutlineShape(new Box( + shape.minX - entity.x + dx, + shape.minY - entity.y + dy, + shape.minZ - entity.z + dz, + shape.maxX - entity.x + dx, + shape.maxY - entity.y + dy, + shape.maxZ - entity.z + dz + ), r, g, b, a); + + if (showEyeHeight.get() && entity instanceof LivingEntity) { + float halfWidth = entity.width / 2.0F; + double eyeY = dy + entity.getEyeHeight(); + WorldRenderer.renderOutlineShape(new Box( + dx - halfWidth, + eyeY - 0.009999999776482582D, + dz - halfWidth, + dx + halfWidth, + eyeY + 0.009999999776482582D, + dz + halfWidth + ), 255, 0, 0, a); + } + + if (showLookVector.get()) { + Vec3d look = entity.getRotationVec(tickDelta); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuffer(); + buffer.begin(GL11.GL_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); + double eyeY = dy + entity.getEyeHeight(); + buffer.vertex(dx, eyeY, dz).color(0, 0, 255, a).nextVertex(); + buffer.vertex(dx + look.x * 2.0D, eyeY + look.y * 2.0D, dz + look.z * 2.0D).color(0, 0, 255, a).nextVertex(); + tesselator.end(); + } + + GL11.glLineWidth(1.0F); + GlStateManager.enableTexture(); + GlStateManager.enableLighting(); + GlStateManager.enableCull(); + GlStateManager.disableBlend(); + GlStateManager.depthMask(true); + } + + HitboxType typeOf(Entity entity) { + boolean self = entity == Minecraft.getInstance().player; + boolean player = entity instanceof PlayerEntity; + boolean arrow = entity instanceof ArrowEntity; + boolean projectile = entity instanceof ThrownEntity + || entity instanceof ProjectileEntity + || entity instanceof FishingBobberEntity; + boolean item = entity instanceof ItemEntity; + boolean hostile = entity instanceof Monster; + boolean living = entity instanceof LivingEntity + && !(entity instanceof PlayerEntity) + && !(entity instanceof ArmorStandEntity); + return HitboxType.classify(self, player, arrow, projectile, item, hostile, living); + } + + private record TypeSettings(BooleanOption show, ColorOption color) { + } +} diff --git a/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/ModsMenuIcons.java b/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/ModsMenuIcons.java index fc2bc5b11..e1adbe209 100644 --- a/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/ModsMenuIcons.java +++ b/versions/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/ModsMenuIcons.java @@ -101,6 +101,7 @@ final class ModsMenuIcons { put("fpsLimiter", Items.CLOCK); put("sky", Items.ENDER_EYE); put("blockOutlines", Item.byBlock(Blocks.STONE)); + put("hitboxes", Items.SLIME_BALL); put("timeChanger", Items.CLOCK); put("beams", Items.BLAZE_ROD); put("levelhead", Items.GOLDEN_HELMET);