diff --git a/src/main/java/com/lambda/mixin/entity/EntityMixin.java b/src/main/java/com/lambda/mixin/entity/EntityMixin.java index bb116af94..f53bb4818 100644 --- a/src/main/java/com/lambda/mixin/entity/EntityMixin.java +++ b/src/main/java/com/lambda/mixin/entity/EntityMixin.java @@ -17,6 +17,7 @@ package com.lambda.mixin.entity; +import com.lambda.Lambda; import com.lambda.event.EventFlow; import com.lambda.event.events.EntityEvent; import com.lambda.event.events.PlayerEvent; @@ -151,11 +152,13 @@ private boolean modifyGetFlagGlowing(boolean original) { @WrapWithCondition(method = "changeLookDirection", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setYaw(F)V")) private boolean wrapSetYaw(Entity instance, float yaw) { + if ((Object) this != getMc().player) return true; return RotationManager.getLockYaw() == null; } @WrapWithCondition(method = "changeLookDirection", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setPitch(F)V")) private boolean wrapSetPitch(Entity instance, float yaw) { + if ((Object) this != Lambda.getMc().player) return true; return RotationManager.getLockPitch() == null; } diff --git a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java b/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java deleted file mode 100644 index b4775b72f..000000000 --- a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.mixin.items; - -import com.lambda.module.modules.render.BlockESP; -import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import net.minecraft.block.BarrierBlock; -import net.minecraft.block.BlockRenderType; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(BarrierBlock.class) -public class BarrierBlockMixin { - /** - * Modifies barrier block render type to {@link BlockRenderType#MODEL} when {@link BlockESP} is enabled and {@link BlockESP#getBarrier()} is true - */ - @ModifyReturnValue(method = "getRenderType", at = @At("RETURN")) - private BlockRenderType modifyGetRenderType(BlockRenderType original, BlockState state) { - if (BlockESP.INSTANCE.isEnabled() - && BlockESP.getBarrier() - && state.getBlock() == Blocks.BARRIER - ) return BlockRenderType.MODEL; - return original; - } -} diff --git a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java b/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java deleted file mode 100644 index 49ff48b7f..000000000 --- a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.mixin.render; - -import com.lambda.module.modules.render.BlockESP; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import net.minecraft.client.render.block.BlockRenderManager; -import net.minecraft.client.render.model.BlockStateModel; -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; - -@Mixin(BlockRenderManager.class) -public abstract class BlockRenderManagerMixin { - @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) - private void getModel(BlockState state, CallbackInfoReturnable cir) { - if (BlockESP.INSTANCE.isEnabled() - && BlockESP.getBarrier() - && state.getBlock() == Blocks.BARRIER - ) cir.setReturnValue(BlockESP.getModel()); - } -} diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java index 98cf293e7..1b91bc5fd 100644 --- a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java +++ b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java @@ -21,6 +21,7 @@ import com.lambda.event.events.RenderEvent; import com.lambda.graphics.RenderMain; import com.lambda.gui.DearImGui; +import com.lambda.module.modules.render.BlockOutline; import com.lambda.module.modules.render.NoRender; import com.lambda.module.modules.render.Zoom; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; @@ -40,6 +41,7 @@ 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; @Mixin(GameRenderer.class) public class GameRendererMixin { @@ -53,8 +55,7 @@ private void updateTargetedEntityInvoke(float tickDelta, CallbackInfo info) { @WrapOperation(method = "renderWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;render(Lnet/minecraft/client/util/ObjectAllocator;Lnet/minecraft/client/render/RenderTickCounter;ZLnet/minecraft/client/render/Camera;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;Lcom/mojang/blaze3d/buffers/GpuBufferSlice;Lorg/joml/Vector4f;Z)V")) void onRenderWorld(WorldRenderer instance, ObjectAllocator allocator, RenderTickCounter tickCounter, boolean renderBlockOutline, Camera camera, Matrix4f positionMatrix, Matrix4f basicProjectionMatrix, Matrix4f projectionMatrix, GpuBufferSlice fogBuffer, Vector4f fogColor, boolean renderSky, Operation original) { original.call(instance, allocator, tickCounter, renderBlockOutline, camera, positionMatrix, basicProjectionMatrix, projectionMatrix, fogBuffer, fogColor, renderSky); - - RenderMain.render3D(positionMatrix, projectionMatrix); + RenderMain.render(positionMatrix, basicProjectionMatrix); } @ModifyExpressionValue(method = "renderWorld", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)) @@ -69,6 +70,7 @@ private void injectShowFloatingItem(ItemStack floatingItem, CallbackInfo ci) { @ModifyReturnValue(method = "getFov", at = @At("RETURN")) private float modifyGetFov(float original) { + Zoom.updateCurrentZoom(); return original / Zoom.getLerpedZoom(); } @@ -76,4 +78,9 @@ private float modifyGetFov(float original) { private void onGuiRenderComplete(RenderTickCounter tickCounter, boolean tick, CallbackInfo ci) { DearImGui.INSTANCE.render(); } + + @Inject(method = "shouldRenderBlockOutline()Z", at = @At("HEAD"), cancellable = true) + private void injectShouldRenderBlockOutline(CallbackInfoReturnable cir) { + if (BlockOutline.INSTANCE.isEnabled()) cir.setReturnValue(false); + } } diff --git a/src/main/java/com/lambda/mixin/render/InGameHudMixin.java b/src/main/java/com/lambda/mixin/render/InGameHudMixin.java index 3fc01c204..bdf2a3ba4 100644 --- a/src/main/java/com/lambda/mixin/render/InGameHudMixin.java +++ b/src/main/java/com/lambda/mixin/render/InGameHudMixin.java @@ -17,6 +17,8 @@ package com.lambda.mixin.render; +import com.lambda.event.EventFlow; +import com.lambda.event.events.HudRenderEvent; import com.lambda.module.modules.render.NoRender; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import net.minecraft.client.gui.DrawContext; @@ -82,4 +84,13 @@ private void injectRenderScoreboardSidebar(DrawContext drawContext, ScoreboardOb private void injectRenderCrosshair(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { if (NoRender.INSTANCE.isEnabled() && NoRender.getNoCrosshair()) ci.cancel(); } + + /** + * Fire HudRenderEvent at the end of HUD rendering to allow Lambda modules + * to render items and other GUI elements using the valid DrawContext. + */ + @Inject(method = "render", at = @At("RETURN")) + private void onRenderEnd(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + EventFlow.post(new HudRenderEvent(context)); + } } diff --git a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java index 5ce465d21..6f82aa2e2 100644 --- a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java +++ b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java @@ -19,12 +19,15 @@ import com.lambda.Lambda; import com.lambda.interaction.managers.rotating.RotationManager; +import com.lambda.module.modules.render.Nametags; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import net.minecraft.client.render.entity.LivingEntityRenderer; import net.minecraft.entity.LivingEntity; 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 static com.lambda.util.math.LinearKt.lerp; @@ -56,4 +59,10 @@ private float wrapGetLerpedPitch(LivingEntity livingEntity, float v, Operation cir) { + if (Nametags.INSTANCE.isEnabled() && Nametags.shouldRenderNametag(livingEntity)) + cir.setReturnValue(false); + } } diff --git a/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt b/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt index 57444d190..68e9c83aa 100644 --- a/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt +++ b/src/main/kotlin/com/lambda/command/commands/PrefixCommand.kt @@ -29,6 +29,7 @@ import com.lambda.command.CommandRegistry import com.lambda.command.LambdaCommand import com.lambda.config.Configuration import com.lambda.config.Setting +import com.lambda.config.SettingCore import com.lambda.util.Communication.info import com.lambda.util.extension.CommandBuilder import com.lambda.util.text.buildText @@ -52,7 +53,8 @@ object PrefixCommand : LambdaCommand( } val prefixChar = prefix.first() val configurable = Configuration.configurableByName("command") ?: return@executeWithResult failure("No command configurable found.") - val setting = configurable.settings.find { it.name == "prefix" } as? Setting<*, Char> + @Suppress("UNCHECKED_CAST") + val setting = configurable.settings.find { it.name == "prefix" } as? Setting, Char> ?: return@executeWithResult failure("Prefix setting is not a Char or can not be found.") setting.trySetValue(prefixChar) return@executeWithResult success() diff --git a/src/main/kotlin/com/lambda/config/AutomationConfig.kt b/src/main/kotlin/com/lambda/config/AutomationConfig.kt index c32f0677f..1acfefddc 100644 --- a/src/main/kotlin/com/lambda/config/AutomationConfig.kt +++ b/src/main/kotlin/com/lambda/config/AutomationConfig.kt @@ -26,7 +26,7 @@ import com.lambda.config.groups.InteractSettings import com.lambda.config.groups.InventorySettings import com.lambda.config.groups.RotationSettings import com.lambda.context.Automated -import com.lambda.event.events.onStaticRender +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer import com.lambda.interaction.construction.simulation.result.Drawable import com.lambda.module.Module import com.lambda.util.NamedEnum @@ -85,10 +85,9 @@ open class AutomationConfig( var drawables = listOf() init { - onStaticRender { esp -> - if (renders) - drawables.forEach { it.render(esp) } - } + tickedRenderer("Ticked Automation Config Renderer") { + if (renders) drawables.forEach { with(it) { render() } } + } } } } diff --git a/src/main/kotlin/com/lambda/config/ConfigEditor.kt b/src/main/kotlin/com/lambda/config/ConfigEditor.kt index 2e74a71b1..25e930e43 100644 --- a/src/main/kotlin/com/lambda/config/ConfigEditor.kt +++ b/src/main/kotlin/com/lambda/config/ConfigEditor.kt @@ -102,6 +102,13 @@ open class SettingGroupEditor(open val c: T) { @SettingEditorDsl fun groups(groups: MutableList>) = settings.forEach { it.groups = groups } + + @SettingEditorDsl + fun visibility(visibility: (() -> Boolean) -> () -> Boolean) { + settings.forEach { + it.visibility = visibility(it.visibility) + } + } } class TypedEditBuilder( diff --git a/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt b/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt index 06f8d7e80..eaa194555 100644 --- a/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt +++ b/src/main/kotlin/com/lambda/config/MutableAutomationConfig.kt @@ -60,7 +60,7 @@ class MutableAutomationConfigImpl : MutableAutomationConfig { if (setting.core.type != newSetting.core.type) throw IllegalStateException("Settings with the same name do not have the same type.") @Suppress("UNCHECKED_CAST") - (setting as Setting, Any>).core = newSetting.core as SettingCore + (setting as Setting, Any>).core = newSetting.core as SettingCore } } } diff --git a/src/main/kotlin/com/lambda/config/Setting.kt b/src/main/kotlin/com/lambda/config/Setting.kt index 75f9ed688..eef00628e 100644 --- a/src/main/kotlin/com/lambda/config/Setting.kt +++ b/src/main/kotlin/com/lambda/config/Setting.kt @@ -90,9 +90,7 @@ import kotlin.reflect.KProperty * ``` * * @property defaultValue The default value of the setting. - * @property description A description of the setting. * @property type The type reflection of the setting. - * @property visibility A function that determines whether the setting is visible. */ abstract class SettingCore( var defaultValue: T, @@ -155,11 +153,12 @@ class Setting, R>( override val description: String, var core: T, val configurable: Configurable, - val visibility: () -> Boolean, + var visibility: () -> Boolean, ) : Nameable, Describable { val originalCore = core var disabled = { false } var groups: MutableList> = mutableListOf() + var buttonMenu: NamedEnum? = null var value by this @@ -227,6 +226,10 @@ class Setting, R>( path?.let { groups.add(listOf(it)) } } + fun buttonMenu(menu: NamedEnum) = apply { + buttonMenu = menu + } + fun trySetValue(newValue: R) { if (newValue == value) { ConfigCommand.info(notChangedMessage()) diff --git a/src/main/kotlin/com/lambda/config/SettingGroup.kt b/src/main/kotlin/com/lambda/config/SettingGroup.kt index 6ee65aae3..6f7249714 100644 --- a/src/main/kotlin/com/lambda/config/SettingGroup.kt +++ b/src/main/kotlin/com/lambda/config/SettingGroup.kt @@ -19,6 +19,7 @@ package com.lambda.config interface ISettingGroup { val settings: MutableList> + val visibility: () -> Boolean } abstract class SettingGroup(c: Configurable) : ISettingGroup { diff --git a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt index f4986db31..1b37e9471 100644 --- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt @@ -32,7 +32,8 @@ import java.awt.Color open class BreakSettings( c: Configurable, - baseGroup: NamedEnum + baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), BreakConfig { private enum class Group(override val displayName: String) : NamedEnum { General("General"), @@ -40,78 +41,78 @@ open class BreakSettings( } // General - override val breakMode by c.setting("Break Mode", BreakMode.Packet).group(baseGroup, Group.General).index() - override val sorter by c.setting("Break Sorter", ActionConfig.SortMode.Tool, "The order in which breaks are performed").group(baseGroup, Group.General).index() - override val rebreak by c.setting("Rebreak", true, "Re-breaks blocks after they've been broken once").group(baseGroup, Group.General).index() + override val breakMode by c.setting("Break Mode", BreakMode.Packet, visibility = visibility).group(baseGroup, Group.General).index() + override val sorter by c.setting("Break Sorter", ActionConfig.SortMode.Tool, "The order in which breaks are performed", visibility = visibility).group(baseGroup, Group.General).index() + override val rebreak by c.setting("Rebreak", true, "Re-breaks blocks after they've been broken once", visibility = visibility).group(baseGroup, Group.General).index() // Double break - override val doubleBreak by c.setting("Double Break", true, "Allows breaking two blocks at once").group(baseGroup, Group.General).index() - override val unsafeCancels by c.setting("Unsafe Cancels", true, "Allows cancelling block breaking even if the server might continue breaking sever side, potentially causing unexpected state changes") { doubleBreak }.group(baseGroup, Group.General).index() + override val doubleBreak by c.setting("Double Break", true, "Allows breaking two blocks at once", visibility = visibility).group(baseGroup, Group.General).index() + override val unsafeCancels by c.setting("Unsafe Cancels", true, "Allows cancelling block breaking even if the server might continue breaking sever side, potentially causing unexpected state changes") { visibility() && doubleBreak }.group(baseGroup, Group.General).index() // Fixes / Delays - override val breakThreshold by c.setting("Break Threshold", 0.70f, 0.1f..1.0f, 0.01f, "The break amount at which the block is considered broken").group(baseGroup, Group.General).index() - override val fudgeFactor by c.setting("Fudge Factor", 1, 0..5, 1, "The number of ticks to add to the break time, usually to account for server lag").group(baseGroup, Group.General).index() - override val serverSwapTicks by c.setting("Server Swap", 0, 0..5, 1, "The number of ticks to give the server time to recognize the player attributes on the swapped item", " tick(s)").group(baseGroup, Group.General).index() + override val breakThreshold by c.setting("Break Threshold", 0.70f, 0.1f..1.0f, 0.01f, "The break amount at which the block is considered broken", visibility = visibility).group(baseGroup, Group.General).index() + override val fudgeFactor by c.setting("Fudge Factor", 1, 0..5, 1, "The number of ticks to add to the break time, usually to account for server lag", visibility = visibility).group(baseGroup, Group.General).index() + override val serverSwapTicks by c.setting("Server Swap", 0, 0..5, 1, "The number of ticks to give the server time to recognize the player attributes on the swapped item", " tick(s)", visibility = visibility).group(baseGroup, Group.General).index() // override val desyncFix by c.setting("Desync Fix", false, "Predicts if the players breaking will be slowed next tick as block break packets are processed using the players next position") { vis() && page == Page.General } - override val breakDelay by c.setting("Break Delay", 0, 0..6, 1, "The delay between breaking blocks", " tick(s)").group(baseGroup, Group.General).index() + override val breakDelay by c.setting("Break Delay", 0, 0..6, 1, "The delay between breaking blocks", " tick(s)", visibility = visibility).group(baseGroup, Group.General).index() // Timing - override val tickStageMask by c.setting("Break Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which break actions can be performed", displayClassName = true).group(baseGroup, Group.General).index() + override val tickStageMask by c.setting("Break Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which break actions can be performed", displayClassName = true, visibility = visibility).group(baseGroup, Group.General).index() // Swap - override val swapMode by c.setting("Break Swap Mode", BreakConfig.SwapMode.End, "Decides when to swap to the best suited tool when breaking a block").group(baseGroup, Group.General).index() + override val swapMode by c.setting("Break Swap Mode", BreakConfig.SwapMode.End, "Decides when to swap to the best suited tool when breaking a block", visibility = visibility).group(baseGroup, Group.General).index() // Swing - override val swing by c.setting("Swing Mode", SwingMode.Constant, "The times at which to swing the players hand").group(baseGroup, Group.General).index() - override val swingType by c.setting("Break Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { swing != SwingMode.None }.group(baseGroup, Group.General).index() + override val swing by c.setting("Swing Mode", SwingMode.Constant, "The times at which to swing the players hand", visibility = visibility).group(baseGroup, Group.General).index() + override val swingType by c.setting("Break Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { visibility() && swing != SwingMode.None }.group(baseGroup, Group.General).index() // Rotate - override val rotate by c.setting("Rotate For Break", false, "Rotate towards block while breaking").group(baseGroup, Group.General).index() + override val rotate by c.setting("Rotate For Break", false, "Rotate towards block while breaking", visibility = visibility).group(baseGroup, Group.General).index() // Pending / Post - override val breakConfirmation by c.setting("Break Confirmation", BreakConfirmationMode.BreakThenAwait, "The style of confirmation used when breaking").group(baseGroup, Group.General).index() - override val breaksPerTick by c.setting("Breaks Per Tick", 5, 1..30, 1, "Maximum instant block breaks per tick").group(baseGroup, Group.General).index() + override val breakConfirmation by c.setting("Break Confirmation", BreakConfirmationMode.BreakThenAwait, "The style of confirmation used when breaking", visibility = visibility).group(baseGroup, Group.General).index() + override val breaksPerTick by c.setting("Breaks Per Tick", 5, 1..30, 1, "Maximum instant block breaks per tick", visibility = visibility).group(baseGroup, Group.General).index() // Block - override val ignoredBlocks by c.setting("Ignored Blocks", emptySet(), description = "Blocks that wont be broken").group(baseGroup, Group.General).index() - override val avoidLiquids by c.setting("Avoid Liquids", true, "Avoids breaking blocks that would cause liquid to spill").group(baseGroup, Group.General).index() - override val avoidSupporting by c.setting("Avoid Supporting", true, "Avoids breaking the block supporting the player").group(baseGroup, Group.General).index() + override val ignoredBlocks by c.setting("Ignored Blocks", emptySet(), description = "Blocks that wont be broken", visibility = visibility).group(baseGroup, Group.General).index() + override val avoidLiquids by c.setting("Avoid Liquids", true, "Avoids breaking blocks that would cause liquid to spill", visibility = visibility).group(baseGroup, Group.General).index() + override val avoidSupporting by c.setting("Avoid Supporting", true, "Avoids breaking the block supporting the player", visibility = visibility).group(baseGroup, Group.General).index() // Tool - override val efficientOnly by c.setting("Efficient Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val suitableToolsOnly by c.setting("Suitable Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val forceFortunePickaxe by c.setting("Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val minFortuneLevel by c.setting("Min Fortune Level", 1, 1..3, 1, "The minimum fortune level to use") { swapMode.isEnabled() && forceFortunePickaxe }.group(baseGroup, Group.General).index() - override val useWoodenTools by c.setting("Use Wooden Tools", true, "Use wooden tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val useStoneTools by c.setting("Use Stone Tools", true, "Use stone tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val useIronTools by c.setting("Use Iron Tools", true, "Use iron tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val useDiamondTools by c.setting("Use Diamond Tools", true, "Use diamond tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val useGoldTools by c.setting("Use Gold Tools", true, "Use gold tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() - override val useNetheriteTools by c.setting("Use Netherite Tools", true, "Use netherite tools when breaking blocks") { swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val efficientOnly by c.setting("Efficient Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val suitableToolsOnly by c.setting("Suitable Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val forceFortunePickaxe by c.setting("Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val minFortuneLevel by c.setting("Min Fortune Level", 1, 1..3, 1, "The minimum fortune level to use") { visibility() && swapMode.isEnabled() && forceFortunePickaxe }.group(baseGroup, Group.General).index() + override val useWoodenTools by c.setting("Use Wooden Tools", true, "Use wooden tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val useStoneTools by c.setting("Use Stone Tools", true, "Use stone tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val useIronTools by c.setting("Use Iron Tools", true, "Use iron tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val useDiamondTools by c.setting("Use Diamond Tools", true, "Use diamond tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val useGoldTools by c.setting("Use Gold Tools", true, "Use gold tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() + override val useNetheriteTools by c.setting("Use Netherite Tools", true, "Use netherite tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(baseGroup, Group.General).index() // Cosmetics - override val sounds by c.setting("Break Sounds", true, "Plays the breaking sounds").group(baseGroup, Group.Cosmetic).index() - override val particles by c.setting("Particles", true, "Renders the breaking particles").group(baseGroup, Group.Cosmetic).index() - override val breakingTexture by c.setting("Breaking Overlay", true, "Overlays the breaking texture at its different stages").group(baseGroup, Group.Cosmetic).index() + override val sounds by c.setting("Break Sounds", true, "Plays the breaking sounds", visibility = visibility).group(baseGroup, Group.Cosmetic).index() + override val particles by c.setting("Particles", true, "Renders the breaking particles", visibility = visibility).group(baseGroup, Group.Cosmetic).index() + override val breakingTexture by c.setting("Breaking Overlay", true, "Overlays the breaking texture at its different stages", visibility = visibility).group(baseGroup, Group.Cosmetic).index() // Modes - override val renders by c.setting("Renders", true, "Enables the render settings for breaking progress").group(baseGroup, Group.Cosmetic).index() - override val animation by c.setting("Animation", AnimationMode.Out, "The style of animation used for the box") { renders }.group(baseGroup, Group.Cosmetic).index() + override val renders by c.setting("Renders", true, "Enables the render settings for breaking progress", visibility = visibility).group(baseGroup, Group.Cosmetic).index() + override val animation by c.setting("Animation", AnimationMode.Out, "The style of animation used for the box") { visibility() && renders }.group(baseGroup, Group.Cosmetic).index() // Fill - override val fill by c.setting("Fill", true, "Renders the sides of the box to display break progress") { renders }.group(baseGroup, Group.Cosmetic).index() - override val dynamicFillColor by c.setting("Dynamic Colour", true, "Enables fill color interpolation from start to finish for fill when breaking a block") { renders && fill }.group(baseGroup, Group.Cosmetic).index() - override val staticFillColor by c.setting("Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill") { renders && !dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index() - override val startFillColor by c.setting("Start Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill at the start of breaking") { renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index() - override val endFillColor by c.setting("End Fill Color", Color(0, 255, 0, 60).brighter(), "The color of the fill at the end of breaking") { renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index() + override val fill by c.setting("Fill", true, "Renders the sides of the box to display break progress") { visibility() && renders }.group(baseGroup, Group.Cosmetic).index() + override val dynamicFillColor by c.setting("Dynamic Colour", true, "Enables fill color interpolation from start to finish for fill when breaking a block") { visibility() && renders && fill }.group(baseGroup, Group.Cosmetic).index() + override val staticFillColor by c.setting("Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill") { visibility() && renders && !dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index() + override val startFillColor by c.setting("Start Fill Color", Color(255, 0, 0, 60).brighter(), "The color of the fill at the start of breaking") { visibility() && renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index() + override val endFillColor by c.setting("End Fill Color", Color(0, 255, 0, 60).brighter(), "The color of the fill at the end of breaking") { visibility() && renders && dynamicFillColor && fill }.group(baseGroup, Group.Cosmetic).index() // Outline - override val outline by c.setting("Outline", true, "Renders the lines of the box to display break progress") { renders }.group(baseGroup, Group.Cosmetic).index() - override val outlineWidth by c.setting("Outline Width", 2, 0..5, 1, "The width of the outline") { renders && outline }.group(baseGroup, Group.Cosmetic).index() - override val dynamicOutlineColor by c.setting("Dynamic Outline Color", true, "Enables color interpolation from start to finish for the outline when breaking a block") { renders && outline }.group(baseGroup, Group.Cosmetic).index() - override val staticOutlineColor by c.setting("Outline Color", Color.RED.brighter(), "The Color of the outline at the start of breaking") { renders && !dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index() - override val startOutlineColor by c.setting("Start Outline Color", Color.RED.brighter(), "The color of the outline at the start of breaking") { renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index() - override val endOutlineColor by c.setting("End Outline Color", Color.GREEN.brighter(), "The color of the outline at the end of breaking") { renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index() + override val outline by c.setting("Outline", true, "Renders the lines of the box to display break progress") { visibility() && renders }.group(baseGroup, Group.Cosmetic).index() + override val outlineWidth by c.setting("Outline Width", 2f, 0f..10f, 0.1f, "The width of the outline") { visibility() && renders && outline }.group(baseGroup, Group.Cosmetic).index() + override val dynamicOutlineColor by c.setting("Dynamic Outline Color", true, "Enables color interpolation from start to finish for the outline when breaking a block") { visibility() && renders && outline }.group(baseGroup, Group.Cosmetic).index() + override val staticOutlineColor by c.setting("Outline Color", Color.RED.brighter(), "The Color of the outline at the start of breaking") { visibility() && renders && !dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index() + override val startOutlineColor by c.setting("Start Outline Color", Color.RED.brighter(), "The color of the outline at the start of breaking") { visibility() && renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index() + override val endOutlineColor by c.setting("End Outline Color", Color.GREEN.brighter(), "The color of the outline at the end of breaking") { visibility() && renders && dynamicOutlineColor && outline }.group(baseGroup, Group.Cosmetic).index() } diff --git a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt index e1fa36913..41d8bb19c 100644 --- a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt @@ -26,6 +26,7 @@ import kotlin.math.max class BuildSettings( c: Configurable, baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), BuildConfig { enum class Group(override val displayName: String) : NamedEnum { General("General"), @@ -33,23 +34,23 @@ class BuildSettings( Scan("Scan") } - override val breakBlocks by c.setting("Break", true, "Break blocks").group(baseGroup, Group.General).index() - override val interactBlocks by c.setting("Place / Interact", true, "Interact blocks").group(baseGroup, Group.General).index() + override val breakBlocks by c.setting("Break", true, "Break blocks", visibility = visibility).group(baseGroup, Group.General).index() + override val interactBlocks by c.setting("Place / Interact", true, "Interact blocks", visibility = visibility).group(baseGroup, Group.General).index() - override val pathing by c.setting("Pathing", false, "Path to blocks").group(baseGroup, Group.General).index() - override val stayInRange by c.setting("Stay In Range", false, "Stay in range of blocks").group(baseGroup, Group.General).index() - override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks").group(baseGroup, Group.General).index() - override val spleefEntities by c.setting("Spleef Entities", false, "Breaks blocks beneath entities blocking placements to get them out of the way").group(baseGroup, Group.General).index() - override val maxPendingActions by c.setting("Max Pending Actions", 15, 1..30, 1, "The maximum count of pending interactions to allow before pausing future interactions").group(baseGroup, Group.General).index() - override val actionTimeout by c.setting("Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks").group(baseGroup, Group.General).index() - override val maxBuildDependencies by c.setting("Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results").group(baseGroup, Group.General).index() + override val pathing by c.setting("Pathing", false, "Path to blocks", visibility = visibility).group(baseGroup, Group.General).index() + override val stayInRange by c.setting("Stay In Range", false, "Stay in range of blocks", visibility = visibility).group(baseGroup, Group.General).index() + override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks", visibility = visibility).group(baseGroup, Group.General).index() + override val spleefEntities by c.setting("Spleef Entities", false, "Breaks blocks beneath entities blocking placements to get them out of the way", visibility = visibility).group(baseGroup, Group.General).index() + override val maxPendingActions by c.setting("Max Pending Actions", 15, 1..30, 1, "The maximum count of pending interactions to allow before pausing future interactions", visibility = visibility).group(baseGroup, Group.General).index() + override val actionTimeout by c.setting("Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks", visibility = visibility).group(baseGroup, Group.General).index() + override val maxBuildDependencies by c.setting("Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results", visibility = visibility).group(baseGroup, Group.General).index() - override var blockReach by c.setting("Interact Reach", 4.5, 1.0..7.0, 0.01, "Maximum block interaction distance").group(baseGroup, Group.Reach).index() - override var entityReach by c.setting("Attack Reach", 3.0, 1.0..7.0, 0.01, "Maximum entity interaction distance").group(baseGroup, Group.Reach).index() + override var blockReach by c.setting("Interact Reach", 4.5, 1.0..7.0, 0.01, "Maximum block interaction distance", visibility = visibility).group(baseGroup, Group.Reach).index() + override var entityReach by c.setting("Attack Reach", 3.0, 1.0..7.0, 0.01, "Maximum entity interaction distance", visibility = visibility).group(baseGroup, Group.Reach).index() override val scanReach: Double get() = max(entityReach, blockReach) - override val checkSideVisibility by c.setting("Visibility Check", true, "Whether to check if an AABB side is visible").group(baseGroup, Group.Scan).index() - override val strictRayCast by c.setting("Strict Raycast", false, "Whether to include the environment to the ray cast context").group(baseGroup, Group.Scan).index() - override val resolution by c.setting("Resolution", 5, 1..20, 1, "The amount of grid divisions per surface of the hit box", "") { strictRayCast }.group(baseGroup, Group.Scan).index() - override val pointSelection by c.setting("Point Selection", PointSelection.Optimum, "The strategy to select the best hit point").group(baseGroup, Group.Scan).index() + override val checkSideVisibility by c.setting("Visibility Check", true, "Whether to check if an AABB side is visible", visibility = visibility).group(baseGroup, Group.Scan).index() + override val strictRayCast by c.setting("Strict Raycast", false, "Whether to include the environment to the ray cast context", visibility = visibility).group(baseGroup, Group.Scan).index() + override val resolution by c.setting("Resolution", 5, 1..20, 1, "The amount of grid divisions per surface of the hit box", "") { visibility() && strictRayCast }.group(baseGroup, Group.Scan).index() + override val pointSelection by c.setting("Point Selection", PointSelection.Optimum, "The strategy to select the best hit point", visibility = visibility).group(baseGroup, Group.Scan).index() } diff --git a/src/main/kotlin/com/lambda/config/groups/EatSettings.kt b/src/main/kotlin/com/lambda/config/groups/EatSettings.kt index 28a7aab51..62f4230ce 100644 --- a/src/main/kotlin/com/lambda/config/groups/EatSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/EatSettings.kt @@ -24,23 +24,24 @@ import net.minecraft.item.Items class EatSettings( c: Configurable, - baseGroup: NamedEnum + baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), EatConfig { val nutritiousFoodDefaults = listOf(Items.APPLE, Items.BAKED_POTATO, Items.BEEF, Items.BEETROOT, Items.BEETROOT_SOUP, Items.BREAD, Items.CARROT, Items.CHICKEN, Items.CHORUS_FRUIT, Items.COD, Items.COOKED_BEEF, Items.COOKED_CHICKEN, Items.COOKED_COD, Items.COOKED_MUTTON, Items.COOKED_PORKCHOP, Items.COOKED_RABBIT, Items.COOKED_SALMON, Items.COOKIE, Items.DRIED_KELP, Items.ENCHANTED_GOLDEN_APPLE, Items.GOLDEN_APPLE, Items.GOLDEN_CARROT, Items.HONEY_BOTTLE, Items.MELON_SLICE, Items.MUSHROOM_STEW, Items.MUTTON, Items.POISONOUS_POTATO, Items.PORKCHOP, Items.POTATO, Items.PUFFERFISH, Items.PUMPKIN_PIE, Items.RABBIT, Items.RABBIT_STEW, Items.ROTTEN_FLESH, Items.SALMON, Items.SPIDER_EYE, Items.SUSPICIOUS_STEW, Items.SWEET_BERRIES, Items.GLOW_BERRIES, Items.TROPICAL_FISH) val resistanceFoodDefaults = listOf(Items.ENCHANTED_GOLDEN_APPLE) val regenerationFoodDefaults = listOf(Items.ENCHANTED_GOLDEN_APPLE, Items.GOLDEN_APPLE) val negativeFoodDefaults = listOf(Items.CHICKEN, Items.POISONOUS_POTATO, Items.PUFFERFISH, Items.ROTTEN_FLESH, Items.SPIDER_EYE) - override val eatOnHunger by c.setting("Eat On Hunger", true, "Whether to eat when hungry").group(baseGroup).index() - override val minFoodLevel by c.setting("Minimum Food Level", 6, 0..20, 1, "The minimum food level to eat food", " food level") { eatOnHunger }.group(baseGroup).index() - override val saturated by c.setting("Saturated", EatConfig.Saturation.EatSmart, "When to stop eating") { eatOnHunger }.group(baseGroup).index() - override val nutritiousFood by c.setting("Nutritious Food", nutritiousFoodDefaults, nutritiousFoodDefaults, "Items that are be considered nutritious") { eatOnHunger }.group(baseGroup).index() - override val selectionPriority by c.setting("Selection Priority", EatConfig.SelectionPriority.MostNutritious, "The priority for selecting food items") { eatOnHunger }.group(baseGroup).index() - override val eatOnFire by c.setting("Eat On Fire", true, "Whether to eat when on fire").group(baseGroup).index() - override val resistanceFood by c.setting("Resistance Food", resistanceFoodDefaults, resistanceFoodDefaults, "Items that give Fire Resistance") { eatOnFire }.group(baseGroup).index() - override val eatOnDamage by c.setting("Eat On Damage", true, "Whether to eat when damaged").group(baseGroup).index() - override val minDamage by c.setting("Minimum Damage", 10, 0..20, 1, "The minimum damage threshold to trigger eating") { eatOnDamage }.group(baseGroup).index() - override val regenerationFood by c.setting("Regeneration Food", regenerationFoodDefaults, regenerationFoodDefaults, "Items that give Regeneration") { eatOnDamage }.group(baseGroup).index() - override val ignoreBadFood by c.setting("Ignore Bad Food", true, "Whether to eat when the food is bad").group(baseGroup).index() - override val badFood by c.setting("Bad Food", negativeFoodDefaults, negativeFoodDefaults, "Items that are considered bad food") { ignoreBadFood }.group(baseGroup).index() + override val eatOnHunger by c.setting("Eat On Hunger", true, "Whether to eat when hungry", visibility = visibility).group(baseGroup).index() + override val minFoodLevel by c.setting("Minimum Food Level", 6, 0..20, 1, "The minimum food level to eat food", " food level") { visibility() && eatOnHunger }.group(baseGroup).index() + override val saturated by c.setting("Saturated", EatConfig.Saturation.EatSmart, "When to stop eating") { visibility() && eatOnHunger }.group(baseGroup).index() + override val nutritiousFood by c.setting("Nutritious Food", nutritiousFoodDefaults, nutritiousFoodDefaults, "Items that are be considered nutritious") { visibility() && eatOnHunger }.group(baseGroup).index() + override val selectionPriority by c.setting("Selection Priority", EatConfig.SelectionPriority.MostNutritious, "The priority for selecting food items") { visibility() && eatOnHunger }.group(baseGroup).index() + override val eatOnFire by c.setting("Eat On Fire", true, "Whether to eat when on fire", visibility = visibility).group(baseGroup).index() + override val resistanceFood by c.setting("Resistance Food", resistanceFoodDefaults, resistanceFoodDefaults, "Items that give Fire Resistance") { visibility() && eatOnFire }.group(baseGroup).index() + override val eatOnDamage by c.setting("Eat On Damage", true, "Whether to eat when damaged", visibility = visibility).group(baseGroup).index() + override val minDamage by c.setting("Minimum Damage", 10, 0..20, 1, "The minimum damage threshold to trigger eating") { visibility() && eatOnDamage }.group(baseGroup).index() + override val regenerationFood by c.setting("Regeneration Food", regenerationFoodDefaults, regenerationFoodDefaults, "Items that give Regeneration") { visibility() && eatOnDamage }.group(baseGroup).index() + override val ignoreBadFood by c.setting("Ignore Bad Food", true, "Whether to eat when the food is bad", visibility = visibility).group(baseGroup).index() + override val badFood by c.setting("Bad Food", negativeFoodDefaults, negativeFoodDefaults, "Items that are considered bad food") { visibility() && ignoreBadFood }.group(baseGroup).index() } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt b/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt index 1d53a3d72..7ce90e2e0 100644 --- a/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt @@ -24,21 +24,22 @@ import com.lambda.util.NamedEnum class FormatterSettings( c: Configurable, vararg baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : FormatterConfig, SettingGroup(c) { - val localeEnum by c.setting("Locale", FormatterConfig.Locales.US, "The regional formatting used for numbers").group(*baseGroup).index() + val localeEnum by c.setting("Locale", FormatterConfig.Locales.US, "The regional formatting used for numbers", visibility = visibility).group(*baseGroup).index() override val locale get() = localeEnum.locale - val sep by c.setting("Separator", FormatterConfig.TupleSeparator.Comma, "Separator for string serialization of tuple data structures").group(*baseGroup).index() - val customSep by c.setting("Custom Separator", "") { sep == FormatterConfig.TupleSeparator.Custom }.group(*baseGroup).index() + val sep by c.setting("Separator", FormatterConfig.TupleSeparator.Comma, "Separator for string serialization of tuple data structures", visibility = visibility).group(*baseGroup).index() + val customSep by c.setting("Custom Separator", "") { visibility() && sep == FormatterConfig.TupleSeparator.Custom }.group(*baseGroup).index() override val separator get() = if (sep == FormatterConfig.TupleSeparator.Custom) customSep else sep.separator - val group by c.setting("Tuple Prefix", FormatterConfig.TupleGrouping.Parentheses).group(*baseGroup).index() + val group by c.setting("Tuple Prefix", FormatterConfig.TupleGrouping.Parentheses, visibility = visibility).group(*baseGroup).index() override val prefix get() = group.prefix override val postfix get() = group.postfix - val floatingPrecision by c.setting("Floating Precision", 3, 0..6, 1, "Precision for floating point numbers").group(*baseGroup).index() + val floatingPrecision by c.setting("Floating Precision", 3, 0..6, 1, "Precision for floating point numbers", visibility = visibility).group(*baseGroup).index() override val precision get() = floatingPrecision - val timeFormat by c.setting("Time Format", FormatterConfig.Time.IsoDateTime).group(*baseGroup).index() + val timeFormat by c.setting("Time Format", FormatterConfig.Time.IsoDateTime, visibility = visibility).group(*baseGroup).index() override val format get() = timeFormat.format } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt b/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt index 34baab4e8..3601e5dad 100644 --- a/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt @@ -26,12 +26,13 @@ import com.lambda.util.NamedEnum class HotbarSettings( c: Configurable, - baseGroup: NamedEnum + baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), HotbarConfig { - override val swapMode by c.setting("Swap Mode", HotbarConfig.SwapMode.Temporary).group(baseGroup).index() - override val keepTicks by c.setting("Keep Ticks", 1, 0..20, 1, "The number of ticks to keep the current hotbar selection active", " ticks") { swapMode == HotbarConfig.SwapMode.Temporary }.group(baseGroup).index() - override val swapDelay by c.setting("Swap Delay", 0, 0..3, 1, "The number of ticks delay before allowing another hotbar selection swap", " ticks").group(baseGroup).index() - override val swapsPerTick by c.setting("Swaps Per Tick", 3, 1..10, 1, "The number of hotbar selection swaps that can take place each tick") { swapDelay <= 0 }.group(baseGroup).index() - override val swapPause by c.setting("Swap Pause", 0, 0..20, 1, "The delay in ticks to pause actions after switching to the slot", " ticks").group(baseGroup).index() - override val tickStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which hotbar actions are performed", displayClassName = true).group(baseGroup).index() + override val swapMode by c.setting("Swap Mode", HotbarConfig.SwapMode.Temporary, visibility = visibility).group(baseGroup).index() + override val keepTicks by c.setting("Keep Ticks", 1, 0..20, 1, "The number of ticks to keep the current hotbar selection active", " ticks") { visibility() && swapMode == HotbarConfig.SwapMode.Temporary }.group(baseGroup).index() + override val swapDelay by c.setting("Swap Delay", 0, 0..3, 1, "The number of ticks delay before allowing another hotbar selection swap", " ticks", visibility = visibility).group(baseGroup).index() + override val swapsPerTick by c.setting("Swaps Per Tick", 3, 1..10, 1, "The number of hotbar selection swaps that can take place each tick") { visibility() && swapDelay <= 0 }.group(baseGroup).index() + override val swapPause by c.setting("Swap Pause", 0, 0..20, 1, "The delay in ticks to pause actions after switching to the slot", " ticks", visibility = visibility).group(baseGroup).index() + override val tickStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which hotbar actions are performed", displayClassName = true, visibility = visibility).group(baseGroup).index() } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt b/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt index 90fc0b8be..36d27a17f 100644 --- a/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt @@ -28,17 +28,18 @@ import com.lambda.util.NamedEnum class InteractSettings( c: Configurable, - baseGroup: NamedEnum + baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), InteractConfig { - override val rotate by c.setting("Rotate For Interact", true, "Rotate towards block while placing").group(baseGroup).index() - override val airPlace by c.setting("Air Place", AirPlaceMode.None, "Allows for placing blocks without adjacent faces").group(baseGroup).index() - override val axisRotateSetting by c.setting("Axis Rotate", true, "Overrides the Rotate For Place setting and rotates the player on each axis to air place rotational blocks") { airPlace.isEnabled }.group(baseGroup).index() - override val sorter by c.setting("Interaction Sorter", ActionConfig.SortMode.Tool, "The order in which placements are performed").group(baseGroup).index() - override val tickStageMask by c.setting("Interaction Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which place actions are performed", displayClassName = true).group(baseGroup).index() - override val interactConfirmationMode by c.setting("Interact Confirmation", InteractConfirmationMode.PlaceThenAwait, "Wait for block placement confirmation").group(baseGroup).index() - override val interactDelay by c.setting("Interact Delay", 0, 0..3, 1, "Tick delay between interacting with another block").group(baseGroup).index() - override val interactionsPerTick by c.setting("Interactions Per Tick", 1, 1..30, 1, "Maximum instant block places per tick").group(baseGroup).index() - override val swing by c.setting("Swing On Interact", true, "Swings the players hand when placing").group(baseGroup).index() - override val swingType by c.setting("Interact Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { swing }.group(baseGroup).index() - override val sounds by c.setting("Place Sounds", true, "Plays the placing sounds").group(baseGroup).index() + override val rotate by c.setting("Rotate For Interact", true, "Rotate towards block while placing", visibility = visibility).group(baseGroup).index() + override val airPlace by c.setting("Air Place", AirPlaceMode.None, "Allows for placing blocks without adjacent faces", visibility = visibility).group(baseGroup).index() + override val axisRotateSetting by c.setting("Axis Rotate", true, "Overrides the Rotate For Place setting and rotates the player on each axis to air place rotational blocks") { visibility() && airPlace.isEnabled }.group(baseGroup).index() + override val sorter by c.setting("Interaction Sorter", ActionConfig.SortMode.Tool, "The order in which placements are performed", visibility = visibility).group(baseGroup).index() + override val tickStageMask by c.setting("Interaction Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which place actions are performed", displayClassName = true, visibility = visibility).group(baseGroup).index() + override val interactConfirmationMode by c.setting("Interact Confirmation", InteractConfirmationMode.PlaceThenAwait, "Wait for block placement confirmation", visibility = visibility).group(baseGroup).index() + override val interactDelay by c.setting("Interact Delay", 0, 0..3, 1, "Tick delay between interacting with another block", visibility = visibility).group(baseGroup).index() + override val interactionsPerTick by c.setting("Interactions Per Tick", 1, 1..30, 1, "Maximum instant block places per tick", visibility = visibility).group(baseGroup).index() + override val swing by c.setting("Swing On Interact", true, "Swings the players hand when placing", visibility = visibility).group(baseGroup).index() + override val swingType by c.setting("Interact Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { visibility() && swing }.group(baseGroup).index() + override val sounds by c.setting("Place Sounds", true, "Plays the placing sounds", visibility = visibility).group(baseGroup).index() } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt index 2dc25e38a..9fd601406 100644 --- a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt @@ -26,7 +26,8 @@ import com.lambda.util.item.ItemUtils class InventorySettings( c: Configurable, - baseGroup: NamedEnum + baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), InventoryConfig { enum class Group(override val displayName: String) : NamedEnum { General("General"), @@ -34,15 +35,15 @@ class InventorySettings( Access("Access") } - override val actionsPerSecond by c.setting("Actions Per Second", 100, 0..100, 1, "How many inventory actions can be performed per tick").group(baseGroup, Group.General).index() - override val tickStageMask by c.setting("Inventory Stage Mask", ALL_STAGES.toSet(), description = "The sub-tick timing at which inventory actions are performed", displayClassName = true).group(baseGroup, Group.General).index() - override val disposables by c.setting("Disposables", ItemUtils.defaultDisposables, description = "Items that will be ignored when checking for a free slot").group(baseGroup, Group.Container).index() - override val swapWithDisposables by c.setting("Swap With Disposables", true, "Swap items with disposable ones").group(baseGroup, Group.Container).index() - override val providerPriority by c.setting("Provider Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when retrieving the item from").group(baseGroup, Group.Container).index() - override val storePriority by c.setting("Store Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when storing the item to").group(baseGroup, Group.Container).index() + override val actionsPerSecond by c.setting("Actions Per Second", 100, 0..100, 1, "How many inventory actions can be performed per tick", visibility = visibility).group(baseGroup, Group.General).index() + override val tickStageMask by c.setting("Inventory Stage Mask", ALL_STAGES.toSet(), description = "The sub-tick timing at which inventory actions are performed", displayClassName = true, visibility = visibility).group(baseGroup, Group.General).index() + override val disposables by c.setting("Disposables", ItemUtils.defaultDisposables, description = "Items that will be ignored when checking for a free slot", visibility = visibility).group(baseGroup, Group.Container).index() + override val swapWithDisposables by c.setting("Swap With Disposables", true, "Swap items with disposable ones", visibility = visibility).group(baseGroup, Group.Container).index() + override val providerPriority by c.setting("Provider Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when retrieving the item from", visibility = visibility).group(baseGroup, Group.Container).index() + override val storePriority by c.setting("Store Priority", InventoryConfig.Priority.WithMinItems, "What container to prefer when storing the item to", visibility = visibility).group(baseGroup, Group.Container).index() - override val accessShulkerBoxes by c.setting("Access Shulker Boxes", false, "Allow access to the player's shulker boxes").group(baseGroup, Group.Access).index() - override val accessChests by c.setting("Access Chests", false, "Allow access to the player's normal chests").group(baseGroup, Group.Access).index() - override val accessEnderChest by c.setting("Access Ender Chest", false, "Allow access to the player's ender chest").group(baseGroup, Group.Access).index() - override val accessStashes by c.setting("Access Stashes", false, "Allow access to the player's stashes").group(baseGroup, Group.Access).index() + override val accessShulkerBoxes by c.setting("Access Shulker Boxes", false, "Allow access to the player's shulker boxes", visibility = visibility).group(baseGroup, Group.Access).index() + override val accessChests by c.setting("Access Chests", false, "Allow access to the player's normal chests", visibility = visibility).group(baseGroup, Group.Access).index() + override val accessEnderChest by c.setting("Access Ender Chest", false, "Allow access to the player's ender chest", visibility = visibility).group(baseGroup, Group.Access).index() + override val accessStashes by c.setting("Access Stashes", false, "Allow access to the player's stashes", visibility = visibility).group(baseGroup, Group.Access).index() } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/LineConfig.kt b/src/main/kotlin/com/lambda/config/groups/LineConfig.kt new file mode 100644 index 000000000..edd9d010a --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/LineConfig.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.graphics.mc.LineDashStyle +import java.awt.Color + +interface LineConfig { + val startColor: Color + val endColor: Color + val width: Float + val dashEnabled: Boolean + val dashLength: Float + val gapLength: Float + val dashOffset: Float + val animated: Boolean + val animationSpeed: Float + + /** + * Get the dash style for rendering, or null if dashing is disabled. + */ + fun getDashStyle(): LineDashStyle? = + if (dashEnabled) LineDashStyle(dashLength, gapLength, dashOffset, animated, animationSpeed) else null +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt b/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt index 8c6fb1746..37b6a8783 100644 --- a/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt @@ -34,31 +34,32 @@ import kotlin.random.Random class RotationSettings( c: Configurable, baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), RotationConfig { - override var rotationMode by c.setting("Mode", RotationMode.Sync, "How the player is being rotated on interaction").group(baseGroup).index() + override var rotationMode by c.setting("Mode", RotationMode.Sync, "How the player is being rotated on interaction", visibility = visibility).group(baseGroup).index() /** How many ticks to keep the rotation before resetting */ - override val keepTicks by c.setting("Keep Rotation", 1, 1..10, 1, "Ticks to keep rotation", " ticks").group(baseGroup).index() + override val keepTicks by c.setting("Keep Rotation", 1, 1..10, 1, "Ticks to keep rotation", " ticks", visibility = visibility).group(baseGroup).index() /** How many ticks to wait before resetting the rotation */ - override val decayTicks by c.setting("Reset Rotation", 1, 1..10, 1, "Ticks before rotation is reset", " ticks").group(baseGroup).index() + override val decayTicks by c.setting("Reset Rotation", 1, 1..10, 1, "Ticks before rotation is reset", " ticks", visibility = visibility).group(baseGroup).index() override val tickStageMask = ALL_STAGES.subList(0, ALL_STAGES.indexOf(TickEvent.Player.Post)).toSet() /** Whether the rotation is instant */ - var instant by c.setting("Instant Rotation", true, "Instantly rotate").group(baseGroup).index() + var instant by c.setting("Instant Rotation", true, "Instantly rotate", visibility = visibility).group(baseGroup).index() /** * The mean (average/base) value used to calculate rotation speed. * This value represents the center of the distribution. */ - var mean by c.setting("Mean", 40.0, 1.0..120.0, 0.1, "Average rotation speed", unit = "°") { !instant }.group(baseGroup).index() + var mean by c.setting("Mean", 40.0, 1.0..120.0, 0.1, "Average rotation speed", unit = "°") { visibility() && !instant }.group(baseGroup).index() /** * The standard deviation for the Gaussian distribution used to calculate rotation speed. * This value represents the spread of rotation speed. */ - var spread by c.setting("Spread", 10.0, 0.0..60.0, 0.1, "Spread of rotation speeds", unit = "°") { !instant }.group(baseGroup).index() + var spread by c.setting("Spread", 10.0, 0.0..60.0, 0.1, "Spread of rotation speeds", unit = "°") { visibility() && !instant }.group(baseGroup).index() /** * We must always provide turn speed to the interpolator because the player's yaw might exceed the -180 to 180 range. diff --git a/src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt b/src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt new file mode 100644 index 000000000..a5b60e732 --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/ScreenLineSettings.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.config.SettingGroup +import com.lambda.util.NamedEnum +import java.awt.Color + +class ScreenLineSettings( + prefix: String, + c: Configurable, + vararg baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, +) : SettingGroup(c), LineConfig { + private enum class Group(override val displayName: String) : NamedEnum { + Color("Color"), + Dash("Dash") + } + + val widthSetting by c.setting("${prefix}Line Width", 10, 1..100, 1, "The width of the line", visibility = visibility).group(*baseGroup).index() + override val width get() = widthSetting * 0.00005f + + override val startColor by c.setting("${prefix}Start Color", Color.WHITE, "The color at the start of the line", visibility = visibility).group(*baseGroup, Group.Color).index() + override val endColor by c.setting("${prefix}End Color", Color.WHITE, "The color at the end of the line", visibility = visibility).group(*baseGroup, Group.Color).index() + + override val dashEnabled by c.setting("${prefix}Dashed", false, "Enable dashed line pattern", visibility = visibility).group(*baseGroup, Group.Dash).index() + val dashLengthSetting by c.setting("${prefix}Dash Length", 30, 1..50, 1, "Length of each dash") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index() + override val dashLength get() = dashLengthSetting * 0.001f + val gapLengthSetting by c.setting("${prefix}Gap Length", 15, 1..50, 1, "Length of gaps between dashes") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index() + override val gapLength get() = gapLengthSetting * 0.001f + override val animated by c.setting("${prefix}Animated", true, "Animate the dash pattern") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index() + val dashOffsetSetting by c.setting("${prefix}Dash Offset", 0, 0..100, 1, "Offset of the dash pattern") { visibility() && dashEnabled && !animated }.group(*baseGroup, Group.Dash).index() + override val dashOffset get() = dashOffsetSetting * 0.01f + val animationSpeedSetting by c.setting("${prefix}Animation Speed", 30, -100..100, 1, "Speed of dash animation (negative = reverse)") { visibility() && dashEnabled && animated }.group(*baseGroup, Group.Dash).index() + override val animationSpeed get() = animationSpeedSetting * 0.1f +} diff --git a/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt b/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt new file mode 100644 index 000000000..01b3a8f1c --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.config.SettingGroup +import com.lambda.util.NamedEnum +import java.awt.Color + +class ScreenTextSettings( + prefix: String, + c: Configurable, + vararg baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, +) : SettingGroup(c), TextConfig { + private enum class Group(override val displayName: String) : NamedEnum { + Outline("Outline"), + Glow("Glow"), + Shadow("Shadow") + } + + override val textColor by c.setting("${prefix}Text Color", Color.WHITE, "The main text color", visibility = visibility).group(*baseGroup).index() + val sizeSetting by c.setting("${prefix}Text Size", 18, 1..50, 1, visibility = visibility).group(*baseGroup).index() + override val size get() = sizeSetting * 0.001f + + override val outlineEnabled by c.setting("${prefix}Outline", false, "Enable text outline", visibility = visibility).group(*baseGroup, Group.Outline).index() + override val outlineColor by c.setting("${prefix}Outline Color", Color.BLACK, "Color of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index() + override val outlineWidth by c.setting("${prefix}Outline Width", 0.1f, 0f..0.4f, 0.005f, "Width of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index() + + override val glowEnabled by c.setting("${prefix}Glow", false, "Enable text glow effect", visibility = visibility).group(*baseGroup, Group.Glow).index() + override val glowColor by c.setting("${prefix}Glow Color", Color.WHITE, "Color of the glow") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index() + override val glowRadius by c.setting("${prefix}Glow Radius", 0.2f, 0f..0.5f, 0.01f, "Radius of the glow effect") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index() + + override val shadowEnabled by c.setting("${prefix}Shadow", true, "Enable text shadow", visibility = visibility).group(*baseGroup, Group.Shadow).index() + override val shadowColor by c.setting("${prefix}Shadow Color", Color(0, 0, 0, 180), "Color of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() + override val shadowOffset by c.setting("${prefix}Shadow Offset", 0.05f, 0f..0.5f, 0.005f, "Distance of shadow from text") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() + override val shadowAngle by c.setting("${prefix}Shadow Angle", 135f, 0f..360f, 1f, "Angle of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() + override val shadowSoftness by c.setting("${prefix}Shadow Softness", 0f, 0f..0.5f, 0.01f, "Softness of shadow edges") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() +} diff --git a/src/main/kotlin/com/lambda/config/groups/Targeting.kt b/src/main/kotlin/com/lambda/config/groups/Targeting.kt index a82766c1c..5c3e94253 100644 --- a/src/main/kotlin/com/lambda/config/groups/Targeting.kt +++ b/src/main/kotlin/com/lambda/config/groups/Targeting.kt @@ -25,6 +25,8 @@ import com.lambda.interaction.managers.rotating.Rotation.Companion.dist import com.lambda.interaction.managers.rotating.Rotation.Companion.rotation import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo import com.lambda.threading.runSafe +import com.lambda.util.EntityUtils.EntityGroup +import com.lambda.util.EntityUtils.entityGroup import com.lambda.util.NamedEnum import com.lambda.util.extension.fullHealth import com.lambda.util.math.distSq @@ -32,9 +34,6 @@ import com.lambda.util.world.fastEntitySearch import net.minecraft.client.network.ClientPlayerEntity import net.minecraft.client.network.OtherClientPlayerEntity import net.minecraft.entity.LivingEntity -import net.minecraft.entity.decoration.ArmorStandEntity -import net.minecraft.entity.mob.HostileEntity -import net.minecraft.entity.passive.PassiveEntity import java.util.* /** @@ -54,78 +53,24 @@ abstract class Targeting( baseGroup: NamedEnum, private val defaultRange: Double, private val maxRange: Double, + override val visibility: () -> Boolean = { true }, ) : SettingGroup(c), TargetingConfig { - /** - * The range within which entities can be targeted. This value is configurable and constrained - * between 1.0 and [maxRange]. - */ - override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05).group(baseGroup) - - /** - * Whether players are included in the targeting scope. - */ - override val players by c.setting("Players", true).group(baseGroup) - - /** - * Whether friends are included in the targeting scope. - * Requires [players] to be true. - */ - override val friends by c.setting("Friends", false) { players }.group(baseGroup) - - /** - * Whether mobs are included in the targeting scope. - */ - private val mobs by c.setting("Mobs", true).group(baseGroup) - - /** - * Whether hostile mobs are included in the targeting scope - */ - private val hostilesSetting by c.setting("Hostiles", true) { mobs }.group(baseGroup) - - /** - * Whether passive animals are included in the targeting scope - */ - private val animalsSetting by c.setting("Animals", true) { mobs }.group(baseGroup) - - /** - * Indicates whether hostile entities are included in the targeting scope. - */ - override val hostiles get() = mobs && hostilesSetting - - /** - * Indicates whether passive animals are included in the targeting scope. - */ - override val animals get() = mobs && animalsSetting - - /** - * Whether invisible entities are included in the targeting scope. - */ - override val invisible by c.setting("Invisible", true).group(baseGroup) - - /** - * Whether dead entities are included in the targeting scope. - */ - override val dead by c.setting("Dead", false).group(baseGroup) - - /** - * Validates whether a given entity is targetable by the player based on current settings. - * - * @param player The [ClientPlayerEntity] performing the targeting. - * @param entity The [LivingEntity] being evaluated. - * @return `true` if the entity is valid for targeting, `false` otherwise. - */ - open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = when { - !friends && entity is OtherClientPlayerEntity && entity.isFriend -> false - !players && entity is OtherClientPlayerEntity -> false - !animals && entity is PassiveEntity -> false - !hostiles && entity is HostileEntity -> false - entity is ArmorStandEntity -> false - - !invisible && entity.isInvisibleTo(player) -> false - !dead && entity.isDead -> false - - else -> true - } + /** + * The range within which entities can be targeted. This value is configurable and constrained + * between 1.0 and [maxRange]. + */ + override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05, visibility = visibility).group(baseGroup) + override val targets by c.setting("Targets", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries, visibility = visibility) + + /** + * Validates whether a given entity is targetable by the player based on current settings. + * + * @param player The [ClientPlayerEntity] performing the targeting. + * @param entity The [LivingEntity] being evaluated. + * @return `true` if the entity is valid for targeting, `false` otherwise. + */ + open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = + entity.entityGroup in targets && (entity !is OtherClientPlayerEntity || !entity.isFriend) /** * Subclass for targeting entities specifically for combat purposes. @@ -138,17 +83,17 @@ abstract class Targeting( baseGroup: NamedEnum, defaultRange: Double = 5.0, maxRange: Double = 16.0, - ) : Targeting(c, baseGroup, defaultRange, maxRange) { - + visibility: () -> Boolean = { true }, + ) : Targeting(c, baseGroup, defaultRange, maxRange, visibility) { /** * The field of view limit for targeting entities. Configurable between 5 and 180 degrees. */ - val fov by c.setting("FOV Limit", 180, 5..180, 1) { priority == Priority.Fov }.group(baseGroup) + val fov by c.setting("FOV Limit", 180, 5..180, 1) { visibility() && priority == Priority.Fov }.group(baseGroup) /** * The priority used to determine which entity is targeted. Configurable with default set to [Priority.Distance]. */ - val priority by c.setting("Priority", Priority.Distance).group(baseGroup) + val priority by c.setting("Priority", Priority.Distance, visibility = visibility).group(baseGroup) /** * Validates whether a given entity is targetable for combat based on the field of view limit and other settings. @@ -160,6 +105,7 @@ abstract class Targeting( override fun validate(player: ClientPlayerEntity, entity: LivingEntity): Boolean { if (fov < 180 && player.rotation dist player.eyePos.rotationTo(entity.pos) > fov) return false if (entity.uuid in illegalTargets) return false + if (entity.isDead) return false return super.validate(player, entity) } @@ -178,18 +124,11 @@ abstract class Targeting( private val illegalTargets = setOf( UUID(5706954458220675710, -6736729783554821869), - UUID(-2945922493004570036, -7599209072395336449) + UUID(-6076316721184881576, -7147993044363569449), + UUID(-2932596226593701300, -7553629058088633089) ) } - /** - * Subclass for targeting entities for ESP (Extrasensory Perception) purposes. - */ - class ESP( - c: Configurable, - baseGroup: NamedEnum, - ) : Targeting(c, baseGroup, 128.0, 1024.0) - /** * Enum representing the different priority factors used for determining the best target. * diff --git a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt index a54df2231..117d18269 100644 --- a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt +++ b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt @@ -17,14 +17,9 @@ package com.lambda.config.groups +import com.lambda.util.EntityUtils + interface TargetingConfig { val targetingRange: Double - - val players: Boolean - val friends: Boolean - val hostiles: Boolean - val animals: Boolean - - val invisible: Boolean - val dead: Boolean + val targets: Collection } diff --git a/src/main/kotlin/com/lambda/config/groups/TextConfig.kt b/src/main/kotlin/com/lambda/config/groups/TextConfig.kt new file mode 100644 index 000000000..20282275d --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/TextConfig.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.graphics.mc.RenderBuilder +import java.awt.Color + +interface TextConfig { + val size: Float + val textColor: Color + + val outlineEnabled: Boolean + val outlineColor: Color + val outlineWidth: Float + + val glowEnabled: Boolean + val glowColor: Color + val glowRadius: Float + + val shadowEnabled: Boolean + val shadowColor: Color + val shadowOffset: Float + val shadowAngle: Float + val shadowSoftness: Float + + /** + * Get the SDF style for text rendering. + */ + fun getSDFStyle(): RenderBuilder.SDFStyle { + val outline = if (outlineEnabled) RenderBuilder.SDFOutline(outlineColor, outlineWidth) else null + val glow = if (glowEnabled) RenderBuilder.SDFGlow(glowColor, glowRadius) else null + val shadow = if (shadowEnabled) RenderBuilder.SDFShadow(shadowColor, shadowOffset, shadowAngle, shadowSoftness) else null + + return RenderBuilder.SDFStyle(textColor, outline, glow, shadow) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt b/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt new file mode 100644 index 000000000..8ac4962db --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.config.SettingGroup +import com.lambda.util.NamedEnum +import java.awt.Color + +/** + * SettingGroup for world-space line configuration. + * Provides settings for line colors, width, dash patterns, and distance scaling. + */ +class WorldLineSettings( + prefix: String, + c: Configurable, + vararg baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, +) : SettingGroup(c), LineConfig { + private enum class Group(override val displayName: String) : NamedEnum { + Color("Color"), + Dash("Dash") + } + + val distanceScaling by c.setting("${prefix}Distance Scaling", true, "Line width stays constant on screen regardless of distance", visibility = visibility).group(*baseGroup).index() + val worldWidthSetting by c.setting("${prefix}Line Width", 5, 1..50, 1, "Line width in world units (blocks)") { visibility() && !distanceScaling }.group(*baseGroup).index() + val screenWidthSetting by c.setting("${prefix}Screen Width", 10, 1..100, 1, "Line width in screen-space (stays constant size)") { visibility() && distanceScaling }.group(*baseGroup).index() + + override val width: Float get() = if (distanceScaling) -screenWidthSetting * 0.00005f // Negative = screen-space mode + else worldWidthSetting * 0.001f // Positive = world units + + override val startColor by c.setting("${prefix}Start Color", Color.WHITE, "The color at the start of the line", visibility = visibility).group(*baseGroup, Group.Color).index() + override val endColor by c.setting("${prefix}End Color", Color.WHITE, "The color at the end of the line", visibility = visibility).group(*baseGroup, Group.Color).index() + + override val dashEnabled by c.setting("${prefix}Dashed", false, "Enable dashed line pattern", visibility = visibility).group(*baseGroup, Group.Dash).index() + val dashLengthSetting by c.setting("${prefix}Dash Length", 50, 1..200, 1, "Length of each dash") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index() + override val dashLength get() = dashLengthSetting * 0.01f + val gapLengthSetting by c.setting("${prefix}Gap Length", 25, 1..200, 1, "Length of gaps between dashes") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index() + override val gapLength get() = gapLengthSetting * 0.01f + override val animated by c.setting("${prefix}Animated", true, "Animate the dash pattern") { visibility() && dashEnabled }.group(*baseGroup, Group.Dash).index() + val dashOffsetSetting by c.setting("${prefix}Dash Offset", 0, 0..100, 1, "Offset of the dash pattern") { visibility() && dashEnabled && !animated }.group(*baseGroup, Group.Dash).index() + override val dashOffset get() = dashOffsetSetting * 0.01f + val animationSpeedSetting by c.setting("${prefix}Animation Speed", 30, -100..100, 1, "Speed of dash animation (negative = reverse)") { visibility() && dashEnabled && animated }.group(*baseGroup, Group.Dash).index() + override val animationSpeed get() = animationSpeedSetting * 0.1f +} diff --git a/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt b/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt new file mode 100644 index 000000000..7b7993205 --- /dev/null +++ b/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.config.SettingGroup +import com.lambda.util.NamedEnum +import java.awt.Color + +class WorldTextSettings( + prefix: String, + c: Configurable, + vararg baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, +) : SettingGroup(c), TextConfig { + private enum class Group(override val displayName: String) : NamedEnum { + Outline("Outline"), + Glow("Glow"), + Shadow("Shadow") + } + + override val textColor by c.setting("${prefix}Text Color", Color.WHITE, "The main text color", visibility = visibility).group(*baseGroup).index() + val sizeSetting by c.setting("${prefix}Text Size", 5, 1..50, 1, visibility = visibility).group(*baseGroup).index() + override val size get() = sizeSetting * 0.1f + + override val outlineEnabled by c.setting("${prefix}Outline", false, "Enable text outline", visibility = visibility).group(*baseGroup, Group.Outline).index() + override val outlineColor by c.setting("${prefix}Outline Color", Color.BLACK, "Color of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index() + override val outlineWidth by c.setting("${prefix}Outline Width", 0.1f, 0f..0.4f, 0.005f, "Width of the outline") { visibility() && outlineEnabled }.group(*baseGroup, Group.Outline).index() + + override val glowEnabled by c.setting("${prefix}Glow", false, "Enable text glow effect", visibility = visibility).group(*baseGroup, Group.Glow).index() + override val glowColor by c.setting("${prefix}Glow Color", Color.WHITE, "Color of the glow") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index() + override val glowRadius by c.setting("${prefix}Glow Radius", 0.2f, 0f..0.5f, 0.01f, "Radius of the glow effect") { visibility() && glowEnabled }.group(*baseGroup, Group.Glow).index() + + override val shadowEnabled by c.setting("${prefix}Shadow", true, "Enable text shadow", visibility = visibility).group(*baseGroup, Group.Shadow).index() + override val shadowColor by c.setting("${prefix}Shadow Color", Color(0, 0, 0, 180), "Color of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() + override val shadowOffset by c.setting("${prefix}Shadow Offset", 0.05f, 0f..0.5f, 0.005f, "Distance of shadow from text") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() + override val shadowAngle by c.setting("${prefix}Shadow Angle", 135f, 0f..360f, 1f, "Angle of the shadow") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() + override val shadowSoftness by c.setting("${prefix}Shadow Softness", 0f, 0f..0.5f, 0.01f, "Softness of shadow edges") { visibility() && shadowEnabled }.group(*baseGroup, Group.Shadow).index() +} diff --git a/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt b/src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt similarity index 61% rename from src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt rename to src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt index 0c0a19a3f..bede02152 100644 --- a/src/main/kotlin/com/lambda/graphics/esp/EspDsl.kt +++ b/src/main/kotlin/com/lambda/event/events/HudRenderEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Lambda + * Copyright 2026 Lambda * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,18 +15,14 @@ * along with this program. If not, see . */ -package com.lambda.graphics.esp +package com.lambda.event.events -import com.lambda.graphics.mc.ChunkedRegionESP -import com.lambda.module.Module +import com.lambda.event.Event +import net.minecraft.client.gui.DrawContext -@DslMarker -annotation class EspDsl - -fun Module.chunkedEsp( - name: String, - depthTest: Boolean = false, - update: ShapeScope.(net.minecraft.world.World, com.lambda.util.world.FastVector) -> Unit -): ChunkedRegionESP { - return ChunkedRegionESP(this, name, depthTest, update) -} +/** + * Event fired during HUD rendering with access to Minecraft's DrawContext. + * Use this for rendering items, textures, and other GUI elements that need + * to integrate with Minecraft's deferred GUI rendering system. + */ +class HudRenderEvent(val context: DrawContext) : Event diff --git a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt index 36e2dbe37..572d434b1 100644 --- a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt +++ b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt @@ -17,23 +17,13 @@ package com.lambda.event.events -import com.lambda.context.SafeContext import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.RenderMain -import com.lambda.graphics.mc.TransientRegionESP - -fun Any.onStaticRender(block: SafeContext.(TransientRegionESP) -> Unit) = - listen { block(RenderMain.StaticESP) } - -fun Any.onDynamicRender(block: SafeContext.(TransientRegionESP) -> Unit) = - listen { block(RenderMain.DynamicESP) } sealed class RenderEvent { - object Upload : Event - object Render : Event + object RenderWorld : Event + object RenderScreen : Event class UpdateTarget : ICancellable by Cancellable() } diff --git a/src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt b/src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt new file mode 100644 index 000000000..58bcd984e --- /dev/null +++ b/src/main/kotlin/com/lambda/event/events/ScreenRenderEvent.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.event.events + +import com.lambda.event.Event + +/** + * Event fired after Minecraft's GUI has been fully rendered. + * + * This fires after guiRenderer.render() in GameRenderer, ensuring that + * any screen-space rendering done in response to this event will appear + * above all of Minecraft's native GUI elements (hotbar, held items, etc.). + * + * Use this event for screen-space rendering that needs to appear on top of + * Minecraft's HUD. For world-space (3D) rendering, use RenderEvent.Render. + */ +object ScreenRenderEvent : Event diff --git a/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/src/main/kotlin/com/lambda/graphics/RenderMain.kt index 1353fec6e..381149694 100644 --- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt +++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt @@ -20,23 +20,16 @@ package com.lambda.graphics import com.lambda.Lambda.mc import com.lambda.event.EventFlow.post import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.graphics.gl.Matrices import com.lambda.graphics.gl.Matrices.resetMatrices -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.renderer.RendererUtils import net.minecraft.util.math.Vec3d import org.joml.Matrix4f import org.joml.Vector2f import org.joml.Vector4f +import kotlin.math.abs object RenderMain { - @JvmStatic - val StaticESP = TransientRegionESP("Static") - - @JvmStatic - val DynamicESP = TransientRegionESP("Dynamic") - val projectionMatrix = Matrix4f() val modelViewMatrix get() = Matrices.peek() @@ -44,13 +37,16 @@ object RenderMain { get() = Matrix4f(projectionMatrix).mul(modelViewMatrix) /** - * Project a world position to screen coordinates. Returns null if the position is behind the - * camera or off-screen. + * Project a world position to normalized screen coordinates (0-1 range). + * This is the format used by RenderBuilder's screen methods (screenText, screenRect, etc). + * + * Always returns coordinates, even if off-screen or behind camera. + * For behind-camera positions, the direction is preserved (useful for tracers). * * @param worldPos The world position to project - * @return Screen coordinates (x, y) in pixels, or null if not visible + * @return Normalized screen coordinates (x, y) */ - fun worldToScreen(worldPos: Vec3d): Vector2f? { + fun worldToScreenNormalized(worldPos: Vec3d): Vector2f? { val camera = mc.gameRenderer?.camera ?: return null val cameraPos = camera.pos @@ -63,49 +59,64 @@ object RenderMain { val vec = Vector4f(relX, relY, relZ, 1f) projModel.transform(vec) - // Behind camera check - if (vec.w <= 0) return null - - // Perspective divide to get NDC - val ndcX = vec.x / vec.w - val ndcY = vec.y / vec.w - val ndcZ = vec.z / vec.w - - // Off-screen check (NDC is -1 to 1) - if (ndcZ < -1 || ndcZ > 1) return null + val isBehind = vec.w < 0 + val w = if (abs(vec.w) < 0.001f) 0.001f else abs(vec.w) + + // Perspective divide to get NDC (-1 to 1) + var ndcX = vec.x / w + var ndcY = vec.y / w + + // When behind camera, extend the direction past the screen edge + // so tracers go off-screen rather than landing on-screen + if (isBehind) { + // Normalize the direction and extend to a fixed off-screen distance + val len = kotlin.math.sqrt(ndcX * ndcX + ndcY * ndcY) + if (len > 0.0001f) { + // Extend to 3.0 in NDC space (well past the -1 to 1 range) + ndcX = (ndcX / len) * 3f + ndcY = (ndcY / len) * 3f + } else { + // If almost directly behind, push down (arbitrary direction) + // With Y-up, negative Y means down + ndcY = -3f + } + } - // NDC to screen coordinates (Y is flipped in screen space) - val window = mc.window - val screenX = (ndcX + 1f) * 0.5f * window.framebufferWidth - val screenY = (1f - ndcY) * 0.5f * window.framebufferHeight + // NDC to normalized 0-1 coordinates + // Y-up convention: 0 = bottom, 1 = top (matches screen rendering) + val normalizedX = (ndcX + 1f) * 0.5f + val normalizedY = (ndcY + 1f) * 0.5f // No flip for Y-up - return Vector2f(screenX, screenY) + return Vector2f(normalizedX, normalizedY) } - /** Check if a world position is visible on screen. */ - fun isOnScreen(worldPos: Vec3d): Boolean = worldToScreen(worldPos) != null + /** Check if a world position is visible on screen (within 0-1 bounds and in front of camera). */ + fun isOnScreen(worldPos: Vec3d): Boolean { + val camera = mc.gameRenderer?.camera ?: return false + val cameraPos = camera.pos + + // Check if in front of camera first + val relX = (worldPos.x - cameraPos.x).toFloat() + val relY = (worldPos.y - cameraPos.y).toFloat() + val relZ = (worldPos.z - cameraPos.z).toFloat() + val vec = Vector4f(relX, relY, relZ, 1f) + projModel.transform(vec) + if (vec.w <= 0) return false + + val pos = worldToScreenNormalized(worldPos) ?: return false + return pos.x in 0f..1f && pos.y in 0f..1f + } @JvmStatic - fun render3D(positionMatrix: Matrix4f, projMatrix: Matrix4f) { + fun render(positionMatrix: Matrix4f, projMatrix: Matrix4f) { resetMatrices(positionMatrix) projectionMatrix.set(projMatrix) - - // Render transient ESPs using the new pipeline - StaticESP.render() // Uses internal depthTest flag (true) - DynamicESP.render() // Uses internal depthTest flag (false) - - RenderEvent.Render.post() - } - - init { - listen { - StaticESP.clear() - DynamicESP.clear() - - RenderEvent.Upload.post() - - StaticESP.upload() - DynamicESP.upload() - } + + // Clear xray depth buffer once per frame before any renderer runs. + // All world-space renderers share this depth state for proper inter-renderer occlusion. + RendererUtils.clearXrayDepthBuffer() + + RenderEvent.RenderWorld.post() + RenderEvent.RenderScreen.post() } } diff --git a/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt b/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt deleted file mode 100644 index b32e03755..000000000 --- a/src/main/kotlin/com/lambda/graphics/esp/RegionESP.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.esp - -import com.lambda.Lambda.mc -import com.lambda.graphics.mc.LambdaRenderPipelines -import com.lambda.graphics.mc.RegionRenderer -import com.lambda.graphics.mc.RenderRegion -import com.lambda.util.extension.tickDelta -import com.mojang.blaze3d.systems.RenderSystem -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.floor -import org.joml.Matrix4f -import org.joml.Vector3f -import org.joml.Vector4f - -/** - * Base class for region-based ESP systems. Provides unified rendering logic and region management. - */ -abstract class RegionESP(val name: String, val depthTest: Boolean) { - protected val renderers = ConcurrentHashMap() - - /** Get or create a ShapeScope for a specific world position. */ - open fun shapes(x: Double, y: Double, z: Double, block: ShapeScope.() -> Unit) {} - - /** Upload collected geometry to GPU. Must be called on main thread. */ - open fun upload() {} - - /** Clear all geometry data. */ - abstract fun clear() - - /** Close and release all GPU resources. */ - open fun close() { - renderers.values.forEach { it.close() } - renderers.clear() - clear() - } - - /** - * Render all active regions. - * @param tickDelta Progress within current tick (used for interpolation) - */ - open fun render(tickDelta: Float = mc.tickDelta) { - val camera = mc.gameRenderer?.camera ?: return - val cameraPos = camera.pos - - val activeRenderers = renderers.values.filter { it.hasData() } - if (activeRenderers.isEmpty()) return - - val modelViewMatrix = com.lambda.graphics.RenderMain.modelViewMatrix - val transforms = activeRenderers.map { renderer -> - val offset = renderer.region.computeCameraRelativeOffset(cameraPos) - val modelView = Matrix4f(modelViewMatrix).translate(offset) - - val dynamicTransform = RenderSystem.getDynamicUniforms() - .write( - modelView, - Vector4f(1f, 1f, 1f, 1f), - Vector3f(0f, 0f, 0f), - Matrix4f() - ) - renderer to dynamicTransform - } - - // Render Faces - RegionRenderer.createRenderPass("$name Faces")?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_QUADS - else LambdaRenderPipelines.ESP_QUADS_THROUGH - pass.setPipeline(pipeline) - RenderSystem.bindDefaultUniforms(pass) - transforms.forEach { (renderer, transform) -> - pass.setUniform("DynamicTransforms", transform) - renderer.renderFaces(pass) - } - } - - // Render Edges - RegionRenderer.createRenderPass("$name Edges")?.use { pass -> - val pipeline = - if (depthTest) LambdaRenderPipelines.ESP_LINES - else LambdaRenderPipelines.ESP_LINES_THROUGH - pass.setPipeline(pipeline) - RenderSystem.bindDefaultUniforms(pass) - transforms.forEach { (renderer, transform) -> - pass.setUniform("DynamicTransforms", transform) - renderer.renderEdges(pass) - } - } - } - - /** - * Compute a unique key for a region based on its coordinates. Prevents collisions between - * regions at different Y levels. - */ - protected fun getRegionKey(x: Double, y: Double, z: Double): Long { - val rx = (RenderRegion.REGION_SIZE * floor(x / RenderRegion.REGION_SIZE)).toInt() - val ry = (RenderRegion.REGION_SIZE * floor(y / RenderRegion.REGION_SIZE)).toInt() - val rz = (RenderRegion.REGION_SIZE * floor(z / RenderRegion.REGION_SIZE)).toInt() - - return getRegionKey(rx, ry, rz) - } - - protected fun getRegionKey(rx: Int, ry: Int, rz: Int): Long { - // 20 bits for X, 20 bits for Z, 24 bits for Y (total 64) - // This supports +- 500k blocks in X/Z and full Y range - return (rx.toLong() and 0xFFFFF) or - ((rz.toLong() and 0xFFFFF) shl 20) or - ((ry.toLong() and 0xFFFFFF) shl 40) - } -} diff --git a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt b/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt deleted file mode 100644 index 14ab277f5..000000000 --- a/src/main/kotlin/com/lambda/graphics/esp/ShapeScope.kt +++ /dev/null @@ -1,416 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.esp - -import com.lambda.graphics.mc.RegionShapeBuilder -import com.lambda.graphics.mc.RegionVertexCollector -import com.lambda.graphics.mc.RenderRegion -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DynamicAABB -import net.minecraft.block.BlockState -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Box -import net.minecraft.util.math.MathHelper -import net.minecraft.util.math.Vec3d -import net.minecraft.util.shape.VoxelShape -import java.awt.Color - -@EspDsl -class ShapeScope(val region: RenderRegion, val collectShapes: Boolean = false) { - internal val builder = RegionShapeBuilder(region) - internal val shapes = if (collectShapes) mutableListOf() else null - - /** Start building a box. */ - fun box(box: Box, id: Any? = null, block: BoxScope.() -> Unit) { - val scope = BoxScope(box, this) - scope.apply(block) - if (collectShapes) { - shapes?.add( - EspShape.BoxShape( - id?.hashCode() ?: box.hashCode(), - box, - scope.filledColor, - scope.outlineColor, - scope.sides, - scope.outlineMode, - scope.thickness - ) - ) - } - } - - /** Draw a line between two points. */ - fun line(start: Vec3d, end: Vec3d, color: Color, width: Float = 1.0f, id: Any? = null) { - builder.line(start, end, color, width) - if (collectShapes) { - shapes?.add( - EspShape.LineShape( - id?.hashCode() ?: (start.hashCode() xor end.hashCode()), - start, - end, - color, - width - ) - ) - } - } - - /** Draw a tracer. */ - fun tracer(from: Vec3d, to: Vec3d, id: Any? = null, block: LineScope.() -> Unit = {}) { - val scope = LineScope(from, to, this) - scope.apply(block) - scope.draw() - if (collectShapes) { - shapes?.add( - EspShape.LineShape( - id?.hashCode() ?: (from.hashCode() xor to.hashCode()), - from, - to, - scope.lineColor, - scope.lineWidth, - scope.lineDashLength, - scope.lineGapLength - ) - ) - } - } - - /** Draw a simple filled box. */ - fun filled(box: Box, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(box, color, sides) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(box.hashCode(), box, color, null, sides)) - } - } - - /** Draw a simple outlined box. */ - fun outline(box: Box, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(box, color, sides, thickness = thickness) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(box.hashCode(), box, null, color, sides, thickness = thickness)) - } - } - - fun filled(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(box, color, sides) - if (collectShapes) { - box.pair?.second?.let { - shapes?.add(EspShape.BoxShape(it.hashCode(), it, color, null, sides)) - } - } - } - - fun outline(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(box, color, sides, thickness = thickness) - if (collectShapes) { - box.pair?.second?.let { - shapes?.add(EspShape.BoxShape(it.hashCode(), it, null, color, sides, thickness = thickness)) - } - } - } - - fun filled(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(pos, color, sides) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), color, null, sides)) - } - } - - fun outline(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(pos, color, sides, thickness = thickness) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), null, color, sides, thickness = thickness)) - } - } - - fun filled(pos: BlockPos, state: BlockState, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(pos, state, color, sides) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), color, null, sides)) - } - } - - fun outline(pos: BlockPos, state: BlockState, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(pos, state, color, sides, thickness = thickness) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), null, color, sides, thickness = thickness)) - } - } - - fun filled(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL) { - builder.filled(shape, color, sides) - if (collectShapes) { - shape.boundingBoxes.forEach { - shapes?.add(EspShape.BoxShape(it.hashCode(), it, color, null, sides)) - } - } - } - - fun outline(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL, thickness: Float = builder.lineWidth) { - builder.outline(shape, color, sides, thickness = thickness) - if (collectShapes) { - shape.boundingBoxes.forEach { - shapes?.add(EspShape.BoxShape(it.hashCode(), it, null, color, sides, thickness = thickness)) - } - } - } - - fun box( - pos: BlockPos, - state: BlockState, - filled: Color, - outline: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = builder.lineWidth - ) { - builder.box(pos, state, filled, outline, sides, mode, thickness = thickness) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), filled, outline, sides, mode, thickness = thickness)) - } - } - - fun box( - pos: BlockPos, - filled: Color, - outline: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = builder.lineWidth - ) { - builder.box(pos, filled, outline, sides, mode, thickness = thickness) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(pos.hashCode(), Box(pos), filled, outline, sides, mode, thickness = thickness)) - } - } - - fun box( - box: Box, - filledColor: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = builder.lineWidth - ) { - builder.box(box, filledColor, outlineColor, sides, mode, thickness = thickness) - if (collectShapes) { - shapes?.add(EspShape.BoxShape(box.hashCode(), box, filledColor, outlineColor, sides, mode, thickness = thickness)) - } - } - - fun box( - box: DynamicAABB, - filledColor: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = builder.lineWidth - ) { - builder.box(box, filledColor, outlineColor, sides, mode, thickness = thickness) - if (collectShapes) { - box.pair?.second?.let { - shapes?.add( - EspShape.BoxShape(it.hashCode(), it, filledColor, outlineColor, sides, mode, thickness = thickness) - ) - } - } - } - - fun box( - entity: net.minecraft.block.entity.BlockEntity, - filled: Color, - outline: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = builder.lineWidth - ) { - builder.box(entity, filled, outline, sides, mode, thickness = thickness) - if (collectShapes) { - shapes?.add( - EspShape.BoxShape( - entity.pos.hashCode(), - Box(entity.pos), - filled, - outline, - sides, - mode, - thickness = thickness - ) - ) - } - } - - fun box( - entity: net.minecraft.entity.Entity, - filled: Color, - outline: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = builder.lineWidth - ) { - builder.box(entity, filled, outline, sides, mode, thickness = thickness) - if (collectShapes) { - shapes?.add( - EspShape.BoxShape( - entity.hashCode(), - entity.boundingBox, - filled, - outline, - sides, - mode, - thickness = thickness - ) - ) - } - } -} - -@EspDsl -class BoxScope(val box: Box, val parent: ShapeScope) { - internal var filledColor: Color? = null - internal var outlineColor: Color? = null - internal var sides: Int = DirectionMask.ALL - internal var outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And - internal var thickness: Float = parent.builder.lineWidth - - fun filled(color: Color, sides: Int = DirectionMask.ALL) { - this.filledColor = color - this.sides = sides - parent.builder.filled(box, color, sides) - } - - fun outline( - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = parent.builder.lineWidth - ) { - this.outlineColor = color - this.sides = sides - this.outlineMode = mode - this.thickness = thickness - parent.builder.outline(box, color, sides, mode, thickness = thickness) - } -} - -@EspDsl -class LineScope(val from: Vec3d, val to: Vec3d, val parent: ShapeScope) { - internal var lineColor: Color = Color.WHITE - internal var lineWidth: Float = 1.0f - internal var lineDashLength: Double? = null - internal var lineGapLength: Double? = null - - fun color(color: Color) { - this.lineColor = color - } - - fun width(width: Float) { - this.lineWidth = width - } - - fun dashed(dashLength: Double = 0.5, gapLength: Double = 0.25) { - this.lineDashLength = dashLength - this.lineGapLength = gapLength - } - - internal fun draw() { - val dLen = lineDashLength - val gLen = lineGapLength - - if (dLen != null && gLen != null) { - parent.builder.dashedLine(from, to, lineColor, dLen, gLen, lineWidth) - } else { - parent.builder.line(from, to, lineColor, lineWidth) - } - } -} - -sealed class EspShape(val id: Int) { - abstract fun renderInterpolated( - prev: EspShape, - tickDelta: Float, - collector: RegionVertexCollector, - region: RenderRegion - ) - - class BoxShape( - id: Int, - val box: Box, - val filledColor: Color?, - val outlineColor: Color?, - val sides: Int = DirectionMask.ALL, - val outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - val thickness: Float = 1.0f - ) : EspShape(id) { - override fun renderInterpolated( - prev: EspShape, - tickDelta: Float, - collector: RegionVertexCollector, - region: RenderRegion - ) { - val interpBox = - if (prev is BoxShape) { - Box( - MathHelper.lerp(tickDelta.toDouble(), prev.box.minX, box.minX), - MathHelper.lerp(tickDelta.toDouble(), prev.box.minY, box.minY), - MathHelper.lerp(tickDelta.toDouble(), prev.box.minZ, box.minZ), - MathHelper.lerp(tickDelta.toDouble(), prev.box.maxX, box.maxX), - MathHelper.lerp(tickDelta.toDouble(), prev.box.maxY, box.maxY), - MathHelper.lerp(tickDelta.toDouble(), prev.box.maxZ, box.maxZ) - ) - } else box - - val shapeBuilder = RegionShapeBuilder(region) - filledColor?.let { shapeBuilder.filled(interpBox, it, sides) } - outlineColor?.let { shapeBuilder.outline(interpBox, it, sides, outlineMode, thickness = thickness) } - - collector.faceVertices.addAll(shapeBuilder.collector.faceVertices) - collector.edgeVertices.addAll(shapeBuilder.collector.edgeVertices) - } - } - - class LineShape( - id: Int, - val from: Vec3d, - val to: Vec3d, - val color: Color, - val width: Float, - val dashLength: Double? = null, - val gapLength: Double? = null - ) : EspShape(id) { - override fun renderInterpolated( - prev: EspShape, - tickDelta: Float, - collector: RegionVertexCollector, - region: RenderRegion - ) { - val iFrom = if (prev is LineShape) prev.from.lerp(from, tickDelta.toDouble()) else from - val iTo = if (prev is LineShape) prev.to.lerp(to, tickDelta.toDouble()) else to - - val shapeBuilder = RegionShapeBuilder(region) - if (dashLength != null && gapLength != null) { - shapeBuilder.dashedLine(iFrom, iTo, color, dashLength, gapLength, width) - } else { - shapeBuilder.line(iFrom, iTo, color, width) - } - - collector.faceVertices.addAll(shapeBuilder.collector.faceVertices) - collector.edgeVertices.addAll(shapeBuilder.collector.edgeVertices) - } - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt new file mode 100644 index 000000000..69d204ccb --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt @@ -0,0 +1,331 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc + +import com.lambda.graphics.util.DirectionMask +import net.minecraft.util.math.Direction +import java.awt.Color + +/** + * DSL builder for creating boxes with fine-grained control over: + * - Which sides to show for outlines vs faces (independently) + * - Individual vertex colors for all 8 corners + * + * Vertex naming convention (looking at box from outside): + * - Bottom corners: bottomNorthWest, bottomNorthEast, bottomSouthWest, bottomSouthEast + * - Top corners: topNorthWest, topNorthEast, topSouthWest, topSouthEast + * + * Usage: + * ``` + * builder.box(myBox) { + * outlineSides = DirectionMask.UP or DirectionMask.DOWN + * faceSides = DirectionMask.ALL + * thickness = 2f + * + * // Set all vertices to one color + * allColors(Color.RED) + * + * // Or set gradient colors + * bottomColor = Color.RED + * topColor = Color.BLUE + * + * // Or set individual vertex colors + * topNorthWest = Color.RED + * topNorthEast = Color.GREEN + * // etc. + * } + * ``` + */ +class BoxBuilder(val lineWidth: Float) { + // Side masks - independent control for outlines and faces + var outlineSides: Int = DirectionMask.ALL + var fillSides: Int = DirectionMask.ALL + + // Outline mode for edge visibility + var outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And + + // Dash style for outline edges (null = solid lines) + var dashStyle: LineDashStyle? = null + + // Bottom layer fill colors + var fillBottomNorthWest: Color = Color.WHITE + var fillBottomNorthEast: Color = Color.WHITE + var fillBottomSouthWest: Color = Color.WHITE + var fillBottomSouthEast: Color = Color.WHITE + + // Top layer fill colors + var fillTopNorthWest: Color = Color.WHITE + var fillTopNorthEast: Color = Color.WHITE + var fillTopSouthWest: Color = Color.WHITE + var fillTopSouthEast: Color = Color.WHITE + + // Bottom layer outline colors + var outlineBottomNorthWest: Color = Color.WHITE + var outlineBottomNorthEast: Color = Color.WHITE + var outlineBottomSouthWest: Color = Color.WHITE + var outlineBottomSouthEast: Color = Color.WHITE + + // Top layer outline colors + var outlineTopNorthWest: Color = Color.WHITE + var outlineTopNorthEast: Color = Color.WHITE + var outlineTopSouthWest: Color = Color.WHITE + var outlineTopSouthEast: Color = Color.WHITE + + /** Set both outline and fill colors at once. */ + @RenderDsl + fun allColors(color: Color) { + outlineColor(color) + fillColor(color) + } + + /** Set outline and fill to different colors. */ + @RenderDsl + fun colors(fill: Color, outline: Color) { + fillColor(fill) + outlineColor(outline) + } + + /** Set all fill (face) colors to a single color. */ + @RenderDsl + fun fillColor(color: Color) { + fillBottomNorthWest = color + fillBottomNorthEast = color + fillBottomSouthWest = color + fillBottomSouthEast = color + fillTopNorthWest = color + fillTopNorthEast = color + fillTopSouthWest = color + fillTopSouthEast = color + } + + /** Set all outline (edge) colors to a single color. */ + @RenderDsl + fun outlineColor(color: Color) { + outlineBottomNorthWest = color + outlineBottomNorthEast = color + outlineBottomSouthWest = color + outlineBottomSouthEast = color + outlineTopNorthWest = color + outlineTopNorthEast = color + outlineTopSouthWest = color + outlineTopSouthEast = color + } + + /** Set all bottom vertices to one color and all top vertices to another (both outline and fill). */ + @RenderDsl + fun gradientY(bottom: Color, top: Color) { + fillGradientY(bottom, top) + outlineGradientY(bottom, top) + } + + /** Set fill gradient along Y axis (bottom to top). */ + @RenderDsl + fun fillGradientY(bottom: Color, top: Color) { + fillBottomNorthWest = bottom + fillBottomNorthEast = bottom + fillBottomSouthWest = bottom + fillBottomSouthEast = bottom + fillTopNorthWest = top + fillTopNorthEast = top + fillTopSouthWest = top + fillTopSouthEast = top + } + + /** Set outline gradient along Y axis (bottom to top). */ + @RenderDsl + fun outlineGradientY(bottom: Color, top: Color) { + outlineBottomNorthWest = bottom + outlineBottomNorthEast = bottom + outlineBottomSouthWest = bottom + outlineBottomSouthEast = bottom + outlineTopNorthWest = top + outlineTopNorthEast = top + outlineTopSouthWest = top + outlineTopSouthEast = top + } + + @RenderDsl + fun gradientX(west: Color, east: Color) { + fillGradientX(west, east) + outlineGradientX(west, east) + } + + /** Set gradient along X axis (west to east) for both outline and fill. */ + @RenderDsl + fun fillGradientX(west: Color, east: Color) { + fillBottomNorthWest = west + fillBottomSouthWest = west + fillTopNorthWest = west + fillTopSouthWest = west + fillBottomNorthEast = east + fillBottomSouthEast = east + fillTopNorthEast = east + fillTopSouthEast = east + } + + @RenderDsl + fun outlineGradientX(west: Color, east: Color) { + outlineBottomNorthWest = west + outlineBottomSouthWest = west + outlineTopNorthWest = west + outlineTopSouthWest = west + outlineBottomNorthEast = east + outlineBottomSouthEast = east + outlineTopNorthEast = east + outlineTopSouthEast = east + } + + /** Set gradient along Z axis (north to south) for both outline and fill. */ + @RenderDsl + fun gradientZ(north: Color, south: Color) { + fillGradientZ(north, south) + outlineGradientZ(north, south) + } + + @RenderDsl + fun fillGradientZ(north: Color, south: Color) { + fillBottomNorthWest = north + fillBottomNorthEast = north + fillTopNorthWest = north + fillTopNorthEast = north + fillBottomSouthWest = south + fillBottomSouthEast = south + fillTopSouthWest = south + fillTopSouthEast = south + } + + @RenderDsl + fun outlineGradientZ(north: Color, south: Color) { + outlineBottomNorthWest = north + outlineBottomNorthEast = north + outlineTopNorthWest = north + outlineTopNorthEast = north + outlineBottomSouthWest = south + outlineBottomSouthEast = south + outlineTopSouthWest = south + outlineTopSouthEast = south + } + + @RenderDsl + fun lineDashStyle(lineDashStyle: LineDashStyle) { + dashStyle = lineDashStyle + } + + @RenderDsl + fun showSides(vararg directions: Direction) { + showFillSides(*directions) + showOutlineSides(*directions) + } + + @RenderDsl + fun showSides(mask: Int) { + showFillSides(mask) + showOutlineSides(mask) + } + + @RenderDsl + fun hideSides(vararg directions: Direction) { + hideFillSides(*directions) + hideOutlineSides(*directions) + } + + @RenderDsl + fun hideSides(mask: Int) { + hideFillSides(mask) + hideOutlineSides(mask) + } + + /** Hide all outline edges. */ + @RenderDsl + fun hideOutline() { + outlineSides = DirectionMask.NONE + } + + /** Hide all faces. */ + @RenderDsl + fun hideFill() { + fillSides = DirectionMask.NONE + } + + /** Show only outline (no faces). */ + @RenderDsl + fun outlineOnly() { + outlineSides = DirectionMask.ALL + fillSides = DirectionMask.NONE + } + + /** Show only faces (no outline). */ + @RenderDsl + fun fillOnly() { + outlineSides = DirectionMask.NONE + fillSides = DirectionMask.ALL + } + + /** Show the specified fill (face) sides, adding to current mask. */ + @RenderDsl + fun showFillSides(vararg directions: Direction) { + directions.forEach { fillSides = fillSides or DirectionMask.run { it.mask } } + } + + /** Show the specified fill (face) sides by mask, adding to current mask. */ + @RenderDsl + fun showFillSides(mask: Int) { + fillSides = fillSides or mask + } + + /** Hide the specified fill (face) sides, removing from current mask. */ + @RenderDsl + fun hideFillSides(vararg directions: Direction) { + directions.forEach { fillSides = fillSides and DirectionMask.run { it.mask }.inv() } + } + + /** Hide the specified fill (face) sides by mask, removing from current mask. */ + @RenderDsl + fun hideFillSides(mask: Int) { + fillSides = fillSides and mask.inv() + } + + /** Show the specified outline sides, adding to current mask. */ + @RenderDsl + fun showOutlineSides(vararg directions: Direction) { + directions.forEach { outlineSides = outlineSides or DirectionMask.run { it.mask } } + } + + /** Show the specified outline sides by mask, adding to current mask. */ + @RenderDsl + fun showOutlineSides(mask: Int) { + outlineSides = outlineSides or mask + } + + /** Hide the specified outline sides, removing from current mask. */ + @RenderDsl + fun hideOutlineSides(vararg directions: Direction) { + directions.forEach { outlineSides = outlineSides and DirectionMask.run { it.mask }.inv() } + } + + /** Hide the specified outline sides by mask, removing from current mask. */ + @RenderDsl + fun hideOutlineSides(mask: Int) { + outlineSides = outlineSides and mask.inv() + } + + @RenderDsl + fun outlineMode(outlineMode: DirectionMask.OutlineMode) { + this.outlineMode = outlineMode + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt deleted file mode 100644 index d367694a5..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/ChunkedRegionESP.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent -import com.lambda.event.events.WorldEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.event.listener.SafeListener.Companion.listenConcurrently -import com.lambda.graphics.esp.RegionESP -import com.lambda.graphics.esp.ShapeScope -import com.lambda.module.Module -import com.lambda.module.modules.client.StyleEditor -import com.lambda.threading.runSafe -import com.lambda.util.world.FastVector -import com.lambda.util.world.fastVectorOf -import net.minecraft.world.World -import net.minecraft.world.chunk.WorldChunk -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque - -/** - * Region-based chunked ESP system using MC 1.21.11's new render pipeline. - * - * This system: - * - Uses region-relative coordinates for precision-safe rendering - * - Maintains per-chunk geometry for efficient updates - * - * @param owner The module that owns this ESP system - * @param name The name of the ESP system - * @param depthTest Whether to use depth testing - * @param update The update function called for each block position - */ -class ChunkedRegionESP( - owner: Module, - name: String, - depthTest: Boolean = false, - private val update: ShapeScope.(World, FastVector) -> Unit -) : RegionESP(name, depthTest) { - private val chunkMap = ConcurrentHashMap() - - private val WorldChunk.regionChunk - get() = chunkMap.getOrPut(getRegionKey(pos.x shl 4, bottomY, pos.z shl 4)) { - RegionChunk(this) - } - - private val uploadQueue = ConcurrentLinkedDeque<() -> Unit>() - private val rebuildQueue = ConcurrentLinkedDeque() - - /** Mark all tracked chunks for rebuild. */ - fun rebuild() { - rebuildQueue.clear() - rebuildQueue.addAll(chunkMap.values) - } - - /** - * Load all currently loaded world chunks and mark them for rebuild. Call this when the module - * is enabled to populate initial chunks. - */ - fun rebuildAll() { - runSafe { - val chunksArray = world.chunkManager.chunks.chunks - (0 until chunksArray.length()).forEach { i -> - chunksArray.get(i)?.regionChunk?.markDirty() - } - } - } - - override fun clear() { - chunkMap.values.forEach { it.close() } - chunkMap.clear() - rebuildQueue.clear() - uploadQueue.clear() - } - - init { - owner.listen { event -> - val pos = event.pos - world.getWorldChunk(pos)?.regionChunk?.markDirty() - - val xInChunk = pos.x and 15 - val zInChunk = pos.z and 15 - - if (xInChunk == 0) world.getWorldChunk(pos.west())?.regionChunk?.markDirty() - if (xInChunk == 15) world.getWorldChunk(pos.east())?.regionChunk?.markDirty() - if (zInChunk == 0) world.getWorldChunk(pos.north())?.regionChunk?.markDirty() - if (zInChunk == 15) world.getWorldChunk(pos.south())?.regionChunk?.markDirty() - } - - owner.listen { event -> event.chunk.regionChunk.markDirty() } - - owner.listen { - val pos = getRegionKey(it.chunk.pos.x shl 4, it.chunk.bottomY, it.chunk.pos.z shl 4) - chunkMap.remove(pos)?.close() - } - - owner.listenConcurrently { - val queueSize = rebuildQueue.size - val polls = minOf(StyleEditor.rebuildsPerTick, queueSize) - repeat(polls) { rebuildQueue.poll()?.rebuild() } - } - - owner.listen { - val polls = minOf(StyleEditor.uploadsPerTick, uploadQueue.size) - repeat(polls) { uploadQueue.poll()?.invoke() } - } - - owner.listen { render() } - } - - /** Per-chunk rendering data. */ - private inner class RegionChunk(val chunk: WorldChunk) { - val region = RenderRegion.forChunk(chunk.pos.x, chunk.pos.z, chunk.bottomY) - private val key = getRegionKey(chunk.pos.x shl 4, chunk.bottomY, chunk.pos.z shl 4) - - private var isDirty = false - - fun markDirty() { - isDirty = true - if (!rebuildQueue.contains(this)) { - rebuildQueue.add(this) - } - } - - fun rebuild() { - if (!isDirty) return - val scope = ShapeScope(region) - - for (x in chunk.pos.startX..chunk.pos.endX) { - for (z in chunk.pos.startZ..chunk.pos.endZ) { - for (y in chunk.bottomY..chunk.height) { - update(scope, chunk.world, fastVectorOf(x, y, z)) - } - } - } - - uploadQueue.add { - val renderer = renderers.getOrPut(key) { RegionRenderer(region) } - renderer.upload(scope.builder.collector) - isDirty = false - } - } - - fun close() { - renderers.remove(key)?.close() - } - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt b/src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt deleted file mode 100644 index 735f17de1..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/ImGuiWorldText.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import com.lambda.graphics.RenderMain -import imgui.ImGui -import imgui.ImVec2 -import net.minecraft.util.math.Vec3d -import java.awt.Color - -/** - * ImGUI-based world text renderer. - * Projects world coordinates to screen space and draws text using ImGUI. - * - * Usage: - * ```kotlin - * // In a GuiEvent.NewFrame listener - * ImGuiWorldText.drawText(entity.pos, "Label", Color.WHITE) - * ``` - */ -object ImGuiWorldText { - - /** - * Draw text at a world position using ImGUI. - * - * @param worldPos World position for the text - * @param text The text to render - * @param color Text color - * @param centered Whether to center the text horizontally - * @param offsetY Vertical offset in screen pixels (negative = up) - */ - fun drawText( - worldPos: Vec3d, - text: String, - color: Color = Color.WHITE, - centered: Boolean = true, - offsetY: Float = 0f - ) { - val screen = RenderMain.worldToScreen(worldPos) ?: return - - val drawList = ImGui.getBackgroundDrawList() - val colorInt = colorToImGui(color) - - val x = if (centered) { - val textSize = ImVec2() - ImGui.calcTextSize(textSize, text) - screen.x - textSize.x / 2f - } else { - screen.x - } - - drawList.addText(x, screen.y + offsetY, colorInt, text) - } - - /** - * Draw text with a shadow/outline effect. - */ - fun drawTextWithShadow( - worldPos: Vec3d, - text: String, - color: Color = Color.WHITE, - shadowColor: Color = Color.BLACK, - centered: Boolean = true, - offsetY: Float = 0f - ) { - val screen = RenderMain.worldToScreen(worldPos) ?: return - - val drawList = ImGui.getBackgroundDrawList() - val textSize = ImVec2() - ImGui.calcTextSize(textSize, text) - - val x = if (centered) screen.x - textSize.x / 2f else screen.x - val y = screen.y + offsetY - - // Draw shadow (offset by 1 pixel) - val shadowInt = colorToImGui(shadowColor) - drawList.addText(x + 1f, y + 1f, shadowInt, text) - - // Draw main text - val colorInt = colorToImGui(color) - drawList.addText(x, y, colorInt, text) - } - - /** - * Draw multiple lines of text stacked vertically. - */ - fun drawMultilineText( - worldPos: Vec3d, - lines: List, - color: Color = Color.WHITE, - centered: Boolean = true, - lineSpacing: Float = 12f, - offsetY: Float = 0f - ) { - val screen = RenderMain.worldToScreen(worldPos) ?: return - - val drawList = ImGui.getBackgroundDrawList() - val colorInt = colorToImGui(color) - - lines.forEachIndexed { index, line -> - val textSize = ImVec2() - ImGui.calcTextSize(textSize, line) - - val x = if (centered) screen.x - textSize.x / 2f else screen.x - val y = screen.y + offsetY + (index * lineSpacing) - - drawList.addText(x, y, colorInt, line) - } - } - - /** - * Draw text with a background box. - */ - fun drawTextWithBackground( - worldPos: Vec3d, - text: String, - textColor: Color = Color.WHITE, - backgroundColor: Color = Color(0, 0, 0, 128), - centered: Boolean = true, - padding: Float = 4f, - offsetY: Float = 0f - ) { - val screen = RenderMain.worldToScreen(worldPos) ?: return - - val drawList = ImGui.getBackgroundDrawList() - val textSize = ImVec2() - ImGui.calcTextSize(textSize, text) - - val x = if (centered) screen.x - textSize.x / 2f else screen.x - val y = screen.y + offsetY - - // Draw background - val bgInt = colorToImGui(backgroundColor) - drawList.addRectFilled( - x - padding, - y - padding, - x + textSize.x + padding, - y + textSize.y + padding, - bgInt, - 2f // corner rounding - ) - - // Draw text - val colorInt = colorToImGui(textColor) - drawList.addText(x, y, colorInt, text) - } - - /** - * Convert java.awt.Color to ImGui color format (ABGR) - */ - private fun colorToImGui(color: Color): Int { - return (color.alpha shl 24) or - (color.blue shl 16) or - (color.green shl 8) or - color.red - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt deleted file mode 100644 index 8b2b0b4b7..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/InterpolatedRegionESP.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import com.lambda.graphics.esp.RegionESP -import com.lambda.graphics.esp.ShapeScope -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.floor - -/** - * Interpolated region-based ESP system for smooth entity rendering. - * - * Unlike TransientRegionESP which rebuilds every tick, this system stores both previous and current - * frame data and interpolates between them during rendering for smooth movement at any framerate. - */ -class InterpolatedRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) { - // Current frame builders (being populated this tick) - private val currBuilders = ConcurrentHashMap() - - // Previous frame data (uploaded last tick) - private val prevBuilders = ConcurrentHashMap() - - // Interpolated collectors for rendering (computed each frame) - private val interpolatedCollectors = - ConcurrentHashMap() - - // Track if we need to re-interpolate - private var lastTickDelta = -1f - private var needsInterpolation = true - - override fun shapes(x: Double, y: Double, z: Double, block: ShapeScope.() -> Unit) { - val key = getRegionKey(x, y, z) - val scope = - currBuilders.getOrPut(key) { - val size = RenderRegion.REGION_SIZE - val rx = (size * floor(x / size)).toInt() - val ry = (size * floor(y / size)).toInt() - val rz = (size * floor(z / size)).toInt() - ShapeScope(RenderRegion(rx, ry, rz), collectShapes = true) - } - scope.apply(block) - } - - override fun clear() { - prevBuilders.clear() - currBuilders.clear() - interpolatedCollectors.clear() - } - - fun tick() { - prevBuilders.clear() - prevBuilders.putAll(currBuilders) - currBuilders.clear() - needsInterpolation = true - } - - override fun upload() { - needsInterpolation = true - } - - override fun render(tickDelta: Float) { - if (needsInterpolation || lastTickDelta != tickDelta) { - interpolate(tickDelta) - uploadInterpolated() - lastTickDelta = tickDelta - needsInterpolation = false - } - super.render(tickDelta) - } - - private fun interpolate(tickDelta: Float) { - interpolatedCollectors.clear() - (prevBuilders.keys + currBuilders.keys).toSet().forEach { key -> - val prevScope = prevBuilders[key] - val currScope = currBuilders[key] - val collector = RegionVertexCollector() - val region = currScope?.region ?: prevScope?.region ?: return@forEach - - val prevShapes = prevScope?.shapes?.associateBy { it.id } ?: emptyMap() - val currShapes = currScope?.shapes?.associateBy { it.id } ?: emptyMap() - - val allIds = (prevShapes.keys + currShapes.keys).toSet() - - for (id in allIds) { - val prev = prevShapes[id] - val curr = currShapes[id] - - when { - prev != null && curr != null -> { - curr.renderInterpolated(prev, tickDelta, collector, region) - } - curr != null -> { - // New shape - just render - curr.renderInterpolated(curr, 1.0f, collector, region) - } - prev != null -> { - // Disappeared - render at previous position - prev.renderInterpolated(prev, 1.0f, collector, region) - } - } - } - - if (collector.faceVertices.isNotEmpty() || collector.edgeVertices.isNotEmpty()) { - interpolatedCollectors[key] = collector - } - } - } - - private fun uploadInterpolated() { - val activeKeys = interpolatedCollectors.keys.toSet() - interpolatedCollectors.forEach { (key, collector) -> - val region = currBuilders[key]?.region ?: prevBuilders[key]?.region ?: return@forEach - - val renderer = renderers.getOrPut(key) { RegionRenderer(region) } - renderer.upload(collector) - } - - renderers.forEach { (key, renderer) -> - if (key !in activeKeys) { - renderer.clearData() - } - } - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt b/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt new file mode 100644 index 000000000..6cd00c498 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc + +import org.joml.Vector3f + +/** + * Configuration for GUI item lighting. + * Controls the two-light shading model used for inventory-style rendering. + * + * @param light0 Primary light direction (normalized) + * @param light1 Fill light direction (normalized) + * @param ambient Base ambient lighting level (0.0-1.0) + * @param strength0 Primary light contribution multiplier + * @param strength1 Fill light contribution multiplier + * @param respectsUseLight When true, respects the layer's useLight flag to skip shading on flat items. + * When false, always applies the specified lighting regardless of item type. + */ +data class ItemLighting( + val light0: Vector3f, + val light1: Vector3f, + val ambient: Float = 0.6f, + val strength0: Float = 0.2f, + val strength1: Float = 0.2f, + val respectsUseLight: Boolean = false +) { + companion object { + /** + * Vanilla inventory lighting for GUI items. + * Respects the per-layer useLight flag to match vanilla behavior: + * - 3D blocks (useLight=true): Apply diffuse shading + * - Flat items (useLight=false): Skip shading, full brightness + * + * Raw light values from DiffuseLighting.java: + * - DEFAULT_DIFFUSION_LIGHT_0 = (0.2, 1.0, -0.7) + * - DEFAULT_DIFFUSION_LIGHT_1 = (-0.2, 1.0, 0.7) + */ + val VANILLA: ItemLighting by lazy { + val light0 = Vector3f(0.2f, 1.0f, -0.7f).normalize() + val light1 = Vector3f(-0.2f, 1.0f, 0.7f).normalize() + ItemLighting(light0, light1, respectsUseLight = true) + } + + /** + * No shading - produces a flat, evenly lit look. + * Ignores the layer's useLight flag. + */ + val NONE = ItemLighting( + light0 = Vector3f(0f, 1f, 0f), + light1 = Vector3f(0f, 1f, 0f), + ambient = 1.0f, + strength0 = 0f, + strength1 = 0f, + respectsUseLight = false + ) + + /** + * Creates lighting with a single directional light. + * Ignores the layer's useLight flag - always applies the specified lighting. + * @param direction The light direction (will be normalized) + * @param strength How strong the directional component is (0.0-1.0) + */ + fun directional(direction: Vector3f, strength: Float = 0.4f): ItemLighting { + val normalized = Vector3f(direction).normalize() + return ItemLighting( + light0 = normalized, + light1 = Vector3f(-normalized.x, normalized.y, -normalized.z).normalize(), + ambient = 1.0f - strength, + strength0 = strength * 0.7f, + strength1 = strength * 0.3f, + respectsUseLight = false + ) + } + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.kt b/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.kt new file mode 100644 index 000000000..005b0efdc --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc + +import net.minecraft.util.Identifier + +/** + * Configuration for GUI item overlay effects (e.g., enchantment glint). + * + * @param texture The overlay texture identifier + * @param scale UV scale multiplier (higher = smaller pattern) + * @param speed Animation speed multiplier + * @param angle Rotation angle in degrees + * @param alpha Overlay opacity (0.0-1.0) + */ +data class ItemOverlay( + val texture: Identifier, + val scale: Float = 1.0f, + val speed: Float = 1.0f, + val angle: Float = 0f, + val alpha: Float = 0.5f +) { + companion object { + /** + * Vanilla enchantment glint effect. + * Matches the animated purple shimmer on enchanted items. + */ + val ENCHANT_GLINT = ItemOverlay( + texture = Identifier.of("minecraft", "textures/misc/enchanted_glint_item.png"), + scale = 8.0f, + speed = 1.0f, + angle = 10f, + alpha = 0.5f + ) + + /** + * Entity glint (used on enchanted armor). + */ + val ENTITY_GLINT = ItemOverlay( + texture = Identifier.of("minecraft", "textures/misc/enchanted_glint_entity.png"), + scale = 8.0f, + speed = 1.0f, + angle = 10f, + alpha = 0.5f + ) + + /** + * Placeholder to explicitly disable glint. + */ + val DISABLED = ItemOverlay( + texture = Identifier.of("minecraft", "textures/misc/unknown.png"), + alpha = 0f + ) + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt index 350a40a77..c2d57686c 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt @@ -23,6 +23,7 @@ import com.mojang.blaze3d.pipeline.RenderPipeline import com.mojang.blaze3d.platform.DepthTestFunction import com.mojang.blaze3d.vertex.VertexFormat import net.minecraft.client.gl.RenderPipelines +import net.minecraft.client.gl.UniformType import net.minecraft.client.render.VertexFormats import net.minecraft.util.Identifier @@ -47,14 +48,14 @@ object LambdaRenderPipelines : Loadable { RenderPipelines.register( RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET) .withLocation(Identifier.of("lambda", "pipeline/esp_lines")) - .withVertexShader(Identifier.of("lambda", "core/advanced_lines")) - .withFragmentShader(Identifier.of("lambda", "core/advanced_lines")) + .withVertexShader(Identifier.of("lambda", "core/world_lines")) + .withFragmentShader(Identifier.of("lambda", "core/world_lines")) .withBlend(BlendFunction.TRANSLUCENT) - .withDepthWrite(false) + .withDepthWrite(false) // No depth write for proper transparency blending .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withCull(false) .withVertexFormat( - VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH, + LambdaVertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH_DASH, VertexFormat.DrawMode.QUADS ) .build() @@ -65,14 +66,14 @@ object LambdaRenderPipelines : Loadable { RenderPipelines.register( RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET) .withLocation(Identifier.of("lambda", "pipeline/esp_lines_through")) - .withVertexShader(Identifier.of("lambda", "core/advanced_lines")) - .withFragmentShader(Identifier.of("lambda", "core/advanced_lines")) + .withVertexShader(Identifier.of("lambda", "core/world_lines")) + .withFragmentShader(Identifier.of("lambda", "core/world_lines")) .withBlend(BlendFunction.TRANSLUCENT) .withDepthWrite(false) .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) .withCull(false) .withVertexFormat( - VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH, + LambdaVertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH_DASH, VertexFormat.DrawMode.QUADS ) .build() @@ -89,7 +90,7 @@ object LambdaRenderPipelines : Loadable { .withVertexShader(Identifier.ofVanilla("core/position_color")) .withFragmentShader(Identifier.ofVanilla("core/position_color")) .withBlend(BlendFunction.TRANSLUCENT) - .withDepthWrite(false) + .withDepthWrite(false) // No depth write for proper transparency blending .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) .withCull(false) .withVertexFormat( @@ -116,4 +117,307 @@ object LambdaRenderPipelines : Loadable { ) .build() ) + + /** + * Pipeline for textured text rendering with alpha blending. + * Uses position_tex_color shader with Sampler0 for font atlas texture. + */ + val TEXT_QUADS: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/text_quads")) + .withVertexShader(Identifier.ofVanilla("core/position_tex_color")) + .withFragmentShader(Identifier.ofVanilla("core/position_tex_color")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + VertexFormats.POSITION_TEXTURE_COLOR, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** Pipeline for text that renders through walls. */ + val TEXT_QUADS_THROUGH: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/text_quads_through")) + .withVertexShader(Identifier.ofVanilla("core/position_tex_color")) + .withFragmentShader(Identifier.ofVanilla("core/position_tex_color")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + VertexFormats.POSITION_TEXTURE_COLOR, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for SDF text rendering with proper smoothstep anti-aliasing. + * Uses lambda:core/sdf_text shaders with per-vertex style parameters. + */ + val SDF_TEXT: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/sdf_text")) + .withVertexShader(Identifier.of("lambda", "core/world_sdf_text")) + .withFragmentShader(Identifier.of("lambda", "core/world_sdf_text")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) // No depth write for proper transparency blending + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** SDF text pipeline that renders through walls. */ + val SDF_TEXT_THROUGH: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/sdf_text_through")) + .withVertexShader(Identifier.of("lambda", "core/world_sdf_text")) + .withFragmentShader(Identifier.of("lambda", "core/world_sdf_text")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + // ============================================================================ + // Screen-Space Pipelines (with layer-based depth for draw order) + // ============================================================================ + + /** + * Pipeline for screen-space faces/quads. + * Uses custom shader with layer support for draw order preservation. + */ + val SCREEN_FACES: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/screen_faces")) + .withVertexShader(Identifier.of("lambda", "core/screen_faces")) + .withFragmentShader(Identifier.of("lambda", "core/screen_faces")) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(true) // Enable depth write for layer ordering + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Enable depth test + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.SCREEN_FACE_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for screen-space lines. + * Uses a custom vertex format with 2D direction for perpendicular offset calculation. + * Includes layer support for draw order preservation. + */ + val SCREEN_LINES: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/screen_lines")) + .withVertexShader(Identifier.of("lambda", "core/screen_lines")) + .withFragmentShader(Identifier.of("lambda", "core/screen_lines")) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(true) // Enable depth write for layer ordering + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Depth test for layer ordering + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.SCREEN_LINE_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for screen-space SDF text rendering. + * Uses custom SDF shader with per-vertex style parameters for anti-aliased text with effects. + * Includes layer support for draw order preservation. + */ + val SCREEN_TEXT: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/screen_text")) + .withVertexShader(Identifier.of("lambda", "core/screen_sdf_text")) + .withFragmentShader(Identifier.of("lambda", "core/screen_sdf_text")) + .withSampler("Sampler0") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(true) // Enable depth write for layer ordering + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) // Enable depth test + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + // ============================================================================ + // Image Rendering Pipelines (with glint overlay support) + // ============================================================================ + + /** + * Pipeline for screen-space image rendering with overlay support. + * Uses two samplers: Sampler0 for main texture, Sampler1 for overlay (glint). + */ + val SCREEN_IMAGE: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/screen_image")) + .withVertexShader(Identifier.of("lambda", "core/screen_image")) + .withFragmentShader(Identifier.of("lambda", "core/screen_image")) + .withSampler("Sampler0") + .withSampler("Sampler1") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(true) // Enable depth write for layer ordering + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.SCREEN_IMAGE_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for world-space billboard image rendering with overlay support. + * Uses anchor-based positioning with optional billboarding. + */ + val WORLD_IMAGE: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/world_image")) + .withVertexShader(Identifier.of("lambda", "core/world_image")) + .withFragmentShader(Identifier.of("lambda", "core/world_image")) + .withSampler("Sampler0") + .withSampler("Sampler1") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) // No depth write for proper transparency blending + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.WORLD_IMAGE_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for world-space billboard image rendering that renders through walls. + */ + val WORLD_IMAGE_THROUGH: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/world_image_through")) + .withVertexShader(Identifier.of("lambda", "core/world_image")) + .withFragmentShader(Identifier.of("lambda", "core/world_image")) + .withSampler("Sampler0") + .withSampler("Sampler1") + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.WORLD_IMAGE_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for world-space 3D model rendering. + * Supports Position, Color, UV0 (Atlas), OverlayUV (Overlay), UV2 (Lightmap), Normal. + */ + val WORLD_MODEL: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/world_model")) + .withVertexShader(Identifier.of("lambda", "core/world_model")) + .withFragmentShader(Identifier.of("lambda", "core/world_model")) + .withSampler("Sampler0") // Atlas + .withSampler("Sampler1") // Overlay + .withSampler("Sampler2") // Lightmap + .withSampler("Sampler3") // Glint + .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(true) + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.WORLD_MODEL_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for screen-space 3D model rendering. + * Same as WORLD_MODEL but without lightmap sampler (Sampler2). + * Includes culling to prevent backfaces from clipping with front faces. + */ + val SCREEN_MODEL: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/screen_model")) + .withVertexShader(Identifier.of("lambda", "core/world_model")) + .withFragmentShader(Identifier.of("lambda", "core/world_model")) + .withSampler("Sampler0") // Atlas + .withSampler("Sampler1") // Overlay + .withSampler("Sampler2") // Lightmap (White/Neutral in screen space) + .withSampler("Sampler3") // Glint + .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(true) + .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.WORLD_MODEL_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) + + /** + * Pipeline for world-space 3D model rendering that renders through walls. + */ + val WORLD_MODEL_THROUGH: RenderPipeline = + RenderPipelines.register( + RenderPipeline.builder(LAMBDA_ESP_SNIPPET) + .withLocation(Identifier.of("lambda", "pipeline/world_model_through")) + .withVertexShader(Identifier.of("lambda", "core/world_model")) + .withFragmentShader(Identifier.of("lambda", "core/world_model")) + .withSampler("Sampler0") + .withSampler("Sampler1") + .withSampler("Sampler2") + .withSampler("Sampler3") // Glint + .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER) + .withBlend(BlendFunction.TRANSLUCENT) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withVertexFormat( + LambdaVertexFormats.WORLD_MODEL_FORMAT, + VertexFormat.DrawMode.QUADS + ) + .build() + ) } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt new file mode 100644 index 000000000..6b076d8a4 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt @@ -0,0 +1,375 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc + +import com.mojang.blaze3d.vertex.VertexFormat +import com.mojang.blaze3d.vertex.VertexFormatElement + +/** + * Custom vertex formats for Lambda's advanced rendering features. + * Extends Minecraft's standard formats with additional attributes. + */ +object LambdaVertexFormats { + /** + * Custom vertex format element for Normal as 3 floats. + * MC's NORMAL uses signed bytes which is unsuitable for world-space direction vectors. + */ + val NORMAL_FLOAT: VertexFormatElement = VertexFormatElement.register( + 30, // ID + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.NORMAL, + 3 // count (x, y, z direction) + ) + + /** + * Custom vertex format element for LineWidth as float. + * Ensures we get a proper float value in the shader. + */ + val LINE_WIDTH_FLOAT: VertexFormatElement = VertexFormatElement.register( + 29, // ID + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 1 // count (single float) + ) + + /** + * Custom vertex format element for dash parameters. + * Contains: dashLength, gapLength, dashOffset, animationSpeed (as vec4 of floats) + * + * Uses ID 31 (high value to avoid conflicts with Minecraft/mods). + * Uses index 0 and GENERIC usage since this is a custom attribute. + */ + val DASH_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 31, // ID (use high value to avoid conflicts with MC/mods) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 4 // count (dashLength, gapLength, dashOffset, animationSpeed) + ) + + /** + * Anchor position element for billboard text. + * Contains the world-space position (camera-relative) that the text is anchored to. + */ + val ANCHOR_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 20, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 3 // count (x, y, z anchor position) + ) + + /** + * Billboard data element for text rendering. + * Contains: scale, billboard flag (0 = billboard towards camera, non-zero = use rotation) + */ + val BILLBOARD_DATA_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 21, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 2 // count (scale, billboardFlag) + ) + + /** + * Extended line format with dash support. + * Layout: Position (vec3), Color (vec4), Normal (vec3 FLOAT), LineWidth (float), Dash (vec4) + * + * Total size: 12 + 4 + 12 + 4 + 16 = 48 bytes + * + * - Position: World-space vertex position (3 floats = 12 bytes) + * - Color: RGBA color (4 bytes) + * - Normal: Segment direction vector as FLOATS (3 floats = 12 bytes) + * - LineWidth: Per-vertex line width in world units (1 float = 4 bytes) + * - Dash: vec4(dashLength, gapLength, dashOffset, animationSpeed) (4 floats = 16 bytes) + */ + val POSITION_COLOR_NORMAL_LINE_WIDTH_DASH: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("Color", VertexFormatElement.COLOR) + .add("Normal", NORMAL_FLOAT) + .add("LineWidth", LINE_WIDTH_FLOAT) + .add("Dash", DASH_ELEMENT) + .build() + + /** + * Billboard text format with anchor position for GPU-based billboard rotation. + * Layout: Position (vec3), UV0 (vec2), Color (vec4), Anchor (vec3), BillboardData (vec2) + * + * Total size: 12 + 8 + 4 + 12 + 8 = 44 bytes + * + * - Position: Local glyph offset (x, y) with z unused (3 floats = 12 bytes) + * - UV0: Texture coordinates (2 floats = 8 bytes) + * - Color: RGBA color with alpha encoding layer type (4 bytes) + * - Anchor: Camera-relative world position of text anchor (3 floats = 12 bytes) + * - BillboardData: vec2(scale, billboardFlag) where billboardFlag 0 = auto-billboard (2 floats = 8 bytes) + */ + val POSITION_TEXTURE_COLOR_ANCHOR: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("UV0", VertexFormatElement.UV0) + .add("Color", VertexFormatElement.COLOR) + .add("Anchor", ANCHOR_ELEMENT) + .add("BillboardData", BILLBOARD_DATA_ELEMENT) + .build() + + /** + * 2D direction element for screen-space lines. + * Contains the line direction vector (dx, dy) used to compute perpendicular offset. + */ + val DIRECTION_2D_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 22, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 2 // count (dx, dy) + ) + + /** + * Layer depth element for screen-space ordering. + * Contains a single float representing the draw order (higher = on top). + */ + val LAYER_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 24, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 1 // count (single float: layer depth) + ) + + /** + * Screen-space face format with layer support for draw order preservation. + * Layout: Position (vec3), Color (vec4), Layer (float) + * + * Total size: 12 + 4 + 4 = 20 bytes + * + * - Position: Screen-space position (x, y, z=0) (3 floats = 12 bytes) + * - Color: RGBA color (4 bytes) + * - Layer: Depth for layering (1 float = 4 bytes) + */ + val SCREEN_FACE_FORMAT: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("Color", VertexFormatElement.COLOR) + .add("Layer", LAYER_ELEMENT) + .build() + + /** + * Screen-space line format with dash support and layer for draw order. + * Layout: Position (vec3), Color (vec4), Direction2D (vec2), LineWidth (float), Dash (vec4), Layer (float) + * + * Total size: 12 + 4 + 8 + 4 + 16 + 4 = 48 bytes + * + * - Position: Screen-space position (x, y, z where z = 0) (3 floats = 12 bytes) + * - Color: RGBA color (4 bytes) + * - Direction2D: Line direction for perpendicular offset (2 floats = 8 bytes) + * - LineWidth: Line width in pixels (1 float = 4 bytes) + * - Dash: vec4(dashLength, gapLength, dashOffset, animationSpeed) (4 floats = 16 bytes) + * - Layer: Depth for layering (1 float = 4 bytes) + */ + val SCREEN_LINE_FORMAT: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("Color", VertexFormatElement.COLOR) + .add("Direction", DIRECTION_2D_ELEMENT) + .add("LineWidth", LINE_WIDTH_FLOAT) + .add("Dash", DASH_ELEMENT) + .add("Layer", LAYER_ELEMENT) + .build() + + // ============================================================================ + // SDF Text Style Vertex Attributes (replaces SDFParams uniform buffer) + // ============================================================================ + + /** + * SDF style parameters as vertex attributes. + * Contains: OutlineWidth, GlowRadius, ShadowSoftness, SDFThreshold (as vec4 of floats) + * + * This replaces the SDFParams uniform buffer, enabling per-vertex style control + * and eliminating the need for style-based batching. + */ + val SDF_STYLE_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 23, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 4 // count (outlineWidth, glowRadius, shadowSoftness, sdfThreshold) + ) + + /** + * Billboard text format with anchor position AND SDF style parameters. + * Layout: Position (vec3), UV0 (vec2), Color (vec4), Anchor (vec3), BillboardData (vec2), SDFStyle (vec4) + * + * Total size: 12 + 8 + 4 + 12 + 8 + 16 = 60 bytes + * + * - Position: Local glyph offset (x, y) with z unused (3 floats = 12 bytes) + * - UV0: Texture coordinates (2 floats = 8 bytes) + * - Color: RGBA color with alpha encoding layer type (4 bytes) + * - Anchor: Camera-relative world position of text anchor (3 floats = 12 bytes) + * - BillboardData: vec2(scale, billboardFlag) (2 floats = 8 bytes) + * - SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) (4 floats = 16 bytes) + */ + val POSITION_TEXTURE_COLOR_ANCHOR_SDF: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("UV0", VertexFormatElement.UV0) + .add("Color", VertexFormatElement.COLOR) + .add("Anchor", ANCHOR_ELEMENT) + .add("BillboardData", BILLBOARD_DATA_ELEMENT) + .add("SDFStyle", SDF_STYLE_ELEMENT) + .build() + + /** + * Screen-space text format with SDF style parameters and layer for draw order. + * Layout: Position (vec3), UV0 (vec2), Color (vec4), SDFStyle (vec4), Layer (float) + * + * Total size: 12 + 8 + 4 + 16 + 4 = 44 bytes + * + * - Position: Screen-space position (x, y, z=0) (3 floats = 12 bytes) + * - UV0: Texture coordinates (2 floats = 8 bytes) + * - Color: RGBA color with alpha encoding layer type (4 bytes) + * - SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) (4 floats = 16 bytes) + * - Layer: Depth for layering (1 float = 4 bytes) + */ + val SCREEN_TEXT_SDF_FORMAT: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("UV0", VertexFormatElement.UV0) + .add("Color", VertexFormatElement.COLOR) + .add("SDFStyle", SDF_STYLE_ELEMENT) + .add("Layer", LAYER_ELEMENT) + .build() + + // ============================================================================ + // Image Rendering Vertex Formats + // ============================================================================ + + /** + * Overlay UV element for image rendering with overlay textures (e.g., enchantment glint). + * Contains: overlayU, overlayV, hasOverlay, diffuseAmount (as vec4 of floats) + */ + val OVERLAY_UV_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 25, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 4 // count (overlayU, overlayV, hasOverlay, diffuseAmount) + ) + + /** + * Screen-space image format with overlay support and layer for draw order. + * Layout: Position (vec3), UV0 (vec2), Color (vec4), OverlayUV (vec3), Layer (float) + * + * Total size: 12 + 8 + 4 + 16 + 4 = 44 bytes + * + * - Position: Screen-space position (x, y, z=0) (3 floats = 12 bytes) + * - UV0: Main texture coordinates (2 floats = 8 bytes) + * - Color: RGBA tint color (4 bytes) + * - OverlayUV: vec4(overlayU, overlayV, hasOverlay, diffuseAmount) (4 floats = 16 bytes) + * - Layer: Depth for layering (1 float = 4 bytes) + */ + val SCREEN_IMAGE_FORMAT: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("UV0", VertexFormatElement.UV0) + .add("Color", VertexFormatElement.COLOR) + .add("OverlayUV", OVERLAY_UV_ELEMENT) + .add("Layer", LAYER_ELEMENT) + .build() + + /** + * World-space image format with anchor for billboarding and overlay support. + * Layout: Position (vec3), UV0 (vec2), Color (vec4), Anchor (vec3), BillboardData (vec2), OverlayUV (vec3) + * + * Total size: 12 + 8 + 4 + 12 + 8 + 16 = 60 bytes + * + * - Position: Local offset (x, y) with z unused (3 floats = 12 bytes) + * - UV0: Main texture coordinates (2 floats = 8 bytes) + * - Color: RGBA tint color (4 bytes) + * - Anchor: Camera-relative world position (3 floats = 12 bytes) + * - BillboardData: vec2(scale, billboardFlag) (2 floats = 8 bytes) + * - OverlayUV: vec4(overlayU, overlayV, hasOverlay, diffuseAmount) (4 floats = 16 bytes) + */ + val WORLD_IMAGE_FORMAT: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("UV0", VertexFormatElement.UV0) + .add("Color", VertexFormatElement.COLOR) + .add("Anchor", ANCHOR_ELEMENT) + .add("BillboardData", BILLBOARD_DATA_ELEMENT) + .add("OverlayUV", OVERLAY_UV_ELEMENT) + .build() + /** + * Edge data element for analytic geometry anti-aliasing. + * Contains face-relative coordinates (0.0 to 1.0) for edge distance calculation. + */ + val EDGE_DATA_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 26, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 2 // count (faceX, faceY) + ) + + /** + * Custom light direction element for per-item shading (primary light). + * Contains the world-space light direction vector. + */ + val LIGHT_DIR_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 27, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 3 // count (x, y, z light direction) + ) + + /** + * Secondary light direction element for vanilla's two-light shading. + * Contains the fill light direction vector. + */ + val LIGHT1_DIR_ELEMENT: VertexFormatElement = VertexFormatElement.register( + 28, // ID (unique, in valid range [0, 32)) + 0, // index + VertexFormatElement.Type.FLOAT, + VertexFormatElement.Usage.GENERIC, + 3 // count (x, y, z light direction) + ) + + /** + * World-space model format with overlay, lightmap, normals, edge data, and custom lighting. + * Layout: Position (vec3), Color (vec4), UV0 (vec2), OverlayUV (vec4), Light (vec2), LightDir (vec3), Light1Dir (vec3), Normal (vec3), EdgeData (vec2) + * + * Total size: 12 + 4 + 8 + 16 + 4 + 12 + 12 + 12 + 8 = 88 bytes + * + * - Position: World-space position (3 floats = 12 bytes) + * - Color: RGBA tint color (4 bytes) + * - UV0: Main texture coordinates (2 floats = 8 bytes) + * - OverlayUV: vec4(overlayU, overlayV, hasOverlay, diffuseAmount) (4 floats = 16 bytes) + * - Light: Lightmap coordinates (2 shorts = 4 bytes) + * - LightDir: Primary light direction (3 floats = 12 bytes) + * - Light1Dir: Fill light direction (3 floats = 12 bytes) + * - Normal: Normal vector as floats (3 floats = 12 bytes) + * - EdgeData: Face-relative coordinates for AA (2 floats = 8 bytes) + */ + val WORLD_MODEL_FORMAT: VertexFormat = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("Color", VertexFormatElement.COLOR) + .add("UV0", VertexFormatElement.UV0) + .add("OverlayUV", OVERLAY_UV_ELEMENT) + .add("Light", VertexFormatElement.UV2) + .add("LightDir", LIGHT_DIR_ELEMENT) + .add("Light1Dir", LIGHT1_DIR_ELEMENT) + .add("Normal", NORMAL_FLOAT) + .add("EdgeData", EDGE_DATA_ELEMENT) + .build() +} + diff --git a/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt new file mode 100644 index 000000000..e1fad6768 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc + +/** + * Configuration for dashed line rendering in world-space. + * + * All measurements are in WORLD UNITS (blocks). For example: + * - dashLength = 0.5 means each dash is half a block long + * - gapLength = 0.25 means gaps are a quarter block + * + * When applied to lines, creates a repeating dash pattern where visible segments + * alternate with gaps. The pattern repeats with a period of (dashLength + gapLength). + * + * Animation is now handled by the shader using Minecraft's GameTime, so the + * animated/animationSpeed properties control whether animation is enabled. + * + * @property dashLength Length of each visible dash segment in world units (blocks) + * @property gapLength Length of each invisible gap segment in world units (blocks) + * @property offset Phase offset to shift the pattern along the line (0.0 to 1.0, normalized) + * @property animated If true, the dash pattern animates (marching ants effect) + * @property animationSpeed Speed multiplier for animation (higher = faster marching) + * + * Usage: + * ``` + * // Simple dashed line (0.5 block dash, 0.25 block gap) + * val dashed = LineDashStyle(dashLength = 0.5f, gapLength = 0.25f) + * + * // Dotted line (equal dash and gap, 0.15 blocks each) + * val dotted = LineDashStyle.dotted() + * + * // Animated marching ants for selection highlight + * val marching = LineDashStyle.marchingAnts() + * ``` + */ +data class LineDashStyle( + val dashLength: Float = 0.5f, + val gapLength: Float = 0.25f, + val offset: Float = 0f, + val animated: Boolean = false, + val animationSpeed: Float = 1f +) { + init { + require(dashLength > 0f) { "dashLength must be positive" } + require(gapLength >= 0f) { "gapLength must be non-negative" } + require(offset in 0f..1f) { "offset must be between 0.0 and 1.0" } + } + + /** Total length of one dash+gap cycle in world units. */ + val cycleLength: Float get() = dashLength + gapLength + + /** Ratio of the dash portion (0.0 to 1.0) within each cycle. */ + val dashRatio: Float get() = dashLength / cycleLength + + companion object { + /** No dashing - solid line. */ + val SOLID: LineDashStyle? = null + + /** + * Create a dotted pattern with equal dash and gap lengths. + * Default: 0.15 blocks each (small dots) + */ + fun dotted(size: Float = 0.15f) = LineDashStyle( + dashLength = size, + gapLength = size + ) + + /** + * Create an animated "marching ants" selection pattern. + * Default: 0.4 block dash, 0.2 block gap, animated + */ + fun marchingAnts( + dashLength: Float = 0.4f, + gapLength: Float = 0.2f, + speed: Float = 1f + ) = LineDashStyle( + dashLength = dashLength, + gapLength = gapLength, + animated = true, + animationSpeed = speed + ) + + /** + * Create a long-dash pattern (3:1 dash to gap ratio). + * Default: 0.75 block dash, 0.25 block gap + */ + fun longDash(dashLength: Float = 0.75f) = LineDashStyle( + dashLength = dashLength, + gapLength = dashLength / 3f + ) + + /** + * Create a short-dash pattern (1:1 ratio, larger than dotted). + * Default: 0.3 block dash and gap + */ + fun shortDash(size: Float = 0.3f) = LineDashStyle( + dashLength = size, + gapLength = size + ) + + // ============================================================================ + // Screen-Space Convenience Methods (Normalized 0-1 Coordinates) + // ============================================================================ + // These use screen-normalized units where 1.0 = full screen dimension. + // Typical values: 0.01 = 1% of screen, 0.02 = 2% of screen, etc. + + /** + * Create a dotted pattern for screen-space rendering. + * Default: 0.01 (1% of screen) for each dot and gap + */ + fun screenDotted(size: Float = 0.01f) = LineDashStyle( + dashLength = size, + gapLength = size + ) + + /** + * Create an animated "marching ants" selection pattern for screen-space. + * Great for selection boxes and interactive UI elements. + * Default: 0.02 dash, 0.01 gap (2% dash, 1% gap) + */ + fun screenMarchingAnts( + dashLength: Float = 0.02f, + gapLength: Float = 0.01f, + speed: Float = 1f + ) = LineDashStyle( + dashLength = dashLength, + gapLength = gapLength, + animated = true, + animationSpeed = speed + ) + + /** + * Create a dashed pattern for screen-space rendering. + * Default: 0.03 dash, 0.015 gap (3% dash, 1.5% gap) + */ + fun screenDashed(dashLength: Float = 0.03f, gapLength: Float = 0.015f) = LineDashStyle( + dashLength = dashLength, + gapLength = gapLength + ) + + /** + * Create a short-dash pattern for screen-space rendering. + * Default: 0.015 (1.5% of screen) for each dash and gap + */ + fun screenShortDash(size: Float = 0.015f) = LineDashStyle( + dashLength = size, + gapLength = size + ) + + /** + * Create a long-dash pattern for screen-space rendering. + * Default: 0.04 dash, 0.013 gap (4% dash, ~1.3% gap - 3:1 ratio) + */ + fun screenLongDash(dashLength: Float = 0.04f) = LineDashStyle( + dashLength = dashLength, + gapLength = dashLength / 3f + ) + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt index 8d7f36f4a..dab46a533 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt @@ -18,6 +18,7 @@ package com.lambda.graphics.mc import com.lambda.Lambda.mc +import com.lambda.graphics.mc.renderer.RendererUtils import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderPass import com.mojang.blaze3d.systems.RenderSystem @@ -25,25 +26,41 @@ import com.mojang.blaze3d.vertex.VertexFormat import java.util.* /** - * Region-based renderer for ESP rendering using MC 1.21.11's new render pipeline. + * Renderer for ESP rendering using MC 1.21.11's new render pipeline. * - * This renderer manages the lifecycle of dedicated GPU buffers for a specific region and provides + * This renderer manages the lifecycle of dedicated GPU buffers and provides * methods to render them within a RenderPass. - * - * @param region The render region this renderer is associated with */ -class RegionRenderer(val region: RenderRegion) { - - // Dedicated GPU buffers for faces and edges +class RegionRenderer { + // Dedicated GPU buffers for world-space faces, edges, and text private var faceVertexBuffer: GpuBuffer? = null private var edgeVertexBuffer: GpuBuffer? = null + private var textVertexBuffer: GpuBuffer? = null + + // Dedicated GPU buffers for screen-space faces, edges, and text + private var screenFaceVertexBuffer: GpuBuffer? = null + private var screenEdgeVertexBuffer: GpuBuffer? = null + private var screenTextVertexBuffer: GpuBuffer? = null + + // Image batches (texture -> buffer) for screen and world space + private var screenImageBatches: List = emptyList() + private var worldImageBatches: List = emptyList() + private var modelBatches: List = emptyList() + private var screenModelBatches: List = emptyList() - // Index counts for draw calls + // Index counts for world-space draw calls private var faceIndexCount = 0 private var edgeIndexCount = 0 + private var textIndexCount = 0 + + // Index counts for screen-space draw calls + private var screenFaceIndexCount = 0 + private var screenEdgeIndexCount = 0 + private var screenTextIndexCount = 0 // State tracking private var hasData = false + private var hasScreenData = false /** * Upload collected vertices from an external collector. This must be called on the main/render @@ -53,19 +70,52 @@ class RegionRenderer(val region: RenderRegion) { */ fun upload(collector: RegionVertexCollector) { val result = collector.upload() + val screenResult = collector.uploadScreen() - // Cleanup old buffers + // Cleanup old world-space buffers faceVertexBuffer?.close() edgeVertexBuffer?.close() + textVertexBuffer?.close() + + // Cleanup old screen-space buffers + screenFaceVertexBuffer?.close() + screenEdgeVertexBuffer?.close() + screenTextVertexBuffer?.close() - // Assign new buffers and counts + // Assign new world-space buffers and counts faceVertexBuffer = result.faces?.buffer faceIndexCount = result.faces?.indexCount ?: 0 edgeVertexBuffer = result.edges?.buffer edgeIndexCount = result.edges?.indexCount ?: 0 - hasData = faceVertexBuffer != null || edgeVertexBuffer != null + textVertexBuffer = result.text?.buffer + textIndexCount = result.text?.indexCount ?: 0 + + // Assign new screen-space buffers and counts + screenFaceVertexBuffer = screenResult.faces?.buffer + screenFaceIndexCount = screenResult.faces?.indexCount ?: 0 + + screenEdgeVertexBuffer = screenResult.edges?.buffer + screenEdgeIndexCount = screenResult.edges?.indexCount ?: 0 + + screenTextVertexBuffer = screenResult.text?.buffer + screenTextIndexCount = screenResult.text?.indexCount ?: 0 + + // Clean up old image batches + screenImageBatches.forEach { it.buffer.close() } + worldImageBatches.forEach { it.buffer.close() } + modelBatches.forEach { it.buffer.close() } + screenModelBatches.forEach { it.buffer.close() } + + // Store new batches + screenImageBatches = screenResult.images + worldImageBatches = result.images + modelBatches = result.models + screenModelBatches = screenResult.models + + hasData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null || worldImageBatches.isNotEmpty() || modelBatches.isNotEmpty() + hasScreenData = screenFaceVertexBuffer != null || screenEdgeVertexBuffer != null || screenTextVertexBuffer != null || screenImageBatches.isNotEmpty() || screenModelBatches.isNotEmpty() } /** @@ -104,38 +154,345 @@ class RegionRenderer(val region: RenderRegion) { renderPass.drawIndexed(0, 0, edgeIndexCount, 1) } + /** + * Render text using the given render pass. + * Note: Caller must bind the font texture and SDF params uniform before calling. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderText(renderPass: RenderPass) { + val vb = textVertexBuffer ?: return + if (textIndexCount == 0) return + + renderPass.setVertexBuffer(0, vb) + // Use vanilla's sequential index buffer for quads + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(textIndexCount) + + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, textIndexCount, 1) + } + + /** Check if this renderer has text data. */ + fun hasTextData(): Boolean = textVertexBuffer != null && textIndexCount > 0 + + // ============================================================================ + // Screen-Space Render Methods + // ============================================================================ + + /** + * Render screen-space faces using the given render pass. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderScreenFaces(renderPass: RenderPass) { + val vb = screenFaceVertexBuffer ?: return + if (screenFaceIndexCount == 0) return + + renderPass.setVertexBuffer(0, vb) + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(screenFaceIndexCount) + + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, screenFaceIndexCount, 1) + } + + /** + * Render screen-space edges using the given render pass. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderScreenEdges(renderPass: RenderPass) { + val vb = screenEdgeVertexBuffer ?: return + if (screenEdgeIndexCount == 0) return + + renderPass.setVertexBuffer(0, vb) + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(screenEdgeIndexCount) + + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, screenEdgeIndexCount, 1) + } + + /** + * Render screen-space text using the given render pass. + * Note: Caller must bind the font texture before calling. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderScreenText(renderPass: RenderPass) { + val vb = screenTextVertexBuffer ?: return + if (screenTextIndexCount == 0) return + + renderPass.setVertexBuffer(0, vb) + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(screenTextIndexCount) + + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, screenTextIndexCount, 1) + } + + /** Check if this renderer has screen-space text data. */ + fun hasScreenTextData(): Boolean = screenTextVertexBuffer != null && screenTextIndexCount > 0 + + /** + * Render screen-space images using the given render pass. + * Each texture batch is rendered separately with its texture bound. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderScreenImages(renderPass: RenderPass) { + if (screenImageBatches.isEmpty()) return + + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST) + + for (batch in screenImageBatches) { + val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler + renderPass.bindTexture("Sampler0", batch.textureView, sampler) + + renderPass.setVertexBuffer(0, batch.buffer) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount) + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + + renderPass.drawIndexed(0, 0, batch.indexCount, 1) + } + } + + /** + * Render screen-space models using the given render pass. + */ + fun renderScreenModels(renderPass: RenderPass) { + if (screenModelBatches.isEmpty()) return + + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST) + + // Get glint texture for enchantment shimmer + val glintTexture = mc.textureManager.getTexture(net.minecraft.client.render.item.ItemRenderer.ITEM_ENCHANTMENT_GLINT)?.glTextureView + + for (batch in screenModelBatches) { + val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler + renderPass.bindTexture("Sampler0", batch.textureView, sampler) + + // Bind glint texture to Sampler3 + if (glintTexture != null) { + renderPass.bindTexture("Sampler3", glintTexture, linearSampler) + } + + renderPass.setVertexBuffer(0, batch.buffer) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount) + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + + renderPass.drawIndexed(0, 0, batch.indexCount, 1) + } + } + + /** Check if this renderer has screen-space image data. */ + fun hasScreenImageData(): Boolean = screenImageBatches.isNotEmpty() + + /** Check if this renderer has screen-space model data. */ + fun hasScreenModelData(): Boolean = screenModelBatches.isNotEmpty() + + /** + * Render world-space images using the given render pass. + * Each texture batch is rendered separately with its texture bound. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderWorldImages(renderPass: RenderPass) { + if (worldImageBatches.isEmpty()) return + + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST) + + for (batch in worldImageBatches) { + val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler + renderPass.bindTexture("Sampler0", batch.textureView, sampler) + renderPass.setVertexBuffer(0, batch.buffer) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount) + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, batch.indexCount, 1) + } + } + + /** Check if this renderer has world-space image data. */ + fun hasWorldImageData(): Boolean = worldImageBatches.isNotEmpty() + + /** + * Render 3D models using the given render pass. + * Each texture batch is rendered separately with its texture bound. + * + * @param renderPass The active RenderPass to record commands into + */ + fun renderModels(renderPass: RenderPass) { + if (modelBatches.isEmpty()) return + + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val linearSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST) + + // Get glint texture for enchantment shimmer + val glintTexture = mc.textureManager.getTexture(net.minecraft.client.render.item.ItemRenderer.ITEM_ENCHANTMENT_GLINT)?.glTextureView + + for (batch in modelBatches) { + val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler + renderPass.bindTexture("Sampler0", batch.textureView, sampler) + + // Bind glint texture to Sampler3 + if (glintTexture != null) { + renderPass.bindTexture("Sampler3", glintTexture, linearSampler) + } + + renderPass.setVertexBuffer(0, batch.buffer) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(batch.indexCount) + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, batch.indexCount, 1) + } + } + + /** Check if this renderer has model data. */ + fun hasModelData(): Boolean = modelBatches.isNotEmpty() + + /** Check if this renderer has any screen-space data to render. */ + fun hasScreenData(): Boolean = hasScreenData + /** Clear all geometry data and release GPU resources. */ fun clearData() { + // Clear world-space buffers faceVertexBuffer?.close() edgeVertexBuffer?.close() + textVertexBuffer?.close() faceVertexBuffer = null edgeVertexBuffer = null + textVertexBuffer = null faceIndexCount = 0 edgeIndexCount = 0 + textIndexCount = 0 hasData = false + + // Clear screen-space buffers + screenFaceVertexBuffer?.close() + screenEdgeVertexBuffer?.close() + screenTextVertexBuffer?.close() + screenFaceVertexBuffer = null + screenEdgeVertexBuffer = null + screenTextVertexBuffer = null + screenFaceIndexCount = 0 + screenEdgeIndexCount = 0 + screenTextIndexCount = 0 + + // Clear image batches + screenImageBatches.forEach { it.buffer.close() } + worldImageBatches.forEach { it.buffer.close() } + modelBatches.forEach { it.buffer.close() } + screenImageBatches = emptyList() + worldImageBatches = emptyList() + modelBatches = emptyList() + + hasScreenData = false } /** Check if this renderer has any data to render. */ fun hasData(): Boolean = hasData - /** Clean up all resources. */ - fun close() { - clearData() - } - companion object { - /** Helper to create a render pass targeting the main framebuffer. */ + /** Helper to create a render pass targeting the main framebuffer with MC's depth. */ fun createRenderPass(label: String): RenderPass? { + return createRenderPass(label, useMcDepth = true) + } + + /** + * Helper to create a render pass for world-space rendering. + * @param label Debug label for the render pass + * @param useMcDepth If true, use MC's depth buffer (normal depth testing against world). + * If false, use Lambda's custom xray depth buffer (self-ordering, ignores MC world). + */ + fun createRenderPass(label: String, useMcDepth: Boolean): RenderPass? { val framebuffer = mc.framebuffer ?: return null + + // Choose depth buffer: + // - true = MC's depth (normal depth testing against world) + // - false = Lambda's xray depth (self-ordering, ignores MC world) + val depthView = if (useMcDepth) { + framebuffer.depthAttachmentView + } else { + RendererUtils.getXrayDepthView() + } + return RenderSystem.getDevice() .createCommandEncoder() .createRenderPass( { label }, framebuffer.colorAttachmentView, OptionalInt.empty(), - framebuffer.depthAttachmentView, + depthView, OptionalDouble.empty() ) } + + /** + * Helper to create a render pass for screen-space rendering (no depth buffer). + * Uses painter's algorithm: last drawn is on top. + * @param label Debug label for the render pass + */ + fun createScreenRenderPass(label: String): RenderPass? { + val framebuffer = mc.framebuffer ?: return null + + return RenderSystem.getDevice() + .createCommandEncoder() + .createRenderPass( + { label }, + framebuffer.colorAttachmentView, + OptionalInt.empty(), + null, // No depth buffer - painter's algorithm + OptionalDouble.empty() + ) + } + + /** + * Helper to create a render pass for screen-space rendering WITH depth testing. + * Enables unified layering across all screen element types (models, faces, text, etc.). + * Uses a SEPARATE screen depth buffer to ensure complete isolation from world-space. + * + * @param label Debug label for the render pass + * @param clearDepth If true, clear the depth buffer to 1.0 (call this on the first screen pass) + */ + fun createScreenRenderPassWithDepth(label: String, clearDepth: Boolean = false): RenderPass? { + val framebuffer = mc.framebuffer ?: return null + val depthView = RendererUtils.getScreenDepthView() // Use separate screen depth buffer + + return RenderSystem.getDevice() + .createCommandEncoder() + .createRenderPass( + { label }, + framebuffer.colorAttachmentView, + OptionalInt.empty(), + depthView, + if (clearDepth) OptionalDouble.of(1.0) else OptionalDouble.empty() + ) + } + + /** + * Render a custom vertex buffer using quads mode. + * Used for styled text rendering where each style has its own buffer. + * + * @param renderPass The active RenderPass to record commands into + * @param buffer The vertex buffer to render + * @param indexCount The number of indices (vertices) to render + */ + fun renderQuadBuffer(renderPass: RenderPass, buffer: GpuBuffer, indexCount: Int) { + if (indexCount == 0) return + + renderPass.setVertexBuffer(0, buffer) + val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS) + val indexBuffer = shapeIndexBuffer.getIndexBuffer(indexCount) + + renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType) + renderPass.drawIndexed(0, 0, indexCount, 1) + } } } diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt deleted file mode 100644 index 8b33498be..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionShapeBuilder.kt +++ /dev/null @@ -1,758 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import com.lambda.Lambda.mc -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.hasDirection -import com.lambda.graphics.renderer.esp.DynamicAABB -import com.lambda.module.modules.client.StyleEditor -import com.lambda.threading.runSafe -import com.lambda.util.BlockUtils.blockState -import com.lambda.util.extension.partialTicks -import net.minecraft.block.BlockState -import net.minecraft.block.entity.BlockEntity -import net.minecraft.entity.Entity -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Box -import net.minecraft.util.math.MathHelper.lerp -import net.minecraft.util.math.Vec3d -import net.minecraft.util.shape.VoxelShape -import java.awt.Color -import kotlin.math.min -import kotlin.math.sqrt - -/** - * Shape builder for region-based rendering. All coordinates are automatically converted to - * region-relative positions. - * - * This class provides drawing primitives for region-based rendering and collects vertex data in thread-safe collections - * for later upload to MC's BufferBuilder. - * - * @param region The render region (provides origin for coordinate conversion) - */ -class RegionShapeBuilder(val region: RenderRegion) { - val collector = RegionVertexCollector() - - val lineWidth: Float - get() = StyleEditor.outlineWidth.toFloat() - - fun box( - entity: BlockEntity, - filled: Color, - outline: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And - ) = box(entity.pos, entity.cachedState, filled, outline, sides, mode) - - fun box( - entity: Entity, - filled: Color, - outline: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And - ) = box(entity.boundingBox, filled, outline, sides, mode) - - /** Convert world coordinates to region-relative. */ - private fun toRelative(x: Double, y: Double, z: Double) = - Triple( - (x - region.originX).toFloat(), - (y - region.originY).toFloat(), - (z - region.originZ).toFloat() - ) - - /** Add a colored quad face (filled rectangle). */ - fun filled( - box: Box, - bottomColor: Color, - topColor: Color = bottomColor, - sides: Int = DirectionMask.ALL - ) { - val (x1, y1, z1) = toRelative(box.minX, box.minY, box.minZ) - val (x2, y2, z2) = toRelative(box.maxX, box.maxY, box.maxZ) - - // Bottom-left-back, bottom-left-front, etc. - if (sides.hasDirection(DirectionMask.EAST)) { - // East face (+X) - faceVertex(x2, y1, z1, bottomColor) - faceVertex(x2, y2, z1, topColor) - faceVertex(x2, y2, z2, topColor) - faceVertex(x2, y1, z2, bottomColor) - } - if (sides.hasDirection(DirectionMask.WEST)) { - // West face (-X) - faceVertex(x1, y1, z1, bottomColor) - faceVertex(x1, y1, z2, bottomColor) - faceVertex(x1, y2, z2, topColor) - faceVertex(x1, y2, z1, topColor) - } - if (sides.hasDirection(DirectionMask.UP)) { - // Top face (+Y) - faceVertex(x1, y2, z1, topColor) - faceVertex(x1, y2, z2, topColor) - faceVertex(x2, y2, z2, topColor) - faceVertex(x2, y2, z1, topColor) - } - if (sides.hasDirection(DirectionMask.DOWN)) { - // Bottom face (-Y) - faceVertex(x1, y1, z1, bottomColor) - faceVertex(x2, y1, z1, bottomColor) - faceVertex(x2, y1, z2, bottomColor) - faceVertex(x1, y1, z2, bottomColor) - } - if (sides.hasDirection(DirectionMask.SOUTH)) { - // South face (+Z) - faceVertex(x1, y1, z2, bottomColor) - faceVertex(x2, y1, z2, bottomColor) - faceVertex(x2, y2, z2, topColor) - faceVertex(x1, y2, z2, topColor) - } - if (sides.hasDirection(DirectionMask.NORTH)) { - // North face (-Z) - faceVertex(x1, y1, z1, bottomColor) - faceVertex(x1, y2, z1, topColor) - faceVertex(x2, y2, z1, topColor) - faceVertex(x2, y1, z1, bottomColor) - } - } - - fun filled(box: Box, color: Color, sides: Int = DirectionMask.ALL) = - filled(box, color, color, sides) - - fun filled(box: DynamicAABB, color: Color, sides: Int = DirectionMask.ALL) { - val pair = box.pair ?: return - val prev = pair.first - val curr = pair.second - val tickDelta = mc.partialTicks - val interpolated = Box( - lerp(tickDelta, prev.minX, curr.minX), - lerp(tickDelta, prev.minY, curr.minY), - lerp(tickDelta, prev.minZ, curr.minZ), - lerp(tickDelta, prev.maxX, curr.maxX), - lerp(tickDelta, prev.maxY, curr.maxY), - lerp(tickDelta, prev.maxZ, curr.maxZ) - ) - filled(interpolated, color, sides) - } - - fun filled( - pos: BlockPos, - state: BlockState, - color: Color, - sides: Int = DirectionMask.ALL - ) = runSafe { - val shape = state.getOutlineShape(world, pos) - if (shape.isEmpty) { - filled(Box(pos), color, sides) - } else { - filled(shape.offset(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()), color, sides) - } - } - - fun filled(pos: BlockPos, color: Color, sides: Int = DirectionMask.ALL) = runSafe { - filled(pos, blockState(pos), color, sides) - } - - fun filled(pos: BlockPos, entity: BlockEntity, color: Color, sides: Int = DirectionMask.ALL) = - filled(pos, entity.cachedState, color, sides) - - fun filled(shape: VoxelShape, color: Color, sides: Int = DirectionMask.ALL) { - shape.boundingBoxes.forEach { filled(it, color, color, sides) } - } - - /** Add outline (lines) for a box. */ - fun outline( - box: Box, - bottomColor: Color, - topColor: Color = bottomColor, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) { - val (x1, y1, z1) = toRelative(box.minX, box.minY, box.minZ) - val (x2, y2, z2) = toRelative(box.maxX, box.maxY, box.maxZ) - - val hasEast = sides.hasDirection(DirectionMask.EAST) - val hasWest = sides.hasDirection(DirectionMask.WEST) - val hasUp = sides.hasDirection(DirectionMask.UP) - val hasDown = sides.hasDirection(DirectionMask.DOWN) - val hasSouth = sides.hasDirection(DirectionMask.SOUTH) - val hasNorth = sides.hasDirection(DirectionMask.NORTH) - - // Top edges - if (mode.check(hasUp, hasNorth)) line(x1, y2, z1, x2, y2, z1, topColor, topColor, thickness) - if (mode.check(hasUp, hasSouth)) line(x1, y2, z2, x2, y2, z2, topColor, topColor, thickness) - if (mode.check(hasUp, hasWest)) line(x1, y2, z1, x1, y2, z2, topColor, topColor, thickness) - if (mode.check(hasUp, hasEast)) line(x2, y2, z2, x2, y2, z1, topColor, topColor, thickness) - - // Bottom edges - if (mode.check(hasDown, hasNorth)) line(x1, y1, z1, x2, y1, z1, bottomColor, bottomColor, thickness) - if (mode.check(hasDown, hasSouth)) line(x1, y1, z2, x2, y1, z2, bottomColor, bottomColor, thickness) - if (mode.check(hasDown, hasWest)) line(x1, y1, z1, x1, y1, z2, bottomColor, bottomColor, thickness) - if (mode.check(hasDown, hasEast)) line(x2, y1, z1, x2, y1, z2, bottomColor, bottomColor, thickness) - - // Vertical edges - if (mode.check(hasWest, hasNorth)) line(x1, y2, z1, x1, y1, z1, topColor, bottomColor, thickness) - if (mode.check(hasNorth, hasEast)) line(x2, y2, z1, x2, y1, z1, topColor, bottomColor, thickness) - if (mode.check(hasEast, hasSouth)) line(x2, y2, z2, x2, y1, z2, topColor, bottomColor, thickness) - if (mode.check(hasSouth, hasWest)) line(x1, y2, z2, x1, y1, z2, topColor, bottomColor, thickness) - } - - fun outline( - box: Box, - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = outline(box, color, color, sides, mode, thickness) - - fun outline( - box: DynamicAABB, - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) { - val pair = box.pair ?: return - val prev = pair.first - val curr = pair.second - val tickDelta = mc.partialTicks - val interpolated = Box( - lerp(tickDelta, prev.minX, curr.minX), - lerp(tickDelta, prev.minY, curr.minY), - lerp(tickDelta, prev.minZ, curr.minZ), - lerp(tickDelta, prev.maxX, curr.maxX), - lerp(tickDelta, prev.maxY, curr.maxY), - lerp(tickDelta, prev.maxZ, curr.maxZ) - ) - outline(interpolated, color, sides, mode, thickness) - } - - fun outline( - pos: BlockPos, - state: BlockState, - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { - val shape = state.getOutlineShape(world, pos) - if (shape.isEmpty) { - outline(Box(pos), color, sides, mode, thickness) - } else { - outline(shape.offset(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()), color, sides, mode, thickness) - } - } - - fun outline( - pos: BlockPos, - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { outline(pos, blockState(pos), color, sides, mode, thickness) } - - fun outline( - pos: BlockPos, - entity: BlockEntity, - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { outline(pos, entity.cachedState, color, sides, mode, thickness) } - - fun outline( - shape: VoxelShape, - color: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) { - shape.boundingBoxes.forEach { outline(it, color, sides, mode, thickness) } - } - - /** Add both filled and outline for a box. */ - fun box( - pos: BlockPos, - state: BlockState, - filledColor: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { - filled(pos, state, filledColor, sides) - outline(pos, state, outlineColor, sides, mode, thickness) - } - - fun box( - pos: BlockPos, - filledColor: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { - filled(pos, filledColor, sides) - outline(pos, outlineColor, sides, mode, thickness) - } - - fun box( - box: Box, - filledColor: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) { - filled(box, filledColor, sides) - outline(box, outlineColor, sides, mode, thickness) - } - - fun box( - box: DynamicAABB, - filledColor: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) { - filled(box, filledColor, sides) - outline(box, outlineColor, sides, mode, thickness) - } - - fun box( - entity: BlockEntity, - filled: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { - filled(entity.pos, entity, filled, sides) - outline(entity.pos, entity, outlineColor, sides, mode, thickness) - } - - fun box( - entity: Entity, - filled: Color, - outlineColor: Color, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And, - thickness: Float = lineWidth - ) = runSafe { - filled(entity.boundingBox, filled, sides) - outline(entity.boundingBox, outlineColor, sides, mode, thickness) - } - - private fun faceVertex(x: Float, y: Float, z: Float, color: Color) { - collector.addFaceVertex(x, y, z, color) - } - - private fun line( - x1: Float, - y1: Float, - z1: Float, - x2: Float, - y2: Float, - z2: Float, - color: Color, - width: Float = lineWidth - ) { - line(x1, y1, z1, x2, y2, z2, color, color, width) - } - - private fun line( - x1: Float, - y1: Float, - z1: Float, - x2: Float, - y2: Float, - z2: Float, - color1: Color, - color2: Color, - width: Float = lineWidth - ) { - // Calculate segment vector (dx, dy, dz) - val dx = x2 - x1 - val dy = y2 - y1 - val dz = z2 - z1 - - // Quad-based lines need 4 vertices per segment - // We pass the full vector as 'Normal' so the shader knows where the other end is - collector.addEdgeVertex(x1, y1, z1, color1, dx, dy, dz, width) - collector.addEdgeVertex(x1, y1, z1, color1, dx, dy, dz, width) - collector.addEdgeVertex(x2, y2, z2, color2, dx, dy, dz, width) - collector.addEdgeVertex(x2, y2, z2, color2, dx, dy, dz, width) - } - - /** - * Draw a dashed line between two world positions. - * - * @param start Start position in world coordinates - * @param end End position in world coordinates - * @param color Line color - * @param dashLength Length of each dash in blocks - * @param gapLength Length of each gap in blocks - * @param width Line width (uses default if null) - */ - fun dashedLine( - start: Vec3d, - end: Vec3d, - color: Color, - dashLength: Double = 0.5, - gapLength: Double = 0.25, - width: Float = lineWidth - ) { - val direction = end.subtract(start) - val totalLength = direction.length() - if (totalLength < 0.001) return - - val normalizedDir = direction.normalize() - var pos = 0.0 - var isDash = true - - while (pos < totalLength) { - val segmentLength = if (isDash) dashLength else gapLength - val segmentEnd = min(pos + segmentLength, totalLength) - - if (isDash) { - val segStart = start.add(normalizedDir.multiply(pos)) - val segEnd = start.add(normalizedDir.multiply(segmentEnd)) - - val (x1, y1, z1) = toRelative(segStart.x, segStart.y, segStart.z) - val (x2, y2, z2) = toRelative(segEnd.x, segEnd.y, segEnd.z) - - lineWithWidth(x1, y1, z1, x2, y2, z2, color, width) - } - - pos = segmentEnd - isDash = !isDash - } - } - - /** Draw a dashed outline for a box. */ - fun dashedOutline( - box: Box, - color: Color, - dashLength: Double = 0.5, - gapLength: Double = 0.25, - sides: Int = DirectionMask.ALL, - mode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And - ) { - val hasEast = sides.hasDirection(DirectionMask.EAST) - val hasWest = sides.hasDirection(DirectionMask.WEST) - val hasUp = sides.hasDirection(DirectionMask.UP) - val hasDown = sides.hasDirection(DirectionMask.DOWN) - val hasSouth = sides.hasDirection(DirectionMask.SOUTH) - val hasNorth = sides.hasDirection(DirectionMask.NORTH) - - // Top edges - if (mode.check(hasUp, hasNorth)) - dashedLine( - Vec3d(box.minX, box.maxY, box.minZ), - Vec3d(box.maxX, box.maxY, box.minZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasUp, hasSouth)) - dashedLine( - Vec3d(box.minX, box.maxY, box.maxZ), - Vec3d(box.maxX, box.maxY, box.maxZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasUp, hasWest)) - dashedLine( - Vec3d(box.minX, box.maxY, box.minZ), - Vec3d(box.minX, box.maxY, box.maxZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasUp, hasEast)) - dashedLine( - Vec3d(box.maxX, box.maxY, box.maxZ), - Vec3d(box.maxX, box.maxY, box.minZ), - color, - dashLength, - gapLength - ) - - // Bottom edges - if (mode.check(hasDown, hasNorth)) - dashedLine( - Vec3d(box.minX, box.minY, box.minZ), - Vec3d(box.maxX, box.minY, box.minZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasDown, hasSouth)) - dashedLine( - Vec3d(box.minX, box.minY, box.maxZ), - Vec3d(box.maxX, box.minY, box.maxZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasDown, hasWest)) - dashedLine( - Vec3d(box.minX, box.minY, box.minZ), - Vec3d(box.minX, box.minY, box.maxZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasDown, hasEast)) - dashedLine( - Vec3d(box.maxX, box.minY, box.minZ), - Vec3d(box.maxX, box.minY, box.maxZ), - color, - dashLength, - gapLength - ) - - // Vertical edges - if (mode.check(hasWest, hasNorth)) - dashedLine( - Vec3d(box.minX, box.maxY, box.minZ), - Vec3d(box.minX, box.minY, box.minZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasNorth, hasEast)) - dashedLine( - Vec3d(box.maxX, box.maxY, box.minZ), - Vec3d(box.maxX, box.minY, box.minZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasEast, hasSouth)) - dashedLine( - Vec3d(box.maxX, box.maxY, box.maxZ), - Vec3d(box.maxX, box.minY, box.maxZ), - color, - dashLength, - gapLength - ) - if (mode.check(hasSouth, hasWest)) - dashedLine( - Vec3d(box.minX, box.maxY, box.maxZ), - Vec3d(box.minX, box.minY, box.maxZ), - color, - dashLength, - gapLength - ) - } - - /** Draw a line between two world positions. */ - fun line(start: Vec3d, end: Vec3d, color: Color, width: Float = lineWidth) { - val (x1, y1, z1) = toRelative(start.x, start.y, start.z) - val (x2, y2, z2) = toRelative(end.x, end.y, end.z) - lineWithWidth(x1, y1, z1, x2, y2, z2, color, width) - } - - /** Draw a polyline through a list of points. */ - fun polyline(points: List, color: Color, width: Float = lineWidth) { - if (points.size < 2) return - for (i in 0 until points.size - 1) { - line(points[i], points[i + 1], color, width) - } - } - - /** Draw a dashed polyline through a list of points. */ - fun dashedPolyline( - points: List, - color: Color, - dashLength: Double = 0.5, - gapLength: Double = 0.25, - width: Float = lineWidth - ) { - if (points.size < 2) return - for (i in 0 until points.size - 1) { - dashedLine(points[i], points[i + 1], color, dashLength, gapLength, width) - } - } - - /** - * Draw a quadratic Bezier curve. - * - * @param p0 Start point - * @param p1 Control point - * @param p2 End point - * @param color Line color - * @param segments Number of line segments (higher = smoother) - */ - fun quadraticBezier( - p0: Vec3d, - p1: Vec3d, - p2: Vec3d, - color: Color, - segments: Int = 16, - width: Float = lineWidth - ) { - val points = CurveUtils.quadraticBezierPoints(p0, p1, p2, segments) - polyline(points, color, width) - } - - /** - * Draw a cubic Bezier curve. - * - * @param p0 Start point - * @param p1 First control point - * @param p2 Second control point - * @param p3 End point - * @param color Line color - * @param segments Number of line segments (higher = smoother) - */ - fun cubicBezier( - p0: Vec3d, - p1: Vec3d, - p2: Vec3d, - p3: Vec3d, - color: Color, - segments: Int = 32, - width: Float = lineWidth - ) { - val points = CurveUtils.cubicBezierPoints(p0, p1, p2, p3, segments) - polyline(points, color, width) - } - - /** - * Draw a Catmull-Rom spline that passes through all control points. - * - * @param controlPoints List of points the spline should pass through (minimum 4) - * @param color Line color - * @param segmentsPerSection Segments between each pair of control points - */ - fun catmullRomSpline( - controlPoints: List, - color: Color, - segmentsPerSection: Int = 16, - width: Float = lineWidth - ) { - val points = CurveUtils.catmullRomSplinePoints(controlPoints, segmentsPerSection) - polyline(points, color, width) - } - - /** - * Draw a smooth path through waypoints using Catmull-Rom splines. Handles endpoints - * naturally by mirroring. - * - * @param waypoints List of points to pass through (minimum 2) - * @param color Line color - * @param segmentsPerSection Smoothness (higher = smoother) - */ - fun smoothPath( - waypoints: List, - color: Color, - segmentsPerSection: Int = 16, - width: Float = lineWidth - ) { - val points = CurveUtils.smoothPath(waypoints, segmentsPerSection) - polyline(points, color, width) - } - - /** Draw a dashed Bezier curve. */ - fun dashedCubicBezier( - p0: Vec3d, - p1: Vec3d, - p2: Vec3d, - p3: Vec3d, - color: Color, - segments: Int = 32, - dashLength: Double = 0.5, - gapLength: Double = 0.25, - width: Float = lineWidth - ) { - val points = CurveUtils.cubicBezierPoints(p0, p1, p2, p3, segments) - dashedPolyline(points, color, dashLength, gapLength, width) - } - - /** Draw a dashed smooth path. */ - fun dashedSmoothPath( - waypoints: List, - color: Color, - segmentsPerSection: Int = 16, - dashLength: Double = 0.5, - gapLength: Double = 0.25, - width: Float = lineWidth - ) { - val points = CurveUtils.smoothPath(waypoints, segmentsPerSection) - dashedPolyline(points, color, dashLength, gapLength, width) - } - - /** - * Draw a circle in a plane. - * - * @param center Center of the circle - * @param radius Radius of the circle - * @param normal Normal vector of the plane (determines orientation) - * @param color Line color - * @param segments Number of segments - */ - fun circle( - center: Vec3d, - radius: Double, - normal: Vec3d = Vec3d(0.0, 1.0, 0.0), - color: Color, - segments: Int = 32, - width: Float = lineWidth - ) { - // Create basis vectors perpendicular to normal - val up = - if (kotlin.math.abs(normal.y) < 0.99) Vec3d(0.0, 1.0, 0.0) - else Vec3d(1.0, 0.0, 0.0) - val u = normal.crossProduct(up).normalize() - val v = u.crossProduct(normal).normalize() - - val points = - (0..segments).map { i -> - val angle = 2.0 * Math.PI * i / segments - val x = kotlin.math.cos(angle) * radius - val y = kotlin.math.sin(angle) * radius - center.add(u.multiply(x)).add(v.multiply(y)) - } - - polyline(points, color, width) - } - - private fun lineWithWidth( - x1: Float, - y1: Float, - z1: Float, - x2: Float, - y2: Float, - z2: Float, - color: Color, - width: Float - ) { - val dx = x2 - x1 - val dy = y2 - y1 - val dz = z2 - z1 - collector.addEdgeVertex(x1, y1, z1, color, dx, dy, dz, width) - collector.addEdgeVertex(x1, y1, z1, color, dx, dy, dz, width) - collector.addEdgeVertex(x2, y2, z2, color, dx, dy, dz, width) - collector.addEdgeVertex(x2, y2, z2, color, dx, dy, dz, width) - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt index c82347817..249622998 100644 --- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt +++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt @@ -19,10 +19,13 @@ package com.lambda.graphics.mc import com.mojang.blaze3d.buffers.GpuBuffer import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.textures.GpuTextureView import com.mojang.blaze3d.vertex.VertexFormat +import com.mojang.blaze3d.vertex.VertexFormatElement import net.minecraft.client.render.BufferBuilder import net.minecraft.client.render.VertexFormats import net.minecraft.client.util.BufferAllocator +import org.lwjgl.system.MemoryUtil import java.awt.Color import java.util.concurrent.ConcurrentLinkedDeque @@ -35,19 +38,56 @@ import java.util.concurrent.ConcurrentLinkedDeque class RegionVertexCollector { val faceVertices = ConcurrentLinkedDeque() val edgeVertices = ConcurrentLinkedDeque() + val textVertices = ConcurrentLinkedDeque() + + // Screen-space vertex collections + val screenFaceVertices = ConcurrentLinkedDeque() + val screenEdgeVertices = ConcurrentLinkedDeque() + val screenTextVertices = ConcurrentLinkedDeque() /** Face vertex data (position + color). */ data class FaceVertex( - val x: Float, - val y: Float, - val z: Float, - val r: Int, - val g: Int, - val b: Int, - val a: Int + val x: Float, val y: Float, val z: Float, + val r: Int, val g: Int, val b: Int, val a: Int ) - /** Edge vertex data (position + color + normal + line width). */ + /** + * Text vertex data for SDF billboard text rendering. + * Uses POSITION_TEXTURE_COLOR_ANCHOR_SDF format for GPU-based billboarding with embedded style. + * + * @param localX Local glyph offset X (before billboard transform) + * @param localY Local glyph offset Y (before billboard transform) + * @param u Texture U coordinate + * @param v Texture V coordinate + * @param r Red color component + * @param g Green color component + * @param b Blue color component + * @param a Alpha component (encodes layer type) + * @param anchorX Camera-relative anchor position X + * @param anchorY Camera-relative anchor position Y + * @param anchorZ Camera-relative anchor position Z + * @param scale Text scale + * @param billboardFlag 0 = billboard towards camera, non-zero = fixed rotation already applied + * @param outlineWidth SDF outline width (0 = no outline) + * @param glowRadius SDF glow radius (0 = no glow) + * @param shadowSoftness SDF shadow softness (0 = no shadow) + * @param threshold SDF edge threshold (default 0.5) + */ + data class TextVertex( + val localX: Float, val localY: Float, + val u: Float, val v: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + val anchorX: Float, val anchorY: Float, val anchorZ: Float, + val scale: Float, + val billboardFlag: Float, + // SDF style params (replaces SDFParams uniform) + val outlineWidth: Float = 0f, + val glowRadius: Float = 0f, + val shadowSoftness: Float = 0f, + val threshold: Float = 0.5f + ) + + /** Edge vertex data (position + color + normal + line width + dash style). */ data class EdgeVertex( val x: Float, val y: Float, @@ -59,15 +99,239 @@ class RegionVertexCollector { val nx: Float, val ny: Float, val nz: Float, - val lineWidth: Float + val lineWidth: Float, + // Dash style parameters (0 = solid line) + val dashLength: Float = 0f, + val gapLength: Float = 0f, + val dashOffset: Float = 0f, + val animationSpeed: Float = 0f // 0 = no animation + ) + + // ============================================================================ + // Screen-Space Vertex Types + // ============================================================================ + + /** Screen-space face vertex data (2D position + color + layer). */ + data class ScreenFaceVertex( + val x: Float, val y: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + val layer: Float // Depth for layering (higher = on top) + ) + + /** Screen-space edge vertex data (2D position + color + direction + width + dash + layer). */ + data class ScreenEdgeVertex( + val x: Float, val y: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + val dx: Float, val dy: Float, + val lineWidth: Float, + // Dash style parameters (0 = solid line) + val dashLength: Float = 0f, + val gapLength: Float = 0f, + val dashOffset: Float = 0f, + val animationSpeed: Float = 0f, + val layer: Float = 0f // Depth for layering (higher = on top) + ) + + /** + * Screen-space text vertex data with SDF style params and layer. + * Uses SCREEN_TEXT_SDF_FORMAT (position + UV + color + style + layer). + * + * @param x Screen-space X position + * @param y Screen-space Y position + * @param u Texture U coordinate + * @param v Texture V coordinate + * @param r Red color component + * @param g Green color component + * @param b Blue color component + * @param a Alpha component (encodes layer type) + * @param outlineWidth SDF outline width (0 = no outline) + * @param glowRadius SDF glow radius (0 = no glow) + * @param shadowSoftness SDF shadow softness (0 = no shadow) + * @param threshold SDF edge threshold (default 0.5) + * @param layer Depth for layering (higher = on top) + */ + data class ScreenTextVertex( + val x: Float, val y: Float, + val u: Float, val v: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + // SDF style params (replaces SDFParams uniform) + val outlineWidth: Float = 0f, + val glowRadius: Float = 0f, + val shadowSoftness: Float = 0f, + val threshold: Float = 0.5f, + val layer: Float = 0f // Depth for layering (higher = on top) + ) + + // ============================================================================ + // Image Vertex Types + // ============================================================================ + + /** + * Screen-space image vertex data with overlay support. + * Uses SCREEN_IMAGE_FORMAT (position + UV + color + overlayUV + layer). + * + * @param x Screen-space X position + * @param y Screen-space Y position + * @param u Main texture U coordinate + * @param v Main texture V coordinate + * @param r Red tint component + * @param g Green tint component + * @param b Blue tint component + * @param a Alpha component + * @param overlayU Overlay texture U coordinate + * @param overlayV Overlay texture V coordinate + * @param hasOverlay 1.0 if overlay should be rendered, 0.0 otherwise + * @param layer Depth for layering (higher = on top) + */ + data class ScreenImageVertex( + val x: Float, val y: Float, + val u: Float, val v: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + val overlayU: Float, val overlayV: Float, + val hasOverlay: Float, + val diffuseAmount: Float, + val layer: Float + ) + + /** + * World-space image vertex data with billboard support and overlay. + * Uses WORLD_IMAGE_FORMAT (position + UV + color + anchor + billboard + overlayUV). + * + * @param localX Local offset X (before billboard transform) + * @param localY Local offset Y (before billboard transform) + * @param u Main texture U coordinate + * @param v Main texture V coordinate + * @param r Red tint component + * @param g Green tint component + * @param b Blue tint component + * @param a Alpha component + * @param anchorX Camera-relative anchor X + * @param anchorY Camera-relative anchor Y + * @param anchorZ Camera-relative anchor Z + * @param scale Image scale + * @param billboardFlag 0 = billboard towards camera, non-zero = fixed rotation + * @param overlayU Overlay texture U coordinate + * @param overlayV Overlay texture V coordinate + * @param hasOverlay 1.0 if overlay should be rendered, 0.0 otherwise + */ + data class WorldImageVertex( + val localX: Float, val localY: Float, + val u: Float, val v: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + val anchorX: Float, val anchorY: Float, val anchorZ: Float, + val scale: Float, + val billboardFlag: Float, + val overlayU: Float, val overlayV: Float, + val hasOverlay: Float, + val diffuseAmount: Float + ) + + /** + * Model vertex data for generic 3D models. + * Uses WORLD_MODEL_FORMAT (position + color + UV0 + overlay + light + lightDir + light1Dir + normal + edgeData). + * + * @param x World X + * @param y World Y + * @param z World Z + * @param u Texture U + * @param v Texture V + * @param r Red + * @param g Green + * @param b Blue + * @param a Alpha + * @param overlayU Overlay U + * @param overlayV Overlay V + * @param hasOverlay 1.0 if overlay active + * @param diffuseAmount Amount of diffuse shading (0 = lightmap only, 1 = full diffuse) + * @param light Packed lightmap coordinates + * @param lx Primary light direction X (pre-transformed for ITEMS_FLAT) + * @param ly Primary light direction Y + * @param lz Primary light direction Z + * @param l1x Secondary/fill light direction X + * @param l1y Secondary/fill light direction Y + * @param l1z Secondary/fill light direction Z + * @param nx Normal X + * @param ny Normal Y + * @param nz Normal Z + * @param edgeX Edge coordinate for AA + * @param edgeY Edge coordinate for AA + */ + data class ModelVertex( + val x: Float, val y: Float, val z: Float, + val u: Float, val v: Float, + val r: Int, val g: Int, val b: Int, val a: Int, + val overlayU: Float, val overlayV: Float, val hasOverlay: Float, + val diffuseAmount: Float, + val light: Int, + val lx: Float, val ly: Float, val lz: Float, + val l1x: Float, val l1y: Float, val l1z: Float, + val nx: Float, val ny: Float, val nz: Float, + val edgeX: Float, val edgeY: Float ) + /** + * Key for image batches - combines texture and filter mode. + * Batches with the same texture but different filter modes are separate. + */ + data class ImageBatchKey( + val textureView: com.mojang.blaze3d.textures.GpuTextureView, + val useNearestFilter: Boolean + ) + + // Image vertex collections - keyed by texture + filter mode for batching + // Each unique key gets its own list of vertices, rendered as separate draw calls + private val screenImageBatches = java.util.concurrent.ConcurrentHashMap>() + private val worldImageBatches = java.util.concurrent.ConcurrentHashMap>() + private val modelBatches = java.util.concurrent.ConcurrentHashMap>() + private val screenModelBatches = java.util.concurrent.ConcurrentHashMap>() + + /** + * Add screen image vertices for a specific texture. + * @param texture The GPU texture view + * @param vertices The vertices to add + * @param useNearestFilter If true, use NEAREST filtering for pixel-perfect rendering + */ + fun addScreenImageVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) { + val key = ImageBatchKey(texture, useNearestFilter) + screenImageBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices) + } + + /** + * Add world image vertices for a specific texture. + * @param texture The GPU texture view + * @param vertices The vertices to add + * @param useNearestFilter If true, use NEAREST filtering for pixel-perfect rendering + */ + fun addWorldImageVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) { + val key = ImageBatchKey(texture, useNearestFilter) + worldImageBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices) + } + + /** + * Add model vertices for a specific texture. + * @param texture The GPU texture view + * @param vertices The vertices to add + * @param useNearestFilter If true, use NEAREST filtering + */ + fun addModelVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) { + val key = ImageBatchKey(texture, useNearestFilter) + modelBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices) + } + + /** + * Add screen model vertices for a specific texture. + */ + fun addScreenModelVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) { + val key = ImageBatchKey(texture, useNearestFilter) + screenModelBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices) + } + /** Add a face vertex. */ fun addFaceVertex(x: Float, y: Float, z: Float, color: Color) { faceVertices.add(FaceVertex(x, y, z, color.red, color.green, color.blue, color.alpha)) } - /** Add an edge vertex. */ + /** Add an edge vertex (solid line). */ fun addEdgeVertex( x: Float, y: Float, @@ -83,15 +347,127 @@ class RegionVertexCollector { ) } + /** Add an edge vertex with dash style. */ + fun addEdgeVertex( + x: Float, + y: Float, + z: Float, + color: Color, + nx: Float, + ny: Float, + nz: Float, + lineWidth: Float, + dashStyle: LineDashStyle? + ) { + if (dashStyle == null) { + addEdgeVertex(x, y, z, color, nx, ny, nz, lineWidth) + } else { + edgeVertices.add( + EdgeVertex( + x, y, z, + color.red, color.green, color.blue, color.alpha, + nx, ny, nz, + lineWidth, + dashStyle.dashLength, + dashStyle.gapLength, + dashStyle.offset, + if (dashStyle.animated) dashStyle.animationSpeed else 0f + ) + ) + } + } + + /** + * Add a billboard text vertex. + * + * @param localX Local glyph offset X (before billboard transform) + * @param localY Local glyph offset Y (before billboard transform) + * @param u Texture U coordinate + * @param v Texture V coordinate + * @param r Red color component + * @param g Green color component + * @param b Blue color component + * @param a Alpha component (encodes layer type) + * @param anchorX Camera-relative anchor X + * @param anchorY Camera-relative anchor Y + * @param anchorZ Camera-relative anchor Z + * @param scale Text scale + * @param billboard True = auto-billboard towards camera, False = fixed rotation (offset already transformed) + */ + fun addTextVertex( + localX: Float, localY: Float, + u: Float, v: Float, + r: Int, g: Int, b: Int, a: Int, + anchorX: Float, anchorY: Float, anchorZ: Float, + scale: Float, + billboard: Boolean + ) { + textVertices.add(TextVertex( + localX, localY, u, v, r, g, b, a, + anchorX, anchorY, anchorZ, scale, + if (billboard) 0f else 1f + )) + } + + // ============================================================================ + // Screen-Space Vertex Add Methods + // ============================================================================ + + /** Add a screen-space face vertex with layer for draw order. */ + fun addScreenFaceVertex(x: Float, y: Float, color: Color, layer: Float) { + screenFaceVertices.add(ScreenFaceVertex(x, y, color.red, color.green, color.blue, color.alpha, layer)) + } + + /** Add a screen-space edge vertex (solid line) with layer for draw order. */ + fun addScreenEdgeVertex(x: Float, y: Float, color: Color, dx: Float, dy: Float, lineWidth: Float, layer: Float) { + screenEdgeVertices.add(ScreenEdgeVertex(x, y, color.red, color.green, color.blue, color.alpha, dx, dy, lineWidth, layer = layer)) + } + + /** Add a screen-space edge vertex with dash style and layer for draw order. */ + fun addScreenEdgeVertex( + x: Float, y: Float, + color: Color, + dx: Float, dy: Float, + lineWidth: Float, + dashStyle: LineDashStyle?, + layer: Float + ) { + if (dashStyle == null) { + addScreenEdgeVertex(x, y, color, dx, dy, lineWidth, layer) + } else { + screenEdgeVertices.add( + ScreenEdgeVertex( + x, y, + color.red, color.green, color.blue, color.alpha, + dx, dy, + lineWidth, + dashStyle.dashLength, + dashStyle.gapLength, + dashStyle.offset, + if (dashStyle.animated) dashStyle.animationSpeed else 0f, + layer + ) + ) + } + } + + /** Add a screen-space text vertex with layer for draw order. */ + fun addScreenTextVertex(x: Float, y: Float, u: Float, v: Float, r: Int, g: Int, b: Int, a: Int, layer: Float) { + screenTextVertices.add(ScreenTextVertex(x, y, u, v, r, g, b, a, layer = layer)) + } + /** * Upload collected data to GPU buffers. Must be called on the main/render thread. * - * @return Pair of (faceBuffer, edgeBuffer) and their index counts, or null if no data + * @return UploadResult containing face, edge, and text buffers with index counts */ fun upload(): UploadResult { val faces = uploadFaces() val edges = uploadEdges() - return UploadResult(faces, edges) + val text = uploadText() + val models = uploadModelBatches() + val images = uploadWorldImageBatches() + return UploadResult(faces, edges, text, models, images) } private fun uploadFaces(): BufferResult { @@ -133,19 +509,41 @@ class RegionVertexCollector { edgeVertices.clear() var result: BufferResult? = null - BufferAllocator(vertices.size * 32).use { allocator -> + // Increased buffer size to accommodate the new dash vec3 (3 floats = 12 bytes extra) + BufferAllocator(vertices.size * 48).use { allocator -> val builder = BufferBuilder( allocator, VertexFormat.DrawMode.QUADS, - VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH + LambdaVertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH_DASH ) vertices.forEach { v -> builder.vertex(v.x, v.y, v.z) .color(v.r, v.g, v.b, v.a) - .normal(v.nx, v.ny, v.nz) - .lineWidth(v.lineWidth) + + // Write Normal as 3 floats (NOT using .normal() which writes bytes) + val normalPointer = builder.beginElement(LambdaVertexFormats.NORMAL_FLOAT) + if (normalPointer != -1L) { + MemoryUtil.memPutFloat(normalPointer, v.nx) + MemoryUtil.memPutFloat(normalPointer + 4L, v.ny) + MemoryUtil.memPutFloat(normalPointer + 8L, v.nz) + } + + // Write LineWidth as float + val widthPointer = builder.beginElement(LambdaVertexFormats.LINE_WIDTH_FLOAT) + if (widthPointer != -1L) { + MemoryUtil.memPutFloat(widthPointer, v.lineWidth) + } + + // Write dash data using access-widened beginElement (vec4) + val dashPointer = builder.beginElement(LambdaVertexFormats.DASH_ELEMENT) + if (dashPointer != -1L) { + MemoryUtil.memPutFloat(dashPointer, v.dashLength) + MemoryUtil.memPutFloat(dashPointer + 4L, v.gapLength) + MemoryUtil.memPutFloat(dashPointer + 8L, v.dashOffset) + MemoryUtil.memPutFloat(dashPointer + 12L, v.animationSpeed) + } } builder.endNullable()?.let { built -> @@ -163,6 +561,519 @@ class RegionVertexCollector { return result ?: BufferResult(null, 0) } + private fun uploadText(): BufferResult { + if (textVertices.isEmpty()) return BufferResult(null, 0) + + val vertices = textVertices.toList() + textVertices.clear() + + var result: BufferResult? = null + // POSITION_TEXTURE_COLOR_ANCHOR_SDF: 12 + 8 + 4 + 12 + 8 + 16 = 60 bytes per vertex + BufferAllocator(vertices.size * 64).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF + ) + + vertices.forEach { v -> + // Position stores local glyph offset (z unused, set to 0) + builder.vertex(v.localX, v.localY, 0f) + .texture(v.u, v.v) + .color(v.r, v.g, v.b, v.a) + + // Write Anchor position (camera-relative world pos) + val anchorPointer = builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT) + if (anchorPointer != -1L) { + MemoryUtil.memPutFloat(anchorPointer, v.anchorX) + MemoryUtil.memPutFloat(anchorPointer + 4L, v.anchorY) + MemoryUtil.memPutFloat(anchorPointer + 8L, v.anchorZ) + } + + // Write Billboard data (scale, billboardFlag) + val billboardPointer = builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT) + if (billboardPointer != -1L) { + MemoryUtil.memPutFloat(billboardPointer, v.scale) + MemoryUtil.memPutFloat(billboardPointer + 4L, v.billboardFlag) + } + + // Write SDF style params (outlineWidth, glowRadius, shadowSoftness, threshold) + val sdfPointer = builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT) + if (sdfPointer != -1L) { + MemoryUtil.memPutFloat(sdfPointer, v.outlineWidth) + MemoryUtil.memPutFloat(sdfPointer + 4L, v.glowRadius) + MemoryUtil.memPutFloat(sdfPointer + 8L, v.shadowSoftness) + MemoryUtil.memPutFloat(sdfPointer + 12L, v.threshold) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda ESP Text Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = BufferResult(buffer, built.drawParameters.indexCount()) + built.close() + } + } + return result ?: BufferResult(null, 0) + } + + // ============================================================================ + // Screen-Space Upload Methods + // ============================================================================ + + private fun uploadScreenFaces(): BufferResult { + if (screenFaceVertices.isEmpty()) return BufferResult(null, 0) + + val vertices = screenFaceVertices.toList() + screenFaceVertices.clear() + + var result: BufferResult? = null + // SCREEN_FACE_FORMAT: 12 + 4 + 4 = 20 bytes per vertex + BufferAllocator(vertices.size * 24).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.SCREEN_FACE_FORMAT + ) + + // For screen-space: use x, y, with z = 0, plus layer + vertices.forEach { v -> + builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a) + + // Write layer for draw order + val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT) + if (layerPointer != -1L) { + MemoryUtil.memPutFloat(layerPointer, v.layer) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Screen Face Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = BufferResult(buffer, built.drawParameters.indexCount()) + built.close() + } + } + return result ?: BufferResult(null, 0) + } + + private fun uploadScreenEdges(): BufferResult { + if (screenEdgeVertices.isEmpty()) return BufferResult(null, 0) + + val vertices = screenEdgeVertices.toList() + screenEdgeVertices.clear() + + var result: BufferResult? = null + // Position (12) + Color (4) + Direction (8) + Width (4) + Dash (16) + Layer (4) = 48 bytes + BufferAllocator(vertices.size * 52).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.SCREEN_LINE_FORMAT + ) + + vertices.forEach { v -> + builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a) + + // Write direction (for calculating perpendicular offset in shader) + val dirPointer = builder.beginElement(LambdaVertexFormats.DIRECTION_2D_ELEMENT) + if (dirPointer != -1L) { + MemoryUtil.memPutFloat(dirPointer, v.dx) + MemoryUtil.memPutFloat(dirPointer + 4L, v.dy) + } + + // Write line width + val widthPointer = builder.beginElement(LambdaVertexFormats.LINE_WIDTH_FLOAT) + if (widthPointer != -1L) { + MemoryUtil.memPutFloat(widthPointer, v.lineWidth) + } + + // Write dash data + val dashPointer = builder.beginElement(LambdaVertexFormats.DASH_ELEMENT) + if (dashPointer != -1L) { + MemoryUtil.memPutFloat(dashPointer, v.dashLength) + MemoryUtil.memPutFloat(dashPointer + 4L, v.gapLength) + MemoryUtil.memPutFloat(dashPointer + 8L, v.dashOffset) + MemoryUtil.memPutFloat(dashPointer + 12L, v.animationSpeed) + } + + // Write layer for draw order + val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT) + if (layerPointer != -1L) { + MemoryUtil.memPutFloat(layerPointer, v.layer) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Screen Edge Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = BufferResult(buffer, built.drawParameters.indexCount()) + built.close() + } + } + return result ?: BufferResult(null, 0) + } + + private fun uploadScreenText(): BufferResult { + if (screenTextVertices.isEmpty()) return BufferResult(null, 0) + + val vertices = screenTextVertices.toList() + screenTextVertices.clear() + + var result: BufferResult? = null + // SCREEN_TEXT_SDF_FORMAT: 12 + 8 + 4 + 16 + 4 = 44 bytes per vertex + BufferAllocator(vertices.size * 48).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT + ) + + // Screen text: position is already final screen coordinates + vertices.forEach { v -> + builder.vertex(v.x, v.y, 0f) + .texture(v.u, v.v) + .color(v.r, v.g, v.b, v.a) + + // Write SDF style params (outlineWidth, glowRadius, shadowSoftness, threshold) + val sdfPointer = builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT) + if (sdfPointer != -1L) { + MemoryUtil.memPutFloat(sdfPointer, v.outlineWidth) + MemoryUtil.memPutFloat(sdfPointer + 4L, v.glowRadius) + MemoryUtil.memPutFloat(sdfPointer + 8L, v.shadowSoftness) + MemoryUtil.memPutFloat(sdfPointer + 12L, v.threshold) + } + + // Write layer for draw order + val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT) + if (layerPointer != -1L) { + MemoryUtil.memPutFloat(layerPointer, v.layer) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Screen Text Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + result = BufferResult(buffer, built.drawParameters.indexCount()) + built.close() + } + } + return result ?: BufferResult(null, 0) + } + + /** + * Upload screen-space data to GPU buffers. + * + * @return ScreenUploadResult containing screen-space face, edge, text, and image buffers + */ + fun uploadScreen(): ScreenUploadResult { + val faces = uploadScreenFaces() + val edges = uploadScreenEdges() + val text = uploadScreenText() + val images = uploadScreenImageBatches() + val models = uploadScreenModelBatches() + return ScreenUploadResult(faces, edges, text, images, models) + } + + /** + * Result for a single texture batch - buffer, index count, and filter mode. + */ + data class TextureBatchResult( + val textureView: com.mojang.blaze3d.textures.GpuTextureView, + val buffer: GpuBuffer, + val indexCount: Int, + val useNearestFilter: Boolean = false + ) + + private fun uploadScreenImageBatches(): List { + if (screenImageBatches.isEmpty()) return emptyList() + + val results = mutableListOf() + + screenImageBatches.forEach { (batchKey, vertexDeque) -> + val vertices = vertexDeque.toList() + vertexDeque.clear() + if (vertices.isEmpty()) return@forEach + + // SCREEN_IMAGE_FORMAT: 12 + 8 + 4 + 12 + 4 = 40 bytes per vertex + BufferAllocator(vertices.size * 44).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.SCREEN_IMAGE_FORMAT + ) + + vertices.forEach { v -> + builder.vertex(v.x, v.y, 0f) + .texture(v.u, v.v) + .color(v.r, v.g, v.b, v.a) + + // Write overlay UV data (overlayU, overlayV, hasOverlay, diffuseAmount) + val overlayPointer = builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT) + if (overlayPointer != -1L) { + MemoryUtil.memPutFloat(overlayPointer, v.overlayU) + MemoryUtil.memPutFloat(overlayPointer + 4L, v.overlayV) + MemoryUtil.memPutFloat(overlayPointer + 8L, v.hasOverlay) + MemoryUtil.memPutFloat(overlayPointer + 12L, v.diffuseAmount) + } + + // Write layer for draw order + val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT) + if (layerPointer != -1L) { + MemoryUtil.memPutFloat(layerPointer, v.layer) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Screen Image Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter)) + built.close() + } + } + } + screenImageBatches.clear() + return results + } + + fun uploadWorldImageBatches(): List { + if (worldImageBatches.isEmpty()) return emptyList() + + val results = mutableListOf() + + worldImageBatches.forEach { (batchKey, vertexDeque) -> + val vertices = vertexDeque.toList() + vertexDeque.clear() + if (vertices.isEmpty()) return@forEach + + // WORLD_IMAGE_FORMAT: 12 + 8 + 4 + 12 + 8 + 12 = 56 bytes per vertex + BufferAllocator(vertices.size * 60).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.WORLD_IMAGE_FORMAT + ) + + vertices.forEach { v -> + builder.vertex(v.localX, v.localY, 0f) + .texture(v.u, v.v) + .color(v.r, v.g, v.b, v.a) + + // Write Anchor position (camera-relative world pos) + val anchorPointer = builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT) + if (anchorPointer != -1L) { + MemoryUtil.memPutFloat(anchorPointer, v.anchorX) + MemoryUtil.memPutFloat(anchorPointer + 4L, v.anchorY) + MemoryUtil.memPutFloat(anchorPointer + 8L, v.anchorZ) + } + + // Write Billboard data (scale, billboardFlag) + val billboardPointer = builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT) + if (billboardPointer != -1L) { + MemoryUtil.memPutFloat(billboardPointer, v.scale) + MemoryUtil.memPutFloat(billboardPointer + 4L, v.billboardFlag) + } + + // Write overlay UV data (overlayU, overlayV, hasOverlay, diffuseAmount) + val overlayPointer = builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT) + if (overlayPointer != -1L) { + MemoryUtil.memPutFloat(overlayPointer, v.overlayU) + MemoryUtil.memPutFloat(overlayPointer + 4L, v.overlayV) + MemoryUtil.memPutFloat(overlayPointer + 8L, v.hasOverlay) + MemoryUtil.memPutFloat(overlayPointer + 12L, v.diffuseAmount) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda World Image Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter)) + built.close() + } + } + } + worldImageBatches.clear() + return results + } + + fun uploadModelBatches(): List { + if (modelBatches.isEmpty()) return emptyList() + + val results = mutableListOf() + + modelBatches.forEach { (batchKey, vertexDeque) -> + val vertices = vertexDeque.toList() + vertexDeque.clear() + if (vertices.isEmpty()) return@forEach + + // WORLD_MODEL_FORMAT: 12 + 4 + 8 + 16 + 4 + 12 + 12 + 12 + 8 = 88 bytes per vertex + BufferAllocator(vertices.size * 88).use { allocator -> + val builder = BufferBuilder( + allocator, + VertexFormat.DrawMode.QUADS, + LambdaVertexFormats.WORLD_MODEL_FORMAT + ) + + vertices.forEach { v -> + builder.vertex(v.x, v.y, v.z) + .color(v.r, v.g, v.b, v.a) + .texture(v.u, v.v) + + // Write Overlay UV + val overlayPointer = builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT) + if (overlayPointer != -1L) { + MemoryUtil.memPutFloat(overlayPointer, v.overlayU) + MemoryUtil.memPutFloat(overlayPointer + 4L, v.overlayV) + MemoryUtil.memPutFloat(overlayPointer + 8L, v.hasOverlay) + MemoryUtil.memPutFloat(overlayPointer + 12L, v.diffuseAmount) + } + + // Write Light (UV2) - 2 shorts + val lightPointer = builder.beginElement(VertexFormatElement.UV2) + if (lightPointer != -1L) { + MemoryUtil.memPutShort(lightPointer, (v.light and 0xFFFF).toShort()) + MemoryUtil.memPutShort(lightPointer + 2L, ((v.light shr 16) and 0xFFFF).toShort()) + } + + // Write LightDir (primary light) + val lightDirPointer = builder.beginElement(LambdaVertexFormats.LIGHT_DIR_ELEMENT) + if (lightDirPointer != -1L) { + MemoryUtil.memPutFloat(lightDirPointer, v.lx) + MemoryUtil.memPutFloat(lightDirPointer + 4L, v.ly) + MemoryUtil.memPutFloat(lightDirPointer + 8L, v.lz) + } + + // Write Light1Dir (fill light) + val light1DirPointer = builder.beginElement(LambdaVertexFormats.LIGHT1_DIR_ELEMENT) + if (light1DirPointer != -1L) { + MemoryUtil.memPutFloat(light1DirPointer, v.l1x) + MemoryUtil.memPutFloat(light1DirPointer + 4L, v.l1y) + MemoryUtil.memPutFloat(light1DirPointer + 8L, v.l1z) + } + + // Write Normal (Float) + val normalPointer = builder.beginElement(LambdaVertexFormats.NORMAL_FLOAT) + if (normalPointer != -1L) { + MemoryUtil.memPutFloat(normalPointer, v.nx) + MemoryUtil.memPutFloat(normalPointer + 4L, v.ny) + MemoryUtil.memPutFloat(normalPointer + 8L, v.nz) + } + + // Write EdgeData (vec2) + val edgePointer = builder.beginElement(LambdaVertexFormats.EDGE_DATA_ELEMENT) + if (edgePointer != -1L) { + MemoryUtil.memPutFloat(edgePointer, v.edgeX) + MemoryUtil.memPutFloat(edgePointer + 4L, v.edgeY) + } + } + + builder.endNullable()?.let { built -> + val gpuDevice = RenderSystem.getDevice() + val buffer = gpuDevice.createBuffer( + { "Lambda Model Buffer" }, + GpuBuffer.USAGE_VERTEX, + built.buffer + ) + results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter)) + built.close() + } + } + } + modelBatches.clear() + return results + } + + private fun uploadScreenModelBatches(): List { + if (screenModelBatches.isEmpty()) return emptyList() + val results = mutableListOf() + screenModelBatches.forEach { (batchKey, vertexDeque) -> + val vertices = vertexDeque.toList() + vertexDeque.clear() + if (vertices.isEmpty()) return@forEach + // 88 bytes per vertex (added Light1Dir) + BufferAllocator(vertices.size * 88).use { allocator -> + val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, LambdaVertexFormats.WORLD_MODEL_FORMAT) + vertices.forEach { v -> + builder.vertex(v.x, v.y, v.z).color(v.r, v.g, v.b, v.a).texture(v.u, v.v) + builder.beginElement(LambdaVertexFormats.OVERLAY_UV_ELEMENT).let { p -> + if (p != -1L) { + MemoryUtil.memPutFloat(p, v.overlayU) + MemoryUtil.memPutFloat(p + 4, v.overlayV) + MemoryUtil.memPutFloat(p + 8, v.hasOverlay) + MemoryUtil.memPutFloat(p + 12, v.diffuseAmount) + } + } + builder.beginElement(VertexFormatElement.UV2).let { p -> + if (p != -1L) { + MemoryUtil.memPutShort(p, (v.light and 0xFFFF).toShort()) + MemoryUtil.memPutShort(p + 2, ((v.light shr 16) and 0xFFFF).toShort()) + } + } + builder.beginElement(LambdaVertexFormats.LIGHT_DIR_ELEMENT).let { p -> + if (p != -1L) { + MemoryUtil.memPutFloat(p, v.lx) + MemoryUtil.memPutFloat(p + 4, v.ly) + MemoryUtil.memPutFloat(p + 8, v.lz) + } + } + builder.beginElement(LambdaVertexFormats.LIGHT1_DIR_ELEMENT).let { p -> + if (p != -1L) { + MemoryUtil.memPutFloat(p, v.l1x) + MemoryUtil.memPutFloat(p + 4, v.l1y) + MemoryUtil.memPutFloat(p + 8, v.l1z) + } + } + builder.beginElement(LambdaVertexFormats.NORMAL_FLOAT).let { p -> + if (p != -1L) { + MemoryUtil.memPutFloat(p, v.nx) + MemoryUtil.memPutFloat(p + 4, v.ny) + MemoryUtil.memPutFloat(p + 8, v.nz) + } + } + builder.beginElement(LambdaVertexFormats.EDGE_DATA_ELEMENT).let { p -> + if (p != -1L) { + MemoryUtil.memPutFloat(p, v.edgeX) + MemoryUtil.memPutFloat(p + 4, v.edgeY) + } + } + } + builder.endNullable()?.let { built -> + val buffer = RenderSystem.getDevice().createBuffer({ "Lambda Screen Model Buffer" }, GpuBuffer.USAGE_VERTEX, built.buffer) + results.add(TextureBatchResult(batchKey.textureView, buffer, built.drawParameters.indexCount(), batchKey.useNearestFilter)) + built.close() + } + } + } + screenModelBatches.clear() + return results + } + data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int) - data class UploadResult(val faces: BufferResult?, val edges: BufferResult?) + data class UploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null, val models: List = emptyList(), val images: List = emptyList()) + data class ScreenUploadResult(val faces: BufferResult?, val edges: BufferResult?, val text: BufferResult? = null, val images: List = emptyList(), val models: List = emptyList()) } + diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt new file mode 100644 index 000000000..3bfd2ec22 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt @@ -0,0 +1,1841 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.graphics.text.FontHandler +import com.lambda.graphics.text.SDFFontAtlas +import com.lambda.graphics.texture.LambdaImageAtlas +import com.lambda.graphics.util.DirectionMask +import com.lambda.graphics.util.DirectionMask.hasDirection +import com.lambda.util.BlockUtils.blockState +import net.minecraft.block.BlockState +import net.minecraft.client.font.TextRenderer +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.item.ItemRenderState +import net.minecraft.client.render.command.OrderedRenderCommandQueue +import net.minecraft.client.render.VertexConsumer +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.command.ModelCommandRenderer +import net.minecraft.client.render.entity.state.EntityRenderState +import net.minecraft.client.render.model.BakedQuad +import net.minecraft.client.render.model.BlockModelPart +import net.minecraft.client.render.model.BlockStateModel +import net.minecraft.client.render.state.CameraRenderState +import net.minecraft.client.texture.Sprite +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import net.minecraft.item.ItemDisplayContext +import net.minecraft.item.ItemStack +import net.minecraft.text.OrderedText +import net.minecraft.text.Text +import net.minecraft.util.math.random.Random +import org.joml.Matrix4f +import org.joml.Quaternionf +import org.joml.Vector3f +import org.joml.Vector4f +import java.awt.Color + +@DslMarker +annotation class RenderDsl + +@Suppress("unused") +@RenderDsl +class RenderBuilder(private val cameraPos: Vec3d) { + val collector = RegionVertexCollector() + + /** Track font atlas for this builder (for rendering) */ + var fontAtlas: SDFFontAtlas? = null + private set + + // Style grouping maps removed - style is now embedded in each text vertex + + // ============================================================================ + // Screen-Space Layer Tracking + // ============================================================================ + // Layer depth for screen-space ordering. + // With orthographic projection (near=-1000, far=1000) and LEQUAL depth test: + // - Higher Z = lower depth = closer to viewer = renders ON TOP + // - Start at -800 and increment, so later calls have higher Z (on top) + + /** Current layer depth for screen-space ordering. + * Range: -1000 (far) to 1000 (near) in our orthographic projection. + */ + private var currentLayer = -800f + + /** Distance between screen layers. Each call moves slightly closer to viewer. */ + private val layerIncrement = 1f + + // Vanilla's raw light directions + private val DEFAULT_LIGHT_DIR = Vector3f(0.2f, 1.0f, -0.7f).normalize() + private val DEFAULT_LIGHT1_DIR = Vector3f(-0.2f, 1.0f, 0.7f).normalize() + + private fun eulerToQuaternion(rot: Vec3d): Quaternionf { + return Quaternionf().rotationYXZ( + Math.toRadians(rot.y).toFloat(), + Math.toRadians(rot.x).toFloat(), + Math.toRadians(rot.z).toFloat() + ) + } + + /** Get next layer depth for screen-space ordering. Later calls render on top. */ + private fun nextLayer(): Float { + val layer = currentLayer + currentLayer += layerIncrement + return layer + } + + fun box( + box: Box, + lineWidth: Float = 0.005f, + builder: (BoxBuilder.() -> Unit)? = null + ) { + val boxBuilder = BoxBuilder(lineWidth).apply { builder?.invoke(this) } + if (boxBuilder.fillSides != DirectionMask.NONE) boxBuilder.boxFaces(box) + if (boxBuilder.outlineSides != DirectionMask.NONE) boxBuilder.boxOutline(box) + } + + context(safeContext: SafeContext) + fun boxes( + pos: BlockPos, + state: BlockState, + lineWidth: Float = 0.005f, + builder: (BoxBuilder.() -> Unit)? = null + ) = with(safeContext) { + val boxes = state.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) } + val boxBuilder = BoxBuilder(lineWidth).apply { builder?.invoke(this) } + boxes.forEach { box -> + if (boxBuilder.fillSides != DirectionMask.NONE) boxBuilder.boxFaces(box) + if (boxBuilder.outlineSides != DirectionMask.NONE) boxBuilder.boxOutline(box) + } + } + + fun box( + pos: BlockPos, + lineWidth: Float = 0.005f, + builder: (BoxBuilder.() -> Unit)? = null + ) = box(Box(pos), lineWidth, builder) + + context(safeContext: SafeContext) + fun boxes( + pos: BlockPos, + lineWidth: Float = 0.005f, + builder: (BoxBuilder.() -> Unit)? = null + ) = boxes(pos, safeContext.blockState(pos), lineWidth, builder) + + fun filledQuadGradient( + corner1: Vec3d, + corner2: Vec3d, + corner3: Vec3d, + corner4: Vec3d, + color: Color + ) { + faceVertex(corner1.x, corner1.y, corner1.z, color) + faceVertex(corner2.x, corner2.y, corner2.z, color) + faceVertex(corner3.x, corner3.y, corner3.z, color) + faceVertex(corner4.x, corner4.y, corner4.z, color) + } + + fun filledQuadGradient( + x1: Double, y1: Double, z1: Double, c1: Color, + x2: Double, y2: Double, z2: Double, c2: Color, + x3: Double, y3: Double, z3: Double, c3: Color, + x4: Double, y4: Double, z4: Double, c4: Color + ) { + faceVertex(x1, y1, z1, c1) + faceVertex(x2, y2, z2, c2) + faceVertex(x3, y3, z3, c3) + faceVertex(x4, y4, z4, c4) + } + + fun lineGradient( + startPos: Vec3d, startColor: Color, + endPos: Vec3d, endColor: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) = lineGradient( + startPos.x, startPos.y, startPos.z, startColor, + endPos.x, endPos.y, endPos.z, endColor, + width, + dashStyle + ) + + fun lineGradient( + x1: Double, y1: Double, z1: Double, c1: Color, + x2: Double, y2: Double, z2: Double, c2: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) = line(x1, y1, z1, x2, y2, z2, c1, c2, width, dashStyle) + + /** Draw a line between two world positions. */ + fun line( + start: Vec3d, + end: Vec3d, + color: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) = line(start.x, start.y, start.z, end.x, end.y, end.z, color, color, width, dashStyle) + + /** Draw a polyline through a list of points. */ + fun polyline( + points: List, + color: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) { + if (points.size < 2) return + for (i in 0 until points.size - 1) { + line(points[i], points[i + 1], color, width, dashStyle) + } + } + + /** + * Draw a quadratic Bezier curve. + * + * @param p0 Start point + * @param p1 Control point + * @param p2 End point + * @param color Line color + * @param segments Number of line segments (higher = smoother) + */ + fun quadraticBezierLine( + p0: Vec3d, + p1: Vec3d, + p2: Vec3d, + color: Color, + segments: Int = 16, + width: Float, + dashStyle: LineDashStyle? = null + ) { + val points = CurveUtils.quadraticBezierPoints(p0, p1, p2, segments) + polyline(points, color, width, dashStyle) + } + + /** + * Draw a cubic Bezier curve. + * + * @param p0 Start point + * @param p1 First control point + * @param p2 Second control point + * @param p3 End point + * @param color Line color + * @param segments Number of line segments (higher = smoother) + */ + fun cubicBezierLine( + p0: Vec3d, + p1: Vec3d, + p2: Vec3d, + p3: Vec3d, + color: Color, + segments: Int = 32, + width: Float, + dashStyle: LineDashStyle? = null + ) { + val points = CurveUtils.cubicBezierPoints(p0, p1, p2, p3, segments) + polyline(points, color, width, dashStyle) + } + + /** + * Draw a Catmull-Rom spline that passes through all control points. + * + * @param controlPoints List of points the spline should pass through (minimum 4) + * @param color Line color + * @param segmentsPerSection Segments between each pair of control points + */ + fun catmullRomSplineLine( + controlPoints: List, + color: Color, + segmentsPerSection: Int = 16, + width: Float, + dashStyle: LineDashStyle? = null + ) { + val points = CurveUtils.catmullRomSplinePoints(controlPoints, segmentsPerSection) + polyline(points, color, width, dashStyle) + } + + /** + * Draw a smooth path through waypoints using Catmull-Rom splines. Handles endpoints + * naturally by mirroring. + * + * @param waypoints List of points to pass through (minimum 2) + * @param color Line color + * @param segmentsPerSection Smoothness (higher = smoother) + */ + fun smoothLine( + waypoints: List, + color: Color, + segmentsPerSection: Int = 16, + width: Float, + dashStyle: LineDashStyle? = null + ) { + val points = CurveUtils.smoothPath(waypoints, segmentsPerSection) + polyline(points, color, width, dashStyle) + } + + /** + * Draw a circle in a plane. + * + * @param center Center of the circle + * @param radius Radius of the circle + * @param normal Normal vector of the plane (determines orientation) + * @param color Line color + * @param segments Number of segments + */ + fun circleLine( + center: Vec3d, + radius: Double, + normal: Vec3d = Vec3d(0.0, 1.0, 0.0), + color: Color, + segments: Int = 32, + width: Float, + dashStyle: LineDashStyle? = null + ) { + // Create basis vectors perpendicular to normal + val up = + if (kotlin.math.abs(normal.y) < 0.99) Vec3d(0.0, 1.0, 0.0) + else Vec3d(1.0, 0.0, 0.0) + val u = normal.crossProduct(up).normalize() + val v = u.crossProduct(normal).normalize() + + val points = + (0..segments).map { i -> + val angle = 2.0 * Math.PI * i / segments + val x = kotlin.math.cos(angle) * radius + val y = kotlin.math.sin(angle) * radius + center.add(u.multiply(x)).add(v.multiply(y)) + } + + polyline(points, color, width, dashStyle) + } + + /** + * Draw billboard text at a world position. + * The text will face the camera by default, or use a custom rotation. + * + * @param text Text to render + * @param pos World position for the text + * @param size Size in world units + * @param font Font atlas to use (null = default font) + * @param style Text style with color and effects (shadow, glow, outline) + * @param centered Center text horizontally + * @param rotation Custom rotation as Euler angles in degrees (x=pitch, y=yaw, z=roll), null = billboard towards camera + */ + fun worldText( + text: String, + pos: Vec3d, + size: Float = 0.5f, + font: SDFFontAtlas? = null, + style: SDFStyle = SDFStyle(), + centered: Boolean = true, + rotation: Vec3d? = null + ) { + val atlas = font ?: FontHandler.getDefaultFont() + fontAtlas = atlas + + // Camera-relative anchor position + val anchorX = (pos.x - cameraPos.x).toFloat() + val anchorY = (pos.y - cameraPos.y).toFloat() + val anchorZ = (pos.z - cameraPos.z).toFloat() + + // Calculate text width for centering (using normalized width which works directly with glyph advances) + val textWidth = if (centered) atlas.getStringWidthNormalized(text, 1f) else 0f + val startX = -textWidth / 2f + + // For fixed rotation, we need to build a rotation matrix to pre-transform offsets + val rotationMatrix: Matrix4f? = if (rotation != null) { + Matrix4f() + .rotateY(Math.toRadians(rotation.y).toFloat()) + .rotateX(Math.toRadians(rotation.x).toFloat()) + .rotateZ(Math.toRadians(rotation.z).toFloat()) + } else null + + // Render layers in order: shadow -> glow -> outline -> main text + // Alpha encodes layer type for shader: <50 = shadow, 50-99 = glow, 100-199 = outline, >=200 = main + + // Shadow layer (alpha < 50 signals shadow) + if (style.shadow != null) { + val shadowColor = style.shadow.color + val offsetX = style.shadow.offsetX + val offsetY = style.shadow.offsetY + buildTextQuads(atlas, text, startX + offsetX, offsetY, + shadowColor.red, shadowColor.green, shadowColor.blue, 25, + anchorX, anchorY, anchorZ, size, rotationMatrix, style) + } + + // Glow layer (alpha 50-99 signals glow) + if (style.glow != null) { + val glowColor = style.glow.color + buildTextQuads(atlas, text, startX, 0f, + glowColor.red, glowColor.green, glowColor.blue, 75, + anchorX, anchorY, anchorZ, size, rotationMatrix, style) + } + + // Outline layer (alpha 100-199 signals outline) + if (style.outline != null) { + val outlineColor = style.outline.color + buildTextQuads(atlas, text, startX, 0f, + outlineColor.red, outlineColor.green, outlineColor.blue, 150, + anchorX, anchorY, anchorZ, size, rotationMatrix, style) + } + + // Main text layer (alpha >= 200 signals main text) + val mainColor = style.color + buildTextQuads(atlas, text, startX, 0f, + mainColor.red, mainColor.green, mainColor.blue, 255, + anchorX, anchorY, anchorZ, size, rotationMatrix, style) + } + + // ============================================================================ + // Screen-Space Rendering Methods (Normalized Coordinates) + // ============================================================================ + // All coordinates use normalized 0-1 range: + // - (0, 0) = bottom-left corner + // - (1, 1) = top-right corner + // - Sizes are also normalized (e.g., 0.1 = 10% of screen dimension) + + /** Get screen width in pixels (uses MC's scaled width). */ + private val screenWidth: Float + get() = mc.window?.scaledWidth?.toFloat() ?: 1920f + + /** Get screen height in pixels (uses MC's scaled height). */ + private val screenHeight: Float + get() = mc.window?.scaledHeight?.toFloat() ?: 1080f + + /** Convert normalized X coordinate (0-1) to pixel coordinate. */ + private fun toPixelX(normalizedX: Float): Float = normalizedX * screenWidth + + /** Convert normalized Y coordinate (0-1) to pixel coordinate. */ + private fun toPixelY(normalizedY: Float): Float = normalizedY * screenHeight + + /** + * Convert normalized size to pixel size. + * Uses height-only scaling to maintain consistent visual size regardless of aspect ratio. + * This matches how world-space elements behave when projected to screen. + */ + private fun toPixelSize(normalizedSize: Float): Float = + normalizedSize * screenHeight + + /** + * Draw a filled quad on screen with gradient colors. + * All coordinates use normalized 0-1 range. + * + * @param x1, y1 First corner position (0-1) and color + * @param x2, y2 Second corner position (0-1) and color + * @param x3, y3 Third corner position (0-1) and color + * @param x4, y4 Fourth corner position (0-1) and color + */ + fun screenQuadGradient( + x1: Float, y1: Float, c1: Color, + x2: Float, y2: Float, c2: Color, + x3: Float, y3: Float, c3: Color, + x4: Float, y4: Float, c4: Color + ) { + val layer = nextLayer() + collector.addScreenFaceVertex(toPixelX(x1), toPixelY(y1), c1, layer) + collector.addScreenFaceVertex(toPixelX(x2), toPixelY(y2), c2, layer) + collector.addScreenFaceVertex(toPixelX(x3), toPixelY(y3), c3, layer) + collector.addScreenFaceVertex(toPixelX(x4), toPixelY(y4), c4, layer) + } + + /** + * Draw a filled quad on screen with a single color. + * All coordinates use normalized 0-1 range. + */ + fun screenQuad( + x1: Float, y1: Float, + x2: Float, y2: Float, + x3: Float, y3: Float, + x4: Float, y4: Float, + color: Color + ) = screenQuadGradient(x1, y1, color, x2, y2, color, x3, y3, color, x4, y4, color) + + /** + * Draw a filled rectangle on screen. + * All values use normalized 0-1 range. + * + * @param x Left edge (0-1, where 0 = left, 1 = right) + * @param y Bottom edge (0-1, where 0 = bottom, 1 = top) + * @param width Rectangle width (0-1, where 1 = full screen width) + * @param height Rectangle height (0-1, where 1 = full screen height) + * @param color Fill color + */ + fun screenRect(x: Float, y: Float, width: Float, height: Float, color: Color) { + val x2 = x + width + val y2 = y + height + screenQuad(x, y, x2, y, x2, y2, x, y2, color) + } + + /** + * Draw a filled rectangle on screen with gradient colors. + * All values use normalized 0-1 range. + * + * @param x Left edge (0-1) + * @param y Bottom edge (0-1, where 0 = bottom, 1 = top) + * @param width Rectangle width (0-1) + * @param height Rectangle height (0-1) + * @param topLeft Color at top-left corner + * @param topRight Color at top-right corner + * @param bottomRight Color at bottom-right corner + * @param bottomLeft Color at bottom-left corner + */ + fun screenRectGradient( + x: Float, y: Float, width: Float, height: Float, + topLeft: Color, topRight: Color, bottomRight: Color, bottomLeft: Color + ) { + val x2 = x + width + val y2 = y + height + screenQuadGradient(x, y, topLeft, x2, y, topRight, x2, y2, bottomRight, x, y2, bottomLeft) + } + + /** + * Draw a line on screen with gradient colors. + * All coordinates use normalized 0-1 range. + * + * @param x1, y1 Start position (0-1) + * @param x2, y2 End position (0-1) + * @param startColor Color at start + * @param endColor Color at end + * @param width Line width (normalized, e.g., 0.005 = 0.5% of screen) + * @param dashStyle Optional dash style for dashed lines + */ + fun screenLineGradient( + x1: Float, y1: Float, startColor: Color, + x2: Float, y2: Float, endColor: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) { + // Convert to pixels + val px1 = toPixelX(x1) + val py1 = toPixelY(y1) + val px2 = toPixelX(x2) + val py2 = toPixelY(y2) + val pixelWidth = toPixelSize(width) + + // Calculate line direction in pixel space + val dx = px2 - px1 + val dy = py2 - py1 + + // Convert dash style lengths to pixels if present + val pixelDashStyle = dashStyle?.let { + LineDashStyle( + dashLength = toPixelSize(it.dashLength), + gapLength = toPixelSize(it.gapLength), + offset = it.offset, + animated = it.animated, + animationSpeed = it.animationSpeed + ) + } + + // Get layer for draw order + val layer = nextLayer() + + // 4 vertices for screen-space line quad + collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle, layer) + collector.addScreenEdgeVertex(px1, py1, startColor, dx, dy, pixelWidth, pixelDashStyle, layer) + collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle, layer) + collector.addScreenEdgeVertex(px2, py2, endColor, dx, dy, pixelWidth, pixelDashStyle, layer) + } + + /** + * Draw a line on screen with a single color. + * All coordinates use normalized 0-1 range. + */ + fun screenLine( + x1: Float, y1: Float, + x2: Float, y2: Float, + color: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) = screenLineGradient(x1, y1, color, x2, y2, color, width, dashStyle) + + // ============================================================================ + // Image Rendering Methods + // ============================================================================ + + /** + * Draw an image on screen at a specific position. + * Uses Lambda's custom image rendering pipeline for direct GPU rendering. + * + * @param image The ImageEntry from LambdaImageAtlas + * @param x X position (0-1, normalized screen coordinates) + * @param y Y position (0-1, normalized screen coordinates) + * @param width Width (0-1, normalized) + * @param height Height (0-1, normalized) + * @param tint Tint color (default white = no tint) + * @param hasOverlay Whether to render an overlay (e.g., enchantment glint) + * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: false) + */ + fun screenImage( + image: LambdaImageAtlas.ImageEntry, + x: Float, y: Float, + width: Float, height: Float, + tint: Color = Color.WHITE, + hasOverlay: Boolean = false, + pixelPerfect: Boolean = false + ) { + val layer = nextLayer() + val x0 = toPixelX(x) + val y0 = toPixelY(y) + val x1 = toPixelX(x + width) + val y1 = toPixelY(y + height) + + val overlayFlag = if (hasOverlay) 1f else 0f + val u0 = image.u0 + val v0 = image.v0 + val u1 = image.u1 + val v1 = image.v1 + + // Calculate animation time for glint effect + // Use Util.getMeasuringTimeMs() for consistent timing matching Minecraft's system + val glintTime = if (hasOverlay) { + (net.minecraft.util.Util.getMeasuringTimeMs() / 1000.0f) % 1000f // Seconds, 0-1000 loop + } else 0f + + // Calculate aspect ratio for square glint tiling + // overlayV carries width/height ratio so shader can correct UVs + val aspectRatio = if (hasOverlay && height != 0f) width / height else 1f + + // Build quad: bottom-left, bottom-right, top-right, top-left (CCW for Y-up) + // overlayU = animation time, overlayV = aspect ratio + val vertices = listOf( + RegionVertexCollector.ScreenImageVertex( + x0, y0, u0, v1, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, 0f, layer + ), + RegionVertexCollector.ScreenImageVertex( + x1, y0, u1, v1, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, 0f, layer + ), + RegionVertexCollector.ScreenImageVertex( + x1, y1, u1, v0, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, 0f, layer + ), + RegionVertexCollector.ScreenImageVertex( + x0, y1, u0, v0, tint.red, tint.green, tint.blue, tint.alpha, + glintTime, aspectRatio, overlayFlag, 0f, layer + ) + ) + collector.addScreenImageVertices(image.textureView, vertices, pixelPerfect) + } + + // ============================================================================ + // Model Rendering Methods (World Space) + // ============================================================================ + + /** + * Render a 3D model in world space. + * + * @param model The BlockModelPart to render (replaces BakedModel in 1.21.11+) + * @param pos World position to render at. If centered is true, this is the center of the model. If false, it's the 0,0,0 corner. + * @param scale XML scale + * @param rotation Rotation quaternion (around the center if centered=true, else around 0,0,0) + * @param color Tint color + * @param light Packed light + * @param overlay Overlay UV + * @param centered If true, shifts model vertices by -0.5 to rotate/scale around the center, then renders at pos. + * @param pixelPerfect If true, use NEAREST filtering. If false (default), use LINEAR. + * @param smartAA If true, uses shader-based analytic anti-aliasing (Pixel Art AA). Requires pixelPerfect=false (automatically handled). + */ + fun model( + model: BlockModelPart, + pos: Vec3d, + scale: Vec3d = Vec3d(1.0, 1.0, 1.0), + rotation: Quaternionf? = null, + color: Color = Color.WHITE, + light: Int = 0xF000F0, + overlay: Int = OverlayTexture.DEFAULT_UV, + centered: Boolean = false, + pixelPerfect: Boolean = false, + smartAA: Boolean = false, + shadingAmount: Float = 0.0f + ) { + val sprite = model.particleSprite() ?: return + val atlas = sprite.atlasId + // We need to resolve the texture view for the sprite's atlas + val textureView = mc.textureManager.getTexture(atlas)?.glTextureView ?: return + + val vertices = ArrayList() + + val scaleVec = Vector3f(scale.x.toFloat(), scale.y.toFloat(), scale.z.toFloat()) + val posVec = Vector3f( + (pos.x - cameraPos.x).toFloat(), + (pos.y - cameraPos.y).toFloat(), + (pos.z - cameraPos.z).toFloat() + ) + + val vertexPos = Vector3f() + val normalVec = Vector3f() + + val tr = color.red + val tg = color.green + val tb = color.blue + val ta = color.alpha + + val olU = (overlay and 0xFFFF).toFloat() + val olV = ((overlay shr 16) and 0xFFFF).toFloat() + + // Encode Overlay + AA flags into hasOverlay + // 0 = None, 1 = Overlay, 2 = AA, 3 = Overlay + AA + var overlayFlag = if (overlay != OverlayTexture.DEFAULT_UV) 1.0f else 0.0f + if (smartAA) { + overlayFlag += 2.0f + } + + val random = Random.create() + val quads = mutableListOf() + + // Collect quads for all directions and null direction + for (direction in net.minecraft.util.math.Direction.entries) { + random.setSeed(42L) + quads.addAll(model.getQuads(direction)) + } + random.setSeed(42L) + quads.addAll(model.getQuads(null)) + + for (quad in quads) { + // Use face normal for all vertices since BakedQuad doesn't have per-vertex normals easily accessible + val face = quad.face + val nx = face.offsetX.toFloat() + val ny = face.offsetY.toFloat() + val nz = face.offsetZ.toFloat() + + // Iterate 4 vertices + for (i in 0 until 4) { + val posVecSrc = quad.getPosition(i) + + // Transform Position + vertexPos.set(posVecSrc.x(), posVecSrc.y(), posVecSrc.z()) + + if (centered) { + // Shift to center (assuming 0..1 model block) + vertexPos.sub(0.5f, 0.5f, 0.5f) + } + + vertexPos.mul(scaleVec) + rotation?.transform(vertexPos) + vertexPos.add(posVec) + + // Transform Normal + normalVec.set(nx, ny, nz) + rotation?.transform(normalVec) + + // Extract UV - Vector2f.toLong packs X (U) in high 32 bits, Y (V) in low 32 bits + val packedUV = quad.getTexcoords(i) + val u = Float.fromBits((packedUV ushr 32).toInt()) + val v = Float.fromBits((packedUV and 0xFFFFFFFFL).toInt()) + + // Edge Data: Map vertex index to a quad corner (0,0 to 1,1) + // Standard quad winding: 0:0,0 | 1:0,1 | 2:1,1 | 3:1,0 (approx) + // Reverted to simple bounds without dilation + val edgeX = if (i == 1 || i == 2) 1.0f else 0.0f + val edgeY = if (i == 2 || i == 3) 1.0f else 0.0f + + vertices.add(RegionVertexCollector.ModelVertex( + vertexPos.x, vertexPos.y, vertexPos.z, + u, v, + tr, tg, tb, ta, + olU, olV, overlayFlag, shadingAmount, + light, + DEFAULT_LIGHT_DIR.x, DEFAULT_LIGHT_DIR.y, DEFAULT_LIGHT_DIR.z, + DEFAULT_LIGHT1_DIR.x, DEFAULT_LIGHT1_DIR.y, DEFAULT_LIGHT1_DIR.z, + normalVec.x, normalVec.y, normalVec.z, + edgeX, edgeY + )) + } + } + + if (vertices.isNotEmpty()) { + // Use Nearest filter only if requested AND Smart AA is NOT used (Smart AA handles sharpness in shader via Linear) + val useNearest = pixelPerfect && !smartAA + collector.addModelVertices(textureView, vertices, useNearest) + } + } // Default filter? + + fun worldGuiItem( + stack: ItemStack, + pos: Vec3d, + scale: Float = 0.5f, + rotation: Vec3d? = null, + centered: Boolean = true, + flat: Boolean = true, + lighting: ItemLighting = ItemLighting.VANILLA, + overlay: ItemOverlay? = null + ) { + if (stack.isEmpty) return + + val renderState = ItemRenderState() + mc.itemModelManager.updateForNonLivingEntity(renderState, stack, ItemDisplayContext.GUI, mc.player ?: return) + + val rot = rotation?.let { eulerToQuaternion(it) } + renderItemState(renderState, pos, scale, rot, centered, isScreen = false, flat = flat, lighting = lighting, overlay = overlay) + } + + fun screenGuiItem( + stack: ItemStack, + x: Float, y: Float, + size: Float = 0.05f, + rotation: Vec3d? = null, + centered: Boolean = true, + lighting: ItemLighting = ItemLighting.VANILLA, + overlay: ItemOverlay? = null + ) { + if (stack.isEmpty) return + + val renderState = ItemRenderState() + mc.itemModelManager.updateForNonLivingEntity(renderState, stack, ItemDisplayContext.GUI, mc.player ?: return) + + // Convert screen pos to camera-relative "pseudo-world" for the generic renderer + val pixelX = toPixelX(x) + val pixelY = toPixelY(y) + val pixelSize = toPixelSize(size) + + val rot = rotation?.let { eulerToQuaternion(it) } + // Screen items are always flat (viewed straight-on) + renderItemState(renderState, Vec3d(pixelX.toDouble(), pixelY.toDouble(), nextLayer().toDouble()), pixelSize, rot, centered, isScreen = true, flat = true, lighting = lighting, overlay = overlay) + } + + private fun renderItemState( + state: ItemRenderState, + pos: Vec3d, + scale: Float, + rotation: Quaternionf?, + centered: Boolean, + isScreen: Boolean, + flat: Boolean = false, + lighting: ItemLighting = ItemLighting.VANILLA, + overlay: ItemOverlay? = null + ) { + val posVec = if (isScreen) { + Vector3f(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat()) + } else { + Vector3f((pos.x - cameraPos.x).toFloat(), (pos.y - cameraPos.y).toFloat(), (pos.z - cameraPos.z).toFloat()) + } + + val lightDirs = Pair(Vector3f(lighting.light0), Vector3f(lighting.light1)) + + val queue = CapturingQueue( + posVec, Vector3f(scale), rotation, centered, flat, lighting, lightDirs, state.isSideLit + ) { vertices, textureView -> + if (isScreen) { + collector.addScreenModelVertices(textureView, vertices, true) + } else { + collector.addModelVertices(textureView, vertices, true) + } + } + + val matrixStack = MatrixStack() + + for (i in 0 until state.layerCount) { + val layer = state.layers[i] + + // Refined Glint Logic: + // - If overlay is ItemOverlay.DISABLED, force glint OFF. + // - If overlay is non-null (any other overlay), force glint ON. + // - If overlay is null, use the item's own enchantment state. + queue.currentGlint = when (overlay) { + ItemOverlay.DISABLED -> false + null -> layer.glint != ItemRenderState.Glint.NONE + else -> true + } + + matrixStack.push() + layer.transform.apply(state.displayContext.isLeftHand, matrixStack.peek()) + + val specialModel = layer.specialModelType + + if (specialModel != null) { + specialModel.render(layer.data, state.displayContext, matrixStack, queue, 15728880, 0, queue.currentGlint, 0) + } else { + val renderLayer = layer.renderLayer + if (renderLayer != null) { + // We pass STANDARD if currentGlint is true, otherwise NONE to ensure our capture can correctly toggle it. + val captureGlint = if (queue.currentGlint) ItemRenderState.Glint.STANDARD else ItemRenderState.Glint.NONE + queue.submitItem(matrixStack, state.displayContext, 15728880, 0, 0, layer.tints, layer.quads, renderLayer, captureGlint) + } + } + + matrixStack.pop() + } + } + + private class CapturingConsumer( + private val vertices: MutableList, + private val posTransform: (Vector3f) -> Unit, + private val normalTransform: (Vector3f) -> Unit, + private val flat: Boolean, + private val rotation: Quaternionf?, + private val glint: Boolean, + private val lightDirs: Pair, + private val shadingAmount: Float, + private val baseLight: Int, + private val baseOverlay: Int + ) : VertexConsumer { + private var x = 0f; private var y = 0f; private var z = 0f + private var color = -1 + private var u = 0f; private var v = 0f + private var currentOverlay = 0 + private var currentLight = 0 + private var nx = 0f; private var ny = 0f; private var nz = 0f + private val quadBuffer = ArrayList(4) + + override fun vertex(float1: Float, float2: Float, float3: Float): VertexConsumer { + this.x = float1; this.y = float2; this.z = float3 + return this + } + + override fun color(argb: Int): VertexConsumer { + this.color = argb + return this + } + + override fun color(r: Int, g: Int, b: Int, a: Int): VertexConsumer { + this.color = (a shl 24) or (r shl 16) or (g shl 8) or b + return this + } + + override fun texture(float1: Float, float2: Float): VertexConsumer { + this.u = float1; this.v = float2 + return this + } + + override fun overlay(int1: Int, int2: Int): VertexConsumer { + this.currentOverlay = (int2 shl 16) or int1 + return this + } + + override fun light(int1: Int, int2: Int): VertexConsumer { + this.currentLight = (int2 shl 16) or int1 + return this + } + + override fun lineWidth(width: Float): VertexConsumer = this + + override fun normal(float1: Float, float2: Float, float3: Float): VertexConsumer { + this.nx = float1; this.ny = float2; this.nz = float3 + commitVertex() + return this + } + + override fun vertex( + x: Float, y: Float, z: Float, + color: Int, u: Float, v: Float, + overlay: Int, light: Int, + normalX: Float, normalY: Float, normalZ: Float + ) { + this.x = x; this.y = y; this.z = z + this.color = color + this.u = u; this.v = v + this.currentOverlay = overlay + this.currentLight = light + this.nx = normalX; this.ny = normalY; this.nz = normalZ + commitVertex() + } + + private fun commitVertex() { + // 1. Coordinates are already transformed by model parts into "Item Space" + val p = Vector3f(this.x, this.y, this.z) + + // 2. Apply Flattening in Item Space (before Lambda world/screen transforms) + if (flat) p.z = 0f + + // 3. Apply Lambda positioning (ESP world pos or GUI position) + posTransform(p) + + // 4. Normal handling + val n = Vector3f(this.nx, this.ny, this.nz) + normalTransform(n) + + val vIdx = quadBuffer.size + val qu = if (vIdx == 1 || vIdx == 2) 1f else 0f + val qv = if (vIdx == 2 || vIdx == 3) 1f else 0f + + val ov = if (currentOverlay != 0) currentOverlay else baseOverlay + val lgt = if (currentLight != 0) currentLight else baseLight + + quadBuffer.add(RegionVertexCollector.ModelVertex( + p.x, p.y, p.z, + u, v, + (color shr 16) and 0xFF, (color shr 8) and 0xFF, color and 0xFF, (color ushr 24) and 0xFF, + (ov and 0xFFFF).toFloat(), (ov ushr 16).toFloat(), + if (glint) 4.0f else 0.0f, + shadingAmount, + lgt, + lightDirs.first.x, lightDirs.first.y, lightDirs.first.z, + lightDirs.second.x, lightDirs.second.y, lightDirs.second.z, + n.x, n.y, n.z, + qu, qv + )) + + if (quadBuffer.size == 4) { + if (flat) { + // Cull backfaces: The normal is already in Item Space. + // Apply Lambda rotation to see if it faces the screen. + val testN = Vector3f(this.nx, this.ny, this.nz) + rotation?.transform(testN) + if (testN.z < 0f) { + quadBuffer.clear() + return + } + } + vertices.addAll(quadBuffer) + quadBuffer.clear() + } + } + } + + private inner class CapturingQueue( + private val pos: Vector3f, + private val scale: Vector3f, + private val rotation: Quaternionf?, + private val centered: Boolean, + private val flat: Boolean, + private val lighting: ItemLighting, + private val lightDirs: Pair, + private val isSideLit: Boolean, + private val onSubmission: (List, com.mojang.blaze3d.textures.GpuTextureView) -> Unit + ) : OrderedRenderCommandQueue { + var currentGlint: Boolean = false + + private fun posTransform(v: Vector3f) { + if (!centered) v.add(0.5f, 0.5f, 0.5f) + if (flat) v.z = 0f + v.mul(scale) + rotation?.transform(v) + v.add(pos) + } + + private fun normalTransform(v: Vector3f) { + rotation?.transform(v) + v.normalize() + } + + override fun submitItem( + matrices: MatrixStack, + displayContext: ItemDisplayContext, + light: Int, + overlay: Int, + outlineColors: Int, + tintLayers: IntArray, + quads: List, + renderLayer: RenderLayer, + glintType: ItemRenderState.Glint + ) { + val sprite = quads.firstOrNull()?.sprite ?: return + val textureView = mc.textureManager.getTexture(sprite.atlasId)?.glTextureView ?: return + + val vertices = ArrayList() + val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f + + // Transform lights by GUI layer matrix to match normal transformation + val l0 = Vector3f(lightDirs.first).mulDirection(matrices.peek().positionMatrix) + val l1 = Vector3f(lightDirs.second).mulDirection(matrices.peek().positionMatrix) + l0.x = -l0.x; l1.x = -l1.x; l0.normalize(); l1.normalize() + + for (quad in quads) { + val nx = quad.face.offsetX.toFloat() + val ny = quad.face.offsetY.toFloat() + val nz = quad.face.offsetZ.toFloat() + + if (flat) { + val testN = Vector3f(nx, ny, nz).mulDirection(matrices.peek().positionMatrix) + rotation?.transform(testN) + if (testN.z < 0f) continue + } + + for (vIdx in 0 until 4) { + val posSrc = quad.getPosition(vIdx) + val vp = matrices.peek().positionMatrix.transformPosition(posSrc.x(), posSrc.y(), posSrc.z(), Vector3f()) + posTransform(vp) + + val vn = matrices.peek().transformNormal(nx, ny, nz, Vector3f()) + normalTransform(vn) + + val packedUV = quad.getTexcoords(vIdx) + val u = java.lang.Float.intBitsToFloat((packedUV ushr 32).toInt()) + val v = java.lang.Float.intBitsToFloat((packedUV and 0xFFFFFFFFL).toInt()) + + val tint = if (quad.hasTint() && quad.tintIndex() < tintLayers.size) tintLayers[quad.tintIndex()] else -1 + val r = if (tint != -1) (tint shr 16 and 0xFF) else 255 + val g = if (tint != -1) (tint shr 8 and 0xFF) else 255 + val b = if (tint != -1) (tint and 0xFF) else 255 + + vertices.add(RegionVertexCollector.ModelVertex( + vp.x, vp.y, vp.z, + u, v, + r, g, b, 255, + (overlay and 0xFFFF).toFloat(), (overlay ushr 16).toFloat(), + if (glintType != ItemRenderState.Glint.NONE) 4.0f else 0.0f, + shadingAmount, + light, + l0.x, l0.y, l0.z, + l1.x, l1.y, l1.z, + vn.x, vn.y, vn.z, + if (vIdx == 1 || vIdx == 2) 1f else 0f, + if (vIdx == 2 || vIdx == 3) 1f else 0f + )) + } + } + onSubmission(vertices, textureView) + } + + override fun submitModel( + model: net.minecraft.client.model.Model, + state: S, + matrices: MatrixStack, + renderLayer: RenderLayer, + light: Int, + overlay: Int, + tintedColor: Int, + sprite: Sprite?, + outlineColor: Int, + crumblingOverlay: ModelCommandRenderer.CrumblingOverlayCommand? + ) { + val textureView = if (sprite != null) { + mc.textureManager.getTexture(sprite.atlasId)?.glTextureView + } else { + mc.textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))?.glTextureView + } ?: return + + val vertices = ArrayList() + val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f + val consumer = CapturingConsumer( + vertices, ::posTransform, ::normalTransform, + flat, rotation, currentGlint, lightDirs, shadingAmount, light, overlay + ) + val wrappedConsumer = sprite?.getTextureSpecificVertexConsumer(consumer) ?: consumer + model.setAngles(state) + model.render(matrices, wrappedConsumer, light, overlay, tintedColor) + onSubmission(vertices, textureView) + } + + override fun submitModelPart( + part: net.minecraft.client.model.ModelPart, + matrices: MatrixStack, + renderLayer: RenderLayer, + light: Int, + overlay: Int, + sprite: Sprite?, + sheeted: Boolean, + hasGlint: Boolean, + tintedColor: Int, + crumblingOverlay: ModelCommandRenderer.CrumblingOverlayCommand?, + i: Int + ) { + val textureView = if (sprite != null) { + mc.textureManager.getTexture(sprite.atlasId)?.glTextureView + } else { + mc.textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))?.glTextureView + } ?: return + + val vertices = ArrayList() + val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f + val consumer = CapturingConsumer( + vertices, ::posTransform, ::normalTransform, + flat, rotation, hasGlint, lightDirs, shadingAmount, light, overlay + ) + val wrappedConsumer = sprite?.getTextureSpecificVertexConsumer(consumer) ?: consumer + part.render(matrices, wrappedConsumer, light, overlay, tintedColor) + onSubmission(vertices, textureView) + } + + override fun getBatchingQueue(order: Int): net.minecraft.client.render.command.RenderCommandQueue = this + override fun submitShadowPieces(matrices: MatrixStack, radius: Float, pieces: List) {} + override fun submitLabel(matrices: MatrixStack, pos: Vec3d?, y: Int, label: Text, ns: Boolean, l: Int, dist: Double, cam: CameraRenderState) {} + override fun submitText(matrices: MatrixStack, x: Float, y: Float, text: OrderedText, ds: Boolean, lt: TextRenderer.TextLayerType, l: Int, c: Int, bc: Int, oc: Int) {} + override fun submitFire(matrices: MatrixStack, state: EntityRenderState, rot: Quaternionf) {} + override fun submitLeash(matrices: MatrixStack, data: EntityRenderState.LeashData) {} + override fun submitBlock(matrices: MatrixStack, state: BlockState, light: Int, overlay: Int, outlineColor: Int) { + val model = mc.blockRenderManager.getModel(state) + val textureView = mc.textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png"))?.glTextureView ?: return + + val vertices = ArrayList() + val shadingAmount = if (lighting.respectsUseLight && !isSideLit) 0.0f else 1.0f + val consumer = CapturingConsumer( + vertices, ::posTransform, ::normalTransform, + flat, rotation, currentGlint, lightDirs, shadingAmount, light, overlay + ) + + val random = Random.create() + val parts = model.getParts(random) + for (part in parts) { + for (direction in net.minecraft.util.math.Direction.entries) { + for (quad in part.getQuads(direction)) { + consumer.quad(matrices.peek(), quad, 1f, 1f, 1f, 1f, light, overlay) + } + } + for (quad in part.getQuads(null)) { + consumer.quad(matrices.peek(), quad, 1f, 1f, 1f, 1f, light, overlay) + } + } + + onSubmission(vertices, textureView) + } + + override fun submitMovingBlock(matrices: MatrixStack, state: net.minecraft.client.render.block.MovingBlockRenderState) { + } + override fun submitBlockStateModel(matrices: MatrixStack, layer: RenderLayer, model: BlockStateModel, r: Float, g: Float, b: Float, l: Int, o: Int, oc: Int) {} + override fun submitCustom(matrices: MatrixStack, layer: RenderLayer, renderer: OrderedRenderCommandQueue.Custom) {} + override fun submitCustom(renderer: OrderedRenderCommandQueue.LayeredCustom) {} + } + + /** + * Draw a billboard image at a world position. + * The image will face the camera by default, or use a custom rotation. + * + * @param image The ImageEntry from LambdaImageAtlas + * @param pos World position for the image + * @param size Size in world units + * @param tint Tint color (default white = no tint) + * @param hasOverlay Whether to render an overlay (e.g., enchantment glint) + * @param aspectRatio Width/height ratio (auto-calculated from image if not specified) + * @param rotation Custom rotation as Euler angles in degrees (x=pitch, y=yaw, z=roll), null = billboard towards camera + * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: false) + */ + fun worldImage( + image: LambdaImageAtlas.ImageEntry, + pos: Vec3d, + size: Float = 0.5f, + tint: Color = Color.WHITE, + hasOverlay: Boolean = false, + aspectRatio: Float? = null, + rotation: Vec3d? = null, + pixelPerfect: Boolean = false + ) { + val ratio = aspectRatio ?: image.aspectRatio + val u0 = image.u0 + val v0 = image.v0 + val u1 = image.u1 + val v1 = image.v1 + + // Camera-relative anchor position + val anchorX = (pos.x - cameraPos.x).toFloat() + val anchorY = (pos.y - cameraPos.y).toFloat() + val anchorZ = (pos.z - cameraPos.z).toFloat() + + // Calculate quad corners (centered on anchor) + val halfWidth = size * ratio / 2f + val halfHeight = size / 2f + + val overlayFlag = if (hasOverlay) 1f else 0f + val billboardFlag = if (rotation == null) 0f else 1f + + // Quad relative glint UVs (0-1 range) + val gx0 = 0f + val gx1 = 1f + val gy0 = 0f // 0 at y0 (bottom) + val gy1 = 1f // 1 at y1 (top) + + // Quad offsets (local space, scaled in shader) + val x0 = -halfWidth / size + val x1 = halfWidth / size + val y0 = -halfHeight / size + val y1 = halfHeight / size + + val vertices = if (rotation == null) { + // Billboard mode: pass local offsets directly, shader handles billboard + listOf( + RegionVertexCollector.WorldImageVertex( + x0, y0, u0, v1, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx0, gy0, overlayFlag, 0f + ), + RegionVertexCollector.WorldImageVertex( + x1, y0, u1, v1, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx1, gy0, overlayFlag, 0f + ), + RegionVertexCollector.WorldImageVertex( + x1, y1, u1, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx1, gy1, overlayFlag, 0f + ), + RegionVertexCollector.WorldImageVertex( + x0, y1, u0, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx0, gy1, overlayFlag, 0f + ) + ) + } else { + // Fixed rotation mode: pre-transform offsets with rotation matrix + val rotationMatrix = Matrix4f() + .rotateY(Math.toRadians(rotation.y).toFloat()) + .rotateX(Math.toRadians(rotation.x).toFloat()) + .rotateZ(Math.toRadians(rotation.z).toFloat()) + + val p0 = transformPoint(rotationMatrix, x0, y0, 0f) + val p1 = transformPoint(rotationMatrix, x1, y0, 0f) + val p2 = transformPoint(rotationMatrix, x1, y1, 0f) + val p3 = transformPoint(rotationMatrix, x0, y1, 0f) + + listOf( + RegionVertexCollector.WorldImageVertex( + p0.x, p0.y, u0, v1, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx0, gy0, overlayFlag, 0f + ), + RegionVertexCollector.WorldImageVertex( + p1.x, p1.y, u1, v1, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx1, gy0, overlayFlag, 0f + ), + RegionVertexCollector.WorldImageVertex( + p2.x, p2.y, u1, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx1, gy1, overlayFlag, 0f + ), + RegionVertexCollector.WorldImageVertex( + p3.x, p3.y, u0, v0, tint.red, tint.green, tint.blue, tint.alpha, + anchorX, anchorY, anchorZ, size, billboardFlag, + gx0, gy1, overlayFlag, 0f + ) + ) + } + collector.addWorldImageVertices(image.textureView, vertices, pixelPerfect) + } + + // ============================================================================ + // Simplified Image API (Identifier-based) + // ============================================================================ + + /** + * Draw a Minecraft texture on screen at a specific position. + * The texture is loaded automatically - no UV coordinates needed. + * + * @param texture Identifier of the texture (e.g., Identifier.ofVanilla("textures/item/diamond.png")) + * @param x X position (0-1, normalized screen coordinates) + * @param y Y position (0-1, normalized screen coordinates) + * @param width Width (0-1, normalized) + * @param height Height (0-1, normalized) + * @param tint Tint color (default white = no tint) + * @param hasOverlay Whether to render an overlay (e.g., enchantment glint) + * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: true for MC textures) + */ + fun screenImage( + texture: Identifier, + x: Float, y: Float, + width: Float, height: Float, + tint: Color = Color.WHITE, + hasOverlay: Boolean = false, + pixelPerfect: Boolean = true + ) { + // Load the texture via LambdaImageAtlas + val imageEntry = LambdaImageAtlas.loadMCTexture(texture) ?: return + screenImage(imageEntry, x, y, width, height, tint, hasOverlay, pixelPerfect) + } + + /** + * Draw a Minecraft texture as a billboard at a world position. + * The texture is loaded automatically - no UV coordinates needed. + * + * @param texture Identifier of the texture + * @param pos World position for the image + * @param size Size in world units + * @param tint Tint color (default white = no tint) + * @param hasOverlay Whether to render an overlay (e.g., enchantment glint) + * @param aspectRatio Width/height ratio (for non-square images) + * @param rotation Custom rotation, null = billboard towards camera + * @param pixelPerfect If true, use NEAREST filtering for crisp pixel art (default: true for MC textures) + */ + fun worldImage( + texture: Identifier, + pos: Vec3d, + size: Float = 0.5f, + tint: Color = Color.WHITE, + hasOverlay: Boolean = false, + aspectRatio: Float? = null, + rotation: Vec3d? = null, + pixelPerfect: Boolean = true + ) { + // Load the texture via LambdaImageAtlas + val imageEntry = LambdaImageAtlas.loadMCTexture(texture) ?: return + val ratio = aspectRatio ?: imageEntry.aspectRatio + worldImage(imageEntry, pos, size, tint, hasOverlay, ratio, rotation, pixelPerfect) + } + + /** + * Draw text on screen at a specific position. + * Position uses normalized 0-1 range, size is normalized. + * + * @param text Text to render + * @param x X position (0-1, where 0 = left, 1 = right) + * @param y Y position (0-1, where 0 = bottom, 1 = top) + * @param size Text size (normalized, e.g., 0.02 = 2% of screen height) + * @param font Font atlas to use (null = default font) + * @param style Text style with color and effects + * @param centered Center text horizontally at the given position + */ + fun screenText( + text: String, + x: Float, + y: Float, + size: Float = 0.02f, + font: SDFFontAtlas? = null, + style: SDFStyle = SDFStyle(), + centered: Boolean = false + ) { + val atlas = font ?: FontHandler.getDefaultFont() + fontAtlas = atlas + + // Convert to pixel coordinates + val pixelX = toPixelX(x) + val pixelY = toPixelY(y) + + // Convert normalized size to target pixel height + val targetPixelHeight = toPixelSize(size) + + // Adjust font size so that text ASCENT (height of capital letters) matches the target pixel height + // getAscent(fontSize) = ascent / baseSize * fontSize + // We want: ascent / baseSize * adjustedFontSize = targetPixelHeight + // So: adjustedFontSize = targetPixelHeight * baseSize / ascent + val pixelSize = targetPixelHeight * atlas.baseSize / atlas.ascent + + // Calculate text width for centering (normalized width converted to pixels) + val normalizedTextWidth = if (centered) atlas.getStringWidthNormalized(text, size) else 0f + val textWidth = normalizedTextWidth * screenWidth + val startX = -textWidth / 2f + + // Render layers in order: shadow -> glow -> outline -> main text + // Each layer gets its own draw depth so they render in correct order + // Alpha encodes layer type for shader + + // Shadow layer + if (style.shadow != null) { + val shadowColor = style.shadow.color + val offsetX = style.shadow.offsetX * pixelSize + // Negate offsetY for Y-up coordinate system (shadow should appear below text) + val offsetY = -style.shadow.offsetY * pixelSize + val layer = nextLayer() + buildScreenTextQuads(atlas, text, startX + offsetX, offsetY, + shadowColor.red, shadowColor.green, shadowColor.blue, 25, + pixelX, pixelY, pixelSize, style, layer) + } + + // Glow layer + if (style.glow != null) { + val glowColor = style.glow.color + val layer = nextLayer() + buildScreenTextQuads(atlas, text, startX, 0f, + glowColor.red, glowColor.green, glowColor.blue, 75, + pixelX, pixelY, pixelSize, style, layer) + } + + // Outline layer + if (style.outline != null) { + val outlineColor = style.outline.color + val layer = nextLayer() + buildScreenTextQuads(atlas, text, startX, 0f, + outlineColor.red, outlineColor.green, outlineColor.blue, 150, + pixelX, pixelY, pixelSize, style, layer) + } + + // Main text layer + val mainColor = style.color + val mainLayer = nextLayer() + buildScreenTextQuads(atlas, text, startX, 0f, + mainColor.red, mainColor.green, mainColor.blue, 255, + pixelX, pixelY, pixelSize, style, mainLayer) + } + + /** + * Build screen-space text quad vertices for a layer. + * Internal method - uses pixel coordinates. Adds vertices directly to collector. + */ + private fun buildScreenTextQuads( + atlas: SDFFontAtlas, + text: String, + startX: Float, // Offset in SCALED pixels (for centering) + startY: Float, // Offset in SCALED pixels + r: Int, g: Int, b: Int, a: Int, + anchorX: Float, anchorY: Float, + pixelSize: Float, // Final text size in pixels + style: SDFStyle, + layer: Float // Layer depth for draw order + ) { + // Extract SDF style params from SDFStyle object + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0f + val shadowSoftness = style.shadow?.softness ?: 0f + val threshold = 0.5f // Default SDF threshold + + // Glyph metrics (advance, bearingX, bearingY) are ALREADY normalized by baseSize in SDFFontAtlas + // Glyph width/height are in PIXELS and need to be normalized + var penX = 0f // Pen position in normalized units + + for (char in text) { + val glyph = atlas.getGlyph(char.code) ?: continue + + // bearingX/Y are already normalized, just multiply by pixelSize + // bearingY is the distance from baseline to glyph top, so with Y-up: + // - glyph top is at baseline + bearingY + // - glyph bottom is at baseline + bearingY - height + val localX0 = penX + glyph.bearingX + val localY1 = glyph.bearingY // Top of glyph (Y-up) + + // width/height are in pixels, need normalization + val localX1 = localX0 + glyph.width / atlas.baseSize + val localY0 = localY1 - glyph.height / atlas.baseSize // Bottom of glyph + + // Scale to final pixels and add anchor + offsets + val x0 = anchorX + startX + localX0 * pixelSize + val y0 = anchorY + startY + localY0 * pixelSize + val x1 = anchorX + startX + localX1 * pixelSize + val y1 = anchorY + startY + localY1 * pixelSize + + // Screen-space text uses simple 2D quads - add directly to collector with style params + // Quad winding: bottom-left, bottom-right, top-right, top-left (CCW for Y-up) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x0, y0, glyph.u0, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x1, y0, glyph.u1, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x1, y1, glyph.u1, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) + collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex( + x0, y1, glyph.u0, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer)) + + // advance is already normalized, just add it + penX += glyph.advance + } + } + + /** + * Build text quad vertices for a layer with specified color and alpha. + * Adds vertices directly to collector with embedded SDF style params. + * + * @param atlas Font atlas + * @param text Text string + * @param startX Starting X offset for text + * @param startY Starting Y offset for text + * @param r Red color component + * @param g Green color component + * @param b Blue color component + * @param a Alpha component (encodes layer type) + * @param anchorX Camera-relative anchor X position + * @param anchorY Camera-relative anchor Y position + * @param anchorZ Camera-relative anchor Z position + * @param scale Text scale + * @param rotationMatrix Optional rotation matrix for fixed rotation mode + */ + private fun buildTextQuads( + atlas: SDFFontAtlas, + text: String, + startX: Float, + startY: Float, + r: Int, g: Int, b: Int, a: Int, + anchorX: Float, anchorY: Float, anchorZ: Float, + scale: Float, + rotationMatrix: Matrix4f?, + style: SDFStyle + ) { + // Extract SDF style params from SDFStyle object + val outlineWidth = style.outline?.width ?: 0f + val glowRadius = style.glow?.radius ?: 0f + val shadowSoftness = style.shadow?.softness ?: 0f + val threshold = 0.5f // Default SDF threshold + + var penX = startX + for (char in text) { + val glyph = atlas.getGlyph(char.code) ?: continue + + val x0 = penX + glyph.bearingX + val y0 = startY - glyph.bearingY + val x1 = x0 + glyph.width / atlas.baseSize + val y1 = y0 + glyph.height / atlas.baseSize + + if (rotationMatrix == null) { + // Billboard mode: pass local offsets directly, shader handles billboard + // Bottom-left, Bottom-right, Top-right, Top-left + collector.textVertices.add(RegionVertexCollector.TextVertex( + x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.textVertices.add(RegionVertexCollector.TextVertex( + x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.textVertices.add(RegionVertexCollector.TextVertex( + x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.textVertices.add(RegionVertexCollector.TextVertex( + x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 0f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + } else { + // Fixed rotation mode: pre-transform offsets with rotation matrix + // Scale is applied in shader, so we just apply rotation here + val p0 = transformPoint(rotationMatrix, x0, -y1, 0f) // Negate Y for flip + val p1 = transformPoint(rotationMatrix, x1, -y1, 0f) + val p2 = transformPoint(rotationMatrix, x1, -y0, 0f) + val p3 = transformPoint(rotationMatrix, x0, -y0, 0f) + + collector.textVertices.add(RegionVertexCollector.TextVertex( + p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.textVertices.add(RegionVertexCollector.TextVertex( + p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.textVertices.add(RegionVertexCollector.TextVertex( + p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + collector.textVertices.add(RegionVertexCollector.TextVertex( + p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, 1f, + outlineWidth, glowRadius, shadowSoftness, threshold)) + } + + penX += glyph.advance + } + } + + private fun BoxBuilder.boxFaces(box: Box) { + // We need to call the internal methods, so we'll use filled() with interpolated colors + // For per-vertex colors on faces, we need direct access to the collector + + if (fillSides.hasDirection(DirectionMask.EAST)) { + // East face (+X): uses NE and SE corners + filledQuadGradient( + box.maxX, box.minY, box.minZ, fillBottomNorthEast, + box.maxX, box.maxY, box.minZ, fillTopNorthEast, + box.maxX, box.maxY, box.maxZ, fillTopSouthEast, + box.maxX, box.minY, box.maxZ, fillBottomSouthEast + ) + } + if (fillSides.hasDirection(DirectionMask.WEST)) { + // West face (-X): uses NW and SW corners + filledQuadGradient( + box.minX, box.minY, box.minZ, fillBottomNorthWest, + box.minX, box.minY, box.maxZ, fillBottomSouthWest, + box.minX, box.maxY, box.maxZ, fillTopSouthWest, + box.minX, box.maxY, box.minZ, fillTopNorthWest + ) + } + if (fillSides.hasDirection(DirectionMask.UP)) { + // Top face (+Y): uses all top corners + filledQuadGradient( + box.minX, box.maxY, box.minZ, fillTopNorthWest, + box.minX, box.maxY, box.maxZ, fillTopSouthWest, + box.maxX, box.maxY, box.maxZ, fillTopSouthEast, + box.maxX, box.maxY, box.minZ, fillTopNorthEast + ) + } + if (fillSides.hasDirection(DirectionMask.DOWN)) { + // Bottom face (-Y): uses all bottom corners + filledQuadGradient( + box.minX, box.minY, box.minZ, fillBottomNorthWest, + box.maxX, box.minY, box.minZ, fillBottomNorthEast, + box.maxX, box.minY, box.maxZ, fillBottomSouthEast, + box.minX, box.minY, box.maxZ, fillBottomSouthWest + ) + } + if (fillSides.hasDirection(DirectionMask.SOUTH)) { + // South face (+Z): uses SW and SE corners + filledQuadGradient( + box.minX, box.minY, box.maxZ, fillBottomSouthWest, + box.maxX, box.minY, box.maxZ, fillBottomSouthEast, + box.maxX, box.maxY, box.maxZ, fillTopSouthEast, + box.minX, box.maxY, box.maxZ, fillTopSouthWest + ) + } + if (fillSides.hasDirection(DirectionMask.NORTH)) { + // North face (-Z): uses NW and NE corners + filledQuadGradient( + box.minX, box.minY, box.minZ, fillBottomNorthWest, + box.minX, box.maxY, box.minZ, fillTopNorthWest, + box.maxX, box.maxY, box.minZ, fillTopNorthEast, + box.maxX, box.minY, box.minZ, fillBottomNorthEast + ) + } + } + + private fun BoxBuilder.boxOutline(box: Box) { + val hasEast = outlineSides.hasDirection(DirectionMask.EAST) + val hasWest = outlineSides.hasDirection(DirectionMask.WEST) + val hasUp = outlineSides.hasDirection(DirectionMask.UP) + val hasDown = outlineSides.hasDirection(DirectionMask.DOWN) + val hasSouth = outlineSides.hasDirection(DirectionMask.SOUTH) + val hasNorth = outlineSides.hasDirection(DirectionMask.NORTH) + + // Top edges (all use top vertex colors) + if (outlineMode.check(hasUp, hasNorth)) { + lineGradient( + box.minX, box.maxY, box.minZ, outlineTopNorthWest, + box.maxX, box.maxY, box.minZ, outlineTopNorthEast, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasUp, hasSouth)) { + lineGradient( + box.minX, box.maxY, box.maxZ, outlineTopSouthWest, + box.maxX, box.maxY, box.maxZ, outlineTopSouthEast, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasUp, hasWest)) { + lineGradient( + box.minX, box.maxY, box.minZ, outlineTopNorthWest, + box.minX, box.maxY, box.maxZ, outlineTopSouthWest, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasUp, hasEast)) { + lineGradient( + box.maxX, box.maxY, box.maxZ, outlineTopSouthEast, + box.maxX, box.maxY, box.minZ, outlineTopNorthEast, + lineWidth, dashStyle + ) + } + + // Bottom edges (all use bottom vertex colors) + if (outlineMode.check(hasDown, hasNorth)) { + lineGradient( + box.minX, box.minY, box.minZ, outlineBottomNorthWest, + box.maxX, box.minY, box.minZ, outlineBottomNorthEast, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasDown, hasSouth)) { + lineGradient( + box.minX, box.minY, box.maxZ, outlineBottomSouthWest, + box.maxX, box.minY, box.maxZ, outlineBottomSouthEast, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasDown, hasWest)) { + lineGradient( + box.minX, box.minY, box.minZ, outlineBottomNorthWest, + box.minX, box.minY, box.maxZ, outlineBottomSouthWest, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasDown, hasEast)) { + lineGradient( + box.maxX, box.minY, box.minZ, outlineBottomNorthEast, + box.maxX, box.minY, box.maxZ, outlineBottomSouthEast, + lineWidth, dashStyle + ) + } + + // Vertical edges (gradient from top to bottom) + if (outlineMode.check(hasWest, hasNorth)) { + lineGradient( + box.minX, box.maxY, box.minZ, outlineTopNorthWest, + box.minX, box.minY, box.minZ, outlineBottomNorthWest, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasNorth, hasEast)) { + lineGradient( + box.maxX, box.maxY, box.minZ, outlineTopNorthEast, + box.maxX, box.minY, box.minZ, outlineBottomNorthEast, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasEast, hasSouth)) { + lineGradient( + box.maxX, box.maxY, box.maxZ, outlineTopSouthEast, + box.maxX, box.minY, box.maxZ, outlineBottomSouthEast, + lineWidth, dashStyle + ) + } + if (outlineMode.check(hasSouth, hasWest)) { + lineGradient( + box.minX, box.maxY, box.maxZ, outlineTopSouthWest, + box.minX, box.minY, box.maxZ, outlineBottomSouthWest, + lineWidth, dashStyle + ) + } + } + + /** Draw a line with world coordinates - handles relative conversion internally */ + private fun line( + x1: Double, y1: Double, z1: Double, + x2: Double, y2: Double, z2: Double, + color1: Color, + color2: Color, + width: Float, + dashStyle: LineDashStyle? = null + ) { + // Convert to camera-relative coordinates + val rx1 = (x1 - cameraPos.x).toFloat() + val ry1 = (y1 - cameraPos.y).toFloat() + val rz1 = (z1 - cameraPos.z).toFloat() + val rx2 = (x2 - cameraPos.x).toFloat() + val ry2 = (y2 - cameraPos.y).toFloat() + val rz2 = (z2 - cameraPos.z).toFloat() + + // Calculate segment vector + val dx = rx2 - rx1 + val dy = ry2 - ry1 + val dz = rz2 - rz1 + + // Quad-based lines need 4 vertices per segment + collector.addEdgeVertex(rx1, ry1, rz1, color1, dx, dy, dz, width, dashStyle) + collector.addEdgeVertex(rx1, ry1, rz1, color1, dx, dy, dz, width, dashStyle) + collector.addEdgeVertex(rx2, ry2, rz2, color2, dx, dy, dz, width, dashStyle) + collector.addEdgeVertex(rx2, ry2, rz2, color2, dx, dy, dz, width, dashStyle) + } + + /** Helper to transform a point by a matrix */ + private fun transformPoint(matrix: Matrix4f, x: Float, y: Float, z: Float): Vector3f { + val result = Vector4f(x, y, z, 1f) + matrix.transform(result) + return Vector3f(result.x, result.y, result.z) + } + + /** Add a face vertex with world coordinates - handles relative conversion internally */ + private fun faceVertex(x: Double, y: Double, z: Double, color: Color) { + val rx = (x - cameraPos.x).toFloat() + val ry = (y - cameraPos.y).toFloat() + val rz = (z - cameraPos.z).toFloat() + collector.addFaceVertex(rx, ry, rz, color) + } + + /** SDF outline effect configuration */ + data class SDFOutline( + val color: Color = Color.BLACK, + val width: Float = 0.1f // 0.0 - 0.3 in SDF units (distance from edge) + ) + + /** SDF glow effect configuration */ + data class SDFGlow( + val color: Color = Color(0, 200, 255, 180), + val radius: Float = 0.2f // Glow spread in SDF units + ) + + /** SDF shadow effect configuration */ + data class SDFShadow( + val color: Color = Color(0, 0, 0, 180), + val offset: Float = 0.05f, // Distance in text units + // Angle in degrees: 0=right, 90=up, 180=left, 270=down (for screen text with Y-up) + // For world text, angle is applied in local text space before billboarding + val angle: Float = 135f, // Default: bottom-right (45° below horizontal) + val softness: Float = 0f // Shadow blur in SDF units + ) { + /** X offset computed from angle and distance */ + val offsetX: Float get() = offset * kotlin.math.cos(Math.toRadians(angle.toDouble())).toFloat() + /** Y offset computed from angle and distance */ + val offsetY: Float get() = offset * kotlin.math.sin(Math.toRadians(angle.toDouble())).toFloat() + } + + /** SDF style configuration for text and other SDF-rendered elements */ + data class SDFStyle( + var color: Color = Color.WHITE, + val outline: SDFOutline? = null, + val glow: SDFGlow? = null, + val shadow: SDFShadow? = SDFShadow() // Default shadow enabled + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt b/src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt deleted file mode 100644 index 6687aa44e..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/RenderRegion.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import net.minecraft.util.math.Vec3d -import org.joml.Vector3f - -/** - * A render region represents a chunk-sized area in the world where vertices are stored relative to - * the region's origin. This solves floating-point precision issues at large world coordinates. - * - * @param originX The X coordinate of the region's origin (typically chunk corner) - * @param originY The Y coordinate of the region's origin - * @param originZ The Z coordinate of the region's origin - */ -class RenderRegion(val originX: Int, val originY: Int, val originZ: Int) { - /** - * Compute the camera-relative offset for this region. This is done in double precision to - * maintain accuracy at large coordinates. - * - * @param cameraPos The camera's world position (double precision) - * @return The offset from camera to region origin (small float, high precision) - */ - fun computeCameraRelativeOffset(cameraPos: Vec3d): Vector3f { - val offsetX = originX.toDouble() - cameraPos.x - val offsetY = originY.toDouble() - cameraPos.y - val offsetZ = originZ.toDouble() - cameraPos.z - return Vector3f(offsetX.toFloat(), offsetY.toFloat(), offsetZ.toFloat()) - } - - companion object { - /** Standard size of a render region (matches Minecraft chunk size). */ - const val REGION_SIZE = 16 - - /** - * Create a region for a chunk position. - * - * @param chunkX Chunk X coordinate - * @param chunkZ Chunk Z coordinate - * @param bottomY World bottom Y coordinate (typically -64) - */ - fun forChunk(chunkX: Int, chunkZ: Int, bottomY: Int) = - RenderRegion(chunkX * 16, bottomY, chunkZ * 16) - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt b/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt deleted file mode 100644 index bc1cd812c..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/TransientRegionESP.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import com.lambda.graphics.esp.RegionESP -import com.lambda.graphics.esp.ShapeScope -import java.util.concurrent.ConcurrentHashMap -import kotlin.math.floor - -/** - * Modern replacement for the legacy Treed system. Handles geometry that is cleared and rebuilt - * every tick. Uses region-based rendering for precision. - */ -class TransientRegionESP(name: String, depthTest: Boolean = false) : RegionESP(name, depthTest) { - private val builders = ConcurrentHashMap() - - /** Get or create a builder for a specific region. */ - override fun shapes(x: Double, y: Double, z: Double, block: ShapeScope.() -> Unit) { - val key = getRegionKey(x, y, z) - val scope = - builders.getOrPut(key) { - val size = RenderRegion.REGION_SIZE - val rx = (size * floor(x / size)).toInt() - val ry = (size * floor(y / size)).toInt() - val rz = (size * floor(z / size)).toInt() - ShapeScope(RenderRegion(rx, ry, rz)) - } - scope.apply(block) - } - - /** Clear all current builders. Call this at the end of every tick. */ - override fun clear() { - builders.clear() - } - - /** Upload collected geometry to GPU. Must be called on main thread. */ - override fun upload() { - val activeKeys = builders.keys().asSequence().toSet() - - builders.forEach { (key, scope) -> - val renderer = renderers.getOrPut(key) { RegionRenderer(scope.region) } - renderer.upload(scope.builder.collector) - } - - renderers.forEach { (key, renderer) -> - if (key !in activeKeys) { - renderer.clearData() - } - } - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt deleted file mode 100644 index 9aa34034d..000000000 --- a/src/main/kotlin/com/lambda/graphics/mc/WorldTextRenderer.kt +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.graphics.mc - -import com.lambda.Lambda.mc -import net.minecraft.client.font.TextRenderer -import net.minecraft.client.render.LightmapTextureManager -import net.minecraft.client.util.math.MatrixStack -import net.minecraft.text.Text -import net.minecraft.util.math.Vec3d -import java.awt.Color - -/** - * Utility for rendering text in 3D world space. - * - * Uses Minecraft's TextRenderer to draw text that faces the camera (billboard style) at any world - * position. Handles Unicode, formatting codes, and integrates with MC's rendering system. - * - * Usage: - * ```kotlin - * // In your render event - * WorldTextRenderer.drawText( - * pos = entity.pos.add(0.0, entity.height + 0.5, 0.0), - * text = entity.name, - * color = Color.WHITE, - * scale = 0.025f - * ) - * ``` - */ -object WorldTextRenderer { - - /** Default scale for world text (MC uses 0.025f for name tags) */ - const val DEFAULT_SCALE = 0.025f - - /** Maximum light level for full brightness */ - private const val FULL_BRIGHT = LightmapTextureManager.MAX_LIGHT_COORDINATE - - /** - * Draw text at a world position, facing the camera. - * - * @param pos World position for the text - * @param text The text to render - * @param color Text color (ARGB) - * @param scale Text scale (0.025f is default name tag size) - * @param shadow Whether to draw drop shadow - * @param seeThrough Whether text should be visible through blocks - * @param centered Whether to center the text horizontally - * @param backgroundColor Background color (0 for no background) - * @param light Light level (uses full bright by default) - */ - fun drawText( - pos: Vec3d, - text: Text, - color: Color = Color.WHITE, - scale: Float = DEFAULT_SCALE, - shadow: Boolean = true, - seeThrough: Boolean = false, - centered: Boolean = true, - backgroundColor: Int = 0, - light: Int = FULL_BRIGHT - ) { - val client = mc - val camera = client.gameRenderer?.camera ?: return - val textRenderer = client.textRenderer ?: return - val immediate = client.bufferBuilders?.entityVertexConsumers ?: return - - val cameraPos = camera.pos - - val matrices = MatrixStack() - matrices.push() - - // Translate to world position relative to camera - matrices.translate(pos.x - cameraPos.x, pos.y - cameraPos.y, pos.z - cameraPos.z) - - // Billboard - face camera using camera rotation directly (same as MC's LabelCommandRenderer) - matrices.multiply(camera.rotation) - - // Scale with negative Y to flip text vertically (matches MC's 0.025, -0.025, 0.025) - matrices.scale(scale, -scale, scale) - - // Calculate text position - val textWidth = textRenderer.getWidth(text) - val x = if (centered) -textWidth / 2f else 0f - - val layerType = - if (seeThrough) TextRenderer.TextLayerType.SEE_THROUGH - else TextRenderer.TextLayerType.NORMAL - - // Draw text - textRenderer.draw( - text, - x, - 0f, - color.rgb, - shadow, - matrices.peek().positionMatrix, - immediate, - layerType, - backgroundColor, - light - ) - - matrices.pop() - - // Flush immediately for world rendering - immediate.draw() - } - - /** - * Draw text at a world position with an outline effect. - * - * @param pos World position for the text - * @param text The text to render - * @param color Text color - * @param outlineColor Outline color - * @param scale Text scale - * @param centered Whether to center the text horizontally - * @param light Light level - */ - fun drawTextWithOutline( - pos: Vec3d, - text: Text, - color: Color = Color.WHITE, - outlineColor: Color = Color.BLACK, - scale: Float = DEFAULT_SCALE, - centered: Boolean = true, - light: Int = FULL_BRIGHT - ) { - val client = mc - val camera = client.gameRenderer?.camera ?: return - val textRenderer = client.textRenderer ?: return - val immediate = client.bufferBuilders?.entityVertexConsumers ?: return - - val cameraPos = camera.pos - - val matrices = MatrixStack() - matrices.push() - - matrices.translate(pos.x - cameraPos.x, pos.y - cameraPos.y, pos.z - cameraPos.z) - - // Billboard - face camera using camera rotation directly (same as MC's LabelCommandRenderer) - matrices.multiply(camera.rotation) - matrices.scale(scale, -scale, scale) - - val textWidth = textRenderer.getWidth(text) - val x = if (centered) -textWidth / 2f else 0f - - textRenderer.drawWithOutline( - text.asOrderedText(), - x, - 0f, - color.rgb, - outlineColor.rgb, - matrices.peek().positionMatrix, - immediate, - light - ) - - matrices.pop() - immediate.draw() - } - - /** Draw a simple string at a world position. */ - fun drawString( - pos: Vec3d, - text: String, - color: Color = Color.WHITE, - scale: Float = DEFAULT_SCALE, - shadow: Boolean = true, - seeThrough: Boolean = false, - centered: Boolean = true - ) { - drawText(pos, Text.literal(text), color, scale, shadow, seeThrough, centered) - } - - /** - * Draw multiple lines of text stacked vertically. - * - * @param pos World position for the top line - * @param lines List of text lines to render - * @param color Text color - * @param scale Text scale - * @param lineSpacing Spacing between lines in scaled units (default 10) - */ - fun drawMultilineText( - pos: Vec3d, - lines: List, - color: Color = Color.WHITE, - scale: Float = DEFAULT_SCALE, - lineSpacing: Float = 10f, - shadow: Boolean = true, - seeThrough: Boolean = false, - centered: Boolean = true - ) { - val client = mc - val camera = client.gameRenderer?.camera ?: return - val textRenderer = client.textRenderer ?: return - val immediate = client.bufferBuilders?.entityVertexConsumers ?: return - - val cameraPos = camera.pos - - val matrices = MatrixStack() - matrices.push() - - matrices.translate(pos.x - cameraPos.x, pos.y - cameraPos.y, pos.z - cameraPos.z) - - // Billboard - face camera using camera rotation directly (same as MC's LabelCommandRenderer) - matrices.multiply(camera.rotation) - matrices.scale(scale, -scale, scale) - - val layerType = - if (seeThrough) TextRenderer.TextLayerType.SEE_THROUGH - else TextRenderer.TextLayerType.NORMAL - - lines.forEachIndexed { index, text -> - val textWidth = textRenderer.getWidth(text) - val x = if (centered) -textWidth / 2f else 0f - val y = index * lineSpacing - - textRenderer.draw( - text, - x, - y, - color.rgb, - shadow, - matrices.peek().positionMatrix, - immediate, - layerType, - 0, - FULL_BRIGHT - ) - } - - matrices.pop() - immediate.draw() - } - - /** - * Draw text with a background box. - * - * @param pos World position - * @param text Text to render - * @param textColor Text color - * @param backgroundColor Background color (with alpha) - * @param scale Text scale - * @param padding Padding around text in pixels - */ - fun drawTextWithBackground( - pos: Vec3d, - text: Text, - textColor: Color = Color.WHITE, - backgroundColor: Color = Color(0, 0, 0, 128), - scale: Float = DEFAULT_SCALE, - padding: Int = 2, - shadow: Boolean = false, - seeThrough: Boolean = false, - centered: Boolean = true - ) { - val client = mc - client.textRenderer ?: return - - // Calculate background color as ARGB int - val bgColorInt = - (backgroundColor.alpha shl 24) or - (backgroundColor.red shl 16) or - (backgroundColor.green shl 8) or - backgroundColor.blue - - drawText( - pos = pos, - text = text, - color = textColor, - scale = scale, - shadow = shadow, - seeThrough = seeThrough, - centered = centered, - backgroundColor = bgColorInt - ) - } - - /** Calculate the width of text in world units at a given scale. */ - fun getTextWidth(text: Text, scale: Float = DEFAULT_SCALE): Float { - val textRenderer = mc.textRenderer ?: return 0f - return textRenderer.getWidth(text) * scale - } - - /** Calculate the height of text in world units at a given scale. */ - fun getTextHeight(scale: Float = DEFAULT_SCALE): Float { - val textRenderer = mc.textRenderer ?: return 0f - return textRenderer.fontHeight * scale - } -} diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt new file mode 100644 index 000000000..cb4c9d7f3 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc.renderer + +import com.lambda.context.SafeContext +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBufferSlice +import com.mojang.blaze3d.systems.RenderPass +import com.mojang.blaze3d.systems.RenderSystem +import kotlin.collections.isNotEmpty + +/** + * Abstract base class for ESP renderers. + * + * Provides shared world-space and screen-space rendering logic while allowing + * subclasses to define their own lifecycle (upload frequency, geometry building, etc.) + * + * Subclasses implement [getRendererTransforms] to return their renderer/transform pairs: + * - ImmediateRenderer/TickedRenderer: returns a single pair (one renderer, one transform) + * - ChunkedRenderer: returns multiple pairs (one per active chunk with per-chunk transforms) + * + * @param name Debug name for render passes + * @param depthTest Whether to use depth testing (true = through walls disabled) + */ +abstract class AbstractRenderer(val name: String, var depthTest: SafeContext.() -> Boolean) { + /** + * Get all renderer/transform pairs to render. + * Each pair contains a RegionRenderer and its associated dynamic transform. + * + * @return List of (RegionRenderer, GpuBufferSlice) pairs, or empty list if nothing to render + */ + protected abstract fun getRendererTransforms(): List> + + /** + * Get all screen-space renderers. + * Returns renderers that have screen-space data to render. + */ + protected abstract fun getScreenRenderers(): List + + /** Current font atlas for text rendering (may be null if no text) */ + protected abstract val currentFontAtlas: SDFFontAtlas? + + /** + * Render world-space geometry (faces, edges, text). + * Iterates over all renderer/transform pairs from getRendererTransforms(). + * + * All world-space renderers share the same xray depth buffer (cleared once per frame in RenderMain). + * - depthTest = true: Uses MC's depth buffer (respects world geometry) + * - depthTest = false: Uses Lambda's xray depth buffer (ignores world, self-ordering only) + */ + fun SafeContext.render() { + val chunks = getRendererTransforms() + if (chunks.isEmpty()) return + + val depth = depthTest() + + // Render Faces + RegionRenderer.createRenderPass("$name Faces", depth)?.use { pass -> + pass.setPipeline(RendererUtils.getFacesPipeline(depth)) + RenderSystem.bindDefaultUniforms(pass) + chunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderFaces(pass) + } + } + + // Render Edges + RegionRenderer.createRenderPass("$name Edges", depth)?.use { pass -> + pass.setPipeline(RendererUtils.getEdgesPipeline(depth)) + RenderSystem.bindDefaultUniforms(pass) + chunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderEdges(pass) + } + } + + // Render Text - style params are now embedded in vertex attributes + val textChunks = chunks.filter { (renderer, _) -> renderer.hasTextData() } + val atlas = currentFontAtlas + if (atlas != null && textChunks.isNotEmpty()) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + RegionRenderer.createRenderPass("$name Text", depth)?.use { pass -> + pass.setPipeline(RendererUtils.getTextPipeline(depth)) + RenderSystem.bindDefaultUniforms(pass) + pass.bindTexture("Sampler0", textureView, sampler) + textChunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderText(pass) + } + } + } + } + + // Render World Images + val imageChunks = chunks.filter { (renderer, _) -> renderer.hasWorldImageData() } + if (imageChunks.isNotEmpty()) { + // Pre-load glint texture before creating render pass + RendererUtils.ensureGlintTextureLoaded() + + RegionRenderer.createRenderPass("$name World Images", depth)?.use { pass -> + pass.setPipeline(RendererUtils.getWorldImagePipeline(depth)) + RenderSystem.bindDefaultUniforms(pass) + + // Bind enchantment glint texture for overlay support + RendererUtils.bindGlintTexture(pass, "Sampler1") + + // Use per-chunk transforms for correct positioning + // Glint animation is calculated in shader using GameTime with corrected speed + imageChunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderWorldImages(pass) + } + } + } + + // Render World Models + val modelChunks = chunks.filter { (renderer, _) -> renderer.hasModelData() } + if (modelChunks.isNotEmpty()) { + RendererUtils.ensureGlintTextureLoaded() + + // Create dedicated glint uniform slice (scale 8.0 for vanilla atlas parity) + val glintUniform = RendererUtils.createGlintUniform(8.0f) + + RegionRenderer.createRenderPass("$name World Models", depth)?.use { pass -> + pass.setPipeline(RendererUtils.getModelPipeline(depth)) + RenderSystem.bindDefaultUniforms(pass) + + // Bind overlay, lightmap, and glint textures + RendererUtils.bindOverlayTexture(pass, "Sampler1") + RendererUtils.bindLightmapTexture(pass, "Sampler2") + RendererUtils.bindGlintTexture(pass, "Sampler3") + + // Set the global glint animation matrix for this pass + pass.setUniform("GlintTransforms", glintUniform) + + // Use original chunk transforms for correct geometry position + modelChunks.forEach { (renderer, transform) -> + pass.setUniform("DynamicTransforms", transform) + renderer.renderModels(pass) + } + } + } + } + + /** + * Render screen-space geometry. Uses orthographic projection for 2D rendering. + * Uses a SEPARATE screen depth buffer for complete isolation from world-space. + * This should be called after world-space render() for proper layering. + * + * Each renderer clears the screen depth buffer at the start to ensure + * complete isolation from other renderers. + */ + fun renderScreen() { + val renderers = getScreenRenderers() + + RendererUtils.withScreenContext { + val dynamicTransform = RendererUtils.createScreenDynamicTransform() + + // Clear the SCREEN depth buffer (separate from world xray depth) + // This ensures complete isolation between renderers + RendererUtils.clearScreenDepthBuffer() + + // Helper to get the right render pass (depth already cleared at start) + fun getScreenPass(label: String): RenderPass? { + return RegionRenderer.createScreenRenderPassWithDepth(label, clearDepth = false) + } + + // Render Screen Models + val modelRenderers = renderers.filter { it.hasScreenModelData() } + if (modelRenderers.isNotEmpty()) { + RendererUtils.ensureGlintTextureLoaded() + + // Create dedicated glint uniform slice (scale 8.0 for vanilla GUI parity) + val glintUniform = RendererUtils.createGlintUniform(8.0f) + + getScreenPass("$name Screen Models")?.use { pass -> + pass.setPipeline(RendererUtils.getModelPipeline(depthTest = true)) + RenderSystem.bindDefaultUniforms(pass) + + // Use global screen dynamic transform for geometry position + pass.setUniform("DynamicTransforms", dynamicTransform) + // Use dedicated glint uniform for animation + pass.setUniform("GlintTransforms", glintUniform) + + // Bind overlay and lightmap textures for item effects + RendererUtils.bindOverlayTexture(pass, "Sampler1") + RendererUtils.bindLightmapTexture(pass, "Sampler2") + // Bind glint texture for enchantment shimmer + RendererUtils.bindGlintTexture(pass, "Sampler3") + + modelRenderers.forEach { it.renderScreenModels(pass) } + } + } + + // Render Screen Faces + getScreenPass("$name Screen Faces")?.use { pass -> + pass.setPipeline(RendererUtils.getScreenFacesPipeline(depthTest = true)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderers.forEach { it.renderScreenFaces(pass) } + } + + // Render Screen Edges + getScreenPass("$name Screen Edges")?.use { pass -> + pass.setPipeline(RendererUtils.getScreenEdgesPipeline(depthTest = true)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + renderers.forEach { it.renderScreenEdges(pass) } + } + + // Render Screen Text - style params are now embedded in vertex attributes + val textRenderers = renderers.filter { it.hasScreenTextData() } + val atlas = currentFontAtlas + if (atlas != null && textRenderers.isNotEmpty()) { + if (!atlas.isUploaded) atlas.upload() + val textureView = atlas.textureView + val sampler = atlas.sampler + if (textureView != null && sampler != null) { + getScreenPass("$name Screen Text")?.use { pass -> + pass.setPipeline(RendererUtils.getScreenTextPipeline(depthTest = true)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", dynamicTransform) + pass.bindTexture("Sampler0", textureView, sampler) + textRenderers.forEach { it.renderScreenText(pass) } + } + } + } + + // Render Screen Images - needs separate transform with glint matrix for animation + val imageRenderers = renderers.filter { it.hasScreenImageData() } + if (imageRenderers.isNotEmpty()) { + // Pre-load glint texture BEFORE creating render pass to avoid command conflicts + RendererUtils.ensureGlintTextureLoaded() + + // Create a fresh dynamic transform with glint matrix calculated NOW (not at build time) + val glintTransform = RendererUtils.createScreenDynamicTransformWithGlint() + + getScreenPass("$name Screen Images")?.use { pass -> + pass.setPipeline(RendererUtils.getScreenImagePipeline(depthTest = true)) + RenderSystem.bindDefaultUniforms(pass) + pass.setUniform("DynamicTransforms", glintTransform) + + // Bind enchantment glint texture for overlay support + RendererUtils.bindGlintTexture(pass, "Sampler1") + + // Each renderer handles its own texture batches + imageRenderers.forEach { it.renderScreenImages(pass) } + } + } + } + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt new file mode 100644 index 000000000..3b1a063e7 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc.renderer + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.events.WorldEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.event.listener.SafeListener.Companion.listenConcurrently +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.text.FontHandler +import com.lambda.graphics.text.SDFFontAtlas +import com.lambda.module.modules.client.StyleEditor +import com.lambda.threading.runSafe +import com.lambda.util.world.FastVector +import com.lambda.util.world.fastVectorOf +import com.mojang.blaze3d.buffers.GpuBufferSlice +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.minecraft.world.chunk.WorldChunk +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedDeque + + +/** + * Chunked ESP system using chunk-origin relative coordinates. + * + * This system: + * - Stores geometry relative to chunk origin (stable, small floats) + * - Only rebuilds when chunks are modified + * - At render time, translates from chunk origin to camera-relative position + * + * @param owner The module that owns this ESP system + * @param name The name of the ESP system + * @param depthTest Whether to use depth testing + * @param update The update function called for each block position + */ +class ChunkedRenderer( + owner: Any, + name: String, + depthTest: SafeContext.() -> Boolean, + private val update: RenderBuilder.(World, FastVector) -> Unit +) : AbstractRenderer(name, depthTest) { + private val chunkMap = ConcurrentHashMap() + + private val WorldChunk.chunkKey: Long + get() = getChunkKey(pos.x, pos.z) + + private val WorldChunk.chunkData + get() = chunkMap.getOrPut(chunkKey) { ChunkData(this) } + + private val rebuildQueue = ConcurrentLinkedDeque() + private val uploadQueue = ConcurrentLinkedDeque<() -> Unit>() + + // Font atlas from the default font handler + override val currentFontAtlas: SDFFontAtlas + get() = FontHandler.getDefaultFont() + + init { + owner.listen { event -> + val pos = event.pos + world.getWorldChunk(pos)?.chunkData?.markDirty() + + val xInChunk = pos.x and 15 + val zInChunk = pos.z and 15 + + if (xInChunk == 0) world.getWorldChunk(pos.west())?.chunkData?.markDirty() + if (xInChunk == 15) world.getWorldChunk(pos.east())?.chunkData?.markDirty() + if (zInChunk == 0) world.getWorldChunk(pos.north())?.chunkData?.markDirty() + if (zInChunk == 15) world.getWorldChunk(pos.south())?.chunkData?.markDirty() + } + + owner.listen { event -> event.chunk.chunkData.markDirty() } + owner.listen { chunkMap.remove(it.chunk.chunkKey)?.clearData() } + + owner.listenConcurrently { + val queueSize = rebuildQueue.size + val polls = minOf(StyleEditor.rebuildsPerTick, queueSize) + repeat(polls) { rebuildQueue.poll()?.rebuild() } + } + + owner.listen { + val polls = minOf(StyleEditor.uploadsPerTick, uploadQueue.size) + repeat(polls) { uploadQueue.poll()?.invoke() } + } + + owner.listen { render() } + owner.listen { renderScreen() } + } + + private fun getChunkKey(chunkX: Int, chunkZ: Int) = + (chunkX.toLong() and 0xFFFFFFFFL) or ((chunkZ.toLong() and 0xFFFFFFFFL) shl 32) + + /** Mark all tracked chunks for rebuild. */ + fun rebuild() { + rebuildQueue.clear() + rebuildQueue.addAll(chunkMap.values) + } + + /** + * Load all currently loaded world chunks and mark them for rebuild. Call this when the module + * is enabled to populate initial chunks. + */ + fun rebuildAll() { + runSafe { + val chunksArray = world.chunkManager.chunks.chunks + (0 until chunksArray.length()).forEach { i -> + chunksArray.get(i)?.chunkData?.markDirty() + } + } + } + + fun clear() { + chunkMap.values.forEach { it.clearData() } + chunkMap.clear() + rebuildQueue.clear() + uploadQueue.clear() + } + + /** + * Get renderer/transform pairs for all active chunks. + * Each chunk has its own renderer and per-chunk transform (chunk-origin to camera). + * Includes fresh glint TextureMat for world image animation. + */ + override fun getRendererTransforms(): List> { + val cameraPos = mc.gameRenderer?.camera?.pos ?: return emptyList() + + val activeChunks = chunkMap.values.filter { it.renderer.hasData() } + if (activeChunks.isEmpty()) return emptyList() + + val modelViewMatrix = RenderMain.modelViewMatrix + + // Pre-compute the glint matrix once for all chunks (same animation for all) + val glintMatrix = RendererUtils.createGlintTransform(0.25f) + + return activeChunks.map { chunkData -> + // Compute chunk-to-camera offset in double precision + val offsetX = (chunkData.originX - cameraPos.x).toFloat() + val offsetY = (chunkData.originY - cameraPos.y).toFloat() + val offsetZ = (chunkData.originZ - cameraPos.z).toFloat() + + val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f).translate(offsetX, offsetY, offsetZ) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), glintMatrix) + + chunkData.renderer to dynamicTransform + } + } + + /** + * Get renderers for screen-space rendering. + * Returns all chunk renderers that have screen data. + */ + override fun getScreenRenderers() = + chunkMap.values + .filter { it.renderer.hasScreenData() } + .map { it.renderer } + + /** Per-chunk data with its own renderer and origin. */ + private inner class ChunkData(val chunk: WorldChunk) { + // Chunk origin in world coordinates + val originX: Double = (chunk.pos.x shl 4).toDouble() + val originY: Double = chunk.bottomY.toDouble() + val originZ: Double = (chunk.pos.z shl 4).toDouble() + + // This chunk's own renderer + val renderer = RegionRenderer() + + fun markDirty() { + if (!rebuildQueue.contains(this)) rebuildQueue.add(this) + } + + /** + * Rebuild geometry relative to chunk origin. + * Coordinates are stored as (worldPos - chunkOrigin).toFloat() + */ + fun rebuild() { + // Use chunk origin as the "camera" position for relative coords + val chunkOriginVec = Vec3d(originX, originY, originZ) + val scope = RenderBuilder(chunkOriginVec) + + for (x in chunk.pos.startX..chunk.pos.endX) { + for (z in chunk.pos.startZ..chunk.pos.endZ) { + for (y in chunk.bottomY..chunk.height) { + update(scope, chunk.world, fastVectorOf(x, y, z)) + } + } + } + + uploadQueue.add { renderer.upload(scope.collector) } + } + + fun clearData() = renderer.clearData() + } + + companion object { + fun Any.chunkedRenderer( + name: String, + depthTest: SafeContext.() -> Boolean = { false }, + update: RenderBuilder.(World, FastVector) -> Unit + ) = ChunkedRenderer(this, name, depthTest, update) + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt new file mode 100644 index 000000000..1b02f7fb8 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc.renderer + +import com.lambda.context.SafeContext +import com.lambda.event.events.RenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBufferSlice +import com.mojang.blaze3d.systems.RenderSystem +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f + +/** + * Interpolated ESP system for smooth entity rendering. + * + * This system rebuilds and uploads vertices every frame with camera-relative coordinates. + * Callers are responsible for providing interpolated positions (e.g., using entity.prevX/x + * with tickDelta). The tick() method clears builders to allow smooth transitions between frames. + */ +class ImmediateRenderer( + owner: Any, + name: String, + depthTest: SafeContext.() -> Boolean, + update: RenderBuilder.(SafeContext) -> Unit +) : AbstractRenderer(name, depthTest) { + private val renderer = RegionRenderer() + + // Font atlas used for current text rendering + private var _currentFontAtlas: SDFFontAtlas? = null + override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas + + init { + owner.listen { + renderer.clearData() + val renderBuilder = RenderBuilder(mc.gameRenderer.camera.pos).also { it.update(SafeContext.create() ?: return@listen) } + upload(renderBuilder) + render() + } + owner.listen { renderScreen() } + } + + /** Upload collected geometry to GPU. Must be called on main thread. */ + fun upload(renderBuilder: RenderBuilder) { + renderer.upload(renderBuilder.collector) + _currentFontAtlas = renderBuilder.fontAtlas + } + + /** + * Get renderer/transform pairs for world-space rendering. + * Returns single renderer with camera-relative transform and fresh glint TextureMat. + */ + override fun getRendererTransforms(): List> { + if (!renderer.hasData()) return emptyList() + + val modelViewMatrix = RenderMain.modelViewMatrix + val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write( + modelView, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + RendererUtils.createGlintTransform(0.125f) // Calibrated glint scale for world images + ) + + return listOf(renderer to dynamicTransform) + } + + /** + * Get renderers for screen-space rendering. + */ + override fun getScreenRenderers() = if (renderer.hasScreenData()) listOf(renderer) else emptyList() + + companion object { + fun Any.immediateRenderer( + name: String, + depthTest: SafeContext.() -> Boolean = { false }, + update: RenderBuilder.(SafeContext) -> Unit + ) = ImmediateRenderer(this, name, depthTest, update) + } +} diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt new file mode 100644 index 000000000..2f66c049a --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt @@ -0,0 +1,470 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc.renderer + +import com.lambda.Lambda.mc +import com.lambda.event.events.HudRenderEvent +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.LambdaRenderPipelines +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.text.SDFFontAtlas +import com.lambda.graphics.texture.LambdaImageAtlas +import com.mojang.blaze3d.buffers.GpuBufferSlice +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.systems.ProjectionType +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.ProjectionMatrix2 +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f + +/** + * Shared utilities for ESP renderers. + * Contains common rendering setup code used by ImmediateRenderer, TickedRenderer, and ChunkedRenderer. + */ +object RendererUtils { + // Shared projection matrix for screen-space rendering + // invertY=false means Y=0 at bottom, Y=height at top (OpenGL/math convention) + private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, false) + + // Vanilla inventory lighting vectors (Studio Lighting) + // These are from net.minecraft.client.render.DiffuseLighting and flipped to be "towards light" + val INVENTORY_LIGHT_0 = Vector3f(-0.2f, 1.0f, -1.0f).normalize() + val INVENTORY_LIGHT_1 = Vector3f(0.2f, 1.0f, 0.0f).normalize() + + + + /** + * Create a dynamic transform uniform with identity matrices for screen-space rendering. + */ + fun createScreenDynamicTransform(): GpuBufferSlice { + val identityMatrix = Matrix4f() + return RenderSystem.getDynamicUniforms() + .write( + identityMatrix, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + identityMatrix + ) + } + + /** + * Create a dynamic transform with glint texture matrix for screen-space IMAGE rendering. + * Note: We use a smaller scale (0.125) compared to vanilla (8.0) because images use + * normalized 0-1 UVs. 0.125 results in the same ~1/8 visual density. + */ + fun createScreenDynamicTransformWithGlint(): GpuBufferSlice { + val identityMatrix = Matrix4f() + return RenderSystem.getDynamicUniforms() + .write( + identityMatrix, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + createGlintTransform(0.125f) // Normalized scale for [0, 1] UV parity + ) + } + + /** + * Create a dynamic transform with glint texture matrix for screen-space MODEL rendering. + * Uses vanilla's 8.0 scale since model UVs are texture-based, not normalized. + */ + fun createScreenModelDynamicTransformWithGlint(): GpuBufferSlice { + val identityMatrix = Matrix4f() + return RenderSystem.getDynamicUniforms() + .write( + identityMatrix, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + createGlintTransform(8.0f) // Vanilla glint scale parity + ) + } + + /** + * Create a glint transformation matrix exactly like Minecraft's TextureTransform.getGlintTransformation(). + * This is called every frame to get smooth animation. + * + * @param scale The UV scale factor (8.0 for items, 0.5 for entities, 0.16 for armor) + */ + fun createGlintTransform(scale: Float): Matrix4f { + // Exactly replicate Minecraft's TextureTransform.getGlintTransformation() + val glintSpeed = mc.options?.glintSpeed?.value ?: 0.5 + val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong() + + // Calculate scroll offsets (0-1 range) + val scrollX = (time % 110000L) / 110000.0f // 110s + val scrollY = (time % 30000L) / 30000.0f // 30s + + // Build matrix: Translation -> Rotation -> Scale (T * R * S) + // This order ensures the translation/scrolling is NOT multiplied by the scale + val matrix = Matrix4f() + matrix.translation(-scrollX, scrollY, 0f) + matrix.rotateZ((Math.PI / 18.0).toFloat()) // 10 degrees (Vanilla) + matrix.scale(scale) + + return matrix + } + + /** + * Create a dynamic uniform slice containing two glint transformation matrices. + * Pack Mat2 into the ModelView slot and Mat1 into the GlintMat slot of the GlintTransforms block. + */ + fun createGlintUniform(scale: Float): GpuBufferSlice { + val glintSpeed = mc.options?.glintSpeed?.value ?: 0.5 + val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong() + + // Up-Left (Vanilla parity in Y-up space) + val scroll1X = (time % 110000L) / 110000.0f + val scroll1Y = (time % 30000L) / 30000.0f + val mat1 = Matrix4f().translation(-scroll1X, scroll1Y, 0f) + .rotateZ((Math.PI / 18.0).toFloat()) // Exactly 10 deg (Vanilla) + .scale(scale) + + return RenderSystem.getDynamicUniforms() + .write( + Matrix4f(), // ModelView slot (Identity) + Vector4f(1f, 1f, 1f, 1f), // Color + Vector3f(0f, 0f, 0f), // Offset + mat1 // GlintMat slot + ) + } + + /** + * Create a dynamic transform with glint texture matrix for world-space MODEL rendering. + * Uses RenderMain.modelViewMatrix with translation zeroed for correct camera-relative positioning. + * Uses vanilla's 8.0 scale since model UVs are texture-based, not normalized. + */ + fun createWorldDynamicTransformWithGlint(): GpuBufferSlice { + // Use modelViewMatrix with translation zeroed out (same pattern as ImmediateRenderer) + val modelViewMatrix = RenderMain.modelViewMatrix + val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f) + + return RenderSystem.getDynamicUniforms() + .write( + modelView, + Vector4f(1f, 1f, 1f, 1f), + Vector3f(0f, 0f, 0f), + createGlintTransform(8.0f) // Vanilla glint scale parity + ) + } + + /** + * Create a dynamic transform for a specific chunk position with glint texture matrix. + * Used for world-space model rendering where items need enchantment glint animation. + * Uses vanilla's 8.0 scale since model UVs are texture-based, not normalized. + * + * @param chunkOffset The chunk's position offset (usually cameraRelative) + */ + fun createChunkTransformWithGlint(chunkOffset: Vector3f): GpuBufferSlice { + return RenderSystem.getDynamicUniforms() + .write( + RenderSystem.getModelViewMatrix(), + Vector4f(1f, 1f, 1f, 1f), + chunkOffset, + createGlintTransform(8.0f) // Vanilla glint scale parity + ) + } + + /** + * Execute a block with screen-space rendering context. + * Sets up orthographic projection and identity model-view, then restores state after. + * + * @param block The rendering code to execute in screen-space context + */ + fun withScreenContext(block: () -> Unit) { + val window = mc.window ?: return + val width = window.scaledWidth.toFloat() + val height = window.scaledHeight.toFloat() + + // Backup current projection matrix + RenderSystem.backupProjectionMatrix() + + // Use orthographic projection matrix + screenProjectionMatrix.set(width, height).let { slice -> + RenderSystem.setProjectionMatrix(slice, ProjectionType.ORTHOGRAPHIC) + } + + // Identity model-view for screen-space + RenderSystem.getModelViewStack().pushMatrix().identity() + + try { + block() + } finally { + // Restore matrices + RenderSystem.getModelViewStack().popMatrix() + RenderSystem.restoreProjectionMatrix() + } + } + + // ============================================================================ + // Pipeline Helpers + // ============================================================================ + + /** + * Get the face/quad pipeline. + * Always uses depth testing. Xray effect is achieved by using Lambda's + * custom depth buffer (which doesn't contain MC world geometry). + */ + fun getFacesPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.ESP_QUADS + + /** + * Get the edge/line pipeline. + * Always uses depth testing for proper self-ordering. + */ + fun getEdgesPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.ESP_LINES + + /** + * Get the SDF text pipeline. + * Always uses depth testing for proper self-ordering. + */ + fun getTextPipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.SDF_TEXT + + /** Get the screen-space faces pipeline. */ + fun getScreenFacesPipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_FACES + + /** Screen-space faces pipeline (legacy, use getScreenFacesPipeline instead). */ + val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_FACES + + /** Get the screen-space edges pipeline. */ + fun getScreenEdgesPipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_LINES + + /** Screen-space edges pipeline (legacy, use getScreenEdgesPipeline instead). */ + val screenEdgesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_LINES + + /** Get the screen-space text pipeline. */ + fun getScreenTextPipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_TEXT + + /** Screen-space text pipeline (legacy, use getScreenTextPipeline instead). */ + val screenTextPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_TEXT + + /** Get the screen-space image pipeline. */ + fun getScreenImagePipeline(depthTest: Boolean = true): RenderPipeline = LambdaRenderPipelines.SCREEN_IMAGE + + /** + * Get the world-space image pipeline. + * Always uses depth testing for proper self-ordering. + */ + fun getWorldImagePipeline(depthTest: Boolean): RenderPipeline = LambdaRenderPipelines.WORLD_IMAGE + + /** + * Get the world-space model pipeline. + * Always uses depth testing for proper self-ordering. + */ + fun getModelPipeline(depthTest: Boolean): RenderPipeline = + if (depthTest) LambdaRenderPipelines.WORLD_MODEL else LambdaRenderPipelines.SCREEN_MODEL + + // Cached glint texture view and sampler + private var glintTextureView: com.mojang.blaze3d.textures.GpuTextureView? = null + private var glintSampler: net.minecraft.client.gl.GpuSampler? = null + private var glintTextureLoaded = false + + /** + * Pre-load the glint texture and process any pending texture loads. + * Must be called BEFORE creating a render pass. + * This avoids the "close the existing render pass" error. + */ + fun ensureGlintTextureLoaded() { + // Process any pending texture loads from background threads + LambdaImageAtlas.processPendingLoads() + + if (!glintTextureLoaded) { + val textureManager = mc.textureManager + val glintId = net.minecraft.util.Identifier.ofVanilla("textures/misc/enchanted_glint_item.png") + val texture = textureManager.getTexture(glintId) + glintTextureView = texture?.glTextureView + glintTextureLoaded = true + } + + // Always ensure sampler is using REPEAT mode + glintSampler = RenderSystem.getSamplerCache().getRepeated(com.mojang.blaze3d.textures.FilterMode.LINEAR) + } + + /** + * Bind the Minecraft enchanted item glint texture to a sampler slot. + * Must call ensureGlintTextureLoaded() BEFORE starting the render pass. + * + * @param pass The render pass to bind the texture to + * @param samplerName The sampler name to bind to (e.g., "Sampler1") + */ + fun bindGlintTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) { + val view = glintTextureView ?: return + val sampler = glintSampler ?: return + pass.bindTexture(samplerName, view, sampler) + } + + /** + * Bind the generic Overlay texture (white/hurt flash) to a sampler slot. + */ + fun bindOverlayTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) { + val overlay = mc.gameRenderer.overlayTexture ?: return + val view = overlay.textureView ?: return + // Overlay texture usually uses LINEAR filtering + val sampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + pass.bindTexture(samplerName, view, sampler) + } + + /** + * Bind the Lightmap texture to a sampler slot. + */ + fun bindLightmapTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) { + val lightmap = mc.gameRenderer.lightmapTextureManager ?: return + val view = lightmap.glTextureView ?: return + // Lightmap uses LINEAR filtering + val sampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR) + pass.bindTexture(samplerName, view, sampler) + } + + // ============================================================================ + // Custom Depth Buffer for Xray Rendering + // ============================================================================ + + // Custom depth buffer for Lambda's xray rendering. + // This allows proper depth ordering among our own renders while ignoring MC's world. + private var xrayDepthTexture: com.mojang.blaze3d.textures.GpuTexture? = null + private var xrayDepthView: com.mojang.blaze3d.textures.GpuTextureView? = null + private var xrayDepthWidth = 0 + private var xrayDepthHeight = 0 + + /** + * Get the xray depth buffer view, creating/resizing if necessary. + * This depth buffer is separate from MC's main depth buffer, allowing + * our renders to show through MC's world while still having correct + * depth ordering among themselves. + * + * @return The depth buffer view, or null if framebuffer is not available + */ + fun getXrayDepthView(): com.mojang.blaze3d.textures.GpuTextureView? { + val framebuffer = mc.framebuffer ?: return null + val width = framebuffer.textureWidth + val height = framebuffer.textureHeight + + // Recreate if size changed or doesn't exist + if (xrayDepthTexture == null || xrayDepthWidth != width || xrayDepthHeight != height) { + // Clean up old resources + xrayDepthView?.close() + xrayDepthTexture?.close() + + // Create new depth texture matching framebuffer size + val gpuDevice = RenderSystem.getDevice() + xrayDepthTexture = gpuDevice.createTexture( + { "Lambda Xray Depth Buffer" }, + 15, // Usage flags (same as MC's depth buffers) + com.mojang.blaze3d.textures.TextureFormat.DEPTH32, + width, + height, + 1, // Layers + 1 // Mip levels + ) + xrayDepthView = gpuDevice.createTextureView(xrayDepthTexture) + xrayDepthWidth = width + xrayDepthHeight = height + } + + return xrayDepthView + } + + /** + * Clear the xray depth buffer to prepare for a new render sequence. + * Should be called once at the start of each frame's xray rendering. + */ + fun clearXrayDepthBuffer() { + val depthView = getXrayDepthView() ?: return + val framebuffer = mc.framebuffer ?: return + + // Create a render pass that just clears the depth buffer + // We pass OptionalDouble.of(1.0) to clear depth to far plane (1.0) + RenderSystem.getDevice() + .createCommandEncoder() + .createRenderPass( + { "Lambda Clear Xray Depth" }, + framebuffer.colorAttachmentView, + java.util.OptionalInt.empty(), // Don't clear color + depthView, + java.util.OptionalDouble.of(1.0) // Clear depth to 1.0 (far) + )?.close() // Immediately close to execute the clear + } + + // ============================================================================ + // Separate Screen-Space Depth Buffer + // ============================================================================ + + // A completely separate depth buffer for screen-space rendering. + // This ensures screen elements don't depth-fight with world-space elements. + private var screenDepthTexture: com.mojang.blaze3d.textures.GpuTexture? = null + private var screenDepthView: com.mojang.blaze3d.textures.GpuTextureView? = null + private var screenDepthWidth = 0 + private var screenDepthHeight = 0 + + /** + * Get the screen-space depth buffer view, creating/resizing if necessary. + * This depth buffer is separate from both MC's depth and the xray depth buffer, + * ensuring screen-space elements are completely isolated. + * + * @return The screen depth buffer view, or null if framebuffer is not available + */ + fun getScreenDepthView(): com.mojang.blaze3d.textures.GpuTextureView? { + val framebuffer = mc.framebuffer ?: return null + val width = framebuffer.textureWidth + val height = framebuffer.textureHeight + + // Recreate if size changed or doesn't exist + if (screenDepthTexture == null || screenDepthWidth != width || screenDepthHeight != height) { + // Clean up old resources + screenDepthView?.close() + screenDepthTexture?.close() + + // Create new depth texture matching framebuffer size + val gpuDevice = RenderSystem.getDevice() + screenDepthTexture = gpuDevice.createTexture( + { "Lambda Screen Depth Buffer" }, + 15, // Usage flags (same as MC's depth buffers) + com.mojang.blaze3d.textures.TextureFormat.DEPTH32, + width, + height, + 1, // Layers + 1 // Mip levels + ) + screenDepthView = gpuDevice.createTextureView(screenDepthTexture) + screenDepthWidth = width + screenDepthHeight = height + } + + return screenDepthView + } + + /** + * Clear the screen depth buffer to prepare for a new screen render sequence. + * Should be called at the start of each renderer's screen rendering. + */ + fun clearScreenDepthBuffer() { + val depthView = getScreenDepthView() ?: return + val framebuffer = mc.framebuffer ?: return + + RenderSystem.getDevice() + .createCommandEncoder() + .createRenderPass( + { "Lambda Clear Screen Depth" }, + framebuffer.colorAttachmentView, + java.util.OptionalInt.empty(), + depthView, + java.util.OptionalDouble.of(1.0) + )?.close() + } +} + diff --git a/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt new file mode 100644 index 000000000..058ad2b17 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.mc.renderer + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.RegionRenderer +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.text.SDFFontAtlas +import com.mojang.blaze3d.buffers.GpuBufferSlice +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.util.math.Vec3d +import org.joml.Matrix4f +import org.joml.Vector3f +import org.joml.Vector4f + + +/** + * Modern replacement for the legacy Treed system. Handles geometry that is cleared and rebuilt + * every tick. + * + * Geometry is stored relative to the camera position at tick time. At render time, we compute + * the delta between tick-camera and current-camera to ensure smooth motion without jitter. + */ +class TickedRenderer( + owner: Any, + name: String, + depthTest: SafeContext.() -> Boolean, + update: RenderBuilder.(SafeContext) -> Unit +) : AbstractRenderer(name, depthTest) { + private val renderer = RegionRenderer() + + // Camera position captured at tick time (when shapes are built) + private var tickCameraPos: Vec3d? = null + + // Font atlas used for current text rendering + private var _currentFontAtlas: SDFFontAtlas? = null + override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas + + init { + owner.listen { + clear() + tickCameraPos = mc.gameRenderer.camera.pos + val renderBuilder = RenderBuilder(tickCameraPos ?: return@listen).also { + it.update(SafeContext.create() ?: return@listen) + } + upload(renderBuilder) + } + + owner.listen { render() } + owner.listen { renderScreen() } + } + + /** Clear all current builders. Call this at the end of every tick. */ + fun clear() { + renderer.clearData() + tickCameraPos = null + } + + /** Upload collected geometry to GPU. Must be called on main thread. */ + fun upload(renderBuilder: RenderBuilder) { + renderer.upload(renderBuilder.collector) + _currentFontAtlas = renderBuilder.fontAtlas + } + + /** + * Get renderer/transform pairs for world-space rendering. + * Computes delta between tick-camera and current-camera for smooth interpolation. + * Includes fresh glint TextureMat for world image animation. + */ + override fun getRendererTransforms(): List> { + val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return emptyList() + val tickCamera = tickCameraPos ?: return emptyList() + if (!renderer.hasData()) return emptyList() + + val modelViewMatrix = RenderMain.modelViewMatrix + + // Compute the camera movement since tick time in double precision + // Geometry is stored relative to tickCamera, so we translate by (tickCamera - currentCamera) + val deltaX = (tickCamera.x - currentCameraPos.x).toFloat() + val deltaY = (tickCamera.y - currentCameraPos.y).toFloat() + val deltaZ = (tickCamera.z - currentCameraPos.z).toFloat() + + val modelView = Matrix4f(modelViewMatrix).m30(0f).m31(0f).m32(0f).translate(deltaX, deltaY, deltaZ) + val dynamicTransform = RenderSystem.getDynamicUniforms() + .write(modelView, Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), RendererUtils.createGlintTransform(0.25f)) + + return listOf(renderer to dynamicTransform) + } + + /** + * Get renderers for screen-space rendering. + */ + override fun getScreenRenderers() = if (renderer.hasScreenData()) listOf(renderer) else emptyList() + + companion object { + fun Any.tickedRenderer( + name: String, + depthTest: SafeContext.() -> Boolean = { false }, + update: RenderBuilder.(SafeContext) -> Unit + ) = TickedRenderer(this, name, depthTest, update) + } +} diff --git a/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt new file mode 100644 index 000000000..8c6561a16 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/FontHandler.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.text + +import java.util.concurrent.ConcurrentHashMap + +/** + * Central handler for font loading and caching. + * + * Manages SDF font atlases with automatic caching by path and size. + * Use this instead of creating SDFFontAtlas instances directly. + * + * Usage: + * ```kotlin + * val font = FontHandler.loadFont("fonts/MyFont.ttf", 128f) + * val defaultFont = FontHandler.getDefaultFont() + * ``` + */ +object FontHandler { + private val fonts = ConcurrentHashMap() + private var defaultFont: SDFFontAtlas? = null + + /** + * Load an SDF font from resources. + * + * @param path Resource path to TTF/OTF file (e.g., "fonts/MinecraftDefault-Regular.ttf") + * @param size Base font size for SDF generation (larger = higher quality, default 128) + * @return The loaded SDFFontAtlas, or null if loading failed + */ + fun loadFont(path: String, size: Float = 128f): SDFFontAtlas? { + val key = "$path@$size" + return fonts.getOrPut(key) { + try { + SDFFontAtlas(path, size) + } catch (e: Exception) { + println("[FontHandler] Failed to load font: $path - ${e.message}") + return null + } + } + } + + /** + * Get or create the default font. + * Uses MinecraftDefault-Regular.ttf at 128px base size. + */ + fun getDefaultFont(size: Float = 128f): SDFFontAtlas { + defaultFont?.let { return it } + + val key = "fonts/MinecraftDefault-Regular.ttf@$size" + val font = fonts[key] ?: run { + val newFont = SDFFontAtlas("fonts/MinecraftDefault-Regular.ttf", size) + fonts[key] = newFont + newFont + } + defaultFont = font + return font + } + + fun isFontLoaded(path: String, size: Float = 128f) = fonts.containsKey("path@$size") + + fun getLoadedFonts(): Set = fonts.keys.toSet() + + /** + * Clean up all loaded fonts and release GPU resources. + * Call this when shutting down or when fonts are no longer needed. + */ + fun cleanup() { + fonts.values.forEach { it.close() } + fonts.clear() + defaultFont = null + } +} diff --git a/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt new file mode 100644 index 000000000..786184b61 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/text/SDFFontAtlas.kt @@ -0,0 +1,746 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.text + +import com.lambda.Lambda.mc +import com.lambda.util.stream +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.textures.FilterMode +import com.mojang.blaze3d.textures.GpuTexture +import com.mojang.blaze3d.textures.GpuTextureView +import com.mojang.blaze3d.textures.TextureFormat +import net.minecraft.client.gl.GpuSampler +import net.minecraft.client.texture.NativeImage +import org.lwjgl.stb.STBTTFontinfo +import org.lwjgl.stb.STBTTVertex +import org.lwjgl.stb.STBTruetype.STBTT_vcurve +import org.lwjgl.stb.STBTruetype.STBTT_vline +import org.lwjgl.stb.STBTruetype.STBTT_vmove +import org.lwjgl.stb.STBTruetype.stbtt_FindGlyphIndex +import org.lwjgl.stb.STBTruetype.stbtt_FreeShape +import org.lwjgl.stb.STBTruetype.stbtt_GetFontVMetrics +import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBitmapBox +import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphBox +import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphHMetrics +import org.lwjgl.stb.STBTruetype.stbtt_GetGlyphShape +import org.lwjgl.stb.STBTruetype.stbtt_InitFont +import org.lwjgl.stb.STBTruetype.stbtt_ScaleForPixelHeight +import org.lwjgl.system.MemoryStack +import org.lwjgl.system.MemoryUtil +import java.nio.ByteBuffer +import kotlin.math.sqrt +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +/** + * Signed Distance Field font atlas for high-quality scalable text rendering. + * + * SDF fonts store the distance to the nearest edge instead of raw coverage, + * enabling crisp text at any scale with effects like outlines and glows. + * + * Uses MC 1.21's GpuTexture APIs for proper texture binding via RenderPass.bindTexture(). + * + * @param fontPath Resource path to TTF/OTF file + * @param baseSize Base font size for SDF generation (larger = more detail, 48-64 recommended) + * @param sdfSpread SDF spread in pixels (how far the distance field extends) + * @param atlasSize Atlas texture dimensions (must be power of 2) + */ +class SDFFontAtlas( + fontPath: String, + val baseSize: Float = 256f, + val sdfSpread: Int = 16, + val atlasSize: Int = 4096 +) : AutoCloseable { + + data class Glyph( + val codepoint: Int, + val width: Int, + val height: Int, + val bearingX: Float, + val bearingY: Float, + val advance: Float, + val u0: Float, val v0: Float, + val u1: Float, val v1: Float + ) + + /** + * Work unit for parallel glyph SDF generation. + * Contains all data needed to generate the SDF independently. + */ + private data class GlyphJob( + val codepoint: Int, + val glyphIndex: Int, + val atlasX: Int, + val atlasY: Int, + val paddedW: Int, + val paddedH: Int, + val glyphW: Int, + val glyphH: Int, + val glyph: Glyph + ) + + private val fontBuffer: ByteBuffer + private val fontInfo: STBTTFontinfo + private var atlasData: ByteArray? = null + private val glyphs = mutableMapOf() + + // MC 1.21 GPU texture objects + private var glTexture: GpuTexture? = null + private var glTextureView: GpuTextureView? = null + private var gpuSampler: GpuSampler? = null + + val lineHeight: Float + val ascent: Float + val descent: Float + val scale: Float + + /** The pixel range used for SDF, needed by shader for proper AA */ + val sdfPixelRange: Float get() = (sdfSpread * 2).toFloat() + + /** Get the texture view for binding in render pass */ + val textureView: GpuTextureView? get() = glTextureView + + /** Get the sampler for binding in render pass */ + val sampler: GpuSampler? get() = gpuSampler + + /** Check if texture is uploaded and ready */ + val isUploaded: Boolean get() = glTexture != null + + init { + // Load font file + val fontBytes = fontPath.stream.readAllBytes() + fontBuffer = MemoryUtil.memAlloc(fontBytes.size).put(fontBytes).flip() + + fontInfo = STBTTFontinfo.create() + if (!stbtt_InitFont(fontInfo, fontBuffer)) { + MemoryUtil.memFree(fontBuffer) + throw RuntimeException("Failed to initialize font: $fontPath") + } + + scale = stbtt_ScaleForPixelHeight(fontInfo, baseSize) + + MemoryStack.stackPush().use { stack -> + val ascentBuf = stack.mallocInt(1) + val descentBuf = stack.mallocInt(1) + val lineGapBuf = stack.mallocInt(1) + stbtt_GetFontVMetrics(fontInfo, ascentBuf, descentBuf, lineGapBuf) + + ascent = ascentBuf[0] * scale + descent = descentBuf[0] * scale + lineHeight = (ascentBuf[0] - descentBuf[0] + lineGapBuf[0]) * scale + } + + atlasData = ByteArray(atlasSize * atlasSize) + buildSDFAtlas() + } + + /** + * Build the SDF atlas using parallel glyph generation. + * + * Phase 1: Sequential layout - calculate glyph positions in the atlas + * Phase 2: Parallel generation - generate SDF for each glyph concurrently + */ + private fun buildSDFAtlas() { + val data = atlasData ?: return + var penX = sdfSpread + var penY = sdfSpread + var rowHeight = 0 + + val codepoints = (32..126) + (160..255) + val jobs = mutableListOf() + + // Phase 1: Calculate all glyph positions (sequential, fast) + MemoryStack.stackPush().use { stack -> + val x0 = stack.mallocInt(1) + val y0 = stack.mallocInt(1) + val x1 = stack.mallocInt(1) + val y1 = stack.mallocInt(1) + val advanceWidth = stack.mallocInt(1) + val leftSideBearing = stack.mallocInt(1) + + for (cp in codepoints) { + val glyphIndex = stbtt_FindGlyphIndex(fontInfo, cp) + if (glyphIndex == 0 && cp != 32) continue + + stbtt_GetGlyphHMetrics(fontInfo, glyphIndex, advanceWidth, leftSideBearing) + stbtt_GetGlyphBitmapBox(fontInfo, glyphIndex, scale, scale, x0, y0, x1, y1) + + val glyphW = x1[0] - x0[0] + val glyphH = y1[0] - y0[0] + val paddedW = glyphW + sdfSpread * 2 + val paddedH = glyphH + sdfSpread * 2 + + if (penX + paddedW >= atlasSize) { + penX = sdfSpread + penY += rowHeight + sdfSpread + rowHeight = 0 + } + + if (penY + paddedH >= atlasSize) { + System.err.println("SDF Atlas overflow at codepoint $cp") + break + } + + val glyph = Glyph( + codepoint = cp, + width = paddedW, + height = paddedH, + bearingX = (x0[0] - sdfSpread) / baseSize, + bearingY = (-y0[0] + sdfSpread) / baseSize, + advance = advanceWidth[0] * scale / baseSize, + u0 = penX.toFloat() / atlasSize, + v0 = penY.toFloat() / atlasSize, + u1 = (penX + paddedW).toFloat() / atlasSize, + v1 = (penY + paddedH).toFloat() / atlasSize + ) + + glyphs[cp] = glyph + + // Only create job if glyph has visible content + if (glyphW > 0 && glyphH > 0) { + jobs.add(GlyphJob( + codepoint = cp, + glyphIndex = glyphIndex, + atlasX = penX, + atlasY = penY, + paddedW = paddedW, + paddedH = paddedH, + glyphW = glyphW, + glyphH = glyphH, + glyph = glyph + )) + } + + penX += paddedW + sdfSpread + rowHeight = maxOf(rowHeight, paddedH) + } + } + + // Phase 2: Generate SDF for each glyph in parallel + runBlocking(Dispatchers.Default) { + for (job in jobs) { + launch { + generateGlyphSDF( + job.glyphIndex, data, + job.atlasX, job.atlasY, + job.paddedW, job.paddedH, + job.glyphW, job.glyphH + ) + } + } + } + } + + /** + * Generate vector-based SDF for a glyph. + * Computes distances directly from bezier curves for smooth edges. + */ + private fun generateGlyphSDF( + glyphIndex: Int, + atlasData: ByteArray, + atlasX: Int, atlasY: Int, + paddedW: Int, paddedH: Int, + glyphW: Int, glyphH: Int + ) { + MemoryStack.stackPush().use { stack -> + // Get glyph bounding box in FONT UNITS + val boxX0 = stack.mallocInt(1) + val boxY0 = stack.mallocInt(1) + val boxX1 = stack.mallocInt(1) + val boxY1 = stack.mallocInt(1) + stbtt_GetGlyphBox(fontInfo, glyphIndex, boxX0, boxY0, boxX1, boxY1) + + val fontX0 = boxX0[0].toFloat() + val fontY0 = boxY0[0].toFloat() + val fontX1 = boxX1[0].toFloat() + val fontY1 = boxY1[0].toFloat() + val fontWidth = fontX1 - fontX0 + val fontHeight = fontY1 - fontY0 + + // Get glyph shape (bezier curves in font units) + val verticesPtr = stack.mallocPointer(1) + val numVertices = stbtt_GetGlyphShape(fontInfo, glyphIndex, verticesPtr) + + if (numVertices <= 0 || fontWidth <= 0 || fontHeight <= 0) { + // Empty glyph (space, etc) - fill with "outside" value + for (py in 0 until paddedH) { + for (px in 0 until paddedW) { + val index = (atlasY + py) * atlasSize + atlasX + px + if (index >= 0 && index < atlasSize * atlasSize) { + atlasData[index] = 0 + } + } + } + return + } + + val vertices = STBTTVertex.create(verticesPtr[0], numVertices) + + try { + // Extract curve segments from vertices (in font units) + val segments = mutableListOf() + var lastX = 0f + var lastY = 0f + + for (i in 0 until numVertices) { + val v = vertices[i] + val type = v.type().toInt() + val x = v.x().toFloat() + val y = v.y().toFloat() + + when (type) { + STBTT_vmove.toInt() -> { + lastX = x + lastY = y + } + STBTT_vline.toInt() -> { + segments.add(LineSegment(lastX, lastY, x, y)) + lastX = x + lastY = y + } + STBTT_vcurve.toInt() -> { + val cx = v.cx().toFloat() + val cy = v.cy().toFloat() + segments.add(QuadraticBezier(lastX, lastY, cx, cy, x, y)) + lastX = x + lastY = y + } + } + } + + // Font units per pixel in the output + // The glyph area (without padding) maps to the font bounding box + val fontUnitsPerPixelX = fontWidth / glyphW + val fontUnitsPerPixelY = fontHeight / glyphH + + // Compute SDF for each pixel in output + for (py in 0 until paddedH) { + for (px in 0 until paddedW) { + // Map output pixel to font units + // px, py are in padded coordinate space + // The glyph occupies pixels [sdfSpread, sdfSpread+glyphW) x [sdfSpread, sdfSpread+glyphH) + val gx = px - sdfSpread // Glyph-local X (0 to glyphW maps to fontX0 to fontX1) + val gy = py - sdfSpread // Glyph-local Y + + // Convert to font units + // X: direct mapping + val fontX = fontX0 + gx * fontUnitsPerPixelX + // Y: font coords have Y up, screen coords have Y down + // gy=0 should map to fontY1 (top), gy=glyphH should map to fontY0 (bottom) + val fontY = fontY1 - gy * fontUnitsPerPixelY + + // Find minimum distance to any curve segment (in font units) + var minDist = Float.MAX_VALUE + for (seg in segments) { + val d = seg.distance(fontX, fontY) + if (d < minDist) { + minDist = d + } + } + + // Determine if inside or outside using winding number + val inside = computeWindingNumber(fontX, fontY, segments) != 0 + val signedDist = if (inside) minDist else -minDist + + // Convert distance from font units to pixels + val avgFontUnitsPerPixel = (fontUnitsPerPixelX + fontUnitsPerPixelY) / 2f + val pixelDist = signedDist / avgFontUnitsPerPixel + + // Normalize: map [-sdfSpread, +sdfSpread] pixels to [0, 1] + val normalizedDist = (pixelDist / sdfSpread + 1f) * 0.5f + val value = (normalizedDist.coerceIn(0f, 1f) * 255).toInt().toByte() + + val index = (atlasY + py) * atlasSize + atlasX + px + if (index >= 0 && index < atlasSize * atlasSize) { + atlasData[index] = value + } + } + } + } finally { + stbtt_FreeShape(fontInfo, vertices) + } + } + } + + /** Curve segment interface */ + private sealed interface CurveSegment { + fun distance(px: Float, py: Float): Float + } + + /** Line segment */ + private data class LineSegment( + val x0: Float, val y0: Float, + val x1: Float, val y1: Float + ) : CurveSegment { + override fun distance(px: Float, py: Float): Float { + val dx = x1 - x0 + val dy = y1 - y0 + val lenSq = dx * dx + dy * dy + if (lenSq < 1e-10f) return sqrt((px - x0) * (px - x0) + (py - y0) * (py - y0)) + + val t = ((px - x0) * dx + (py - y0) * dy) / lenSq + val tc = t.coerceIn(0f, 1f) + val nearX = x0 + tc * dx + val nearY = y0 + tc * dy + return sqrt((px - nearX) * (px - nearX) + (py - nearY) * (py - nearY)) + } + } + + /** Quadratic bezier curve */ + private data class QuadraticBezier( + val x0: Float, val y0: Float, + val cx: Float, val cy: Float, + val x1: Float, val y1: Float + ) : CurveSegment { + override fun distance(px: Float, py: Float): Float { + // Use iterative refinement for accurate bezier distance + // First pass: coarse sampling to find approximate t + var bestT = 0f + var minDist = Float.MAX_VALUE + + // Coarse pass: 32 samples + for (i in 0..32) { + val t = i / 32f + val d = distAtT(px, py, t) + if (d < minDist) { + minDist = d + bestT = t + } + } + + // Refinement: search around bestT with smaller steps + val step = 1f / 64f + var tLo = (bestT - step * 2).coerceIn(0f, 1f) + var tHi = (bestT + step * 2).coerceIn(0f, 1f) + + for (i in 0..16) { + val t = tLo + (tHi - tLo) * i / 16f + val d = distAtT(px, py, t) + if (d < minDist) { + minDist = d + bestT = t + } + } + + return minDist + } + + private fun distAtT(px: Float, py: Float, t: Float): Float { + val u = 1f - t + val bx = u * u * x0 + 2 * u * t * cx + t * t * x1 + val by = u * u * y0 + 2 * u * t * cy + t * t * y1 + return sqrt((px - bx) * (px - bx) + (py - by) * (py - by)) + } + + /** Get subdivided points for winding calculation */ + fun getSubdividedPoints(numSegments: Int = 8): List> { + val points = mutableListOf>() + for (i in 0..numSegments) { + val t = i.toFloat() / numSegments + val u = 1f - t + val bx = u * u * x0 + 2 * u * t * cx + t * t * x1 + val by = u * u * y0 + 2 * u * t * cy + t * t * y1 + points.add(Pair(bx, by)) + } + return points + } + } + + /** Compute winding number to determine if point is inside the glyph */ + private fun computeWindingNumber(px: Float, py: Float, segments: List): Int { + var winding = 0 + for (seg in segments) { + when (seg) { + is LineSegment -> { + winding += windingForLine(px, py, seg.x0, seg.y0, seg.x1, seg.y1) + } + is QuadraticBezier -> { + // Subdivide bezier into line segments for accurate winding + val points = seg.getSubdividedPoints(8) + for (i in 0 until points.size - 1) { + val (ax, ay) = points[i] + val (bx, by) = points[i + 1] + winding += windingForLine(px, py, ax, ay, bx, by) + } + } + } + } + return winding + } + + /** Compute winding contribution for a single line segment */ + private fun windingForLine(px: Float, py: Float, x0: Float, y0: Float, x1: Float, y1: Float): Int { + if (y0 <= py) { + if (y1 > py) { + val cross = (x1 - x0) * (py - y0) - (px - x0) * (y1 - y0) + if (cross > 0) return 1 + } + } else { + if (y1 <= py) { + val cross = (x1 - x0) * (py - y0) - (px - x0) * (y1 - y0) + if (cross < 0) return -1 + } + } + return 0 + } + + /** + * Compute signed distance field using Euclidean Distance Transform (EDT). + * Uses the Felzenszwalb-Huttenlocher algorithm for O(n) linear time. + * + * @param coverage Grayscale values 0-1 where > 0.5 is "inside" + * @param width Image width + * @param height Image height + * @return Signed distance field (positive = inside, negative = outside) + */ + private fun computeEDT(coverage: FloatArray, width: Int, height: Int): FloatArray { + val INF = 1e10f + + // Create binary inside/outside arrays based on coverage threshold + val inside = FloatArray(width * height) { i -> + if (coverage[i] > 0.5f) 0f else INF + } + val outside = FloatArray(width * height) { i -> + if (coverage[i] <= 0.5f) 0f else INF + } + + // Compute EDT for both inside and outside + edtTransform(inside, width, height) + edtTransform(outside, width, height) + + // Combine into signed distance field + // distOutside - distInside: positive inside glyph, negative outside + val sdf = FloatArray(width * height) + for (i in 0 until width * height) { + val distInside = sqrt(inside[i]) + val distOutside = sqrt(outside[i]) + sdf[i] = distOutside - distInside + } + + return sdf + } + + /** + * 2D Euclidean Distance Transform using Felzenszwalb-Huttenlocher algorithm. + * Transforms the input array in-place to contain squared distances. + */ + private fun edtTransform(data: FloatArray, width: Int, height: Int) { + val INF = 1e10f + val maxDim = maxOf(width, height) + + // Temporary arrays for 1D transform + val f = FloatArray(maxDim) + val d = FloatArray(maxDim) + val v = IntArray(maxDim) + val z = FloatArray(maxDim + 1) + + // Transform columns + for (x in 0 until width) { + for (y in 0 until height) { + f[y] = data[y * width + x] + } + edt1d(f, d, v, z, height) + for (y in 0 until height) { + data[y * width + x] = d[y] + } + } + + // Transform rows + for (y in 0 until height) { + for (x in 0 until width) { + f[x] = data[y * width + x] + } + edt1d(f, d, v, z, width) + for (x in 0 until width) { + data[y * width + x] = d[x] + } + } + } + + /** + * 1D squared Euclidean distance transform. + * f = input function, d = output distances + */ + private fun edt1d(f: FloatArray, d: FloatArray, v: IntArray, z: FloatArray, n: Int) { + val INF = 1e10f + var k = 0 + v[0] = 0 + z[0] = -INF + z[1] = INF + + for (q in 1 until n) { + var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]) + while (s <= z[k]) { + k-- + s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]) + } + k++ + v[k] = q + z[k] = s + z[k + 1] = INF + } + + k = 0 + for (q in 0 until n) { + while (z[k + 1] < q) { + k++ + } + val dist = q - v[k] + d[q] = dist * dist + f[v[k]] + } + } + + /** + * Upload atlas to GPU using MC 1.21 APIs. + * Must be called on render thread. + */ + fun upload() { + if (glTexture != null) return + val data = atlasData ?: return + + RenderSystem.assertOnRenderThread() + + val gpuDevice = RenderSystem.getDevice() + + // Create RGBA8 texture - the shader samples red channel for SDF value + glTexture = gpuDevice.createTexture( + "Lambda SDF FontAtlas", + 5, // COPY_DST (1) | TEXTURE_BINDING (4) + TextureFormat.RGBA8, + atlasSize, atlasSize, + 1, 1 + ) + + glTextureView = gpuDevice.createTextureView(glTexture) + + // Use LINEAR filtering for smooth SDF interpolation + gpuSampler = RenderSystem.getSamplerCache().get(FilterMode.LINEAR) + + // Create NativeImage with SDF value in alpha channel for transparency blending + // The position_tex_color shader multiplies texture.rgba by vertex color + // So we need SDF in alpha, with white RGB for the text color from vertex + val nativeImage = NativeImage(atlasSize, atlasSize, false) + for (y in 0 until atlasSize) { + for (x in 0 until atlasSize) { + val sdfValue = data[y * atlasSize + x].toInt() and 0xFF + // ABGR format: alpha=sdfValue, blue=255, green=255, red=255 + // SDF in alpha allows proper transparency blending + val abgr = (sdfValue shl 24) or (255 shl 16) or (255 shl 8) or 255 + nativeImage.setColor(x, y, abgr) + } + } + + RenderSystem.getDevice().createCommandEncoder().writeToTexture(glTexture, nativeImage) + nativeImage.close() + + atlasData = null + } + + fun getGlyph(codepoint: Int): Glyph? = glyphs[codepoint] + + private fun getStringWidth(text: String, fontSize: Float): Float { + var width = 0f + for (char in text) { + val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue + width += glyph.advance * fontSize + } + return width + } + + /** Get screen width in pixels (uses MC's scaled width). */ + private val screenWidth: Float + get() = mc.window.scaledWidth.toFloat() + + /** Get screen height in pixels (uses MC's scaled height). */ + private val screenHeight: Float + get() = mc.window.scaledHeight.toFloat() + + /** + * Get the width of text using normalized size (0-1 range, matching screenText). + * @param text The text string to measure + * @param normalizedSize Text size in normalized units (e.g., 0.02 = 2% of screen) + * @return Width in normalized units (0-1 range relative to screen width) + */ + fun getStringWidthNormalized(text: String, normalizedSize: Float): Float { + // Apply the same baseSize/ascent correction that screenText uses + // so dimensions match what actually gets rendered + val targetPixelHeight = normalizedSize * screenHeight + val pixelSize = targetPixelHeight * baseSize / ascent + val pixelWidth = getStringWidth(text, pixelSize) + return pixelWidth / screenWidth + } + + /** + * Get the descent using normalized size (0-1 range, matching screenText). + * @param normalizedSize Text size in normalized units + * @return Descent in normalized units (0-1 range relative to screen height) + */ + fun getDescentNormalized(normalizedSize: Float): Float { + // descent / ascent = proportion of ascent that is descent + return normalizedSize * descent / ascent + } + + /** + * Get both width and height of text using normalized size (0-1 range, matching screenText). + * @param text The text string to measure + * @param normalizedSize Text size in normalized units + * @return Pair of (width, height) in normalized units + */ + fun getStringDimensionsNormalized(text: String, normalizedSize: Float): Pair { + return Pair(getStringWidthNormalized(text, normalizedSize), normalizedSize) + } + + /** + * Get the normalized size needed to make text fit a target width. + * This is the inverse of getStringWidthNormalized. + * @param text The text string to measure + * @param targetWidthNormalized The desired width in normalized units (0-1 range relative to screen width) + * @return The normalized size that would produce the target width + */ + fun getSizeForWidthNormalized(text: String, targetWidthNormalized: Float): Float { + // Calculate the raw advance width of the text (sum of glyph advances) + var rawAdvance = 0f + for (char in text) { + val glyph = glyphs[char.code] ?: glyphs[' '.code] ?: continue + rawAdvance += glyph.advance + } + if (rawAdvance <= 0f) return 0f + + // Width formula from getStringWidthNormalized: + // targetPixelHeight = normalizedSize * screenHeight + // pixelSize = targetPixelHeight * baseSize / ascent + // pixelWidth = rawAdvance * pixelSize (since getStringWidth multiplies advance by fontSize) + // normalizedWidth = pixelWidth / screenWidth + // + // Solving for normalizedSize: + // normalizedWidth = (rawAdvance * normalizedSize * screenHeight * baseSize / ascent) / screenWidth + // normalizedSize = (normalizedWidth * screenWidth * ascent) / (rawAdvance * screenHeight * baseSize) + return (targetWidthNormalized * screenWidth * ascent) / (rawAdvance * screenHeight * baseSize) + } + + override fun close() { + glTextureView?.close() + glTextureView = null + glTexture?.close() + glTexture = null + gpuSampler = null + atlasData = null + MemoryUtil.memFree(fontBuffer) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt b/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt new file mode 100644 index 000000000..4f8f4d325 --- /dev/null +++ b/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt @@ -0,0 +1,399 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.graphics.texture + +import com.lambda.Lambda.mc +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.textures.GpuTextureView +import net.minecraft.client.texture.AbstractTexture +import net.minecraft.client.texture.MissingSprite +import net.minecraft.client.texture.Sprite +import net.minecraft.client.texture.SpriteAtlasTexture +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier +import java.awt.image.BufferedImage +import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentHashMap + +/** + * Central service for loading and caching textures as GPU resources for Lambda's + * custom image rendering system. + * + * Provides access to: + * - Custom Lambda textures from resources + * - Minecraft textures (e.g., enchantment glint) + * - Item sprites from MC's atlas + * + * All textures are returned as [ImageEntry] which contains GPU texture views, + * UV coordinates, and dimensions for rendering. + */ +object LambdaImageAtlas { + + /** + * Represents a renderable image entry with GPU resources and UV coordinates. + * + * @property textureView The GPU texture view for binding + * @property u0 Left UV coordinate (0-1) + * @property v0 Top UV coordinate (0-1) + * @property u1 Right UV coordinate (0-1) + * @property v1 Bottom UV coordinate (0-1) + * @property width Width in pixels (for aspect ratio calculations) + * @property height Height in pixels + */ + data class ImageEntry( + val textureView: GpuTextureView, + val u0: Float, + val v0: Float, + val u1: Float, + val v1: Float, + val width: Int, + val height: Int + ) { + /** Aspect ratio (width / height) for maintaining proportions. */ + val aspectRatio: Float get() = if (height != 0) width.toFloat() / height.toFloat() else 1f + + /** Check if this is a full texture (UV spans 0-1). */ + val isFullTexture: Boolean get() = u0 == 0f && v0 == 0f && u1 == 1f && v1 == 1f + } + + // Cache for MC textures loaded by identifier + private val mcTextureCache = ConcurrentHashMap() + + // Cached glint texture entry + private var glintEntry: ImageEntry? = null + + // Cached missing texture entry + private var missingEntry: ImageEntry? = null + + /** + * Load a Minecraft texture by its identifier. + * + * @param id The texture identifier (e.g., Identifier.ofVanilla("textures/misc/enchanted_glint_item.png")) + * @return ImageEntry for the texture, or null if not found + */ + // Queue for textures requested from background threads + private val pendingLoadQueue = java.util.concurrent.ConcurrentLinkedQueue() + + /** + * Load a Minecraft texture by its identifier. + * + * Thread-safe: Can be called from any thread. + * - If called from render thread: loads and caches immediately + * - If called from background thread: returns cached value if available, + * otherwise queues for loading and returns null + * + * @param id The texture identifier (e.g., Identifier.ofVanilla("textures/misc/enchanted_glint_item.png")) + * @return ImageEntry for the texture, or null if not found/not yet loaded + */ + fun loadMCTexture(id: Identifier): ImageEntry? { + // Check cache first (thread-safe) + mcTextureCache[id]?.let { return it } + + // If not on render thread, queue for loading and return null + if (!RenderSystem.isOnRenderThread()) { + pendingLoadQueue.add(id) + return null + } + + // Process any pending loads while we're on the render thread + processPendingLoads() + + return mcTextureCache.getOrPut(id) { + val textureManager = mc.textureManager + val texture: AbstractTexture = textureManager.getTexture(id) ?: return@getOrPut null + + val gpuTextureView = texture.glTextureView ?: return@getOrPut null + + // For full textures, we use default dimensions since GpuTexture dimensions are private + // The actual dimensions can be retrieved from the texture view's underlying texture + ImageEntry( + textureView = gpuTextureView, + u0 = 0f, v0 = 0f, + u1 = 1f, v1 = 1f, + width = 256, // Default size for standalone textures + height = 256 + ) + } + } + + /** + * Process any pending texture load requests. + * Must be called from the render thread. + */ + fun processPendingLoads() { + if (!RenderSystem.isOnRenderThread()) return + + var id = pendingLoadQueue.poll() + while (id != null) { + // Try to load the texture (will add to cache if successful) + val textureManager = mc.textureManager + val texture: AbstractTexture? = textureManager.getTexture(id) + + if (texture != null) { + val gpuTextureView = texture.glTextureView + if (gpuTextureView != null) { + mcTextureCache.putIfAbsent(id, ImageEntry( + textureView = gpuTextureView, + u0 = 0f, v0 = 0f, + u1 = 1f, v1 = 1f, + width = 256, + height = 256 + )) + } + } + id = pendingLoadQueue.poll() + } + } + + /** + * Get the enchantment glint texture for overlay rendering. + */ + fun getGlintTexture(): ImageEntry? { + if (glintEntry != null) return glintEntry + + val id = Identifier.ofVanilla("textures/misc/enchanted_glint_item.png") + glintEntry = loadMCTexture(id) + return glintEntry + } + + /** + * Get the missing/error texture. + */ + fun getMissingTexture(): ImageEntry? { + if (missingEntry != null) return missingEntry + + val textureManager = mc.textureManager + val atlas = textureManager.getTexture(Identifier.ofVanilla("textures/atlas/blocks.png")) + as? SpriteAtlasTexture ?: return null + val sprite = atlas.getSprite(MissingSprite.getMissingSpriteId()) + + missingEntry = createEntryFromSprite(sprite, atlas) + return missingEntry + } + + /** + * Get the sprite for an ItemStack from Minecraft's item atlas. + * + * This extracts the 2D item texture with proper UV coordinates + * for rendering flat item representations. + * + * @param stack The ItemStack to get the sprite for + * @return ImageEntry with sprite UV coordinates, or missing texture if not found + */ + fun getItemSprite(stack: ItemStack): ImageEntry? { + if (stack.isEmpty) return getMissingTexture() + + RenderSystem.assertOnRenderThread() + + // Get the item's registry ID and use it to load the texture + val itemId = net.minecraft.registry.Registries.ITEM.getId(stack.item) + return loadItemTexture(itemId) ?: getMissingTexture() + } + + /** + * Load an item texture by trying common texture paths. + * + * This is useful for simple items where you know the item ID. + * For block items, it will try both item and block texture paths. + * + * @param itemId The item's registry ID (e.g., Identifier.ofVanilla("diamond")) + * @return ImageEntry for the texture, or null if not found + */ + fun loadItemTexture(itemId: Identifier): ImageEntry? { + RenderSystem.assertOnRenderThread() + + // Try item texture first (most common) + val itemTextureId = Identifier.of(itemId.namespace, "textures/item/${itemId.path}.png") + val itemEntry = loadMCTexture(itemTextureId) + if (itemEntry != null) return itemEntry + + // Try block texture for block items + val blockTextureId = Identifier.of(itemId.namespace, "textures/block/${itemId.path}.png") + return loadMCTexture(blockTextureId) + } + + /** + * Check if an ItemStack has enchantment glint. + */ + fun hasGlint(stack: ItemStack): Boolean { + return stack.hasGlint() + } + + /** + * Create an ImageEntry from a Minecraft Sprite. + */ + private fun createEntryFromSprite(sprite: Sprite, atlas: SpriteAtlasTexture): ImageEntry? { + val gpuTextureView = atlas.glTextureView ?: return null + + return ImageEntry( + textureView = gpuTextureView, + u0 = sprite.minU, + v0 = sprite.minV, + u1 = sprite.maxU, + v1 = sprite.maxV, + width = sprite.contents.width, + height = sprite.contents.height + ) + } + + // ============================================================================ + // Custom Texture Upload + // ============================================================================ + + // Cache for uploaded custom textures + private val uploadedTextureCache = ConcurrentHashMap() + private var nextTextureId = 0 + + /** + * Represents an uploaded custom texture with its GPU resources. + */ + data class UploadedTexture( + val id: String, + val texture: net.minecraft.client.texture.NativeImageBackedTexture, + val entry: ImageEntry + ) + + /** + * Upload a BufferedImage as a texture for rendering. + * The texture is cached and can be reused. + * + * @param image The BufferedImage to upload + * @param name Optional name for caching (if same name is used, returns cached version) + * @return ImageEntry for the uploaded texture + */ + fun upload(image: BufferedImage, name: String? = null): ImageEntry? { + RenderSystem.assertOnRenderThread() + + val cacheKey = name ?: "uploaded_${nextTextureId++}" + + // Check cache first + uploadedTextureCache[cacheKey]?.let { return it.entry } + + // Convert BufferedImage to NativeImage + val nativeImage = net.minecraft.client.texture.NativeImage(image.width, image.height, true) + for (y in 0 until image.height) { + for (x in 0 until image.width) { + val argb = image.getRGB(x, y) + // Convert ARGB to ABGR (NativeImage uses ABGR) + val a = (argb shr 24) and 0xFF + val r = (argb shr 16) and 0xFF + val g = (argb shr 8) and 0xFF + val b = argb and 0xFF + val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r + nativeImage.setColor(x, y, abgr) + } + } + + // Create texture and upload + val nativeTexture = net.minecraft.client.texture.NativeImageBackedTexture({ cacheKey }, nativeImage) + val textureId = Identifier.of("lambda", "uploaded/$cacheKey") + + // Get texture view via texture manager after registration + mc.textureManager.registerTexture(textureId, nativeTexture) + val gpuTextureView = nativeTexture.glTextureView ?: return null + + val entry = ImageEntry( + textureView = gpuTextureView, + u0 = 0f, v0 = 0f, + u1 = 1f, v1 = 1f, + width = image.width, + height = image.height + ) + + uploadedTextureCache[cacheKey] = UploadedTexture(cacheKey, nativeTexture, entry) + return entry + } + + /** + * Upload a ByteBuffer as a texture for rendering. + * The buffer should contain RGBA pixel data. + * + * @param buffer The ByteBuffer containing RGBA pixel data + * @param width Width of the image in pixels + * @param height Height of the image in pixels + * @param name Optional name for caching + * @return ImageEntry for the uploaded texture + */ + fun upload(buffer: ByteBuffer, width: Int, height: Int, name: String? = null): ImageEntry? { + RenderSystem.assertOnRenderThread() + + val cacheKey = name ?: "uploaded_${nextTextureId++}" + + // Check cache first + uploadedTextureCache[cacheKey]?.let { return it.entry } + + // Create NativeImage from ByteBuffer + val nativeImage = net.minecraft.client.texture.NativeImage(width, height, true) + buffer.rewind() + for (y in 0 until height) { + for (x in 0 until width) { + val r = buffer.get().toInt() and 0xFF + val g = buffer.get().toInt() and 0xFF + val b = buffer.get().toInt() and 0xFF + val a = buffer.get().toInt() and 0xFF + // NativeImage uses ABGR + val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r + nativeImage.setColor(x, y, abgr) + } + } + + // Create texture and upload + val nativeTexture = net.minecraft.client.texture.NativeImageBackedTexture({ cacheKey }, nativeImage) + val textureId = Identifier.of("lambda", "uploaded/$cacheKey") + mc.textureManager.registerTexture(textureId, nativeTexture) + val gpuTextureView = nativeTexture.glTextureView ?: return null + + val entry = ImageEntry( + textureView = gpuTextureView, + u0 = 0f, v0 = 0f, + u1 = 1f, v1 = 1f, + width = width, + height = height + ) + + uploadedTextureCache[cacheKey] = UploadedTexture(cacheKey, nativeTexture, entry) + return entry + } + + /** + * Remove an uploaded texture from cache and release GPU resources. + * + * @param name The name used when uploading the texture + */ + fun removeUploaded(name: String) { + uploadedTextureCache.remove(name)?.let { uploaded -> + mc.textureManager.destroyTexture(Identifier.of("lambda", "uploaded/$name")) + } + } + + /** + * Clear all cached textures. Should be called on resource reload. + */ + fun clearCache() { + mcTextureCache.clear() + glintEntry = null + missingEntry = null + + // Clean up uploaded textures + uploadedTextureCache.values.forEach { uploaded -> + mc.textureManager.destroyTexture(Identifier.of("lambda", "uploaded/${uploaded.id}")) + } + uploadedTextureCache.clear() + } +} + diff --git a/src/main/kotlin/com/lambda/graphics/renderer/esp/DirectionMask.kt b/src/main/kotlin/com/lambda/graphics/util/DirectionMask.kt similarity index 98% rename from src/main/kotlin/com/lambda/graphics/renderer/esp/DirectionMask.kt rename to src/main/kotlin/com/lambda/graphics/util/DirectionMask.kt index 1b510e3b1..d6bbd37a1 100644 --- a/src/main/kotlin/com/lambda/graphics/renderer/esp/DirectionMask.kt +++ b/src/main/kotlin/com/lambda/graphics/util/DirectionMask.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.esp +package com.lambda.graphics.util import com.lambda.util.world.FastVector import com.lambda.util.world.offset diff --git a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt similarity index 74% rename from src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt rename to src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt index 159b32268..f7bd7a3a4 100644 --- a/src/main/kotlin/com/lambda/graphics/renderer/esp/DynamicAABB.kt +++ b/src/main/kotlin/com/lambda/graphics/util/DynamicAABB.kt @@ -15,9 +15,12 @@ * along with this program. If not, see . */ -package com.lambda.graphics.renderer.esp +package com.lambda.graphics.util +import com.lambda.Lambda.mc import com.lambda.util.extension.prevPos +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.lerp import com.lambda.util.math.minus import net.minecraft.entity.Entity import net.minecraft.util.math.Box @@ -35,12 +38,23 @@ class DynamicAABB { return this } + fun box(tickDelta: Double): Box? = + prev?.let { prev -> + curr?.let { curr -> + lerp(tickDelta, prev, curr) + } + } + fun reset() { prev = null curr = null } companion object { + val Entity.interpolatedBox + get() = boundingBox.let { box -> + lerp(mc.tickDelta, box.offset(prevPos - pos), box) + } val Entity.dynamicBox get() = DynamicAABB().apply { update(boundingBox.offset(prevPos - pos)) diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt index 770468d52..7baae282d 100644 --- a/src/main/kotlin/com/lambda/gui/DearImGui.kt +++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt @@ -36,7 +36,6 @@ import imgui.glfw.ImGuiImplGlfw import net.minecraft.client.gl.GlBackend import net.minecraft.client.texture.GlTexture import org.lwjgl.opengl.GL30.GL_FRAMEBUFFER -import org.lwjgl.opengl.GL32C import kotlin.math.abs object DearImGui : Loadable { diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt index 7785c578e..fdf2fd14e 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt @@ -19,12 +19,12 @@ package com.lambda.interaction.construction.simulation import com.lambda.context.Automated import com.lambda.context.SafeContext -import com.lambda.graphics.esp.ShapeScope -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer import com.lambda.interaction.construction.blueprint.Blueprint +import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.result.BuildResult import com.lambda.interaction.construction.simulation.result.Drawable -import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.threading.runSafeAutomated import com.lambda.util.BlockUtils.blockState import com.lambda.util.world.FastVector @@ -64,9 +64,9 @@ data class Simulation( .map { PossiblePos(it.key.toBlockPos(), it.value.count { it.rank.ordinal < 4 }) } class PossiblePos(val pos: BlockPos, val interactions: Int) : Drawable { - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(Vec3d.ofBottomCenter(pos).playerBox(), Color(0, 255, 0, 50), Color(0, 255, 0, 50)) + override fun RenderBuilder.render() { + box(Vec3d.ofBottomCenter(pos).playerBox()) { + colors(Color(0, 255, 0, 50), Color(0, 255, 0, 50)) } } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt index 17ecce3ef..6d58ea018 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/BreakContext.kt @@ -18,7 +18,8 @@ package com.lambda.interaction.construction.simulation.context import com.lambda.context.Automated -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer import com.lambda.interaction.managers.rotating.RotationRequest import com.lambda.interaction.material.StackSelection import com.lambda.threading.runSafe @@ -59,9 +60,9 @@ data class BreakContext( override val sorter get() = breakConfig.sorter - override fun render(esp: TransientRegionESP) { - esp.shapes(blockPos.x.toDouble(), blockPos.y.toDouble(), blockPos.z.toDouble()) { - box(blockPos, baseColor, sideColor) + override fun RenderBuilder.render() { + box(blockPos) { + colors(baseColor, sideColor) } } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt index cd78d3f52..8ee811ad0 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt @@ -18,7 +18,8 @@ package com.lambda.interaction.construction.simulation.context import com.lambda.context.Automated -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer import com.lambda.interaction.construction.simulation.processing.PreProcessingInfo import com.lambda.interaction.managers.hotbar.HotbarRequest import com.lambda.interaction.managers.interacting.InteractRequest @@ -46,15 +47,15 @@ data class InteractContext( override val sorter get() = interactConfig.sorter - override fun render(esp: TransientRegionESP) { - esp.shapes(hitResult.pos.x, hitResult.pos.y, hitResult.pos.z) { - val box = with(hitResult.pos) { - Box( - x - 0.05, y - 0.05, z - 0.05, - x + 0.05, y + 0.05, z + 0.05, - ).offset(hitResult.side.doubleVector.multiply(0.05)) - } - box(box, baseColor, sideColor) + override fun RenderBuilder.render() { + val box = with(hitResult.pos) { + Box( + x - 0.05, y - 0.05, z - 0.05, + x + 0.05, y + 0.05, z + 0.05, + ).offset(hitResult.side.doubleVector.multiply(0.05)) + } + box(box) { + colors(baseColor, sideColor) } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt index ac339712a..b140940a6 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/Drawable.kt @@ -17,11 +17,12 @@ package com.lambda.interaction.construction.simulation.result -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer /** * Represents a [BuildResult] that can be rendered in-game. */ interface Drawable { - fun render(esp: TransientRegionESP) + fun RenderBuilder.render() } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt index ed85fd9e8..472088bb2 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/BreakResult.kt @@ -20,8 +20,9 @@ package com.lambda.interaction.construction.simulation.result.results import baritone.api.pathing.goals.GoalBlock import baritone.api.pathing.goals.GoalInverted import com.lambda.context.AutomatedSafeContext -import com.lambda.graphics.mc.TransientRegionESP -import com.lambda.graphics.renderer.esp.DirectionMask.mask +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer +import com.lambda.graphics.util.DirectionMask.mask import com.lambda.interaction.construction.simulation.context.BreakContext import com.lambda.interaction.construction.simulation.result.BuildResult import com.lambda.interaction.construction.simulation.result.ComparableResult @@ -55,8 +56,8 @@ sealed class BreakResult : BuildResult() { ) : Contextual, Drawable, BreakResult() { override val rank = Rank.BreakSuccess - override fun render(esp: TransientRegionESP) { - context.render(esp) + override fun RenderBuilder.render() { + with(context) { render() } } } @@ -72,9 +73,10 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BreakNotExposed private val color = Color(46, 0, 0, 30) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color, side.mask) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) + hideSides(side.mask.inv()) } } @@ -122,9 +124,9 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BreakSubmerge private val color = Color(114, 27, 255, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } } @@ -140,14 +142,14 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BreakIsBlockedByFluid private val color = Color(50, 12, 112, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - val center = pos.toCenterPos() - val box = Box( - center.x - 0.1, center.y - 0.1, center.z - 0.1, - center.x + 0.1, center.y + 0.1, center.z + 0.1 - ) - box(box, color, color) + override fun RenderBuilder.render() { + val center = pos.toCenterPos() + val box = Box( + center.x - 0.1, center.y - 0.1, center.z - 0.1, + center.x + 0.1, center.y + 0.1, center.z + 0.1 + ) + box(box) { + allColors(color) } } } @@ -164,9 +166,9 @@ sealed class BreakResult : BuildResult() { override val goal = GoalInverted(GoalBlock(pos)) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt index 0694c3c6d..3c5fd125f 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/GenericResult.kt @@ -19,7 +19,8 @@ package com.lambda.interaction.construction.simulation.result.results import baritone.api.pathing.goals.GoalNear import com.lambda.context.AutomatedSafeContext -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer import com.lambda.interaction.construction.simulation.result.BuildResult import com.lambda.interaction.construction.simulation.result.ComparableResult import com.lambda.interaction.construction.simulation.result.Drawable @@ -53,15 +54,15 @@ sealed class GenericResult : BuildResult() { override val rank = Rank.NotVisible private val color = Color(46, 0, 0, 80) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - val box = with(pos) { - Box( - x - 0.05, y - 0.05, z - 0.05, - x + 0.05, y + 0.05, z + 0.05, - ).offset(pos) - } - box(box, color, color) + override fun RenderBuilder.render() { + val box = with(pos) { + Box( + x - 0.05, y - 0.05, z - 0.05, + x + 0.05, y + 0.05, z + 0.05, + ).offset(pos) + } + box(box) { + allColors(color) } } @@ -102,14 +103,14 @@ sealed class GenericResult : BuildResult() { neededSelection.transferByTask(HotbarContainer)?.execute(task) } - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - val center = pos.toCenterPos() - val box = Box( - center.x - 0.1, center.y - 0.1, center.z - 0.1, - center.x + 0.1, center.y + 0.1, center.z + 0.1 - ) - box(box, color, color) + override fun RenderBuilder.render() { + val center = pos.toCenterPos() + val box = Box( + center.x - 0.1, center.y - 0.1, center.z - 0.1, + center.x + 0.1, center.y + 0.1, center.z + 0.1 + ) + box(box) { + allColors(color) } } } @@ -135,14 +136,14 @@ sealed class GenericResult : BuildResult() { override val goal = GoalNear(pos, 3) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - val center = pos.toCenterPos() - val box = Box( - center.x - 0.1, center.y - 0.1, center.z - 0.1, - center.x + 0.1, center.y + 0.1, center.z + 0.1 - ) - box(box, color, color) + override fun RenderBuilder.render() { + val center = pos.toCenterPos() + val box = Box( + center.x - 0.1, center.y - 0.1, center.z - 0.1, + center.x + 0.1, center.y + 0.1, center.z + 0.1 + ) + box(box) { + allColors(color) } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt index b4537bcec..b72feb920 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/InteractResult.kt @@ -19,7 +19,8 @@ package com.lambda.interaction.construction.simulation.result.results import baritone.api.pathing.goals.GoalBlock import baritone.api.pathing.goals.GoalInverted -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer import com.lambda.interaction.construction.simulation.context.InteractContext import com.lambda.interaction.construction.simulation.result.BuildResult import com.lambda.interaction.construction.simulation.result.Contextual @@ -57,8 +58,8 @@ sealed class InteractResult : BuildResult() { ) : Contextual, Drawable, InteractResult() { override val rank = Rank.PlaceSuccess - override fun render(esp: TransientRegionESP) { - context.render(esp) + override fun RenderBuilder.render() { + with(context) { render() } } } @@ -82,15 +83,15 @@ sealed class InteractResult : BuildResult() { override val rank = Rank.PlaceNoIntegrity private val color = Color(252, 3, 3, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - val box = with(simulated.hitPos) { - Box( - x - 0.05, y - 0.05, z - 0.05, - x + 0.05, y + 0.05, z + 0.05, - ).offset(simulated.side.doubleVector.multiply(0.05)) - } - box(box, color, color) + override fun RenderBuilder.render() { + val box = with(simulated.hitPos) { + Box( + x - 0.05, y - 0.05, z - 0.05, + x + 0.05, y + 0.05, z + 0.05, + ).offset(simulated.side.doubleVector.multiply(0.05)) + } + box(box) { + allColors(color) } } } @@ -121,15 +122,15 @@ sealed class InteractResult : BuildResult() { override val rank = Rank.PlaceBlockedByEntity private val color = Color(252, 3, 3, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - val box = with(hitPos) { - Box( - x - 0.05, y - 0.05, z - 0.05, - x + 0.05, y + 0.05, z + 0.05, - ).offset(side.doubleVector.multiply(0.05)) - } - box(box, color, color) + override fun RenderBuilder.render() { + val box = with(hitPos) { + Box( + x - 0.05, y - 0.05, z - 0.05, + x + 0.05, y + 0.05, z + 0.05, + ).offset(side.doubleVector.multiply(0.05)) + } + box(box) { + allColors(color) } } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt index baa2a2513..db73c3773 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/result/results/PreSimResult.kt @@ -18,8 +18,8 @@ package com.lambda.interaction.construction.simulation.result.results import baritone.api.pathing.goals.GoalBlock -import com.lambda.graphics.esp.ShapeScope -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.TickedRenderer import com.lambda.interaction.construction.simulation.result.BuildResult import com.lambda.interaction.construction.simulation.result.ComparableResult import com.lambda.interaction.construction.simulation.result.Drawable @@ -56,9 +56,9 @@ sealed class PreSimResult : BuildResult() { override val goal = GoalBlock(pos) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } @@ -80,9 +80,9 @@ sealed class PreSimResult : BuildResult() { override val rank = Rank.BreakRestricted private val color = Color(255, 0, 0, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } } @@ -100,9 +100,9 @@ sealed class PreSimResult : BuildResult() { override val rank get() = Rank.BreakNoPermission private val color = Color(255, 0, 0, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } } @@ -118,9 +118,9 @@ sealed class PreSimResult : BuildResult() { override val rank = Rank.OutOfWorld private val color = Color(3, 148, 252, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } } @@ -138,9 +138,9 @@ sealed class PreSimResult : BuildResult() { override val rank = Rank.Unbreakable private val color = Color(11, 11, 11, 100) - override fun render(esp: TransientRegionESP) { - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - box(pos, color, color) + override fun RenderBuilder.render() { + box(pos) { + allColors(color) } } } diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt index 2cba3128b..ce6f35f3c 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakConfig.kt @@ -73,7 +73,7 @@ interface BreakConfig : ActionConfig, ISettingGroup { val renders: Boolean val fill: Boolean val outline: Boolean - val outlineWidth: Int + val outlineWidth: Float val animation: AnimationMode val dynamicFillColor: Boolean diff --git a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt index b985c82e9..a1c44be64 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt @@ -23,10 +23,9 @@ import com.lambda.event.events.ConnectionEvent import com.lambda.event.events.EntityEvent import com.lambda.event.events.TickEvent import com.lambda.event.events.WorldEvent -import com.lambda.event.events.onDynamicRender import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe -import com.lambda.graphics.renderer.esp.DynamicAABB +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.context.BreakContext @@ -77,7 +76,8 @@ import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta import com.lambda.util.BlockUtils.isEmpty import com.lambda.util.BlockUtils.isNotBroken import com.lambda.util.BlockUtils.isNotEmpty -import com.lambda.util.extension.partialTicks +import com.lambda.util.ChatUtils.colors +import com.lambda.util.extension.tickDelta import com.lambda.util.item.ItemUtils.block import com.lambda.util.math.lerp import com.lambda.util.player.gamemode @@ -218,71 +218,70 @@ object BreakManager : Manager( ?.internalOnItemDrop(it.entity) } - onDynamicRender { esp -> - val activeStack = breakInfos - .filterNotNull() - .firstOrNull()?.swapStack ?: return@onDynamicRender + listenUnsafe(priority = Int.MIN_VALUE) { + primaryBreak = null + secondaryBreak = null + breakCooldown = 0 + } - breakInfos - .filterNotNull() - .forEach { info -> - if (!info.breaking) return@forEach - - val config = info.breakConfig - if (!config.renders) return@onDynamicRender - val swapMode = info.breakConfig.swapMode - val breakDelta = info.request.runSafeAutomated { - info.context.cachedState.calcBreakDelta( - info.context.blockPos, - if (info.type != RedundantSecondary && - swapMode.isEnabled() && - swapMode != BreakConfig.SwapMode.Start + immediateRenderer("BreakManager Immediate Renderer") { safeContext -> + with(safeContext) { + val activeStack = breakInfos + .filterNotNull() + .firstOrNull()?.swapStack ?: return@immediateRenderer + + breakInfos + .filterNotNull() + .forEach { info -> + if (!info.breaking) return@forEach + + val config = info.breakConfig + if (!config.renders) return@immediateRenderer + val swapMode = info.breakConfig.swapMode + val breakDelta = info.request.runSafeAutomated { + info.context.cachedState.calcBreakDelta( + info.context.blockPos, + if (info.type != RedundantSecondary && + swapMode.isEnabled() && + swapMode != BreakConfig.SwapMode.Start ) activeStack - else null - ).toDouble() - } - val currentDelta = info.breakingTicks * breakDelta - - val threshold = if (info.type == Primary) info.breakConfig.breakThreshold else 1f - val adjustedThreshold = threshold + (breakDelta * config.fudgeFactor) - - val currentProgress = currentDelta / adjustedThreshold - val nextTicksProgress = (currentDelta + breakDelta) / adjustedThreshold - val interpolatedProgress = lerp(mc.partialTicks, currentProgress, nextTicksProgress) - - val fillColor = if (config.dynamicFillColor) lerp( - interpolatedProgress, - config.startFillColor, - config.endFillColor - ) - else config.staticFillColor - val outlineColor = if (config.dynamicOutlineColor) lerp( - interpolatedProgress, - config.startOutlineColor, - config.endOutlineColor - ) - else config.staticOutlineColor - - val pos = info.context.blockPos - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + else null + ).toDouble() + } + val currentDelta = info.breakingTicks * breakDelta + + val threshold = if (info.type == Primary) info.breakConfig.breakThreshold else 1f + val adjustedThreshold = threshold + (breakDelta * config.fudgeFactor) + + val currentProgress = currentDelta / adjustedThreshold + val nextTicksProgress = (currentDelta + breakDelta) / adjustedThreshold + val interpolatedProgress = lerp(mc.tickDelta, currentProgress, nextTicksProgress) + + val fillColor = if (config.dynamicFillColor) lerp( + interpolatedProgress, + config.startFillColor, + config.endFillColor + ) + else config.staticFillColor + val outlineColor = if (config.dynamicOutlineColor) lerp( + interpolatedProgress, + config.startOutlineColor, + config.endOutlineColor + ) + else config.staticOutlineColor + + val pos = info.context.blockPos info.context.cachedState.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) - }.forEach boxes@{ box -> + }.forEach { box -> val animationMode = info.breakConfig.animation - val currentProgressBox = interpolateBox(box, currentProgress, animationMode) - val nextProgressBox = interpolateBox(box, nextTicksProgress, animationMode) - val dynamicAABB = DynamicAABB().update(currentProgressBox).update(nextProgressBox) - if (config.fill) filled(dynamicAABB, fillColor) - if (config.outline) outline(dynamicAABB, outlineColor) + val interpolatedBox = interpolateBox(box, interpolatedProgress, animationMode) + box(interpolatedBox, info.breakConfig.outlineWidth) { + colors(fillColor, outlineColor) + } } } - } - } - - listenUnsafe(priority = Int.MIN_VALUE) { - primaryBreak = null - secondaryBreak = null - breakCooldown = 0 + } } return "Loaded Break Manager" diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt index d17e62cef..9c9a64412 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationConfig.kt @@ -46,7 +46,7 @@ interface RotationConfig : ISettingGroup { val tickStageMask: Set - open class Instant(mode: RotationMode) : RotationConfig { + open class Instant(mode: RotationMode, override val visibility: () -> Boolean = { true }) : RotationConfig { override val settings = mutableListOf>() override val rotationMode = mode override val keepTicks = 1 diff --git a/src/main/kotlin/com/lambda/module/hud/FPS.kt b/src/main/kotlin/com/lambda/module/hud/FPS.kt index e2ec81ae1..dbdf15590 100644 --- a/src/main/kotlin/com/lambda/module/hud/FPS.kt +++ b/src/main/kotlin/com/lambda/module/hud/FPS.kt @@ -39,7 +39,7 @@ object FPS : HudModule( var fps = 0 init { - listen { + listen { var currentFps = 0 if (average) { frames.add(Unit) diff --git a/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt b/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt index d01d101eb..52b220f83 100644 --- a/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt +++ b/src/main/kotlin/com/lambda/module/modules/chat/AntiSpam.kt @@ -137,8 +137,9 @@ object AntiSpam : Module( name: String, c: Configurable, baseGroup: NamedEnum, + override val visibility: () -> Boolean = { true }, ) : ReplaceConfig, SettingGroup(c) { - override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace).group(baseGroup) - override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup) + override val action by setting("$name Action Strategy", ReplaceConfig.ActionStrategy.Replace, visibility = visibility).group(baseGroup) + override val replace by setting("$name Replace Strategy", ReplaceConfig.ReplaceStrategy.CensorAll) { visibility() && action == ReplaceConfig.ActionStrategy.Replace }.group(baseGroup) } } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt b/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt index 3ada7d918..cbb60ac9c 100644 --- a/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt +++ b/src/main/kotlin/com/lambda/module/modules/combat/AutoDisconnect.kt @@ -31,7 +31,7 @@ import com.lambda.util.Formatting.format import com.lambda.util.combat.CombatUtils.hasDeadlyCrystal import com.lambda.util.combat.DamageUtils.isFallDeadly import com.lambda.util.extension.fullHealth -import com.lambda.util.extension.tickDelta +import com.lambda.util.extension.tickDeltaF import com.lambda.util.player.SlotUtils.allStacks import com.lambda.util.text.buildText import com.lambda.util.text.color @@ -230,7 +230,7 @@ object AutoDisconnect : Module( }), Creeper({ creeper }, { fastEntitySearch(15.0).find { - it.getLerpedFuseTime(mc.tickDelta) > 0.0 + it.getLerpedFuseTime(mc.tickDeltaF) > 0.0 && it.pos.distanceTo(player.pos) <= 5.0 }?.let { creeper -> buildText { diff --git a/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt b/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt index 322486cd7..a3234fb4f 100644 --- a/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt +++ b/src/main/kotlin/com/lambda/module/modules/combat/AutoTotem.kt @@ -31,7 +31,7 @@ import com.lambda.util.NamedEnum import com.lambda.util.combat.CombatUtils.hasDeadlyCrystal import com.lambda.util.combat.DamageUtils.isFallDeadly import com.lambda.util.extension.fullHealth -import com.lambda.util.extension.tickDelta +import com.lambda.util.extension.tickDeltaF import com.lambda.util.world.fastEntitySearch import net.minecraft.entity.mob.CreeperEntity import net.minecraft.entity.player.PlayerEntity @@ -85,7 +85,7 @@ object AutoTotem : Module( enum class Reason(val check: SafeContext.() -> Boolean) { Health({ player.fullHealth < minimumHealth }), Creeper({ creeper && fastEntitySearch(15.0).any { - it.getLerpedFuseTime(mc.tickDelta) > 0.0 + it.getLerpedFuseTime(mc.tickDeltaF) > 0.0 && it.pos.distanceTo(player.pos) <= 5.0 } }), Player({ players && fastEntitySearch(minPlayerDistance.toDouble()).any { otherPlayer -> diff --git a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt index 499db8337..309b19ba2 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/BlockTest.kt @@ -17,9 +17,10 @@ package com.lambda.module.modules.debug -import com.lambda.event.events.onStaticRender +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.ChatUtils.colors import com.lambda.util.world.blockSearch import net.minecraft.block.Blocks import net.minecraft.util.math.Vec3i @@ -47,13 +48,15 @@ object BlockTest : Module( private val outlineColor = Color(100, 150, 255, 51) init { - onStaticRender { esp -> - blockSearch(range, step = step) { _, state -> - state.isOf(Blocks.DIAMOND_BLOCK) - }.forEach { (pos, state) -> - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { + tickedRenderer("BlockTest Ticked Renderer") { safeContext -> + with(safeContext) { + blockSearch(range, step = step) { _, state -> + state.isOf(Blocks.DIAMOND_BLOCK) + }.forEach { (pos, state) -> state.getOutlineShape(world, pos).boundingBoxes.forEach { box -> - box(box.offset(pos), filledColor, outlineColor) + box(box.offset(pos), 1.5f) { + colors(filledColor, outlineColor) + } } } } diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt index 7520f308a..ff70da49f 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/RenderTest.kt @@ -17,12 +17,13 @@ package com.lambda.module.modules.debug -import com.lambda.event.events.onDynamicRender -import com.lambda.event.events.onStaticRender -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DynamicAABB.Companion.dynamicBox +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer +import com.lambda.graphics.util.DynamicAABB.Companion.dynamicBox import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.ChatUtils.colors +import com.lambda.util.extension.tickDelta import com.lambda.util.math.setAlpha import com.lambda.util.world.entitySearch import net.minecraft.entity.LivingEntity @@ -45,18 +46,20 @@ object RenderTest : Module( private val filledColor = outlineColor.setAlpha(0.2) init { - onDynamicRender { esp -> - entitySearch(8.0) - .forEach { entity -> - esp.shapes(entity.x, entity.y, entity.z) { - box(entity.dynamicBox, filledColor, outlineColor, DirectionMask.ALL, DirectionMask.OutlineMode.And) + immediateRenderer("RenderTest Immediate Renderer") { safeContext -> + with(safeContext) { + entitySearch(8.0) + .forEach { entity -> + box(entity.dynamicBox.box(mc.tickDelta) ?: return@forEach, 1.5f) { + colors(filledColor, outlineColor) + } } - } + } } - onStaticRender { esp -> - esp.shapes(player.x, player.y, player.z) { - box(Box.of(player.pos, 0.3, 0.3, 0.3), filledColor, outlineColor) + tickedRenderer("RenderTest Ticked Renderer") { safeContext -> + box(Box.of(safeContext.player.pos, 0.3, 0.3, 0.3), 1.5f) { + colors(filledColor, outlineColor) } } } diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt new file mode 100644 index 000000000..9669aaf63 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt @@ -0,0 +1,466 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.debug + +import com.lambda.graphics.mc.ItemLighting +import com.lambda.graphics.mc.ItemOverlay +import com.lambda.graphics.mc.LineDashStyle.Companion.marchingAnts +import com.lambda.graphics.mc.LineDashStyle.Companion.screenMarchingAnts +import com.lambda.graphics.mc.RenderBuilder.SDFGlow +import com.lambda.graphics.mc.RenderBuilder.SDFOutline +import com.lambda.graphics.mc.RenderBuilder.SDFShadow +import com.lambda.graphics.mc.RenderBuilder.SDFStyle +import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runSafe +import com.lambda.util.extension.prevPos +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.lerp +import com.lambda.util.world.toBlockPos +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.util.Identifier +import net.minecraft.util.math.ChunkPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d +import net.minecraft.util.math.random.Random +import org.joml.Quaternionf +import java.awt.Color + +/** + * Test module for ChunkedRenderer - renders blocks around the player using chunk-based caching. + * Geometry is cached per-chunk and only rebuilt when chunks change. + */ +object ChunkedRendererTest : Module( + name = "ChunkedRendererTest", + description = "Test module for ChunkedRenderer - cached chunk-based rendering", + tag = ModuleTag.DEBUG, +) { + init { + chunkedRenderer("ChunkedRendererTest", depthTest = { false }) { world, pos -> + runSafe { + if (player.chunkPos != ChunkPos(pos.toBlockPos())) return@chunkedRenderer + + val startPos = lerp(mc.tickDelta, player.prevPos, player.pos) + lineGradient( + startPos, + Color.BLUE, + startPos.offset(Direction.EAST, 5.0), + Color.RED, + 0.1f, + marchingAnts(1f) + ) + worldText( + "Test sdf font!", + startPos.offset(Direction.EAST, 5.0), + style = SDFStyle( + outline = SDFOutline(), + glow = SDFGlow(), + shadow = SDFShadow() + ) + ) + + // Screen-space test renders (normalized 0-1 coordinates) + // Test screen rect with gradient + screenRectGradient( + 0.02f, 0.1f, 0.15f, 0.05f, // x, y, width, height (0-1) + Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW + ) + + // Test screen rect with solid color + screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180)) + + // Test screen line + screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts()) + + // Test screen line with gradient + screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f) + + // Test screen text + screenText( + "Screen Space Text!", + 0.02f, + 0.30f, + size = 0.025f, // 2.5% of screen + style = SDFStyle( + color = Color.WHITE, + outline = SDFOutline(), + shadow = SDFShadow() + ) + ) + + // Test centered screen text + screenText( + "Centered Screen Text", + 0.5f, // 50% from left = center + 0.05f, // 5% from top + size = 0.03f, // 3% of screen + style = SDFStyle( + color = Color.YELLOW, + glow = SDFGlow(Color(255, 200, 0, 150)), + shadow = SDFShadow() + ), + centered = true + ) + + // Test screen image with tint + screenImage( + texture = Identifier.ofVanilla("textures/item/netherite_sword.png"), + x = 0.08f, y = 0.48f, + width = 0.1f, height = 0.1f * mc.window.width / mc.window.height.toFloat(), + hasOverlay = true // With glint + ) + + // Test world image - billboard facing the camera + val worldImagePos = startPos.offset(Direction.NORTH, 3.0).add(0.0, 1.5, 0.0) + worldImage( + texture = Identifier.ofVanilla("textures/item/diamond.png"), + pos = worldImagePos, + size = 0.8f, + tint = Color.WHITE + ) + + // Test world image with glint + worldImage( + texture = Identifier.ofVanilla("textures/item/netherite_sword.png"), + pos = worldImagePos.offset(Direction.EAST, 2.0), + size = 0.8f, + tint = Color.WHITE, + hasOverlay = true + ) + + // Test 3D Model (Chunked - Static) + // Render a Stone Block + val stoneState = net.minecraft.block.Blocks.STONE.defaultState + val stoneModel = mc.bakedModelManager.blockModels.getModel(stoneState) + val stoneRandom = Random.create() + val stoneParts = stoneModel.getParts(stoneRandom) + + stoneParts.forEach { part -> + model( + part, + pos = startPos.offset(Direction.WEST, 2.0).add(0.0, 1.0, 0.0), + scale = Vec3d(1.0, 1.0, 1.0) + ) + } + } + } + } +} + +/** + * Test module for TickedRenderer - rebuilds geometry every tick. + * Uses tick-camera relative coordinates with render-time delta interpolation. + */ +object TickedRendererTest : Module( + name = "TickedRendererTest", + description = "Test module for TickedRenderer - tick-based rendering with interpolation", + tag = ModuleTag.DEBUG, +) { + private val throughWalls by setting("Through Walls", true) + + init { + tickedRenderer("TickedRendererTest", depthTest = { !throughWalls }) { safeContext -> + with(safeContext) { + val startPos = lerp(mc.tickDelta, player.prevPos, player.pos) + lineGradient( + startPos, + Color.BLUE, + startPos.offset(Direction.EAST, 5.0), + Color.RED, + 0.1f, + marchingAnts(1f) + ) + worldText( + "Test sdf font!", + startPos.offset(Direction.EAST, 5.0), + style = SDFStyle( + outline = SDFOutline(), + glow = SDFGlow(), + shadow = SDFShadow() + ) + ) + + // Screen-space test renders (normalized 0-1 coordinates) + // Test screen rect with gradient + screenRectGradient( + 0.02f, 0.1f, 0.15f, 0.05f, // x, y, width, height (0-1) + Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW + ) + + // Test screen rect with solid color + screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180)) + + // Test screen line + screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts()) + + // Test screen line with gradient + screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f) + + // Test screen text + screenText( + "Screen Space Text!", + 0.02f, + 0.30f, + size = 0.025f, // 2.5% of screen + style = SDFStyle( + color = Color.WHITE, + outline = SDFOutline(), + shadow = SDFShadow() + ) + ) + + // Test centered screen text + screenText( + "Centered Screen Text", + 0.5f, // 50% from left = center + 0.05f, // 5% from top + size = 0.03f, // 3% of screen + style = SDFStyle( + color = Color.YELLOW, + glow = SDFGlow(Color(255, 200, 0, 150)), + shadow = SDFShadow() + ), + centered = true + ) + + // Test screen image with tint + screenImage( + texture = Identifier.ofVanilla("textures/item/netherite_sword.png"), + x = 0.08f, y = 0.48f, + width = 0.1f, height = 0.1f * mc.window.width / mc.window.height.toFloat(), + hasOverlay = true // With glint + ) + + // Test world image - billboard facing the camera + val worldImagePos = startPos.offset(Direction.NORTH, 3.0).add(0.0, 1.5, 0.0) + worldImage( + texture = Identifier.ofVanilla("textures/item/diamond.png"), + pos = worldImagePos, + size = 0.8f, + tint = Color.WHITE + ) + + // Test world image with glint + worldImage( + texture = Identifier.ofVanilla("textures/item/netherite_sword.png"), + pos = worldImagePos.offset(Direction.EAST, 2.0), + size = 0.8f, + tint = Color.WHITE, + hasOverlay = true + ) + + // Test 3D Model (Ticked - Interpolated) + // Render a Gold Block + val goldState = net.minecraft.block.Blocks.GOLD_BLOCK.defaultState + val goldModel = mc.bakedModelManager.blockModels.getModel(goldState) + val goldRandom = net.minecraft.util.math.random.Random.create() + val goldParts = goldModel.getParts(goldRandom) + + goldParts.forEach { part -> + model( + part, + pos = startPos.offset(Direction.WEST, 3.0).add(0.0, 1.0, 0.0), + scale = Vec3d(1.0, 1.0, 1.0) + ) + } + } + } + } +} + +/** + * Test module for ImmediateRenderer - rebuilds geometry every frame. + * Uses render-camera relative coordinates for smooth interpolated rendering. + */ +object ImmediateRendererTest : Module( + name = "ImmediateRendererTest", + description = "Test module for ImmediateRenderer - frame-based interpolated rendering", + tag = ModuleTag.DEBUG, +) { + private val throughWalls by setting("Through Walls", true) + + init { + immediateRenderer("ImmediateRendererTest", depthTest = { !throughWalls }) { safeContext -> + with(safeContext) { + val startPos = lerp(mc.tickDelta, player.prevPos, player.pos) + lineGradient( + startPos, + Color.BLUE, + startPos.offset(Direction.EAST, 5.0), + Color.RED, + 0.1f, + marchingAnts(1f) + ) + worldText( + "Test sdf font!", + startPos.offset(Direction.EAST, 5.0), + style = SDFStyle( + outline = SDFOutline(), + glow = SDFGlow(), + shadow = SDFShadow() + ) + ) + + // Screen-space test renders (normalized 0-1 coordinates) + // Test screen rect with gradient + screenRectGradient( + 0.02f, 0.1f, 0.15f, 0.05f, // x, y, width, height (0-1) + Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW + ) + + // Test screen rect with solid color + screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180)) + + // Test screen line + screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts()) + + // Test screen line with gradient + screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f) + + // Test screen text + screenText( + "Screen Space Text!", + 0.02f, + 0.30f, + size = 0.025f, // 2.5% of screen + style = SDFStyle( + color = Color.WHITE, + outline = SDFOutline(), + shadow = SDFShadow() + ) + ) + + // Test centered screen text + screenText( + "Centered Screen Text", + 0.5f, // 50% from left = center + 0.05f, // 5% from top + size = 0.03f, // 3% of screen + style = SDFStyle( + color = Color.YELLOW, + glow = SDFGlow(Color(255, 200, 0, 150)), + shadow = SDFShadow() + ), + centered = true + ) + + // Test screen image with tint + screenImage( + texture = Identifier.ofVanilla("textures/item/netherite_sword.png"), + x = 0.08f, y = 0.48f, + width = 0.1f, height = 0.1f * mc.window.width / mc.window.height.toFloat(), + hasOverlay = true // With glint + ) + + // Test world image - billboard facing the camera + val worldImagePos = startPos.offset(Direction.NORTH, 3.0).add(0.0, 1.5, 0.0) + worldImage( + texture = Identifier.ofVanilla("textures/item/diamond.png"), + pos = worldImagePos, + size = 0.8f, + tint = Color.WHITE + ) + + // Test world image with glint + worldImage( + texture = Identifier.ofVanilla("textures/item/netherite_sword.png"), + pos = worldImagePos.offset(Direction.EAST, 2.0), + size = 0.8f, + tint = Color.WHITE, + hasOverlay = true + ) + + // Test 3D Model (Immediate - Rotating) + // Render a Diamond Block + val diamondState = net.minecraft.block.Blocks.DIAMOND_BLOCK.defaultState + val diamondModel = mc.bakedModelManager.blockModels.getModel(diamondState) + val diamondParts = diamondModel.getParts(Random.create()) + + val time = System.currentTimeMillis() % 2000L / 2000f + val rotation = Quaternionf().rotateY(time * Math.PI.toFloat() * 2f) + + diamondParts.forEach { part -> + model( + part, + pos = startPos.offset(Direction.WEST, 4.0).add(0.0, 1.0, 0.0), + scale = Vec3d(0.7, 0.7, 0.7), + rotation = rotation, + centered = true, + pixelPerfect = true, + smartAA = true + ) + } + + // ========== High-Fidelity GUI Item Rendering Tests ========== + // 1. World-space GUI Item (Netherite Sword) + worldGuiItem( + stack = ItemStack(Items.NETHERITE_SWORD), + pos = startPos.offset(Direction.NORTH, 2.0).add(0.0, 1.5, 0.0), + scale = 0.5f, + ) + + // 2. World-space GUI Block (Grass Block) + worldGuiItem( + stack = ItemStack(Items.GRASS_BLOCK), + pos = startPos.offset(Direction.NORTH, 2.0).offset(Direction.WEST, 1.0).add(0.0, 1.5, 0.0), + scale = 0.5f, + overlay = ItemOverlay.ENCHANT_GLINT, + ) + + // 3. Screen-space GUI Item + screenGuiItem( + stack = ItemStack(Items.DIAMOND_PICKAXE), + x = 0.5f, y = 0.4f, + size = 0.08f, + ) + + // ========== New: Flat & Shaded GUI Items ========== + val lightTime = (System.currentTimeMillis() % 4000L / 4000f) * 360f + + // 4. Flat World Block (Grass Block) + worldGuiItem( + stack = ItemStack(Items.GRASS_BLOCK), + pos = startPos.offset(Direction.NORTH, 3.0), + scale = 0.5f, + flat = true, + overlay = ItemOverlay.ENCHANT_GLINT + ) + + // 5. Custom Shading World Item (Golden Apple) + worldGuiItem( + stack = ItemStack(Items.GOLDEN_APPLE), + pos = startPos.offset(Direction.NORTH, 3.0).offset(Direction.EAST, 1.0), + scale = 0.5f, + ) + + // 6. Flat Screen Item (Enchanted Book) + screenGuiItem( + stack = ItemStack(Items.GRASS_BLOCK), + x = 0.6f, y = 0.4f, + size = 0.08f, + rotation = Vec3d(0.0, 0.0, lightTime.toDouble()), // Spinning on screen + lighting = ItemLighting.NONE + ) + } + } + } +} diff --git a/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt b/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt new file mode 100644 index 000000000..4d75eaf83 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/debug/SettingsTestModule.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.debug + +import com.lambda.config.groups.ScreenLineSettings +import com.lambda.config.groups.WorldLineSettings +import com.lambda.config.groups.ScreenTextSettings +import com.lambda.config.groups.WorldTextSettings +import com.lambda.event.events.RenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.renderer.ImmediateRenderer +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.extension.prevPos +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.lerp +import net.minecraft.util.math.Direction +import java.awt.Color + +object SettingsTestModule : Module( + name = "SettingsTestModule", + description = "Test module for Line and Text Config Settings", + tag = ModuleTag.DEBUG +) { + private enum class Page(override val displayName: String) : com.lambda.util.NamedEnum { + WorldLine("World Line"), + ScreenLine("Screen Line"), + WorldText("World Text"), + ScreenText("Screen Text") + } + + private val worldLineConfig = WorldLineSettings( + "World Line ", + this, + Page.WorldLine, + ) + + private val screenLineConfig = ScreenLineSettings( + "Screen Line ", + this, + Page.ScreenLine, + ) + + private val worldTextConfig = WorldTextSettings( + "World Text ", + this, + Page.WorldText + ) + + private val textConfig = ScreenTextSettings( + "Screen Text ", + this, + Page.ScreenText + ) + +// private val renderer = ImmediateRenderer("SettingsTestRenderer") + +// init { +// listen { +// renderer.tick() +// renderer.shapes { +// val startPos = lerp(mc.tickDelta, player.prevPos, player.pos).offset(Direction.NORTH, 3.0) +// +// // Render line using config +// lineGradient( +// startPos, +// lineConfig.startColor, +// startPos.offset(Direction.EAST, 3.0), +// lineConfig.endColor, +// lineConfig.lineWidth, +// lineConfig.getDashStyle() +// ) +// +// // Render text using config +// worldText( +// "Configured Text", +// startPos.add(0.0, 1.0, 0.0), +// style = textConfig.getSDFStyle() +// ) +// } +// renderer.render() +// } +// +// onDisable { renderer.close() } +// } +} diff --git a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt index a6b255d66..ffa456841 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt @@ -17,14 +17,14 @@ package com.lambda.module.modules.movement +import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.ConnectionEvent import com.lambda.event.events.PacketEvent import com.lambda.event.events.TickEvent -import com.lambda.event.events.onDynamicRender import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.esp.ShapeScope -import com.lambda.graphics.renderer.esp.DynamicAABB +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.util.DynamicAABB import com.lambda.gui.components.ClickGuiLayout import com.lambda.module.Module import com.lambda.module.modules.combat.KillAura @@ -33,6 +33,7 @@ import com.lambda.util.ClientPacket import com.lambda.util.PacketUtils.handlePacketSilently import com.lambda.util.PacketUtils.sendPacketSilently import com.lambda.util.ServerPacket +import com.lambda.util.extension.tickDelta import com.lambda.util.math.dist import com.lambda.util.math.lerp import com.lambda.util.math.minus @@ -111,19 +112,6 @@ object BackTrack : Module( poolPackets() } - onDynamicRender { esp -> - val target = target ?: return@onDynamicRender - - val c1 = ClickGuiLayout.primaryColor - val c2 = Color.RED - val p = target.hurtTime / 10.0 - val c = lerp(p, c1, c2) - - esp.shapes(target.pos.x, target.pos.y, target.pos.z) { - box(box, c.multAlpha(0.3), c.multAlpha(0.8)) - } - } - listen { event -> if (!outbound || target == null) return@listen sendPool.add(event.packet to currentTime) @@ -179,6 +167,20 @@ object BackTrack : Module( onDisable { poolPackets(true) } + + immediateRenderer("BackTrack Immediate Renderer") { + val target = target ?: return@immediateRenderer + + val c1 = ClickGuiLayout.primaryColor + val c2 = Color.RED + val p = target.hurtTime / 10.0 + val c = lerp(p, c1, c2) + + box(box.box(mc.tickDelta) ?: return@immediateRenderer, 0f) { + hideOutline() + gradientY(c.multAlpha(0.3), c.multAlpha(0.8)) + } + } } private fun SafeContext.poolPackets(all: Boolean = false) { diff --git a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt index f94ddb8fd..280aa7e05 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/Blink.kt @@ -17,13 +17,13 @@ package com.lambda.module.modules.movement +import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.PacketEvent -import com.lambda.event.events.RenderEvent -import com.lambda.event.events.onDynamicRender import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.esp.ShapeScope -import com.lambda.graphics.renderer.esp.DynamicAABB +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer +import com.lambda.graphics.util.DynamicAABB import com.lambda.gui.components.ClickGuiLayout import com.lambda.module.Module import com.lambda.module.modules.combat.KillAura @@ -31,6 +31,7 @@ import com.lambda.module.tag.ModuleTag import com.lambda.util.PacketUtils.handlePacketSilently import com.lambda.util.PacketUtils.sendPacketSilently import com.lambda.util.ServerPacket +import com.lambda.util.extension.tickDelta import com.lambda.util.math.minus import com.lambda.util.math.setAlpha import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket @@ -59,20 +60,19 @@ object Blink : Module( private var lastBox = Box(BlockPos.ORIGIN) init { - listen { + tickedRenderer("Blink Ticked Renderer") { safeContext -> val time = System.currentTimeMillis() - if (isActive && time - lastUpdate < delay) return@listen + if (isActive && time - lastUpdate < delay) return@tickedRenderer lastUpdate = time - poolPackets() + with(safeContext) { poolPackets() } } - onDynamicRender { esp -> + immediateRenderer("Blink Immediate Renderer") { val color = ClickGuiLayout.primaryColor - val pos = player.pos - esp.shapes(pos.x, pos.y, pos.z) { - box(box.update(lastBox), color.setAlpha(0.3), color) + box(box.update(lastBox).box(mc.tickDelta) ?: return@immediateRenderer, 1.5f) { + colors(color.setAlpha(0.3), color) } } diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt index 20787710a..e0ceed0b9 100644 --- a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt +++ b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt @@ -19,8 +19,8 @@ package com.lambda.module.modules.network import com.lambda.context.SafeContext import com.lambda.event.events.PacketEvent -import com.lambda.event.events.RenderEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.threading.runConcurrent @@ -53,10 +53,10 @@ object PacketDelay : Module( private var inboundLastUpdate = 0L init { - listen { - if (mode != Mode.Static) return@listen + tickedRenderer("PacketDelay Ticked Renderer") { safeContext -> + if (mode != Mode.Static) return@tickedRenderer - flushPools(System.currentTimeMillis()) + with(safeContext) { flushPools(System.currentTimeMillis()) } } listen(Int.MIN_VALUE) { event -> diff --git a/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt b/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt index c5b3b0b1b..b7c139997 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/AirPlace.kt @@ -24,8 +24,8 @@ import com.lambda.context.SafeContext import com.lambda.event.events.ButtonEvent import com.lambda.event.events.PlayerEvent import com.lambda.event.events.TickEvent -import com.lambda.event.events.onStaticRender import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.context.BuildContext import com.lambda.interaction.construction.verify.TargetState @@ -48,6 +48,7 @@ import net.minecraft.world.RaycastContext import org.lwjgl.glfw.GLFW import java.awt.Color import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.invoke object AirPlace : Module( name = "AirPlace", @@ -63,6 +64,7 @@ object AirPlace : Module( private val scrollBind by setting("Scroll Bind", Bind(KeyCode.Unbound.code, GLFW.GLFW_MOD_CONTROL), "Allows you to hold the ctrl key and scroll to adjust distance").group(Group.General) private val outlineColor by setting("Outline Color", Color.WHITE).group(Group.Render) + private val outlineWidth by setting("Outline Width", 1.5f, 0.5f..10f, 0.1f) private var placementPos: BlockPos? = null private var placementState: BlockState? = null @@ -106,13 +108,13 @@ object AirPlace : Module( listen { if (airPlace()) it.cancel() } listen { if (airPlace()) it.cancel() } - onStaticRender { esp -> + tickedRenderer("Air Place Ticked Renderer") { safeContext -> placementPos?.let { pos -> - val boxes = placementState?.getOutlineShape(world, pos)?.boundingBoxes + val boxes = placementState?.getOutlineShape(safeContext.world, pos)?.boundingBoxes ?: listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)) - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - boxes.forEach { box -> - outline(box.offset(pos), outlineColor) + boxes.forEach { box -> + box(box, outlineWidth) { + hideFill() } } } diff --git a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index bc16a1d11..037d6a29b 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -22,8 +22,8 @@ import com.lambda.config.applyEdits import com.lambda.context.SafeContext import com.lambda.event.events.PlayerEvent import com.lambda.event.events.TickEvent -import com.lambda.event.events.onStaticRender import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.context.BreakContext import com.lambda.interaction.construction.simulation.context.BuildContext @@ -71,6 +71,7 @@ object PacketMine : Module( private val staticColor by setting("Color", Color(255, 0, 0, 60)) { renderQueue && !dynamicColor }.group(Group.Renders) private val startColor by setting("Start Color", Color(255, 255, 0, 60), "The color of the start (closest to breaking) of the queue") { renderQueue && dynamicColor }.group(Group.Renders) private val endColor by setting("End Color", Color(255, 0, 0, 60), "The color of the end (farthest from breaking) of the queue") { renderQueue && dynamicColor }.group(Group.Renders) + private val outlineWidth by setting("Outline Width", 1.5f, 0.5f..10f, 0.1f) private val pendingActions = ConcurrentLinkedQueue() @@ -173,27 +174,30 @@ object PacketMine : Module( } } - onStaticRender { esp -> + tickedRenderer("PacketMine Ticked Renderer") { safeContext -> if (renderRebreak) { rebreakPos?.let { pos -> - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { - outline(pos, rebreakColor) + box(pos, outlineWidth) { + hideFill() + outlineColor(rebreakColor) } } } - if (!renderQueue) return@onStaticRender - queueSorted.forEachIndexed { index, positions -> - positions.forEach { pos -> - val color = if (dynamicColor) lerp(index / queuePositions.size.toDouble(), startColor, endColor) - else staticColor - val boxes = when (renderMode) { - RenderMode.State -> blockState(pos).getOutlineShape(world, pos).boundingBoxes - RenderMode.Box -> listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)) - }.map { lerp(renderSize.toDouble(), Box(it.center, it.center), it).offset(pos) } + if (!renderQueue) return@tickedRenderer + with(safeContext) { + queueSorted.forEachIndexed { index, positions -> + positions.forEach { pos -> + val color = if (dynamicColor) lerp(index / queuePositions.size.toDouble(), startColor, endColor) + else staticColor + val boxes = when (renderMode) { + RenderMode.State -> blockState(pos).getOutlineShape(world, pos).boundingBoxes + RenderMode.Box -> listOf(Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)) + }.map { lerp(renderSize.toDouble(), Box(it.center, it.center), it).offset(pos) } - esp.shapes(pos.x.toDouble(), pos.y.toDouble(), pos.z.toDouble()) { boxes.forEach { box -> - box(box, color, color.setAlpha(1.0)) + box(box, outlineWidth) { + colors(color, color.setAlpha(1.0)) + } } } } diff --git a/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt b/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt index c97f8242a..02341d866 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt @@ -17,7 +17,7 @@ package com.lambda.module.modules.player -import com.lambda.event.events.onStaticRender +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer import com.lambda.interaction.BaritoneManager import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint @@ -65,9 +65,10 @@ object WorldEater : Module( BaritoneManager.cancel() } - onStaticRender { esp -> - esp.shapes(pos1.x.toDouble(), pos1.y.toDouble(), pos1.z.toDouble()) { - outline(Box.enclosing(pos1, pos2), Color.BLUE) + tickedRenderer("WorldEater Ticked Renderer") { + box(Box.enclosing(pos1, pos2), 1.5f) { + hideFill() + outlineColor(Color.BLUE) } } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt deleted file mode 100644 index 84a031bdc..000000000 --- a/src/main/kotlin/com/lambda/module/modules/render/BlockESP.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.module.modules.render - -import com.lambda.Lambda.mc -import com.lambda.config.settings.collections.CollectionSetting.Companion.onDeselect -import com.lambda.config.settings.collections.CollectionSetting.Companion.onSelect -import com.lambda.context.SafeContext -import com.lambda.graphics.esp.chunkedEsp -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh -import com.lambda.module.Module -import com.lambda.module.tag.ModuleTag -import com.lambda.threading.runSafe -import com.lambda.util.extension.blockColor -import com.lambda.util.extension.getBlockState -import com.lambda.util.world.toBlockPos -import net.minecraft.block.Blocks -import net.minecraft.client.render.model.BlockStateModel -import net.minecraft.util.math.Box -import java.awt.Color - -object BlockESP : Module( - name = "BlockESP", - description = "Render block ESP", - tag = ModuleTag.RENDER, -) { - private val searchBlocks by setting("Search Blocks", true, "Search for blocks around the player") - private val blocks by setting("Blocks", setOf(Blocks.BEDROCK), description = "Render blocks") { searchBlocks } - .onSelect { rebuildMesh(this, null, null) } - .onDeselect { rebuildMesh(this, null, null) } - - private var drawFaces: Boolean by setting("Draw Faces", true, "Draw faces of blocks") { searchBlocks }.onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) drawOutlines = true } - private var drawOutlines: Boolean by setting("Draw Outlines", true, "Draw outlines of blocks") { searchBlocks }.onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) drawFaces = true } - private val mesh by setting("Mesh", true, "Connect similar adjacent blocks") { searchBlocks }.onValueChange(::rebuildMesh) - - private val useBlockColor by setting("Use Block Color", false, "Use the color of the block instead") { searchBlocks }.onValueChange(::rebuildMesh) - private val blockColorAlpha by setting("Block Color Alpha", 0.3, 0.1..1.0, 0.05) { searchBlocks && useBlockColor }.onValueChange { _, _ -> ::rebuildMesh } - - private val faceColor by setting("Face Color", Color(100, 150, 255, 51), "Color of the surfaces") { searchBlocks && drawFaces && !useBlockColor }.onValueChange(::rebuildMesh) - private val outlineColor by setting("Outline Color", Color(100, 150, 255, 128), "Color of the outlines") { searchBlocks && drawOutlines && !useBlockColor }.onValueChange(::rebuildMesh) - private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { searchBlocks && drawOutlines }.onValueChange(::rebuildMesh) - - private val outlineMode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode") { searchBlocks }.onValueChange(::rebuildMesh) - - @JvmStatic - val barrier by setting("Solid Barrier Block", true, "Render barrier blocks") - - // ToDo: I wanted to render this as a transparent / translucent block with a red tint. - // Like the red stained glass block without the texture sprite. - // Creating a custom baked model for this would be needed but seems really hard to do. - // mc.blockRenderManager.getModel(Blocks.RED_STAINED_GLASS.defaultState) - @JvmStatic - val model: BlockStateModel get() = mc.bakedModelManager.missingModel - - private val esp = chunkedEsp("BlockESP") { world, position -> - val state = world.getBlockState(position) - if (state.block !in blocks) return@chunkedEsp - - val sides = if (mesh) { - buildSideMesh(position) { - world.getBlockState(it).block in blocks - } - } else DirectionMask.ALL - - runSafe { - // TODO: Add custom color option when map options are implemented - val extractedColor = blockColor(state, position.toBlockPos()) - val finalColor = Color(extractedColor.red, extractedColor.green, extractedColor.blue, (blockColorAlpha * 255).toInt()) - val pos = position.toBlockPos() - val shape = state.getOutlineShape(world, pos) - val worldBox = if (shape.isEmpty) Box(pos) else shape.boundingBox.offset(pos) - box(worldBox) { - if (drawFaces) - filled(if (useBlockColor) finalColor else faceColor, sides) - if (drawOutlines) - outline(if (useBlockColor) extractedColor else BlockESP.outlineColor, sides, BlockESP.outlineMode, thickness = outlineWidth) - } - } - } - - init { - onEnable { esp.rebuildAll() } - onDisable { esp.close() } - } - - private fun rebuildMesh(ctx: SafeContext, from: Any?, to: Any?): Unit = esp.rebuild() -} diff --git a/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt new file mode 100644 index 000000000..9b3413f7c --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/BlockOutline.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.render + +import com.lambda.config.applyEdits +import com.lambda.config.groups.WorldLineSettings +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.lerp +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.util.math.Box +import java.awt.Color + +object BlockOutline : Module( + name = "BlockOutline", + description = "Overrides the default block outline rendering", + tag = ModuleTag.RENDER +) { + private val fill by setting("Fill", true) + private val fillColor by setting("Fill Color", Color(255, 255, 255, 20)) { fill } + private val outline by setting("Outline", true) + private val outlineColor by setting("Outline Color", Color(255, 255, 255, 120)) { outline } + private val lineConfig = WorldLineSettings("Outline ", this) { outline }.apply { + applyEdits { + hide(::startColor, ::endColor) + } + } + private val interpolate by setting("Interpolate", true) + private val esp by setting("ESP", true) + + var previous: List? = null + + init { + immediateRenderer("BlockOutline Immediate Renderer", depthTest = { !esp }) { safeContext -> + with(safeContext) { + val hitResult = mc.crosshairTarget?.blockResult ?: return@immediateRenderer + val pos = hitResult.blockPos + val blockState = blockState(pos) + val boxes = blockState + .getOutlineShape(world, pos) + .boundingBoxes + .let { boxes -> + boxes.mapIndexed { index, box -> + val offset = box.offset(pos) + val interpolated = previous?.let { previous -> + if (!interpolate || previous.size < boxes.size) null + else lerp(mc.tickDelta, previous[index], offset) + } ?: offset + interpolated.expand(0.0001) + } + } + + boxes.forEach { box -> + box(box, lineConfig.width) { + colors(fillColor, outlineColor) + if (!fill) hideFill() + if (!outline) hideOutline() + } + } + } + } + + listen { + val hitResult = mc.crosshairTarget?.blockResult ?: return@listen + val state = blockState(hitResult.blockPos) + previous = state + .getOutlineShape(world, hitResult.blockPos).boundingBoxes + .map { it.offset(hitResult.blockPos) } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt index 506a98a2f..a09f5d698 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt @@ -17,21 +17,16 @@ package com.lambda.module.modules.render -import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.GuiEvent import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent +import com.lambda.event.events.ScreenRenderEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.RenderMain -import com.lambda.graphics.mc.InterpolatedRegionESP -import com.lambda.graphics.mc.TransientRegionESP +import com.lambda.graphics.mc.renderer.ImmediateRenderer import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.NamedEnum -import com.lambda.util.extension.tickDelta -import com.lambda.util.math.setAlpha -import com.lambda.util.world.entitySearch +import com.lambda.util.extension.tickDeltaF import imgui.ImGui import net.minecraft.entity.Entity import net.minecraft.entity.ItemEntity @@ -54,209 +49,34 @@ object EntityESP : Module( description = "Highlight entities with smooth interpolated rendering", tag = ModuleTag.RENDER ) { - private val esp = InterpolatedRegionESP("EntityESP") - - private data class LabelData( - val screenX: Float, - val screenY: Float, - val text: String, - val color: Color, - val scale: Float - ) - - private val pendingLabels = mutableListOf() - - private val range by setting("Range", 64.0, 8.0..256.0, 1.0, "Maximum render distance").group(Group.General) - private val throughWalls by setting("Through Walls", true, "Render through blocks").group(Group.General) - private val self by setting("Self", false, "Render own player in third person").group(Group.General) - - private val players by setting("Players", true, "Highlight players").group(Group.Entities) - private val hostiles by setting("Hostiles", true, "Highlight hostile mobs").group(Group.Entities) - private val passives by setting("Passives", false, "Highlight passive mobs (animals)").group(Group.Entities) - private val neutrals by setting("Neutrals", false, "Highlight neutral mobs").group(Group.Entities) - private val items by setting("Items", false, "Highlight dropped items").group(Group.Entities) - private val projectiles by setting("Projectiles", false, "Highlight projectiles").group(Group.Entities) - private val vehicles by setting("Vehicles", false, "Highlight boats and minecarts").group(Group.Entities) - private val crystals by setting("Crystals", true, "Highlight end crystals").group(Group.Entities) - private val armorStands by setting("Armor Stands", false, "Highlight armor stands").group(Group.Entities) - - private val drawBoxes by setting("Boxes", true, "Draw entity boxes").group(Group.Render) - private val drawFilled by setting("Filled", true, "Fill entity boxes") { drawBoxes }.group(Group.Render) - private val drawOutline by setting("Outline", true, "Draw box outlines") { drawBoxes }.group(Group.Render) - private val filledAlpha by setting("Filled Alpha", 0.2, 0.0..1.0, 0.05) { drawBoxes && drawFilled }.group(Group.Render) - private val outlineAlpha by setting("Outline Alpha", 0.8, 0.0..1.0, 0.05) { drawBoxes && drawOutline }.group(Group.Render) - private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { drawBoxes && drawOutline }.group(Group.Render) - - private val tracers by setting("Tracers", true, "Draw lines to entities").group(Group.Tracers) - private val tracerOrigin by setting("Tracer Origin", TracerOrigin.Eyes, "Where tracers start from") { tracers }.group(Group.Tracers) - private val tracerWidth by setting("Tracer Width", 1.5f, 0.5f..5f, 0.5f) { tracers }.group(Group.Tracers) - private val dashedTracers by setting("Dashed Tracers", false, "Use dashed lines for tracers") { tracers }.group(Group.Tracers) - private val dashLength by setting("Dash Length", 1.0, 0.25..2.0, 0.25) { tracers && dashedTracers }.group(Group.Tracers) - private val gapLength by setting("Gap Length", 0.5, 0.1..1.0, 0.1) { tracers && dashedTracers }.group(Group.Tracers) - - private val nameTags by setting("Name Tags", false, "Show entity name tags").group(Group.NameTags) - private val nameTagDistance by setting("Show Distance", true, "Show distance in name tags") { nameTags }.group(Group.NameTags) - private val nameTagHealth by setting("Show Health", true, "Show health in name tags") { nameTags }.group(Group.NameTags) - private val nameTagBackground by setting("Name Background", true, "Draw background behind name tags") { nameTags }.group(Group.NameTags) - - private val playerColor by setting("Player Color", Color(255, 50, 50), "Color for players").group(Group.Colors) - private val hostileColor by setting("Hostile Color", Color(255, 100, 0), "Color for hostile mobs").group(Group.Colors) - private val passiveColor by setting("Passive Color", Color(50, 255, 50), "Color for passive mobs").group(Group.Colors) - private val neutralColor by setting("Neutral Color", Color(255, 255, 50), "Color for neutral mobs").group(Group.Colors) - private val itemColor by setting("Item Color", Color(100, 100, 255), "Color for items").group(Group.Colors) - private val projectileColor by setting("Projectile Color", Color(200, 200, 200), "Color for projectiles").group(Group.Colors) - private val vehicleColor by setting("Vehicle Color", Color(150, 100, 50), "Color for vehicles").group(Group.Colors) - private val crystalColor by setting("Crystal Color", Color(255, 0, 255), "Color for end crystals").group(Group.Colors) - private val otherColor by setting("Other Color", Color(200, 200, 200), "Color for other entities").group(Group.Colors) - - init { - listen { - esp.tick() - - entitySearch(range) { shouldRender(it) }.forEach { entity -> - val color = getEntityColor(entity) - val box = entity.boundingBox - - esp.shapes(entity.x, entity.y, entity.z) { - if (drawBoxes) { - box(box, entity.id) { - if (drawFilled) - filled(color.setAlpha(filledAlpha)) - if (drawOutline) - outline( - color.setAlpha(outlineAlpha), - thickness = outlineWidth - ) - } - } - } - } - - esp.upload() - } - - listen { - val tickDelta = mc.tickDelta - esp.render(tickDelta) - - // Clear pending labels from previous frame - pendingLabels.clear() - - if (tracers || nameTags) { - val tracerEsp = TransientRegionESP( - "EntityESP-Tracers", - depthTest = !throughWalls - ) - entitySearch(range) { shouldRender(it) }.forEach { entity -> - val color = getEntityColor(entity) - val entityPos = getInterpolatedPos(entity, tickDelta) - - if (tracers) { - val startPos = getTracerStartPos(tickDelta) - val endPos = entityPos.add(0.0, entity.height / 2.0, 0.0) - - tracerEsp.shapes(entity.x, entity.y, entity.z) { - tracer(startPos, endPos, entity.id) { - color(color.setAlpha(outlineAlpha)) - width(tracerWidth) - if (dashedTracers) dashed(dashLength, gapLength) - } - } - } - - if (nameTags) { - val namePos = entityPos.add(0.0, entity.height + 0.3, 0.0) - // Project to screen coords NOW while matrices are - // valid - val screen = RenderMain.worldToScreen(namePos) - if (screen != null) { - val nameText = buildNameTag(entity) - // Calculate distance-based scale (closer = - // larger) - val distance = player.pos.distanceTo(namePos).toFloat() - val scale = (1.0f / (distance * 0.1f + 1f)).coerceIn(0.5f, 2.0f) - pendingLabels.add( - LabelData( - screen.x, - screen.y, - nameText, - color, - scale - ) - ) - } - } - } - - tracerEsp.upload() - tracerEsp.render() - tracerEsp.close() - } - } - - // Draw ImGUI labels using pre-computed screen coordinates - listen { - val drawList = ImGui.getBackgroundDrawList() - val font = ImGui.getFont() - - pendingLabels.forEach { label -> - val fontSize = (font.fontSize * label.scale).toInt() - val textSize = imgui.ImVec2() - ImGui.calcTextSize(textSize, label.text) - - // Scale text size based on our custom scale - val tw = textSize.x * label.scale - val th = textSize.y * label.scale - - // Center text horizontally - val x = label.screenX - tw / 2f - val y = label.screenY - - // Color conversion (ABGR for ImGui) - val textColor = - (label.color.alpha shl 24) or - (label.color.blue shl 16) or - (label.color.green shl 8) or - label.color.red - val shadowColor = (200 shl 24) or 0 // Black with alpha - - if (nameTagBackground) { - val bgColor = (160 shl 24) or 0 // Black with 160 alpha - val padX = 4f * label.scale - val padY = 2f * label.scale - drawList.addRectFilled( - x - padX, - y - padY, - x + tw + padX, - y + th + padY, - bgColor, - 3f * label.scale - ) - } else { - // Shadow - drawList.addText( - font, - fontSize, - imgui.ImVec2( - x + 1f * label.scale, - y + 1f * label.scale - ), - shadowColor, - label.text - ) - } - drawList.addText( - font, - fontSize, - imgui.ImVec2(x, y), - textColor, - label.text - ) - } - } - - onDisable { esp.close() } - } + private val throughWalls by setting("Through Walls", true, "Render through blocks") + private val self by setting("Self", false, "Render own player in third person") + + private val players by setting("Players", true, "Highlight players") + private val hostiles by setting("Hostiles", true, "Highlight hostile mobs") + private val passives by setting("Passives", false, "Highlight passive mobs (animals)") + private val neutrals by setting("Neutrals", false, "Highlight neutral mobs") + private val items by setting("Items", false, "Highlight dropped items") + private val projectiles by setting("Projectiles", false, "Highlight projectiles") + private val vehicles by setting("Vehicles", false, "Highlight boats and minecarts") + private val crystals by setting("Crystals", true, "Highlight end crystals") + private val armorStands by setting("Armor Stands", false, "Highlight armor stands") + + private val drawBoxes by setting("Boxes", true, "Draw entity boxes") + private val drawFilled by setting("Filled", true, "Fill entity boxes") { drawBoxes } + private val drawOutline by setting("Outline", true, "Draw box outlines") { drawBoxes } + private val filledAlpha by setting("Filled Alpha", 0.2, 0.0..1.0, 0.05) { drawBoxes && drawFilled } + private val outlineAlpha by setting("Outline Alpha", 0.8, 0.0..1.0, 0.05) { drawBoxes && drawOutline } + + private val playerColor by setting("Player Color", Color(255, 50, 50), "Color for players") + private val hostileColor by setting("Hostile Color", Color(255, 100, 0), "Color for hostile mobs") + private val passiveColor by setting("Passive Color", Color(50, 255, 50), "Color for passive mobs") + private val neutralColor by setting("Neutral Color", Color(255, 255, 50), "Color for neutral mobs") + private val itemColor by setting("Item Color", Color(100, 100, 255), "Color for items") + private val projectileColor by setting("Projectile Color", Color(200, 200, 200), "Color for projectiles") + private val vehicleColor by setting("Vehicle Color", Color(150, 100, 50), "Color for vehicles") + private val crystalColor by setting("Crystal Color", Color(255, 0, 255), "Color for end crystals") + private val otherColor by setting("Other Color", Color(200, 200, 200), "Color for other entities") private fun SafeContext.shouldRender(entity: Entity): Boolean { if (entity == player && !self) return false @@ -292,56 +112,4 @@ object EntityESP : Module( else -> otherColor } } - - private fun getInterpolatedPos(entity: Entity, tickDelta: Float): Vec3d { - val x = entity.lastRenderX + (entity.x - entity.lastRenderX) * tickDelta - val y = entity.lastRenderY + (entity.y - entity.lastRenderY) * tickDelta - val z = entity.lastRenderZ + (entity.z - entity.lastRenderZ) * tickDelta - return Vec3d(x, y, z) - } - - private fun SafeContext.getTracerStartPos(tickDelta: Float): Vec3d { - val playerPos = getInterpolatedPos(player, tickDelta) - - return when (tracerOrigin) { - TracerOrigin.Feet -> playerPos - TracerOrigin.Center -> playerPos.add(0.0, player.height / 2.0, 0.0) - TracerOrigin.Eyes -> - playerPos.add(0.0, player.standingEyeHeight.toDouble(), 0.0) - TracerOrigin.Crosshair -> { - val camera = mc.gameRenderer?.camera ?: return playerPos - camera.pos.add(Vec3d(camera.horizontalPlane).multiply(0.1)) - } - } - } - - private fun SafeContext.buildNameTag(entity: Entity): String { - val builder = StringBuilder() - val name = entity.displayName?.string ?: entity.type.name.string - builder.append(name) - if (nameTagHealth && entity is LivingEntity) { - builder.append(" [${entity.health.toInt()}/${entity.maxHealth.toInt()}]") - } - if (nameTagDistance) { - val dist = player.distanceTo(entity).toInt() - builder.append(" ${dist}m") - } - return builder.toString() - } - - enum class TracerOrigin(override val displayName: String) : NamedEnum { - Feet("Feet"), - Center("Center"), - Eyes("Eyes"), - Crosshair("Crosshair") - } - - private enum class Group(override val displayName: String) : NamedEnum { - General("General"), - Entities("Entities"), - Render("Render"), - Tracers("Tracers"), - NameTags("Name Tags"), - Colors("Colors") - } } diff --git a/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt new file mode 100644 index 000000000..08b7952ed --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.render + +import com.lambda.Lambda.mc +import com.lambda.config.applyEdits +import com.lambda.config.groups.ScreenTextSettings +import com.lambda.friend.FriendManager.isFriend +import com.lambda.graphics.RenderMain.worldToScreenNormalized +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.text.FontHandler.getDefaultFont +import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.EntityUtils +import com.lambda.util.EntityUtils.entityGroup +import com.lambda.util.NamedEnum +import com.lambda.util.extension.fullHealth +import com.lambda.util.extension.maxFullHealth +import com.lambda.util.math.MathUtils.roundToStep +import com.lambda.util.math.distSq +import com.lambda.util.math.lerp +import net.minecraft.client.network.OtherClientPlayerEntity +import net.minecraft.entity.Entity +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.math.Vec3d +import org.joml.component1 +import org.joml.component2 +import java.awt.Color + +//ToDo: implement all settings +object Nametags : Module( + name = "Nametags", + description = "Displays information about entities above them", + tag = ModuleTag.RENDER +) { + private enum class Group(override val displayName: String) : NamedEnum { + General("General"), + Text("Text") + } + + private enum class TextGroup(override val displayName: String): NamedEnum { + Friend("Friend"), + Other("Other") + } + + private val entities by setting("Entities", setOf(EntityUtils.EntityGroup.Player), EntityUtils.EntityGroup.entries).group(Group.General) + private val itemScale by setting("Item Scale", 3f, 0.4f..5f, 0.01f).group(Group.General) + private val yOffset by setting("Y Offset", 0.2, 0.0..1.0, 0.01).group(Group.General) + private val spacing by setting("Spacing", 0, 0..10, 1).group(Group.General) + private val self by setting("Self", false).group(Group.General) + private val health by setting("Health", false).group(Group.General) + private val ping by setting("Ping", true).group(Group.General) + private val gear by setting("Gear", true).group(Group.General) + private val mainItem by setting("Main Item", true) { gear }.group(Group.General) + private val offhandItem by setting("Offhand Item", true) { gear }.group(Group.General) + private val itemName by setting("Item Name", true).group(Group.General) + private val itemNameScale by setting("Item Name Scale", 0.7f, 0.1f..1.0f, 0.01f) { itemName }.group(Group.General) + private val itemCount by setting("Item Count", true).group(Group.General) + private val durabilityMode by setting("Durability Mode", DurabilityMode.Text) { gear }.group(Group.General) + //ToDo: Implement +// private val enchantments by setting("Enchantments", false) { gear } + + private val friendTextConfig = ScreenTextSettings("Friend ", this, Group.Text, TextGroup.Friend).apply { + applyEdits { + ::textColor.edit { defaultValue(Color(0, 255, 255, 255)) } + } + } + private val otherTextConfig = ScreenTextSettings("Other ", this, Group.Text, TextGroup.Other) + + var heightWidthRatio = 0f + var trueItemScaleX = 0f + var trueItemScaleY = 0f + var trueSpacingX = 0f + var trueSpacingY = 0f + + init { + immediateRenderer("Nametags Immediate Renderer") { safeContext -> + with(safeContext) { + heightWidthRatio = mc.window.height / mc.window.width.toFloat() + trueItemScaleY = itemScale * 0.01f + trueItemScaleX = trueItemScaleY * heightWidthRatio + trueSpacingY = spacing * 0.0005f + trueSpacingX = trueSpacingY * heightWidthRatio + + world.entities + .sortedByDescending { it distSq mc.gameRenderer.camera.pos } + .forEach { entity -> + val textConfig = if (entity is OtherClientPlayerEntity && entity.isFriend) friendTextConfig else otherTextConfig + val textStyle = textConfig.getSDFStyle() + val textSize = textConfig.size + if (!shouldRenderNametag(entity)) return@forEach + val nameText = entity.displayName?.string ?: return@forEach + val box = entity.interpolatedBox + val boxCenter = box.center + var (anchorX, anchorY) = + worldToScreenNormalized(Vec3d(boxCenter.x, box.maxY + yOffset, boxCenter.z)) + ?: return@forEach + + if (entity !is LivingEntity) { + screenText(nameText, anchorX, anchorY + (textSize / 2f), textSize, style = textStyle, centered = true) + return@forEach + } + + if (itemName && !entity.mainHandStack.isEmpty) { + val itemNameText = entity.mainHandStack.name.string + val itemNameScale = textSize * itemNameScale + screenText(itemNameText, anchorX, anchorY, itemNameScale, centered = true) + anchorY += (itemNameScale * 1.1f) + trueSpacingY + } + + val nameWidth = getDefaultFont().getStringWidthNormalized(nameText, textSize) + + val healthCount = if (health) entity.fullHealth else -1.0 + val healthText = if (health) " ${healthCount.roundToStep(0.01)}" else "" + val healthWidth = + getDefaultFont().getStringWidthNormalized(healthText, textSize) + .let { if (healthCount > 0) it + trueSpacingX else it } + + val pingCount = if (ping && entity is PlayerEntity) connection.getPlayerListEntry(entity.uuid)?.latency ?: -1 else -1 + val pingText = if (pingCount >= 0) " [$pingCount]" else "" + val pingWidth = + getDefaultFont().getStringWidthNormalized(pingText, textSize) + .let { if (pingCount >= 0) it + trueSpacingX else it } + + var combinedWidth = nameWidth + healthWidth + pingWidth + val nameX = anchorX - (combinedWidth / 2) + screenText(nameText, nameX, anchorY, textSize, style = textStyle) + if (healthCount >= 0) { + val healthColor = lerp(entity.fullHealth / entity.maxFullHealth, Color.RED, Color.GREEN).brighter() + screenText(healthText, nameX + nameWidth + trueSpacingX, anchorY, textSize, style = textStyle.apply { color = healthColor }) + } + if (pingCount >= 0) { + val pingColor = lerp(pingCount / 500.0, Color.GREEN, Color.RED).brighter() + screenText(pingText, nameX + nameWidth + healthWidth + trueSpacingX, anchorY, textSize, style = textStyle.apply { color = pingColor }) + } + + if (!gear) return@forEach + + if (EquipmentSlot.entries.none { it.index in 1..4 && !entity.getEquippedStack(it).isEmpty }) { + if (mainItem && !entity.mainHandStack.isEmpty) + renderItem(entity.mainHandStack, nameX - trueItemScaleX - trueSpacingX - (trueItemScaleX * 0.1f), anchorY) + if (offhandItem && !entity.offHandStack.isEmpty) + renderItem(entity.offHandStack, anchorX + (combinedWidth / 2) + trueSpacingX, anchorY) + } else drawArmorAndItems(entity, anchorX, anchorY + textSize + trueSpacingY) + } + } + } + } + + private fun RenderBuilder.drawArmorAndItems(entity: LivingEntity, x: Float, y: Float) { + val stepAmount = trueItemScaleX + trueSpacingX + var iteratorX = x - (stepAmount * 3) + (trueSpacingX / 2) + if (mainItem && !entity.mainHandStack.isEmpty) renderItem(entity.mainHandStack, iteratorX, y) + iteratorX += stepAmount + val headStack = entity.getEquippedStack(EquipmentSlot.HEAD) + val chestStack = entity.getEquippedStack(EquipmentSlot.CHEST) + val legsStack = entity.getEquippedStack(EquipmentSlot.LEGS) + val feetStack = entity.getEquippedStack(EquipmentSlot.FEET) + if (!headStack.isEmpty) renderItem(headStack, iteratorX, y) + iteratorX += stepAmount + if (!chestStack.isEmpty) renderItem(chestStack, iteratorX, y) + iteratorX += stepAmount + if (!legsStack.isEmpty) renderItem(legsStack, iteratorX, y) + iteratorX += stepAmount + if (!feetStack.isEmpty) renderItem(feetStack, iteratorX, y) + iteratorX += stepAmount + if (offhandItem && !entity.offHandStack.isEmpty) renderItem(entity.offHandStack, iteratorX, y) + } + + private fun RenderBuilder.renderItem(stack: ItemStack, x: Float, y: Float) { + screenGuiItem(stack, x, y, trueItemScaleY, centered = false) + var iteratorY = y + iteratorY += trueItemScaleY + if (durabilityMode != DurabilityMode.None && stack.isDamageable) { + val dura = (1 - (stack.damage / stack.maxDamage.toDouble())) + if (durabilityMode.bar) { + val yOffset = trueItemScaleY / 16 + val xOffset = trueItemScaleX / 16 + val maxWidth = xOffset * 14 + screenRect(x + xOffset, y + yOffset, maxWidth, yOffset * 2, Color.BLACK) + screenRect(x + xOffset, y + (yOffset * 2), maxWidth * dura.toFloat(), yOffset, lerp(dura, Color.RED, Color.GREEN).brighter()) + } + if (durabilityMode.text) { + val duraText = "${(dura * 100).toInt()}%" + val textSize = getDefaultFont().getSizeForWidthNormalized(duraText, trueItemScaleX) * 0.9f + screenText(duraText, x + (trueItemScaleX / 2), iteratorY, textSize.coerceAtMost(trueItemScaleY * 0.33f), centered = true, style = RenderBuilder.SDFStyle(color = lerp(dura, Color.RED, Color.GREEN).brighter())) + } + } + if (itemCount && stack.isStackable && stack.count > 1) { + val countText = "${stack.count}" + val textSize = trueItemScaleY / 2 + val textWidth = getDefaultFont().getStringWidthNormalized(countText, textSize) + screenText(countText, x - (textWidth - trueItemScaleX), y, textSize) + } + } + + @JvmStatic + fun shouldRenderNametag(entity: Entity) = + entity.entityGroup in entities && (self || entity !== mc.player) && (entity !is LivingEntity || entity.isAlive) + + private enum class DurabilityMode(val text: Boolean, val bar: Boolean) { + None(false, false), + Text(true, false), + Bar(false, true), + Both(true, true) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt index 09b287385..5998ed581 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt @@ -19,10 +19,18 @@ package com.lambda.module.modules.render import com.lambda.module.Module import com.lambda.module.tag.ModuleTag -import com.lambda.util.DynamicReflectionSerializer.remappedName +import com.lambda.util.EntityUtils.blockEntityMap +import com.lambda.util.EntityUtils.bossEntityMap +import com.lambda.util.EntityUtils.createNameMap +import com.lambda.util.EntityUtils.decorationEntityMap +import com.lambda.util.EntityUtils.miscEntityMap +import com.lambda.util.EntityUtils.mobEntityMap +import com.lambda.util.EntityUtils.passiveEntityMap +import com.lambda.util.EntityUtils.playerEntityMap +import com.lambda.util.EntityUtils.projectileEntityMap +import com.lambda.util.EntityUtils.vehicleEntityMap import com.lambda.util.NamedEnum import com.lambda.util.reflections.scanResult -import io.github.classgraph.ClassInfo import net.minecraft.block.entity.BlockEntity import net.minecraft.client.particle.Particle import net.minecraft.entity.Entity @@ -34,20 +42,7 @@ object NoRender : Module( description = "Disables rendering of certain things", tag = ModuleTag.RENDER, ) { - private val entities = scanResult - .getSubclasses(Entity::class.java) - .filter { !it.isAbstract && it.name.startsWith("net.minecraft") } - private val particleMap = createParticleNameMap() - private val blockEntityMap = createBlockEntityNameMap() - private val playerEntityMap = createEntityNameMap("net.minecraft.client.network.") - private val bossEntityMap = createEntityNameMap("net.minecraft.entity.boss.") - private val decorationEntityMap = createEntityNameMap("net.minecraft.entity.decoration.") - private val mobEntityMap = createEntityNameMap("net.minecraft.entity.mob.") - private val passiveEntityMap = createEntityNameMap("net.minecraft.entity.passive.") - private val projectileEntityMap = createEntityNameMap("net.minecraft.entity.projectile.") - private val vehicleEntityMap = createEntityNameMap("net.minecraft.entity.vehicle.") - private val miscEntityMap = createEntityNameMap("net.minecraft.entity.", strictDir = true) private enum class Group(override val displayName: String) : NamedEnum { Hud("Hud"), @@ -112,37 +107,6 @@ object NoRender : Module( .filter { !it.isAbstract } .createNameMap("net.minecraft.client.particle.", "Particle") - private fun createEntityNameMap(directory: String, strictDir: Boolean = false) = - entities.createNameMap(directory, "Entity", strictDir) - - private fun createBlockEntityNameMap() = - scanResult - .getSubclasses(BlockEntity::class.java) - .filter { !it.isAbstract }.createNameMap("net.minecraft.block.entity", "BlockEntity") - - private fun Collection.createNameMap( - directory: String, - removePattern: String = "", - strictDirectory: Boolean = false - ) = map { - val remappedName = it.name.remappedName - val displayName = remappedName - .substring(remappedName.indexOfLast { it == '.' } + 1) - .replace(removePattern, "") - .fancyFormat() - MappingInfo(it.simpleName, remappedName, displayName) - } - .sortedBy { it.displayName.lowercase() } - .filter { info -> - if (strictDirectory) - info.remapped.startsWith(directory) && !info.remapped.substring(directory.length).contains(".") - else info.remapped.startsWith(directory) - } - .associate { it.raw to it.displayName } - - private fun String.fancyFormat() = - replace("$", " - ").replace("(?. - */ - -package com.lambda.module.modules.render - -import com.lambda.Lambda.mc -import com.lambda.context.SafeContext -import com.lambda.event.events.MovementEvent -import com.lambda.event.events.PlayerEvent -import com.lambda.event.events.RenderEvent -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib -import com.lambda.graphics.buffer.vertex.attributes.VertexMode -import com.lambda.graphics.gl.GlStateUtils.withBlendFunc -import com.lambda.graphics.gl.GlStateUtils.withDepth -import com.lambda.graphics.gl.Matrices -import com.lambda.graphics.gl.Matrices.buildWorldProjection -import com.lambda.graphics.gl.Matrices.withVertexTransform -import com.lambda.graphics.pipeline.VertexBuilder -import com.lambda.graphics.pipeline.VertexPipeline -import com.lambda.graphics.shader.Shader -import com.lambda.gui.components.ClickGuiLayout -import com.lambda.interaction.managers.rotating.Rotation -import com.lambda.module.Module -import com.lambda.module.tag.ModuleTag -import com.lambda.util.extension.partialTicks -import com.lambda.util.math.DOWN -import com.lambda.util.math.MathUtils.random -import com.lambda.util.math.UP -import com.lambda.util.math.lerp -import com.lambda.util.math.multAlpha -import com.lambda.util.math.plus -import com.lambda.util.math.times -import com.lambda.util.math.transform -import com.lambda.util.player.MovementUtils.moveDelta -import com.lambda.util.world.raycast.InteractionMask -import com.mojang.blaze3d.opengl.GlConst.GL_ONE -import com.mojang.blaze3d.opengl.GlConst.GL_SRC_ALPHA -import net.minecraft.entity.Entity -import net.minecraft.util.math.Vec3d -import org.joml.Matrix4f -import kotlin.math.sin - -// FixMe: Do not call render stuff in the initialization block -object Particles : Module( - name = "Particles", - description = "Spawns fancy particles", - tag = ModuleTag.RENDER, -) { - // ToDo: resort, cleanup settings - private val duration by setting("Duration", 5.0, 1.0..500.0, 1.0) - private val fadeDuration by setting("Fade Ticks", 5.0, 1.0..30.0, 1.0) - private val spawnAmount by setting("Spawn Amount", 20, 3..500, 1) - private val sizeSetting by setting("Size", 2.0, 0.1..50.0, 0.1) - private val alphaSetting by setting("Alpha", 1.5, 0.01..2.0, 0.01) - private val speedH by setting("Speed H", 1.0, 0.0..10.0, 0.1) - private val speedV by setting("Speed V", 1.0, 0.0..10.0, 0.1) - private val inertia by setting("Inertia", 0.0, 0.0..1.0, 0.01) - private val gravity by setting("Gravity", 0.2, 0.0..1.0, 0.01) - private val onMove by setting("On Move", false) - - private val environment by setting("Environment", true) - private val environmentSpawnAmount by setting("E Spawn Amount", 10, 3..100, 1) { environment } - private val environmentSize by setting("E Size", 2.0, 0.1..50.0, 0.1) { environment } - private val environmentRange by setting("E Spread", 5.0, 1.0..20.0, 0.1) { environment } - private val environmentSpeedH by setting("E Speed H", 0.0, 0.0..10.0, 0.1) { environment } - private val environmentSpeedV by setting("E Speed V", 0.1, 0.0..10.0, 0.1) { environment } - - private var particles = mutableListOf() - private val pipeline = VertexPipeline(VertexMode.Triangles, VertexAttrib.Group.PARTICLE) - private val shader = Shader("shaders/vertex/particles.glsl", "shaders/fragment/particles.glsl") - - init { - listen { - if (environment) spawnForEnvironment() - particles.removeIf(Particle::update) - } - - listen { - // Todo: interpolated tickbased upload? - val builder = pipeline.build() - particles.forEach { it.build(builder) } - - withBlendFunc(GL_SRC_ALPHA, GL_ONE) { - shader.use() - pipeline.upload(builder) - withDepth(false, pipeline::render) - pipeline.clear() - } - } - - listen { event -> - spawnForEntity(event.entity) - } - - listen { - if (!onMove || player.moveDelta < 0.05) return@listen - spawnForEntity(player) - } - } - - private fun spawnForEntity(entity: Entity) { - repeat(spawnAmount) { - val i = (it + 1) / spawnAmount.toDouble() - - val pos = entity.pos - val height = entity.boundingBox.lengthY - val spawnHeight = height * transform(i, 0.0, 1.0, 0.2, 0.8) - val particlePos = pos.add(0.0, spawnHeight, 0.0) - val particleMotion = Rotation( - random(-180.0, 180.0), - random(-90.0, 90.0) - ).vector * Vec3d(speedH, speedV, speedH) * 0.1 - - particles += Particle(particlePos, particleMotion, false) - } - } - - private fun SafeContext.spawnForEnvironment() { - if (mc.paused) return - repeat(environmentSpawnAmount) { - var particlePos = player.pos + Rotation(random(-180.0, 180.0), 0.0).vector * random(0.0, environmentRange) - - Rotation.DOWN.rayCast(6.0, particlePos + UP * 2.0, true, InteractionMask.Block)?.pos?.let { - particlePos = it + UP * 0.03 - } ?: return@repeat - - val particleMotion = Rotation( - random(-180.0, 180.0), - random(-90.0, 90.0) - ).vector * Vec3d(environmentSpeedH, environmentSpeedV, environmentSpeedH) * 0.1 - - particles += Particle(particlePos, particleMotion, true) - } - } - - private class Particle( - initialPosition: Vec3d, - initialMotion: Vec3d, - val lay: Boolean, - ) { - private val fadeTicks = fadeDuration - - private var age = 0 - private val maxAge = (duration + random(0.0, 20.0)).toInt() - - private var prevPos = initialPosition - private var position = initialPosition - private var motion = initialMotion - - private val projRotation = if (lay) Matrices.ProjRotationMode.Up else Matrices.ProjRotationMode.ToCamera - - fun update(): Boolean { - if (mc.paused) return false - age++ - - prevPos = position - - if (!lay) motion += DOWN * gravity * 0.01 - motion *= 0.9 + inertia * 0.1 - - position += motion - - return age > maxAge + fadeTicks * 2 + 5 - } - - fun build(builder: VertexBuilder) = builder.apply { - val smoothAge = age + mc.partialTicks - val colorTicks = smoothAge * 0.1 / ClickGuiLayout.colorSpeed - - val alpha = when { - smoothAge < fadeTicks -> smoothAge / fadeTicks - smoothAge in fadeTicks..fadeTicks + maxAge -> 1.0 - else -> { - val min = fadeTicks + maxAge - val max = fadeTicks * 2 + maxAge - transform(smoothAge, min, max, 1.0, 0.0) - } - } - - val (c1, c2) = ClickGuiLayout.primaryColor to ClickGuiLayout.secondaryColor - val color = lerp(sin(colorTicks) * 0.5 + 0.5, c1, c2).multAlpha(alpha * alphaSetting) - - val position = lerp(mc.partialTicks, prevPos, position) - val size = if (lay) environmentSize else sizeSetting * lerp(alpha, 0.5, 1.0) - - withVertexTransform(buildWorldProjection(position, size, projRotation)) { - buildQuad( - vertex { - vec3m(-1.0, -1.0, 0.0).vec2(0.0, 0.0).color(color) - }, - vertex { - vec3m(-1.0, 1.0, 0.0).vec2(0.0, 1.0).color(color) - }, - vertex { - vec3m(1.0, 1.0, 0.0).vec2(1.0, 1.0).color(color) - }, - vertex { - vec3m(1.0, -1.0, 0.0).vec2(1.0, 0.0).color(color) - } - ) - } - } - } -} diff --git a/src/main/kotlin/com/lambda/module/modules/render/Search.kt b/src/main/kotlin/com/lambda/module/modules/render/Search.kt new file mode 100644 index 000000000..f621cb3ad --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/Search.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.render + +import com.lambda.config.applyEdits +import com.lambda.config.groups.ScreenLineSettings +import com.lambda.config.groups.WorldLineSettings +import com.lambda.config.settings.collections.CollectionSetting.Companion.onDeselect +import com.lambda.config.settings.collections.CollectionSetting.Companion.onSelect +import com.lambda.context.SafeContext +import com.lambda.event.events.WorldEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.RenderMain +import com.lambda.graphics.mc.RenderBuilder +import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.util.DirectionMask +import com.lambda.graphics.util.DirectionMask.buildSideMesh +import com.lambda.graphics.util.DynamicAABB.Companion.interpolatedBox +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runSafe +import com.lambda.util.EntityUtils.decorationEntityMap +import com.lambda.util.EntityUtils.entityGroup +import com.lambda.util.extension.blockColor +import com.lambda.util.extension.entityColor +import com.lambda.util.extension.getBlockState +import com.lambda.util.math.setAlpha +import com.lambda.util.world.toBlockPos +import io.ktor.util.collections.ConcurrentMap +import net.fabricmc.fabric.mixin.block.BlockStateMixin +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.entity.Entity +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import java.awt.Color + +object Search : Module( + name = "Search", + description = "Highlight blocks within the rendered world", + tag = ModuleTag.RENDER, +) { + private val blocks by setting("Blocks", setOf(Blocks.CHEST, Blocks.ENDER_CHEST, Blocks.NETHER_PORTAL, Blocks.END_PORTAL, Blocks.END_PORTAL_FRAME, Blocks.END_GATEWAY), description = "Render blocks") + .onSelect { rebuildMesh(this) }.onDeselect { rebuildMesh(this) } + private val entities by setting("Entities", decorationEntityMap.values) + .onSelect { rebuildMesh(this) }.onDeselect { rebuildMesh(this) } + + private var fill: Boolean by setting("Fill", true, "Fill the faces of blocks").onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) outline = true } + private var outline: Boolean by setting("Outline", true, "Draw the outlines of blocks").onValueChange(::rebuildMesh).onValueChange { _, to -> if (!to) fill = true } + private val tracers by setting("Tracers", true, "Draw a line from your cursor to the highlighted position") + private val mesh by setting("Mesh", true, "Connect similar adjacent blocks").onValueChange(::rebuildMesh) + + private val useNaturalColor by setting("Use Natural Color", false, "Use the color of the block instead").onValueChange(::rebuildMesh) + private val naturalColorAlpha by setting("Natural Color Alpha", 0.3, 0.1..1.0, 0.05) { useNaturalColor }.onValueChange(::rebuildMesh) + private val naturalTracerAlpha by setting("Natural Tracer Alpha", 1.0, 0.1..1.0, 0.05) { useNaturalColor }.onValueChange(::rebuildMesh) + private val minimumNaturalBrightness by setting("Min Brightness", 150, 0..255, 1) { useNaturalColor }.onValueChange(::rebuildMesh) + + private val blockFillColor by setting("Block Fill Color", Color(100, 150, 255, 51), "Color of the surfaces") { fill && !useNaturalColor }.onValueChange(::rebuildMesh) + private val blockLineColor by setting("Block Line Color", Color(100, 150, 255, 128)) { outline && !useNaturalColor }.onValueChange(::rebuildMesh) + private val entityFillColor by setting("Entity Fill Color", Color(100, 150, 255, 51)) { fill && !useNaturalColor }.onValueChange(::rebuildMesh) + private val entityOutlineColor by setting("Entity Outline Color", Color(100, 150, 255, 128)) { outline && !useNaturalColor }.onValueChange(::rebuildMesh) + + private val blockOutlineMode by setting("Block Outline Mode", DirectionMask.OutlineMode.And, "Outline mode") { outline }.onValueChange(::rebuildMesh) + private val outlineConfig = WorldLineSettings("Outline ", this) { outline }.apply { + applyEdits { + hide(::startColor, ::endColor) + settings.forEach { it.onValueChange(::rebuildMesh) } + } + } + private val tracerConfig = ScreenLineSettings("Tracer ", this).apply { + applyEdits { + editTyped(::startColor, ::endColor) { + visibility { { !useNaturalColor } } + } + } + } + + private val tracerBlockPositions = ConcurrentMap>>() + + val chunkedRenderer = chunkedRenderer("Chunked Search") { world, position -> + runSafe { + val pos = position.toBlockPos() + val state = world.getBlockState(pos) + if (state.block !in blocks) { + tracerBlockPositions.remove(pos) + return@chunkedRenderer + } + + val sides = if (mesh) { + buildSideMesh(position) { + world.getBlockState(it).block in blocks + } + } else DirectionMask.ALL + + val lineColor = getBlockColor(state, position.toBlockPos()) + val fillColor = Color(lineColor.red, lineColor.green, lineColor.blue, (naturalColorAlpha * 255).toInt()) + val shape = state.getOutlineShape(world, pos) + val boxes = + if (shape.isEmpty) listOf(Box(pos)) + else shape.boundingBoxes.map { it.offset(pos) } + if (tracers) { + val center = + shape + .boundingBoxes + .reduce(Box::union) + .offset(pos) + .center + tracerBlockPositions[pos] = Pair(center, getTracerColors(lineColor)) + } + box( + boxes, + sides.inv(), + if (useNaturalColor) fillColor else blockFillColor, + if (useNaturalColor) lineColor else blockLineColor + ) + } + } + + init { + immediateRenderer("Immediate Search") { safeContext -> + safeContext.world.entities.forEach { entity -> + if (entity.entityGroup.nameToDisplayNameMap[entity::class.simpleName] in entities) { + val entityColor = getEntityColor(entity) + box( + listOf(entity.interpolatedBox), + DirectionMask.NONE, + if (useNaturalColor) entityColor.setAlpha(naturalColorAlpha) else entityFillColor, + if (useNaturalColor) entityColor else entityOutlineColor + ) + if (tracers) tracer(Pair(entity.interpolatedBox.center, getTracerColors(entityColor))) + } + } + if (tracers) tracerBlockPositions.values.forEach { tracer(it) } + } + + listen { event -> + if (tracers) tracerBlockPositions.keys.removeIf { it in event.chunk.pos } + } + } + + private fun RenderBuilder.tracer(pair: Pair>) { + val endPoint = RenderMain.worldToScreenNormalized(pair.first) ?: return + val startColor = if (useNaturalColor) pair.second.first else tracerConfig.startColor + val endColor = if (useNaturalColor) pair.second.second else tracerConfig.endColor + screenLineGradient( + 0.5f, 0.5f, + startColor, + endPoint.x, endPoint.y, + endColor, + tracerConfig.width, + tracerConfig.getDashStyle() + ) + } + + private fun RenderBuilder.box(boxes: List, ignoreSides: Int, fillColor: Color, lineColor: Color) { + boxes.forEach { box -> + box(box, outlineConfig.width) { + hideSides(ignoreSides) + if (fill) fillColor(fillColor) else hideFill() + if (!outline) hideOutline() + else { + outlineColor(lineColor) + outlineConfig.getDashStyle()?.let { lineDashStyle(it) } + outlineMode(blockOutlineMode) + } + } + } + } + + private fun getTracerColors(naturalColor: Color): Pair = + if (useNaturalColor) { + val adjustedNaturalColor = naturalColor.setAlpha(naturalTracerAlpha) + Pair(adjustedNaturalColor, adjustedNaturalColor) + } else Pair(tracerConfig.startColor, tracerConfig.endColor) + + private fun getEntityColor(entity: Entity) = + entityColor(entity).ensureMinBrightness(minimumNaturalBrightness) + private fun SafeContext.getBlockColor(state: BlockState, pos: BlockPos) = + blockColor(state, pos).ensureMinBrightness(minimumNaturalBrightness) + + /** + * Ensures a color meets the minimum brightness threshold. + * If the color is too dark, scales up the RGB values proportionally. + */ + private fun Color.ensureMinBrightness(minBrightness: Int): Color { + if (minBrightness <= 0) return this + + val brightness = maxOf(red, green, blue) + if (brightness >= minBrightness) return this + if (brightness == 0) return Color(minBrightness, minBrightness, minBrightness, alpha) + + val scale = minBrightness.toFloat() / brightness + return Color( + (red * scale).toInt().coerceIn(0, 255), + (green * scale).toInt().coerceIn(0, 255), + (blue * scale).toInt().coerceIn(0, 255), + alpha + ) + } + + private fun rebuildMesh(ctx: SafeContext, from: Any? = null, to: Any? = null): Unit = chunkedRenderer.rebuild() +} diff --git a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt deleted file mode 100644 index e20dbe40d..000000000 --- a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.module.modules.render - -import com.lambda.context.SafeContext -import com.lambda.graphics.esp.ShapeScope -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh -import com.lambda.event.events.onStaticRender -import com.lambda.module.Module -import com.lambda.module.tag.ModuleTag -import com.lambda.util.world.blockEntitySearch -import com.lambda.util.world.entitySearch -import com.lambda.threading.runSafe -import com.lambda.util.NamedEnum -import com.lambda.util.extension.blockColor -import com.lambda.util.math.setAlpha -import net.minecraft.block.entity.BarrelBlockEntity -import net.minecraft.block.entity.BlastFurnaceBlockEntity -import net.minecraft.block.entity.BlockEntity -import net.minecraft.block.entity.BrewingStandBlockEntity -import net.minecraft.block.entity.ChestBlockEntity -import net.minecraft.block.entity.DispenserBlockEntity -import net.minecraft.block.entity.EnderChestBlockEntity -import net.minecraft.block.entity.FurnaceBlockEntity -import net.minecraft.block.entity.HopperBlockEntity -import net.minecraft.block.entity.ShulkerBoxBlockEntity -import net.minecraft.block.entity.SmokerBlockEntity -import net.minecraft.block.entity.TrappedChestBlockEntity -import net.minecraft.entity.Entity -import net.minecraft.entity.decoration.ItemFrameEntity -import net.minecraft.entity.vehicle.AbstractMinecartEntity -import net.minecraft.entity.vehicle.MinecartEntity -import java.awt.Color - -object StorageESP : Module( - name = "StorageESP", - description = "Render storage blocks/entities", - tag = ModuleTag.RENDER, -) { - private val distance by setting("Distance", 64.0, 10.0..256.0, 1.0, "Maximum distance for rendering").group(Group.General) - private var drawFaces: Boolean by setting("Draw Faces", true, "Draw faces of blocks").group(Group.Render) - private var drawEdges: Boolean by setting("Draw Edges", true, "Draw edges of blocks").group(Group.Render) - private val mode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode").group(Group.Render) - private val mesh by setting("Mesh", true, "Connect similar adjacent blocks").group(Group.Render) - private val useBlockColor by setting("Use Block Color", true, "Use the color of the block instead").group(Group.Color) - private val facesAlpha by setting("Faces Alpha", 0.3, 0.1..1.0, 0.05).group(Group.Color) - private val edgesAlpha by setting("Edges Alpha", 0.3, 0.1..1.0, 0.05).group(Group.Color) - private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { drawEdges }.group(Group.Render) - - // TODO: - // val blockColors by setting("Block Colors", mapOf()) { page == Page.Color - // && - // !useBlockColor } - // val renders by setting("Render Blocks", mapOf()) { page == Page.General - // } - // - // TODO: Create enum of MapColors - - // I used this to extract the colors as rgb format - // > function extract(color) { - // ... console.log((color >> 16) & 0xFF) - // ... console.log((color >> 8) & 0xFF) - // ... console.log(color & 0xFF) - // ... } - - private val barrelColor by setting("Barrel Color", Color(143, 119, 72)) { !useBlockColor }.group(Group.Color) - private val blastFurnaceColor by setting("Blast Furnace Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color) - private val brewingStandColor by setting("Brewing Stand Color", Color(167, 167, 167)) { !useBlockColor }.group(Group.Color) - private val trappedChestColor by setting("Trapped Chest Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color) - private val chestColor by setting("Chest Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color) - private val dispenserColor by setting("Dispenser Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color) - private val enderChestColor by setting("Ender Chest Color", Color(127, 63, 178)) { !useBlockColor }.group(Group.Color) - private val furnaceColor by setting("Furnace Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color) - private val hopperColor by setting("Hopper Color", Color(76, 76, 76)) { !useBlockColor }.group(Group.Color) - private val smokerColor by setting("Smoker Color", Color(112, 112, 112)) { !useBlockColor }.group(Group.Color) - private val shulkerColor by setting("Shulker Color", Color(178, 76, 216)) { !useBlockColor }.group(Group.Color) - private val itemFrameColor by setting("Item Frame Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color) - private val cartColor by setting("Minecart Color", Color(102, 127, 51)) { !useBlockColor }.group(Group.Color) - - private val entities = setOf( - BarrelBlockEntity::class, - BlastFurnaceBlockEntity::class, - BrewingStandBlockEntity::class, - TrappedChestBlockEntity::class, - ChestBlockEntity::class, - DispenserBlockEntity::class, - EnderChestBlockEntity::class, - FurnaceBlockEntity::class, - HopperBlockEntity::class, - SmokerBlockEntity::class, - ShulkerBoxBlockEntity::class, - AbstractMinecartEntity::class, - ItemFrameEntity::class, - MinecartEntity::class, - ) - - init { - onStaticRender { esp -> - blockEntitySearch(distance) - .filter { it::class in entities } - .forEach { be -> - esp.shapes(be.pos.x.toDouble(), be.pos.y.toDouble(), be.pos.z.toDouble()) { - build(be, excludedSides(be)) - } - } - - val mineCarts = - entitySearch(distance).filter { - it::class in entities - } - val itemFrames = - entitySearch(distance).filter { - it::class in entities - } - (mineCarts + itemFrames).forEach { entity -> - esp.shapes(entity.getX(), entity.getY(), entity.getZ()) { - build(entity, DirectionMask.ALL) - } - } - } - } - - private fun SafeContext.excludedSides(blockEntity: BlockEntity): Int { - val isFullCube = blockEntity.cachedState.isFullCube(world, blockEntity.pos) - return if (mesh && isFullCube) { - buildSideMesh(blockEntity.pos) { neighbor -> - val other = - world.getBlockEntity(neighbor) ?: return@buildSideMesh false - val otherFullCube = other.cachedState.isFullCube(world, other.pos) - val sameType = - blockEntity.cachedState.block == other.cachedState.block - val searchedFor = other::class in entities - - searchedFor && otherFullCube && sameType - } - } else DirectionMask.ALL - } - - private fun ShapeScope.build(block: BlockEntity, sides: Int) = runSafe { - val color = - if (useBlockColor) blockColor(block.cachedState, block.pos) - else block.color ?: return@runSafe - box(block, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth) - } - - private fun ShapeScope.build(entity: Entity, sides: Int) = runSafe { - val color = entity.color ?: return@runSafe - box(entity, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth) - } - - private val BlockEntity?.color - get() = - when (this) { - is BarrelBlockEntity -> barrelColor - is BlastFurnaceBlockEntity -> blastFurnaceColor - is BrewingStandBlockEntity -> brewingStandColor - is TrappedChestBlockEntity -> trappedChestColor - is ChestBlockEntity -> chestColor - is DispenserBlockEntity -> dispenserColor - is EnderChestBlockEntity -> enderChestColor - is FurnaceBlockEntity -> furnaceColor - is HopperBlockEntity -> hopperColor - is SmokerBlockEntity -> smokerColor - is ShulkerBoxBlockEntity -> shulkerColor - else -> null - } - - private val Entity?.color - get() = - when (this) { - is AbstractMinecartEntity -> cartColor - is ItemFrameEntity -> itemFrameColor - else -> null - } - - private enum class Group(override val displayName: String) : NamedEnum { - General("General"), - Render("Render"), - Color("Color") - } -} diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt new file mode 100644 index 000000000..39cf1df67 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2026 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.render + +import com.lambda.config.applyEdits +import com.lambda.config.groups.ScreenLineSettings +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.ScreenRenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.friend.FriendManager.isFriend +import com.lambda.graphics.RenderMain.worldToScreenNormalized +import com.lambda.graphics.mc.renderer.ImmediateRenderer +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.EntityUtils.EntityGroup +import com.lambda.util.EntityUtils.entityGroup +import com.lambda.util.NamedEnum +import com.lambda.util.extension.prevPos +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.dist +import com.lambda.util.math.lerp +import net.minecraft.client.network.OtherClientPlayerEntity +import org.joml.Vector2f +import org.joml.component1 +import org.joml.component2 +import java.awt.Color + +object Tracers : Module( + name = "Tracers", + description = "Draws lines to entities within the world", + tag = ModuleTag.RENDER +) { + private enum class Group(override val displayName: String) : NamedEnum { + General("General"), + Color("Color"), + LineStyle("Line Style") + } + + private enum class LineGroup(override val displayName: String) : NamedEnum { + Friend("Friend"), + Other("Other") + } + + private val target by setting("Target", TracerMode.Feet).group(Group.General) + private val stem by setting("Stem", true).group(Group.General) + private val entities by setting("Entities", setOf(EntityGroup.Player, EntityGroup.Mob, EntityGroup.Boss), EntityGroup.entries).group(Group.General) + private val friendColor by setting("Friend Color", Color(0, 255, 255, 255)).group(Group.Color) + private val playerDistanceGradient by setting("Player Distance Gradient", true) { EntityGroup.Player in entities }.group(Group.Color) + private val playerDistanceColorFar by setting("Player Far Color", Color.GREEN) { EntityGroup.Player in entities && playerDistanceGradient }.group(Group.Color) + private val playerDistanceColorClose by setting("Player Close Color", Color.RED) { EntityGroup.Player in entities && playerDistanceGradient }.group(Group.Color) + private val playerColor by setting("Players", Color.RED) { EntityGroup.Player in entities && !playerDistanceGradient }.group(Group.Color) + private val mobColor by setting("Mobs", Color(255, 80, 0, 255)) { EntityGroup.Mob in entities }.group(Group.Color) + private val passiveColor by setting("Passives", Color.BLUE) { EntityGroup.Passive in entities }.group(Group.Color) + private val projectileColor by setting("Projectiles", Color.LIGHT_GRAY) { EntityGroup.Projectile in entities }.group(Group.Color) + private val vehicleColor by setting("Vehicles", Color.WHITE) { EntityGroup.Vehicle in entities }.group(Group.Color) + private val decorationColor by setting("Decorations", Color.PINK) { EntityGroup.Decoration in entities }.group(Group.Color) + private val bossColor by setting("Bosses", Color(255, 0, 255, 255)) { EntityGroup.Boss in entities }.group(Group.Color) + private val miscColor by setting("Miscellaneous", Color.magenta) { EntityGroup.Misc in entities }.group(Group.Color) + + private val friendLineConfig = ScreenLineSettings("Friend ", this, Group.LineStyle, LineGroup.Friend).apply { + applyEdits { hide(::startColor, ::endColor) } + } + private val otherLineConfig = ScreenLineSettings("Other ", this, Group.LineStyle, LineGroup.Other).apply { + applyEdits { hide(::startColor, ::endColor) } + } + + init { + immediateRenderer("Tracers Immediate Renderer") { safeContext -> + with(safeContext) { + world.entities.forEach { entity -> + if (entity === player) return@forEach + val entityGroup = entity.entityGroup + if (entityGroup !in entities) return@forEach + val color = if (entity is OtherClientPlayerEntity) { + if (entity.isFriend) friendColor + else { + if (playerDistanceGradient) { + val distance = player dist entity + lerp(distance / 60.0, playerDistanceColorClose, playerDistanceColorFar) + } else playerColor + } + } else when (entityGroup) { + EntityGroup.Player -> playerColor + EntityGroup.Mob -> mobColor + EntityGroup.Passive -> passiveColor + EntityGroup.Projectile -> projectileColor + EntityGroup.Vehicle -> vehicleColor + EntityGroup.Decoration -> decorationColor + EntityGroup.Boss -> bossColor + else -> miscColor + } + val lineConfig = if (entity is OtherClientPlayerEntity && entity.isFriend) friendLineConfig else otherLineConfig + val lerpedPos = lerp(mc.tickDelta, entity.prevPos, entity.pos) + val lerpedEyePos = lerpedPos.add(0.0, entity.standingEyeHeight.toDouble(), 0.0) + val targetPos = when(target) { + TracerMode.Feet -> lerpedPos + TracerMode.Middle -> lerpedPos.add(0.0, entity.standingEyeHeight / 2.0, 0.0) + TracerMode.Eyes -> lerpedEyePos + } + val (toX, toY) = worldToScreenNormalized(targetPos) ?: return@forEach + screenLine(0.5f, 0.5f, toX, toY, color, lineConfig.width, lineConfig.getDashStyle()) + if (stem) { + val (lowerX, lowerY) = + if (target == TracerMode.Feet) Vector2f(toX, toY) + else worldToScreenNormalized(lerpedPos) ?: return@forEach + val (upperX, upperY) = + if (target == TracerMode.Eyes) Vector2f(toX, toY) + else worldToScreenNormalized(lerpedEyePos) ?: return@forEach + screenLine(lowerX, lowerY, upperX, upperY, color, lineConfig.width, lineConfig.getDashStyle()) + } + } + } + } + } + + private enum class TracerMode { + Feet, + Middle, + Eyes + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt index 26b7a44e7..963685d61 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt @@ -61,10 +61,6 @@ object Zoom : Module( event.cancel() } - listen(alwaysListen = true) { - updateCurrentZoom() - } - onEnable { updateZoomTime() } diff --git a/src/main/kotlin/com/lambda/util/DebugInfoHud.kt b/src/main/kotlin/com/lambda/util/DebugInfoHud.kt index 198eb0a1d..cee004368 100644 --- a/src/main/kotlin/com/lambda/util/DebugInfoHud.kt +++ b/src/main/kotlin/com/lambda/util/DebugInfoHud.kt @@ -23,7 +23,7 @@ import com.lambda.command.CommandRegistry import com.lambda.event.EventFlow import com.lambda.module.ModuleRegistry import com.lambda.util.Formatting.format -import com.lambda.util.extension.tickDelta +import com.lambda.util.extension.tickDeltaF import net.minecraft.util.Formatting import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult @@ -55,7 +55,7 @@ object DebugInfoHud { null -> add("Crosshair Target: None") } - add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDelta)?.format()}") + add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDeltaF)?.format()}") return } diff --git a/src/main/kotlin/com/lambda/util/EntityUtils.kt b/src/main/kotlin/com/lambda/util/EntityUtils.kt index ebecce7da..9dacb7753 100644 --- a/src/main/kotlin/com/lambda/util/EntityUtils.kt +++ b/src/main/kotlin/com/lambda/util/EntityUtils.kt @@ -17,11 +17,51 @@ package com.lambda.util +import com.lambda.util.DynamicReflectionSerializer.remappedName import com.lambda.util.math.MathUtils.floorToInt +import com.lambda.util.reflections.scanResult +import io.github.classgraph.ClassInfo +import io.github.classgraph.ClassInfoList +import net.minecraft.block.entity.BlockEntity import net.minecraft.entity.Entity import net.minecraft.util.math.BlockPos +import kotlin.jvm.java object EntityUtils { + val entities: Collection = scanResult + .getSubclasses(Entity::class.java) + .filter { !it.isAbstract && it.name.startsWith("net.minecraft") } + + val blockEntityMap = createBlockEntityNameMap() + val playerEntityMap = createEntityNameMap("net.minecraft.client.network.") + val bossEntityMap = createEntityNameMap("net.minecraft.entity.boss.") + val decorationEntityMap = createEntityNameMap("net.minecraft.entity.decoration.") + val mobEntityMap = createEntityNameMap("net.minecraft.entity.mob.") + val passiveEntityMap = createEntityNameMap("net.minecraft.entity.passive.") + val projectileEntityMap = createEntityNameMap("net.minecraft.entity.projectile.") + val vehicleEntityMap = createEntityNameMap("net.minecraft.entity.vehicle.") + val miscEntityMap = createEntityNameMap("net.minecraft.entity.", strictDir = true) + + enum class EntityGroup(val nameToDisplayNameMap: Map) { + Player(createEntityNameMap("net.minecraft.client.network.")), + Mob(createEntityNameMap("net.minecraft.entity.mob.")), + Passive(createEntityNameMap("net.minecraft.entity.passive.")), + Projectile(createEntityNameMap("net.minecraft.entity.projectile.")), + Vehicle(createEntityNameMap("net.minecraft.entity.vehicle.")), + Decoration(createEntityNameMap("net.minecraft.entity.decoration.")), + Boss(createEntityNameMap("net.minecraft.entity.boss.")), + Misc(createEntityNameMap("net.minecraft.entity.", strictDir = true)), + Block(createBlockEntityNameMap()) + } + + val Entity.entityGroup get() = entityGroup() + val BlockEntity.entityGroup: EntityGroup get() = entityGroup() + + private fun Any.entityGroup(): EntityGroup { + val simpleName = javaClass.simpleName + return EntityGroup.entries.first { simpleName in it.nameToDisplayNameMap } + } + fun Entity.getPositionsWithinHitboxXZ(minY: Int, maxY: Int): Set { val hitbox = boundingBox val minX = hitbox.minX.floorToInt() @@ -38,4 +78,41 @@ object EntityUtils { } return positions } + + private fun createEntityNameMap(directory: String, strictDir: Boolean = false) = + entities.createNameMap(directory, "Entity", strictDir) + + private fun createBlockEntityNameMap() = + scanResult + .getSubclasses(BlockEntity::class.java) + .filter { !it.isAbstract } + .createNameMap("net.minecraft.block.entity", "BlockEntity") + + fun Collection.createNameMap( + directory: String, + removePattern: String = "", + strictDirectory: Boolean = false + ) = map { + val remappedName = it.name.remappedName + val displayName = remappedName + .substring(remappedName.indexOfLast { it == '.' } + 1) + .replace(removePattern, "") + .fancyFormat() + MappingInfo(it.simpleName, remappedName, displayName) + }.sortedBy { it.displayName.lowercase() } + .filter { info -> + if (strictDirectory) + info.remapped.startsWith(directory) && !info.remapped.substring(directory.length).contains(".") + else info.remapped.startsWith(directory) + } + .associate { it.raw to it.displayName } + + private fun String.fancyFormat() = + replace("$", " - ").replace("(?>() + override val visibility: () -> Boolean = { true } override val locale: Locale = Locale.US override val separator: String = "," override val prefix: String = "(" diff --git a/src/main/kotlin/com/lambda/util/extension/Entity.kt b/src/main/kotlin/com/lambda/util/extension/Entity.kt index c7bc6e583..8d790cddd 100644 --- a/src/main/kotlin/com/lambda/util/extension/Entity.kt +++ b/src/main/kotlin/com/lambda/util/extension/Entity.kt @@ -31,6 +31,9 @@ val Entity.rotation val LivingEntity.fullHealth: Double get() = health + absorptionAmount.toDouble() +val LivingEntity.maxFullHealth: Double + get() = maxHealth + maxAbsorption.toDouble() + var LivingEntity.isElytraFlying get() = isGliding set(value) { diff --git a/src/main/kotlin/com/lambda/util/extension/Mixin.kt b/src/main/kotlin/com/lambda/util/extension/Mixin.kt index 30c210929..7b539430e 100644 --- a/src/main/kotlin/com/lambda/util/extension/Mixin.kt +++ b/src/main/kotlin/com/lambda/util/extension/Mixin.kt @@ -19,8 +19,8 @@ package com.lambda.util.extension import net.minecraft.client.MinecraftClient -val MinecraftClient.partialTicks - get() = tickDelta.toDouble() - val MinecraftClient.tickDelta + get() = tickDeltaF.toDouble() + +val MinecraftClient.tickDeltaF get() = renderTickCounter.getTickProgress(true) diff --git a/src/main/kotlin/com/lambda/util/extension/World.kt b/src/main/kotlin/com/lambda/util/extension/World.kt index a1b2f5124..64bba101f 100644 --- a/src/main/kotlin/com/lambda/util/extension/World.kt +++ b/src/main/kotlin/com/lambda/util/extension/World.kt @@ -17,20 +17,36 @@ package com.lambda.util.extension +import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.util.world.FastVector import com.lambda.util.world.toBlockPos import com.lambda.util.world.x import com.lambda.util.world.y import com.lambda.util.world.z +import net.minecraft.block.Block import net.minecraft.block.BlockState import net.minecraft.block.Blocks +import net.minecraft.client.render.entity.EntityRenderer +import net.minecraft.client.render.model.BlockStateManagers +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.Sprite +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.decoration.ItemFrameEntity +import net.minecraft.entity.decoration.painting.PaintingEntity import net.minecraft.fluid.FluidState import net.minecraft.fluid.Fluids +import net.minecraft.util.Atlases +import net.minecraft.util.Identifier import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.ColorHelper import net.minecraft.util.shape.VoxelShape import net.minecraft.world.World import java.awt.Color +import java.util.concurrent.ConcurrentHashMap + +private val blockColorCache = ConcurrentHashMap() val SafeContext.worldName: String get() = when { @@ -53,16 +69,306 @@ fun SafeContext.collisionShape(state: BlockState, pos: BlockPos): VoxelShape = fun SafeContext.outlineShape(state: BlockState, pos: BlockPos) = state.getOutlineShape(world, pos).offset(pos) +/** + * Gets the average color of a block from its texture. + * Results are cached by block type for performance. + * Falls back to map color if texture access fails. + */ fun SafeContext.blockColor(state: BlockState, pos: BlockPos): Color { - return when (state.block) { - Blocks.ENDER_CHEST -> Color(0xFF00FF) - Blocks.NETHER_PORTAL -> Color(0xaa00aa) - Blocks.END_PORTAL -> Color(0xFF00FF) - else -> - Color(state.getMapColor(world, pos).color, false) + return blockColorCache.getOrPut(state.block) { + calculateAverageTextureColor(state) ?: Color(state.getMapColor(world, pos).color, false) + } +} + +private val entityColorCache = ConcurrentHashMap, Color>() + +/** + * Gets the average color of an entity from its texture. + * Results are cached by entity type for performance. + * Falls back to gray if texture access fails. + */ +fun entityColor(entity: Entity): Color { + return entityColorCache.getOrPut(entity.type) { + calculateEntityTextureColor(entity) ?: Color(128, 128, 128) } } +/** + * Calculates the average color from an entity's texture. + * Uses the entity renderer to get the texture identifier, then samples the texture. + * Supports both living and non-living entities using reflection to find texture methods/fields. + */ +private fun calculateEntityTextureColor(entity: Entity): Color? { + return try { + // Special handling for sprite-based entities + if (entity is ItemFrameEntity) return calculateItemFrameColor(entity) + if (entity is PaintingEntity) return calculatePaintingColor(entity) + + val renderer = mc.entityRenderDispatcher.getRenderer(entity) ?: return null + val textureId = getTextureFromRenderer(entity, renderer) ?: return null + calculateAverageColorFromTexture(textureId) + } catch (_: Exception) { null } +} + +/** + * Attempts to get the texture identifier from an entity renderer using reflection. + * Tries the following approaches in order: + * 1. Look for a getTexture(RenderState) method and call it + * 2. Look for Identifier-typed fields (like 'texture' or 'TEXTURE') + */ +private fun getTextureFromRenderer(entity: Entity, renderer: EntityRenderer<*, *>): Identifier? { + val rendererClass = renderer.javaClass + + // Approach 1: Try to find and call a getTexture method + val textureFromMethod = tryGetTextureFromMethod(entity, renderer, rendererClass) + if (textureFromMethod != null) return textureFromMethod + + // Approach 2: Try to find an Identifier field + val textureFromField = tryGetTextureFromField(renderer, rendererClass) + if (textureFromField != null) return textureFromField + + return null +} + +/** + * Tries to get texture by calling a getTexture(RenderState) method via reflection. + */ +private fun tryGetTextureFromMethod(entity: Entity, renderer: EntityRenderer<*, *>, rendererClass: Class<*>): Identifier? { + return try { + // Find getTexture method that takes a single parameter + val getTextureMethod = rendererClass.methods.find { method -> + method.name == "getTexture" && + method.parameterCount == 1 && + Identifier::class.java.isAssignableFrom(method.returnType) + } ?: return null + + getTextureMethod.isAccessible = true + + // Create and update render state + val createStateMethod = rendererClass.getMethod("createRenderState") + val state = createStateMethod.invoke(renderer) + + // Try to update the state with entity data + try { + val updateStateMethod = rendererClass.methods.find { + it.name == "updateRenderState" && it.parameterCount == 3 + } + updateStateMethod?.invoke(renderer, entity, state, 0f) + } catch (_: Exception) { /* State update is optional */ } + + getTextureMethod.invoke(renderer, state) as? Identifier + } catch (_: Exception) { null } +} + +/** + * Tries to get texture from an Identifier-typed field via reflection. + * Searches for common field names like 'texture', 'TEXTURE', etc. + */ +private fun tryGetTextureFromField(renderer: EntityRenderer<*, *>, rendererClass: Class<*>): Identifier? { + return try { + // Look through all fields in the class hierarchy for Identifier types + var currentClass: Class<*>? = rendererClass + while (currentClass != null && currentClass != EntityRenderer::class.java) { + for (field in currentClass.declaredFields) { + if (Identifier::class.java.isAssignableFrom(field.type)) { + field.isAccessible = true + val value = field.get(renderer) + if (value is Identifier) return value + + // Also try static fields + if (java.lang.reflect.Modifier.isStatic(field.modifiers)) { + val staticValue = field.get(null) + if (staticValue is Identifier) return staticValue + } + } + } + currentClass = currentClass.superclass + } + null + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from an item frame entity using the block model sprite. + */ +private fun calculateItemFrameColor(entity: ItemFrameEntity): Color? { + return try { + val isGlow = entity.type == EntityType.GLOW_ITEM_FRAME + val hasMap = entity.getMapId(entity.heldItemStack) != null + val blockState = BlockStateManagers.getStateForItemFrame(isGlow, hasMap) + val sprite = mc.blockRenderManager.models.getModelParticleSprite(blockState) + calculateAverageColorFromSprite(sprite) + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from a painting entity using the painting variant's sprite. + */ +private fun calculatePaintingColor(entity: PaintingEntity): Color? { + return try { + val variant = entity.variant.value() + val paintingAtlas = mc.atlasManager.getAtlasTexture(Atlases.PAINTINGS) + val sprite = paintingAtlas.getSprite(variant.assetId()) + calculateAverageColorFromSprite(sprite) + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from a sprite. + */ +private fun calculateAverageColorFromSprite(sprite: Sprite): Color? { + return try { + val contents = sprite.contents + val image = contents.image + + val width = contents.width + val height = contents.height + + var totalR = 0L + var totalG = 0L + var totalB = 0L + var totalWeight = 0L + + for (y in 0 until height) { + for (x in 0 until width) { + val argb = image.getColorArgb(x, y) + val alpha = ColorHelper.getAlpha(argb) + + if (alpha == 0) continue + + val r = ColorHelper.getRed(argb) + val g = ColorHelper.getGreen(argb) + val b = ColorHelper.getBlue(argb) + + totalR += r.toLong() * alpha + totalG += g.toLong() * alpha + totalB += b.toLong() * alpha + totalWeight += alpha.toLong() + } + } + + if (totalWeight == 0L) return null + + val avgR = (totalR / totalWeight).toInt().coerceIn(0, 255) + val avgG = (totalG / totalWeight).toInt().coerceIn(0, 255) + val avgB = (totalB / totalWeight).toInt().coerceIn(0, 255) + + Color(avgR, avgG, avgB) + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from a texture identifier. + */ +private fun calculateAverageColorFromTexture(textureId: Identifier): Color? { + return try { + // Try to load the texture from the resource manager + val resource = mc.resourceManager.getResource(textureId).orElse(null) ?: run { + // If that fails, try constructing a texture path with .png extension + val pngId = Identifier.of(textureId.namespace, textureId.path + ".png") + mc.resourceManager.getResource(pngId).orElse(null) + } ?: return null + + resource.inputStream.use { inputStream -> + val image = NativeImage.read(inputStream) + + val width = image.width + val height = image.height + + var totalR = 0L + var totalG = 0L + var totalB = 0L + var totalWeight = 0L + + for (y in 0 until height) { + for (x in 0 until width) { + val abgr = image.getColorArgb(x, y) + val alpha = ColorHelper.getAlpha(abgr) + + if (alpha == 0) continue + + val r = ColorHelper.getRed(abgr) + val g = ColorHelper.getGreen(abgr) + val b = ColorHelper.getBlue(abgr) + + totalR += r.toLong() * alpha + totalG += g.toLong() * alpha + totalB += b.toLong() * alpha + totalWeight += alpha.toLong() + } + } + + image.close() + + if (totalWeight == 0L) return null + + val avgR = (totalR / totalWeight).toInt().coerceIn(0, 255) + val avgG = (totalG / totalWeight).toInt().coerceIn(0, 255) + val avgB = (totalB / totalWeight).toInt().coerceIn(0, 255) + + Color(avgR, avgG, avgB) + } + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from a block's particle sprite texture. + * Uses alpha-weighted averaging to ignore transparent pixels. + * Applies block tints (for grass, leaves, water, etc.) using BlockColors. + */ +private fun calculateAverageTextureColor(state: BlockState): Color? { + return try { + val sprite = mc.blockRenderManager.models.getModelParticleSprite(state) + val contents = sprite.contents + val image = contents.image + + val width = contents.width + val height = contents.height + + var totalR = 0L + var totalG = 0L + var totalB = 0L + var totalWeight = 0L + + for (y in 0 until height) { + for (x in 0 until width) { + val argb = image.getColorArgb(x, y) + val alpha = ColorHelper.getAlpha(argb) + + if (alpha == 0) continue + + val r = ColorHelper.getRed(argb) + val g = ColorHelper.getGreen(argb) + val b = ColorHelper.getBlue(argb) + + totalR += r.toLong() * alpha + totalG += g.toLong() * alpha + totalB += b.toLong() * alpha + totalWeight += alpha.toLong() + } + } + + if (totalWeight == 0L) return null + + var avgR = (totalR / totalWeight).toInt().coerceIn(0, 255) + var avgG = (totalG / totalWeight).toInt().coerceIn(0, 255) + var avgB = (totalB / totalWeight).toInt().coerceIn(0, 255) + + val tint = mc.blockColors.getColor(state, null, null, 0) + if (tint != -1) { + val tintR = ColorHelper.getRed(tint) + val tintG = ColorHelper.getGreen(tint) + val tintB = ColorHelper.getBlue(tint) + + avgR = (avgR * tintR / 255).coerceIn(0, 255) + avgG = (avgG * tintG / 255).coerceIn(0, 255) + avgB = (avgB * tintB / 255).coerceIn(0, 255) + } + + Color(avgR, avgG, avgB) + } catch (_: Exception) { null } +} + fun World.getBlockState(x: Int, y: Int, z: Int): BlockState { if (isOutOfHeightLimit(y)) return Blocks.VOID_AIR.defaultState diff --git a/src/main/kotlin/com/lambda/util/math/Color.kt b/src/main/kotlin/com/lambda/util/math/Color.kt index 3c65162d6..5c821be1a 100644 --- a/src/main/kotlin/com/lambda/util/math/Color.kt +++ b/src/main/kotlin/com/lambda/util/math/Color.kt @@ -20,6 +20,9 @@ package com.lambda.util.math import net.minecraft.util.math.Vec3d import java.awt.Color +fun Color.setAlpha(value: Int) = + Color(red, green, blue, value.coerceIn(0, 255)) + fun Color.setAlpha(value: Double) = Color(red, green, blue, (value * 255.0).coerceIn(0.0, 255.0).toInt()) diff --git a/src/main/kotlin/com/lambda/util/reflections/Reflections.kt b/src/main/kotlin/com/lambda/util/reflections/Reflections.kt index c7bcfbeb9..36570a564 100644 --- a/src/main/kotlin/com/lambda/util/reflections/Reflections.kt +++ b/src/main/kotlin/com/lambda/util/reflections/Reflections.kt @@ -37,7 +37,6 @@ val KClass<*>.className: String get() = java.name .substringAfter("${java.packageName}.") .replace('$', '.') - /** * This function returns a instance of subtype [T]. * diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh deleted file mode 100644 index 7727eca9d..000000000 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ /dev/null @@ -1,50 +0,0 @@ -#version 330 - -#moj_import -#moj_import - -in vec4 vertexColor; -noperspective in float v_LineDist; -noperspective in float v_LineWidth; -noperspective in vec2 v_DistPixels; -noperspective in float v_LineLength; -in float sphericalVertexDistance; -in float cylindricalVertexDistance; - -out vec4 fragColor; - -void main() { - // Closest point on the center line segment [0, L] - float closestX = clamp(v_DistPixels.x, 0.0, v_LineLength); - vec2 closestPoint = vec2(closestX, 0.0); - - // Pixel distance from the closest point (Round Capsule SDF) - float dist = length(v_DistPixels - closestPoint); - - // SDF value: distance from the capsule edge - float sdf = dist - (v_LineWidth / 2.0); - - // Ultra-sharp edges (AA transition of 0.3 pixels total) - float alpha; - if (v_LineWidth >= 1.0) { - alpha = smoothstep(0.15, -0.15, sdf); - } else { - // Super thin lines: reduce opacity instead of shrinking width - float transverseAlpha = (1.0 - smoothstep(0.0, 1.0, abs(v_DistPixels.y))) * v_LineWidth; - alpha = transverseAlpha; - } - - // Aggressive fade for tiny segments far away to prevent blobbing - // If a segment is less than 0.8px on screen, fade it out to nothing - float lengthFade = clamp(v_LineLength / 0.8, 0.0, 1.0); - alpha *= lengthFade * lengthFade; // Quadratic falloff for tiny segments - - if (alpha <= 0.0) { - discard; - } - - vec4 color = vertexColor * ColorModulator; - color.a *= alpha; - - fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); -} diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh deleted file mode 100644 index 46e84da2a..000000000 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh +++ /dev/null @@ -1,74 +0,0 @@ -#version 330 - -#moj_import -#moj_import -#moj_import -#moj_import - -in vec3 Position; -in vec4 Color; -in vec3 Normal; -in float LineWidth; - -out vec4 vertexColor; -noperspective out float v_LineDist; -noperspective out float v_LineWidth; -noperspective out vec2 v_DistPixels; -noperspective out float v_LineLength; -out float sphericalVertexDistance; -out float cylindricalVertexDistance; - -const float VIEW_SHRINK = 1.0 - (1.0 / 256.0); - -void main() { - int vertexIndex = gl_VertexID % 4; - bool isStart = (vertexIndex < 2); - - float actualWidth = max(LineWidth, 0.1); - float padding = 0.5; // AA padding - float halfWidthExtended = actualWidth / 2.0 + padding; - - // Transform start and end - vec4 posStart = ProjMat * ModelViewMat * vec4(isStart ? Position : Position - Normal, 1.0); - vec4 posEnd = ProjMat * ModelViewMat * vec4(isStart ? Position + Normal : Position, 1.0); - - vec3 ndcStart = posStart.xyz / posStart.w; - vec3 ndcEnd = posEnd.xyz / posEnd.w; - - // Screen space coordinates - vec2 screenStart = (ndcStart.xy * 0.5 + 0.5) * ScreenSize; - vec2 screenEnd = (ndcEnd.xy * 0.5 + 0.5) * ScreenSize; - - vec2 delta = screenEnd - screenStart; - float lenPixels = length(delta); - - // Stable direction - vec2 lineDir = (lenPixels > 0.001) ? delta / lenPixels : vec2(1.0, 0.0); - vec2 lineNormal = vec2(-lineDir.y, lineDir.x); - - // Quad vertex layout - float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; - float longitudinalSide = isStart ? -1.0 : 1.0; - - // Expansion in pixels: full radius + padding to contain capsule end - vec2 offsetPixels = lineNormal * side * halfWidthExtended + lineDir * longitudinalSide * halfWidthExtended; - - // Current point NDC - vec3 ndcThis = isStart ? ndcStart : ndcEnd; - float wThis = isStart ? posStart.w : posEnd.w; - - // Convert pixel offset back to NDC - vec2 offsetNDC = (offsetPixels / ScreenSize) * 2.0; - gl_Position = vec4((ndcThis + vec3(offsetNDC, 0.0)) * wThis, wThis); - - vertexColor = Color; - - // Pass coordinates for SDF - v_LineDist = side; - v_DistPixels = vec2(isStart ? -halfWidthExtended : lenPixels + halfWidthExtended, side * halfWidthExtended); - v_LineWidth = actualWidth; - v_LineLength = lenPixels; - - sphericalVertexDistance = fog_spherical_distance(Position); - cylindricalVertexDistance = fog_cylindrical_distance(Position); -} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh b/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh new file mode 100644 index 000000000..b6bdde8e1 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh @@ -0,0 +1,24 @@ +#version 330 + +#moj_import + +// Inputs from vertex shader +in vec4 v_Color; +in float v_Layer; + +out vec4 fragColor; + +void main() { + // Apply color modulator + vec4 color = v_Color * ColorModulator; + + // Discard nearly transparent fragments + if (color.a < 0.004) { + discard; + } + + fragColor = color; + + // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL) + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh b/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh new file mode 100644 index 000000000..8e0ef30a9 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh @@ -0,0 +1,22 @@ +#version 330 + +#moj_import +#moj_import + +// Vertex inputs - matches SCREEN_FACE_FORMAT +in vec3 Position; // Screen-space position (x, y, 0) +in vec4 Color; +in float Layer; // Layer depth for draw order + +// Outputs to fragment shader +out vec4 v_Color; +out float v_Layer; + +void main() { + // Transform to clip space + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + // Pass data to fragment shader + v_Color = Color; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_image.fsh b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh new file mode 100644 index 000000000..f2c8f6645 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh @@ -0,0 +1,58 @@ +#version 330 + +#moj_import +#moj_import + +// Sampler for main texture +uniform sampler2D Sampler0; +// Sampler for overlay texture (e.g., enchantment glint) +uniform sampler2D Sampler1; + +// Inputs from vertex shader +in vec2 v_TexCoord; +in vec4 v_Color; +in vec4 v_OverlayUV; // (overlayU, overlayV, hasOverlay, diffuseAmount) +in float v_Layer; + +out vec4 fragColor; + +void main() { + // Sample main texture + vec4 texColor = texture(Sampler0, v_TexCoord); + + // Apply tint color + vec4 color = texColor * v_Color * ColorModulator; + + // Discard nearly transparent fragments + if (color.a < 0.004) { + discard; + } + + // Apply overlay (enchantment glint) if present + // v_OverlayUV.y = aspect ratio (width/height) for square tiling + // v_OverlayUV.z = hasOverlay flag (1.0 = enabled) + if (v_OverlayUV.z > 0.5) { + float aspectRatio = v_OverlayUV.y; + + // Use texture coordinates corrected for aspect ratio + // This ensures square tiling regardless of image dimensions + vec2 baseUV = vec2(v_TexCoord.x * aspectRatio, v_TexCoord.y); + + // Apply TextureMat from DynamicTransforms - this contains the glint transform + // calculated at render time using Util.getMeasuringTimeMs(), exactly like vanilla! + // TextureMat = translation(-scrollX, scrollY) * rotateZ(π/18) * scale + vec4 transformedUV = TextureMat * vec4(baseUV, 0.0, 1.0); + + // Sample glint texture using transformed coordinates + vec4 glint = texture(Sampler1, fract(transformedUV.xy)); + + // Apply with squared additive blending (matching 1.21 model parity) + vec3 layer = glint.rgb * glint.a * 0.75; // GLINT_ALPHA = 0.75 + color.rgb += (layer * layer); + } + + fragColor = color; + + // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL) + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_image.vsh b/src/main/resources/assets/lambda/shaders/core/screen_image.vsh new file mode 100644 index 000000000..d4ee9619f --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_image.vsh @@ -0,0 +1,28 @@ +#version 330 + +#moj_import +#moj_import + +// Vertex inputs - matches SCREEN_IMAGE_FORMAT +in vec3 Position; // Screen-space position (x, y, 0) +in vec2 UV0; // Main texture UV coordinates +in vec4 Color; // Tint color +in vec4 OverlayUV; // vec4(overlayU, overlayV, hasOverlay, diffuseAmount) +in float Layer; // Layer depth for draw order + +// Outputs to fragment shader +out vec2 v_TexCoord; +out vec4 v_Color; +out vec4 v_OverlayUV; +out float v_Layer; + +void main() { + // Transform to clip space + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + // Pass data to fragment shader + v_TexCoord = UV0; + v_Color = Color; + v_OverlayUV = OverlayUV; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh new file mode 100644 index 000000000..eef02b698 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -0,0 +1,121 @@ +#version 330 + +#moj_import +#moj_import + +// Inputs from vertex shader +in vec4 v_Color; +in vec2 v_ExpandedPos; // Fragment position (expanded for AA) +flat in vec2 v_LineStart; // Line start point +flat in vec2 v_LineEnd; // Line end point +flat in float v_LineWidth; // Line width +flat in float v_SegmentLength; // Segment length +flat in vec4 v_Dash; // Dash params (x=dashLen, y=gapLen, z=offset, w=speed) +in float v_Layer; // Layer depth for draw order + +out vec4 fragColor; + +void main() { + // ===== CAPSULE SDF ===== + vec2 lineDir = normalize(v_LineEnd - v_LineStart); + vec2 perpDir = vec2(-lineDir.y, lineDir.x); + + vec2 toFragment = v_ExpandedPos - v_LineStart; + float projLength = dot(toFragment, lineDir); + float perpDist = abs(dot(toFragment, perpDir)); + + // Calculate stable pixel size from screen-space position derivatives + // This is more reliable than fwidth(dist2D) which can be unstable at edges + vec2 dPos_dx = dFdx(v_ExpandedPos); + vec2 dPos_dy = dFdy(v_ExpandedPos); + // Average pixel size in screen units + float pixelSize = (length(dPos_dx) + length(dPos_dy)) * 0.5; + + // For end caps, compute actual distance to endpoints + float dist2D; + if (projLength < 0.0) { + dist2D = length(v_ExpandedPos - v_LineStart); + } else if (projLength > v_SegmentLength) { + dist2D = length(v_ExpandedPos - v_LineEnd); + } else { + dist2D = perpDist; + } + + // Calculate screen line width in pixels + float screenLineWidth = v_LineWidth / max(pixelSize, 0.0001); + + // Minimum 1-pixel rendering width - thinner lines scale alpha instead of getting gaps + float minWidth = pixelSize; // 1 pixel + float effectiveRadius = max(v_LineWidth * 0.5, minWidth * 0.5); + + // Alpha scaling: lines < 1px get proportionally reduced opacity + float alphaScale = min(screenLineWidth, 1.0); + + // SDF: distance to capsule surface + float sdf = dist2D - effectiveRadius; + + // AA: 1 pixel transition for crisp edges + float aaWidth = pixelSize; + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + // Apply alpha scaling for sub-pixel lines + alpha *= alphaScale; + + if (alpha < 0.004) { + discard; + } + + // ===== DASH PATTERN ===== + float dashLength = v_Dash.x; + float gapLength = v_Dash.y; + float dashOffset = v_Dash.z; + float animationSpeed = v_Dash.w; + + // Only apply dash if dashLength > 0 (0 = solid line) + if (dashLength > 0.0) { + float cycleLength = dashLength + gapLength; + + // Calculate animated offset + float animatedOffset = dashOffset; + if (animationSpeed != 0.0) { + animatedOffset -= GameTime * animationSpeed * 1200.0; + } + + // Use UNCLAMPED projLength so dashes continue through endcaps + float dashPos = projLength + animatedOffset * cycleLength; + float posInCycle = mod(dashPos, cycleLength); + + // SDF for dash edges with anti-aliasing + float dashSdf; + if (posInCycle > dashLength) { + // In gap region - positive SDF + float distToGapEnd = cycleLength - posInCycle; + dashSdf = min(posInCycle - dashLength, distToGapEnd); + } else { + // In dash region - negative SDF (distance to nearest gap) + float distToDashEnd = dashLength - posInCycle; + float distFromDashStart = posInCycle; + dashSdf = -min(distToDashEnd, distFromDashStart); + } + + // Apply anti-aliasing at dash edges (use fwidth of SDF for consistent AA with capsule) + float dashAaWidth = fwidth(dashSdf); + float dashAlpha = 1.0 - smoothstep(-dashAaWidth, dashAaWidth, dashSdf); + + if (dashAlpha <= 0.0) { + discard; + } + + alpha *= dashAlpha; + } + + // Apply color + vec4 color = v_Color * ColorModulator; + color.a *= alpha; + + fragColor = color; + + // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL) + // Layer starts at -800 and increments, so later elements have higher values + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh new file mode 100644 index 000000000..e268a7f96 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh @@ -0,0 +1,70 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +// Vertex inputs - matches SCREEN_LINE_FORMAT +in vec3 Position; // Screen-space position (x, y, 0) +in vec4 Color; +in vec2 Direction; // Line direction vector to OTHER endpoint (length = segment length) +in float LineWidth; // Line width in pixels +in vec4 Dash; // Dash parameters (dashLength, gapLength, offset, animSpeed) +in float Layer; // Layer depth for draw order + +// Outputs to fragment shader +out vec4 v_Color; +out vec2 v_ExpandedPos; // Expanded screen position +flat out vec2 v_LineStart; // Line start point +flat out vec2 v_LineEnd; // Line end point +flat out float v_LineWidth; // Line width +flat out float v_SegmentLength; // Segment length +flat out vec4 v_Dash; // Dash parameters (future: passed from vertex) +out float v_Layer; // Layer depth for draw order + +void main() { + // Determine which corner of the quad this vertex is + int vertexIndex = gl_VertexID % 4; + bool isStart = (vertexIndex < 2); + float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; + + // Calculate segment properties + float segmentLength = length(Direction); + vec2 lineDir = Direction / max(segmentLength, 0.001); + + // Line center (reconstruct for each vertex consistently) + vec2 lineCenter = isStart ? (Position.xy + Direction * 0.5) : (Position.xy - Direction * 0.5); + + // Reconstruct endpoints from center + vec2 lineStart = lineCenter - lineDir * (segmentLength * 0.5); + vec2 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); + vec2 thisPoint = isStart ? lineStart : lineEnd; + + // Perpendicular direction for line thickness + vec2 perpDir = vec2(-lineDir.y, lineDir.x); + + // Expand for AA (capsule shape) - ensure minimum expansion for thin lines + float halfWidth = LineWidth / 2.0; + float aaPadding = max(LineWidth * 0.5, 2.0); // At least 2 pixels for AA gradient + float halfWidthPadded = halfWidth + aaPadding; + + // Expand vertex + vec2 perpOffset = perpDir * side * halfWidthPadded; + float longitudinal = isStart ? -1.0 : 1.0; + vec2 longOffset = lineDir * longitudinal * halfWidthPadded; + + vec2 expandedPos = thisPoint + perpOffset + longOffset; + + // Transform to clip space + gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 0.0, 1.0); + + // Pass data to fragment shader + v_Color = Color; + v_ExpandedPos = expandedPos; + v_LineStart = lineStart; + v_LineEnd = lineEnd; + v_LineWidth = LineWidth; + v_SegmentLength = segmentLength; + v_Dash = Dash; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh new file mode 100644 index 000000000..15ab758ef --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh @@ -0,0 +1,73 @@ +#version 330 + +#moj_import + +uniform sampler2D Sampler0; + +// Inputs from vertex shader +in vec2 texCoord0; +in vec4 vertexColor; +// SDF style params from vertex shader: (outlineWidth, glowRadius, shadowSoftness, threshold) +in vec4 sdfStyleParams; +in float v_Layer; // Layer depth for draw order + +out vec4 fragColor; + +void main() { + // Extract SDF parameters from vertex attributes + float OutlineWidth = sdfStyleParams.x; + float GlowRadius = sdfStyleParams.y; + float ShadowSoftness = sdfStyleParams.z; + float SDFThreshold = sdfStyleParams.w; + + // Sample the SDF texture - use ALPHA channel + vec4 texSample = texture(Sampler0, texCoord0); + float sdfValue = texSample.a; + + // Screen-space anti-aliasing + float smoothing = fwidth(sdfValue) * 0.5; + + // Decode layer type from vertex alpha + int layerType = int(vertexColor.a * 255.0 + 0.5); + + float alpha; + + if (layerType >= 200) { + // Main text layer - sharp edge at threshold + alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + } else if (layerType >= 100) { + // Outline layer - uses OutlineWidth + float outlineEdge = SDFThreshold - OutlineWidth; + alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, sdfValue); + // Mask out the main text area + float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + alpha = alpha * (1.0 - textMask); + } else if (layerType >= 50) { + // Glow layer - starts from outline edge (if outline enabled) or text edge + // Only expand past outline if OutlineWidth is actually set + float glowEdge = (OutlineWidth > 0.001) ? (SDFThreshold - OutlineWidth) : SDFThreshold; + float glowStart = glowEdge - GlowRadius; + float glowEnd = glowEdge; + alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6; + // Mask out the text and outline area + float outlineMask = smoothstep(glowEdge - smoothing, glowEdge + smoothing, sdfValue); + alpha = alpha * (1.0 - outlineMask); + } else { + // Shadow layer - uses ShadowSoftness + float shadowStart = SDFThreshold - ShadowSoftness - 0.15; + float shadowEnd = SDFThreshold - 0.1; + alpha = smoothstep(shadowStart, shadowEnd, sdfValue) * 0.5; + } + + // Apply vertex color (RGB from vertex, alpha computed above) + vec4 result = vec4(vertexColor.rgb, alpha); + + // Discard nearly transparent fragments + if (result.a <= 0.001) discard; + + // Apply color modulator (no fog for screen-space) + fragColor = result * ColorModulator; + + // Map layer to depth: higher layer = smaller depth = renders on top (LEQUAL) + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh new file mode 100644 index 000000000..06c67b55b --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh @@ -0,0 +1,28 @@ +#version 330 + +#moj_import +#moj_import + +// Vertex inputs (SCREEN_TEXT_SDF_FORMAT) +in vec3 Position; +in vec2 UV0; +in vec4 Color; +// SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) +in vec4 SDFStyle; +in float Layer; // Layer depth for draw order + +// Outputs to fragment shader +out vec2 texCoord0; +out vec4 vertexColor; +out vec4 sdfStyleParams; +out float v_Layer; // Layer depth for draw order + +void main() { + // Screen-space position - already in screen coordinates + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + texCoord0 = UV0; + vertexColor = Color; + sdfStyleParams = SDFStyle; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_image.fsh b/src/main/resources/assets/lambda/shaders/core/world_image.fsh new file mode 100644 index 000000000..575885929 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_image.fsh @@ -0,0 +1,46 @@ +#version 330 + +#moj_import +#moj_import + +// Sampler for main texture +uniform sampler2D Sampler0; +// Sampler for overlay texture (e.g., enchantment glint) +uniform sampler2D Sampler1; + +// Inputs from vertex shader +in vec2 v_TexCoord; +in vec4 v_Color; +in vec4 v_OverlayUV; // (time, unused, hasOverlay, diffuseAmount) + +out vec4 fragColor; + +void main() { + // Sample main texture + vec4 texColor = texture(Sampler0, v_TexCoord); + + // Apply tint color + vec4 color = texColor * v_Color * ColorModulator; + + // Discard nearly transparent fragments + if (color.a < 0.004) { + discard; + } + + // Apply overlay (enchantment glint) if present + // v_OverlayUV.y = aspect ratio (width/height) for square tiling + // v_OverlayUV.z = hasOverlay flag (1.0 = enabled) + if (v_OverlayUV.z > 0.5) { + // Use v_TexCoord (Atlas UVs) for parity with model glint logic + vec4 transformedUV = TextureMat * vec4(v_TexCoord, 0.0, 1.0); + + // Sample glint texture using transformed coordinates + vec4 glint = texture(Sampler1, fract(transformedUV.xy)); + + // Apply with squared additive blending (matching 1.21 model parity) + vec3 layer = glint.rgb * glint.a * 0.75; // GLINT_ALPHA = 0.75 + color.rgb += (layer * layer); + } + + fragColor = color; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_image.vsh b/src/main/resources/assets/lambda/shaders/core/world_image.vsh new file mode 100644 index 000000000..9ab6ad168 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_image.vsh @@ -0,0 +1,41 @@ +#version 330 + +#moj_import +#moj_import + +// Vertex inputs - matches WORLD_IMAGE_FORMAT +in vec3 Position; // Local offset (x, y, z) +in vec2 UV0; // Main texture UV coordinates +in vec4 Color; // Tint color +in vec3 Anchor; // Camera-relative world anchor position +in vec2 BillboardData;// (scale, billboardFlag) +in vec4 OverlayUV; // (overlayU, overlayV, hasOverlay, diffuseAmount) + +// Outputs to fragment shader +out vec2 v_TexCoord; +out vec4 v_Color; +out vec4 v_OverlayUV; + +void main() { + float scale = BillboardData.x; + float billboardFlag = BillboardData.y; + + vec4 mvPos; + if (billboardFlag < 0.5) { + // Billboard mode: face the camera perfectly + // 1. Transform anchor to camera space + mvPos = ModelViewMat * vec4(Anchor, 1.0); + // 2. Apply local offset in camera-aligned XY plane + mvPos.xy += Position.xy * scale; + } else { + // Fixed rotation mode: everything is pre-calculated/transformed + // worldPos = Anchor + Position * scale + mvPos = ModelViewMat * vec4(Anchor + Position * scale, 1.0); + } + + gl_Position = ProjMat * mvPos; + + v_TexCoord = UV0; + v_Color = Color; + v_OverlayUV = OverlayUV; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_lines.fsh b/src/main/resources/assets/lambda/shaders/core/world_lines.fsh new file mode 100644 index 000000000..93751bc41 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_lines.fsh @@ -0,0 +1,131 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +// Inputs from vertex shader +in vec4 v_Color; +in vec3 v_WorldPos; // Position before expansion (interpolated along line) +in vec3 v_ExpandedPos; // Position after expansion (interpolated - fragment position) +flat in vec3 v_Normal; // Raw Normal input (line direction * length) +flat in vec3 v_LineCenter; // Line center (same for all vertices) +flat in float v_LineWidth; // Line width +flat in float v_SegmentLength; // Segment length +flat in float v_IsStart; // 1.0 if from start vertex +flat in vec4 v_Dash; // x = dashLength, y = gapLength, z = dashOffset, w = animationSpeed +in float sphericalVertexDistance; +in float cylindricalVertexDistance; + +out vec4 fragColor; + +void main() { + // Reconstruct line geometry from flat varyings + vec3 lineDir = normalize(v_Normal); + float halfLength = v_SegmentLength / 2.0; + + // Compute line start and end from center + vec3 lineStart = v_LineCenter - lineDir * halfLength; + vec3 lineEnd = v_LineCenter + lineDir * halfLength; + + // ===== CAPSULE SDF ===== + vec3 toFragment = v_ExpandedPos - lineStart; + float projLength = dot(toFragment, lineDir); + + // Perpendicular distance + vec3 perpVec = toFragment - lineDir * projLength; + float perpDist = length(perpVec); + + // Calculate stable pixel size from screen-space position derivatives + // This is more reliable than fwidth(sdf) which can be unstable at edges + vec3 dPos_dx = dFdx(v_ExpandedPos); + vec3 dPos_dy = dFdy(v_ExpandedPos); + float pixelSize = (length(dPos_dx) + length(dPos_dy)) * 0.5; + + // For end caps, compute actual distance to endpoints + float dist3D; + if (projLength < 0.0) { + dist3D = length(v_ExpandedPos - lineStart); + } else if (projLength > v_SegmentLength) { + dist3D = length(v_ExpandedPos - lineEnd); + } else { + dist3D = perpDist; + } + + // Calculate screen line width in pixels + float screenLineWidth = v_LineWidth / max(pixelSize, 0.0001); + + // Minimum 1-pixel rendering width - thinner lines scale alpha instead of getting gaps + float minWidth = pixelSize; // 1 pixel + float effectiveRadius = max(v_LineWidth * 0.5, minWidth * 0.5); + + // Alpha scaling: lines < 1px get proportionally reduced opacity + float alphaScale = min(screenLineWidth, 1.0); + + // SDF: distance to capsule surface + float sdf = dist3D - effectiveRadius; + + // AA: 1 pixel transition for crisp edges + float aaWidth = pixelSize; + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + // Apply alpha scaling for sub-pixel lines + alpha *= alphaScale; + + if (alpha < 0.004) { + discard; + } + + // ===== DASH PATTERN ===== + float dashLength = v_Dash.x; + float gapLength = v_Dash.y; + float dashOffset = v_Dash.z; + float animationSpeed = v_Dash.w; + + // Only apply dash if dashLength > 0 (0 = solid line) + if (dashLength > 0.0) { + float cycleLength = dashLength + gapLength; + + // Calculate animated offset + float animatedOffset = dashOffset; + if (animationSpeed != 0.0) { + animatedOffset -= GameTime * animationSpeed * 1200.0; + } + + // Use UNCLAMPED projLength so dashes continue through endcaps + float dashPos = projLength + animatedOffset * cycleLength; + float posInCycle = mod(dashPos, cycleLength); + + // SDF for dash edges with anti-aliasing + float dashSdf; + if (posInCycle > dashLength) { + // In gap region - positive SDF + float distToGapEnd = cycleLength - posInCycle; + dashSdf = min(posInCycle - dashLength, distToGapEnd); + } else { + // In dash region - negative SDF (distance to nearest gap) + float distToDashEnd = dashLength - posInCycle; + float distFromDashStart = posInCycle; + dashSdf = -min(distToDashEnd, distFromDashStart); + } + + // Apply anti-aliasing at dash edges (use fwidth of SDF for consistent AA with capsule) + float dashAaWidth = fwidth(dashSdf); + float dashAlpha = 1.0 - smoothstep(-dashAaWidth, dashAaWidth, dashSdf); + + if (dashAlpha <= 0.0) { + discard; + } + + alpha *= dashAlpha; + } + + // Apply color + vec4 color = v_Color * ColorModulator; + color.a *= alpha; + + // Apply fog + fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, + FogEnvironmentalStart, FogEnvironmentalEnd, + FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_lines.vsh b/src/main/resources/assets/lambda/shaders/core/world_lines.vsh new file mode 100644 index 000000000..d142b2c59 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_lines.vsh @@ -0,0 +1,101 @@ +#version 330 + +#moj_import +#moj_import +#moj_import +#moj_import + +// Vertex inputs +in vec3 Position; +in vec4 Color; +in vec3 Normal; // Direction vector to other endpoint (length = segment length) +in float LineWidth; // Line width: positive = world units, negative = screen-space fraction +in vec4 Dash; // Dash parameters + +// Outputs to fragment shader +out vec4 v_Color; +out vec3 v_WorldPos; +out vec3 v_ExpandedPos; +flat out vec3 v_Normal; +flat out vec3 v_LineCenter; +flat out float v_LineWidth; // Always positive (actual width in world units) +flat out float v_SegmentLength; +flat out float v_IsStart; +flat out vec4 v_Dash; +out float sphericalVertexDistance; +out float cylindricalVertexDistance; + +void main() { + int vertexIndex = gl_VertexID % 4; + bool isStart = (vertexIndex < 2); + float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; + + float segmentLength = length(Normal); + vec3 lineDir = Normal / segmentLength; + + vec3 lineCenter = isStart ? (Position + Normal * 0.5) : (Position - Normal * 0.5); + + vec3 lineStart = lineCenter - lineDir * (segmentLength * 0.5); + vec3 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); + vec3 thisPoint = isStart ? lineStart : lineEnd; + + // Extract camera position from ModelViewMat + mat3 rotationInv = transpose(mat3(ModelViewMat)); + vec3 translation = vec3(ModelViewMat[3]); + vec3 cameraPos = rotationInv * (-translation); + + vec3 toCamera = normalize(cameraPos - lineCenter); + + vec3 perpDir = cross(lineDir, toCamera); + if (length(perpDir) < 0.001) { + perpDir = cross(lineDir, vec3(0.0, 1.0, 0.0)); + if (length(perpDir) < 0.001) { + perpDir = cross(lineDir, vec3(1.0, 0.0, 0.0)); + } + } + perpDir = normalize(perpDir); + + // Calculate actual line width in world units + float actualLineWidth; + if (LineWidth < 0.0) { + // Distance-scaled mode: negative value = screen-space fraction + // Convert screen fraction to world units at this distance + float screenFraction = -LineWidth; + float distToCamera = length(cameraPos - lineCenter); + + // Extract tan(fov/2) from projection matrix: ProjMat[1][1] = 1/tan(fov/2) + float tanHalfFov = 1.0 / ProjMat[1][1]; + + // At distance d, visible height = 2 * d * tan(fov/2) + // world width = screenFraction * visible height + actualLineWidth = screenFraction * 2.0 * distToCamera * tanHalfFov; + } else { + actualLineWidth = LineWidth; + } + + // Expand for AA + float halfWidth = actualLineWidth / 2.0; + float aaPadding = actualLineWidth * 0.3; + float halfWidthPadded = halfWidth + aaPadding; + + vec3 perpOffset = perpDir * side * halfWidthPadded; + float longitudinal = isStart ? -1.0 : 1.0; + vec3 longOffset = lineDir * longitudinal * halfWidthPadded; + + vec3 expandedPos = thisPoint + perpOffset + longOffset; + + gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 1.0); + + v_Color = Color; + v_WorldPos = thisPoint; + v_ExpandedPos = expandedPos; + v_Normal = Normal; + v_LineCenter = lineCenter; + v_LineWidth = actualLineWidth; // Pass the computed world-unit width + v_SegmentLength = segmentLength; + v_IsStart = isStart ? 1.0 : 0.0; + v_Dash = Dash; + + sphericalVertexDistance = fog_spherical_distance(Position); + cylindricalVertexDistance = fog_cylindrical_distance(Position); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_model.fsh b/src/main/resources/assets/lambda/shaders/core/world_model.fsh new file mode 100644 index 000000000..5e192d610 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_model.fsh @@ -0,0 +1,126 @@ +#version 330 + +// Sampler0: Main Atlas +// Sampler1: Overlay Texture (White/Hurt flash) +// Sampler2: Lightmap +// Sampler3: Glint Texture (enchantment shimmer) +#moj_import + +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; +uniform sampler2D Sampler2; +uniform sampler2D Sampler3; + +layout(std140) uniform GlintTransforms { + mat4 GlintMat2; // Mat2 packed into the ModelView slot + vec4 UnusedColor; + vec3 UnusedOffset; + mat4 GlintMat; // Mat1 original +}; + +in vec2 v_TexCoord; +in vec4 v_Color; +in vec4 v_OverlayUV; // (u, v, mode, diffuseAmount) +in vec2 v_LightCoord; +in vec2 v_EdgeData; +in vec3 v_Normal; +in vec3 v_LightDir; // Primary light (pre-transformed) +in vec3 v_Light1Dir; // Fill light (pre-transformed) + +out vec4 fragColor; + +// DEBUG MODE: +// 0 = Normal rendering +// 1 = Show normals as RGB (red=+X, green=+Y, blue=+Z) +// 2 = Show light0 direction as RGB +// 3 = Show dot product with light0 as grayscale +// 4 = Show which faces are lit (d0 > 0.5 = red, d1 > 0.5 = green) +// 5 = Show glint scroll values as colors (R=scrollX, G=scrollY, B=mode/10) +// 6 = Show raw glint texture with base UVs (no animation) +// 7 = Show computed glint UVs as color (R=U fract, G=V fract, B=0.5) +#define DEBUG_MODE 0 + +// Glint constants matching vanilla (from TextureTransform.getGlintTransformation) +const float GLINT_ALPHA = 0.75; // Default from options.getGlintStrength() + +void main() { + float mode = floor(v_OverlayUV.z + 0.5); + // Mode bits: 1=overlay, 2=AA, 4=glint + bool useOverlay = (mod(mode, 2.0) >= 1.0); + bool useAA = (mod(floor(mode / 2.0), 2.0) >= 1.0); + bool useGlint = (mode >= 4.0); + + // 1. Determine Texture Coordinates (with optional Sharp AA) + vec2 texCoord = v_TexCoord; + if (useAA) { + vec2 texSize = vec2(textureSize(Sampler0, 0)); + vec2 pixels = texCoord * texSize; + vec2 t = fract(pixels - 0.5); + vec2 d = fwidth(pixels); + vec2 sharp_t = clamp((t - 0.5) / d + 0.5, 0.0, 1.0); + texCoord = (floor(pixels - 0.5) + sharp_t + 0.5) / texSize; + } + + // 2. Sample Base Texture from atlas + vec4 texColor = texture(Sampler0, texCoord); + if (texColor.a < 0.1) discard; + + // 3. Apply Multiplier (v_Color) and Global Modulator + vec4 baseColor = texColor * v_Color * ColorModulator; + + // 4. Calculate lighting + vec3 litColor = baseColor.rgb; + + // Handle Shading: + // v_OverlayUV.w is 'shadingAmount': 1.0 = shaded, 0.0 = unshaded. + // NOTE: For non-3D items, shadingAmount is 0.0. We want factor 1.0 (unshaded), not 0.0 (black). + if (v_OverlayUV.w >= 0.0) { + vec3 n = normalize(v_Normal); + vec3 light0 = normalize(v_LightDir); + vec3 light1 = normalize(v_Light1Dir); + + float d0 = max(0.0, dot(light0, n)); + float d1 = max(0.0, dot(light1, n)); + + // Vanilla's diffuse lighting formula: diffuse = max(0, n.l) * 0.6 + 0.4 + float diffuse = min(1.0, (d0 + d1) * 0.6 + 0.4); + + // Final light factor: mix between 1.0 (unshaded) and 'diffuse' (shaded) based on shadingAmount. + float lightFactor = mix(1.0, diffuse, v_OverlayUV.w); + litColor *= lightFactor; + } else { + // For world objects without directional shading, use lightmap + litColor *= texture(Sampler2, v_LightCoord).rgb; + } + + // 5. Apply Hurt Flash / Overlay + if (useOverlay) { + vec4 overlayColor = texture(Sampler1, v_OverlayUV.xy); + litColor = mix(litColor, overlayColor.rgb, overlayColor.a); + } + + // 6. Apply Enchantment Glint (Absolute 1.21 Parity) + if (useGlint) { + // Use v_TexCoord (Atlas UVs) for parity with model glint logic + vec2 transformedUV = (GlintMat * vec4(v_TexCoord, 0.0, 1.0)).xy; + vec3 glint = texture(Sampler3, fract(transformedUV)).rgb; + + // Intensity = (sample * alpha)^2 + vec3 layer = glint * GLINT_ALPHA; + litColor += (layer * layer); + + // Debug visualizations + #if DEBUG_MODE == 5 + fragColor = vec4(v_EdgeData.x, v_EdgeData.y, 0.0, 1.0); return; + #elif DEBUG_MODE == 6 + fragColor = texture(Sampler3, v_EdgeData); return; + #elif DEBUG_MODE == 7 + fragColor = vec4(fract(guv.x), fract(guv.y), 0.5, 1.0); return; + #elif DEBUG_MODE == 8 + // Show diffuse shading amount + fragColor = vec4(vec3(v_OverlayUV.w), 1.0); return; + #endif + } + + fragColor = vec4(litColor, baseColor.a); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_model.vsh b/src/main/resources/assets/lambda/shaders/core/world_model.vsh new file mode 100644 index 000000000..b969f8902 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_model.vsh @@ -0,0 +1,40 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +// Attributes matching WORLD_MODEL_FORMAT +in vec3 Position; +in vec4 Color; +in vec2 UV0; +in vec4 OverlayUV; // (u, v, mode, diffuseAmount) +in ivec2 UV2; // Lightmap coords +in vec3 LightDir; // Primary light direction (pre-transformed for ITEMS_FLAT) +in vec3 Light1Dir; // Fill light direction (pre-transformed) +in vec3 Normal; +in vec2 EdgeData; + +out vec2 v_TexCoord; +out vec4 v_Color; +out vec4 v_OverlayUV; +out vec2 v_LightCoord; +out vec2 v_EdgeData; +out vec3 v_Normal; +out vec3 v_LightDir; // Primary light +out vec3 v_Light1Dir; // Fill light + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + v_Color = Color; + v_TexCoord = UV0; + v_OverlayUV = OverlayUV; + v_LightCoord = (vec2(UV2) + 0.5) / 256.0; // Normalize light coords + v_EdgeData = EdgeData; + + // Both normals and light directions are already transformed on CPU + v_Normal = Normal; + v_LightDir = LightDir; + v_Light1Dir = Light1Dir; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh new file mode 100644 index 000000000..756998738 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh @@ -0,0 +1,73 @@ +#version 330 +#moj_import +#moj_import + +uniform sampler2D Sampler0; + +in vec2 texCoord0; +in vec4 vertexColor; +in float sphericalVertexDistance; +in float cylindricalVertexDistance; +// SDF style params from vertex shader: (outlineWidth, glowRadius, shadowSoftness, threshold) +in vec4 sdfStyleParams; + +out vec4 fragColor; + +void main() { + // Extract SDF parameters from vertex attributes + float OutlineWidth = sdfStyleParams.x; + float GlowRadius = sdfStyleParams.y; + float ShadowSoftness = sdfStyleParams.z; + float SDFThreshold = sdfStyleParams.w; + + // Sample the SDF texture - use ALPHA channel + vec4 texSample = texture(Sampler0, texCoord0); + float sdfValue = texSample.a; + + // Screen-space anti-aliasing + float smoothing = fwidth(sdfValue) * 0.5; + + // Decode layer type from vertex alpha + int layerType = int(vertexColor.a * 255.0 + 0.5); + + float alpha; + + if (layerType >= 200) { + // Main text layer - sharp edge at threshold + alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + } else if (layerType >= 100) { + // Outline layer - uses OutlineWidth + float outlineEdge = SDFThreshold - OutlineWidth; + alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, sdfValue); + // Mask out the main text area + float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + alpha = alpha * (1.0 - textMask); + } else if (layerType >= 50) { + // Glow layer - starts from outline edge (if outline enabled) or text edge + // Only expand past outline if OutlineWidth is actually set + float glowEdge = (OutlineWidth > 0.001) ? (SDFThreshold - OutlineWidth) : SDFThreshold; + float glowStart = glowEdge - GlowRadius; + float glowEnd = glowEdge; + alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6; + // Mask out the text and outline area + float outlineMask = smoothstep(glowEdge - smoothing, glowEdge + smoothing, sdfValue); + alpha = alpha * (1.0 - outlineMask); + } else { + // Shadow layer - uses ShadowSoftness + float shadowStart = SDFThreshold - ShadowSoftness - 0.15; + float shadowEnd = SDFThreshold - 0.1; + alpha = smoothstep(shadowStart, shadowEnd, sdfValue) * 0.5; + } + + // Apply vertex color (RGB from vertex, alpha computed above) + vec4 result = vec4(vertexColor.rgb, alpha); + + // Discard nearly transparent fragments + if (result.a <= 0.001) discard; + + // Apply color modulator and fog + result *= ColorModulator; + fragColor = apply_fog(result, sphericalVertexDistance, cylindricalVertexDistance, + FogEnvironmentalStart, FogEnvironmentalEnd, + FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); +} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh new file mode 100644 index 000000000..8aa50c8b0 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh @@ -0,0 +1,84 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +// Position contains local glyph offset (x, y) with z unused +in vec3 Position; +in vec2 UV0; +in vec4 Color; +// Anchor is the camera-relative world position of the text +in vec3 Anchor; +// BillboardData.x = scale, BillboardData.y = billboardFlag (0 = auto-billboard) +in vec2 BillboardData; +// SDFStyle: vec4(outlineWidth, glowRadius, shadowSoftness, threshold) +in vec4 SDFStyle; + +out vec2 texCoord0; +out vec4 vertexColor; +out float sphericalVertexDistance; +out float cylindricalVertexDistance; +// Pass SDF style to fragment shader +out vec4 sdfStyleParams; + +void main() { + float scale = BillboardData.x; + float billboardFlag = BillboardData.y; + + vec3 worldPos; + + if (billboardFlag == 0.0) { + // Billboard mode: compute right/up vectors from ModelViewMat + // ModelViewMat transforms from world to view space + // To billboard, we need right and up vectors in world space + // For a view matrix, the inverse transpose gives us camera orientation + // The first column of ModelViewMat is the right vector (in view space) + // The second column is the up vector + // Since ModelViewMat = View, and we want to face the camera: + // right = normalize(ModelViewMat[0].xyz) + // up = normalize(ModelViewMat[1].xyz) + + vec3 right = vec3(ModelViewMat[0][0], ModelViewMat[1][0], ModelViewMat[2][0]); + vec3 up = vec3(ModelViewMat[0][1], ModelViewMat[1][1], ModelViewMat[2][1]); + + // Apply scale (negative Y to flip for correct text orientation) + float scaledX = Position.x * scale; + float scaledY = Position.y * -scale; // Negate Y to flip + + // Compute world position from anchor + billboard offset + worldPos = Anchor + right * scaledX + up * scaledY; + } else { + // Fixed rotation mode: position is already transformed, just add anchor + // In this case, Position.xy contains the pre-transformed local offset (scaled) + // We still need to apply the anchor offset + worldPos = Anchor + Position * scale; + } + + gl_Position = ProjMat * ModelViewMat * vec4(worldPos, 1.0); + + // Apply per-layer depth bias to prevent z-fighting between text effect layers + // Layer type is encoded in Color.a: 200+ = text, 100+ = outline, 50+ = glow, <50 = shadow + // Each layer needs a unique depth offset so they don't fight + // Order from back to front: shadow < glow < outline < text + int layerType = int(Color.a * 255.0 + 0.5); + float layerOffset; + if (layerType >= 200) { + layerOffset = 0.0004; // Text - closest to camera + } else if (layerType >= 100) { + layerOffset = 0.0003; // Outline + } else if (layerType >= 50) { + layerOffset = 0.0002; // Glow + } else { + layerOffset = 0.0001; // Shadow - furthest back + } + + gl_Position.z -= layerOffset * gl_Position.w; + + texCoord0 = UV0; + vertexColor = Color; + sdfStyleParams = SDFStyle; + + sphericalVertexDistance = fog_spherical_distance(worldPos); + cylindricalVertexDistance = fog_cylindrical_distance(worldPos); +} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/font.glsl b/src/main/resources/assets/lambda/shaders/fragment/font.glsl deleted file mode 100644 index afb98a7ff..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/font.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 420 - -in vec2 v_TexCoord; -in vec4 v_Color; - -out vec4 color; - -float sdf(float channel, float min, float max) { - return 1.0 - smoothstep(min, max, 1.0 - channel); -} - -void main() -{ - bool isEmoji = v_TexCoord.x < 0.0; - - if (isEmoji) { - vec4 c = texture(u_EmojiTexture, -v_TexCoord); - color = vec4(c.rgb, sdf(c.a, u_SDFMin, u_SDFMax)) * v_Color; - return; - } - - float sdf = sdf(texture(u_FontTexture, v_TexCoord).r, u_SDFMin, u_SDFMax); - color = vec4(1.0, 1.0, 1.0, sdf) * v_Color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/particles.glsl b/src/main/resources/assets/lambda/shaders/fragment/particles.glsl deleted file mode 100644 index 967ef6a91..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/particles.glsl +++ /dev/null @@ -1,12 +0,0 @@ -#version 330 core - -in vec2 v_TexCoord; -in vec4 v_Color; - -out vec4 color; - -void main() -{ - float a = 1.0 - length(v_TexCoord - 0.5) * 2.0; - color = v_Color * vec4(1.0, 1.0, 1.0, a); -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl deleted file mode 100644 index 2eb6b4242..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl +++ /dev/null @@ -1,9 +0,0 @@ -#version 420 - -in vec4 v_Color; -out vec4 color; - -void main() -{ - color = v_Color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl deleted file mode 100644 index efac6ba5c..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 core - -in vec2 v_TexCoord; -out vec4 color; - -uniform sampler2D u_Texture; - -void main() -{ - color = texture(u_Texture, v_TexCoord); -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl deleted file mode 100644 index 09f3f6b96..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 core - -in vec2 v_TexCoord; -in vec4 v_Color; - -out vec4 color; - -uniform sampler2D u_Texture; - -void main() -{ - color = texture(u_Texture, v_TexCoord) * v_Color -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/post/sdf.glsl b/src/main/resources/assets/lambda/shaders/post/sdf.glsl deleted file mode 100644 index fa3566da6..000000000 --- a/src/main/resources/assets/lambda/shaders/post/sdf.glsl +++ /dev/null @@ -1,34 +0,0 @@ -attributes { - vec4 pos; - vec2 uv; -}; - -uniforms { - sampler2D u_Texture; # fragment - vec2 u_TexelSize; # fragment -}; - -export { - vec2 v_TexCoord; # uv -}; - -#define SPREAD 4 - -void fragment() { - vec4 colors = vec4(0.0); - vec4 blurWeight = vec4(0.0); - - for (int x = -SPREAD; x <= SPREAD; ++x) { - for (int y = -SPREAD; y <= SPREAD; ++y) { - vec2 offset = vec2(x, y) * u_TexelSize; - - vec4 color = texture(u_Texture, v_TexCoord + offset); - vec4 weight = exp(-color * color); - - colors += color * weight; - blurWeight += weight; - } - } - - color = colors / blurWeight; -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/shared/hsb.glsl b/src/main/resources/assets/lambda/shaders/shared/hsb.glsl deleted file mode 100644 index 25b9eb99d..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/hsb.glsl +++ /dev/null @@ -1,30 +0,0 @@ -vec3 hsb2rgb(vec3 hsb) { - float C = hsb.z * hsb.y; - float X = C * (1.0 - abs(mod(hsb.x / 60.0, 2.0) - 1.0)); - float m = hsb.z - C; - - vec3 rgb; - - if (0.0 <= hsb.x && hsb.x < 60.0) { - rgb = vec3(C, X, 0.0); - } else if (60.0 <= hsb.x && hsb.x < 120.0) { - rgb = vec3(X, C, 0.0); - } else if (120.0 <= hsb.x && hsb.x < 180.0) { - rgb = vec3(0.0, C, X); - } else if (180.0 <= hsb.x && hsb.x < 240.0) { - rgb = vec3(0.0, X, C); - } else if (240.0 <= hsb.x && hsb.x < 300.0) { - rgb = vec3(X, 0.0, C); - } else { - rgb = vec3(C, 0.0, X); - } - - return (rgb + vec3(m)); -}# - -float hue(vec2 uv) { - vec2 centered = uv * 2.0 - 1.0; - float hue = degrees(atan(centered.y, centered.x)) + 180.0; - - return hue; -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/shared/rect.glsl b/src/main/resources/assets/lambda/shaders/shared/rect.glsl deleted file mode 100644 index 4fe1b2220..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/rect.glsl +++ /dev/null @@ -1,45 +0,0 @@ -attributes { - vec4 pos; - vec2 uv; - vec4 color; -}; - -uniforms { - vec2 u_Size; # fragment - - float u_RoundLeftTop; # fragment - float u_RoundLeftBottom; # fragment - float u_RoundRightBottom; # fragment - float u_RoundRightTop; # fragment -}; - -export { - vec2 v_TexCoord; # uv - vec4 v_Color; # color -}; - -#include "shade" - -#define NOISE_GRANULARITY 0.004 -#define SMOOTHING 0.3 - -#define noise getNoise() - -vec4 getNoise() { - // https://shader-tutorial.dev/advanced/color-banding-dithering/ - float random = fract(sin(dot(v_TexCoord, vec2(12.9898, 78.233))) * 43758.5453); - float ofs = mix(-NOISE_GRANULARITY, NOISE_GRANULARITY, random); - return vec4(ofs, ofs, ofs, 0.0); -}# - -float signedDistance(in vec4 r) { - r.xy = (v_TexCoord.x > 0.5) ? r.xy : r.zw; - r.x = (v_TexCoord.y > 0.5) ? r.x : r.y; - - vec2 q = u_Size * (abs(v_TexCoord - 0.5) - 0.5) + r.x; - return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; -}# - -float signedDistance() { - return signedDistance(vec4(u_RoundRightBottom, u_RoundRightTop, u_RoundLeftBottom, u_RoundLeftTop)); -}# diff --git a/src/main/resources/assets/lambda/shaders/shared/sdf.glsl b/src/main/resources/assets/lambda/shaders/shared/sdf.glsl deleted file mode 100644 index 165043f1c..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/sdf.glsl +++ /dev/null @@ -1,3 +0,0 @@ -float sdf(float channel, float min, float max) { - return 1.0 - smoothstep(min, max, 1.0 - channel); -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/shared/shade.glsl b/src/main/resources/assets/lambda/shaders/shared/shade.glsl deleted file mode 100644 index 9371edc22..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/shade.glsl +++ /dev/null @@ -1,22 +0,0 @@ -uniforms { - float u_Shade; # fragment - float u_ShadeTime; # fragment - vec4 u_ShadeColor1; # fragment - vec4 u_ShadeColor2; # fragment - vec2 u_ShadeSize; # fragment -}; - -export { - vec2 v_Position; # gl_Position.xy * 0.5 + 0.5 -}; - -#define shade getShadeColor() - -vec4 getShadeColor() { - if (u_Shade != 1.0) return vec4(1.0); - - vec2 pos = v_Position * u_ShadeSize; - float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5; - - return mix(u_ShadeColor1, u_ShadeColor2, p); -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl b/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl deleted file mode 100644 index aad9c7385..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl +++ /dev/null @@ -1,17 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos1; -layout (location = 1) in vec3 pos2; -layout (location = 2) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; -uniform float u_TickDelta; - -void main() -{ - gl_Position = u_ProjModel * u_View * vec4(mix(pos1, pos2, u_TickDelta), 1.0); - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl b/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl deleted file mode 100644 index ba73727da..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl +++ /dev/null @@ -1,15 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; - -void main() -{ - gl_Position = u_ProjModel * u_View * vec4(pos, 1.0); - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/font.glsl b/src/main/resources/assets/lambda/shaders/vertex/font.glsl deleted file mode 100644 index e8a0f3b92..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/font.glsl +++ /dev/null @@ -1,19 +0,0 @@ -#version 330 core - -layout (location = 0) in vec4 pos; -layout (location = 1) in vec2 uv; -layout (location = 2) in vec4 color; // Does this fuck the padding ? - -out vec2 v_TexCoord; -out vec4 v_Color; - -uniform sampler2D u_FontTexture; -uniform sampler2D u_EmojiTexture; -uniform float u_SDFMin; -uniform float u_SDFMin; - -void main() -{ - v_TexCoord = uv; - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/particles.glsl b/src/main/resources/assets/lambda/shaders/vertex/particles.glsl deleted file mode 100644 index 08e0e0a43..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/particles.glsl +++ /dev/null @@ -1,19 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec2 uv; -layout (location = 2) in vec4 color; - -out vec2 v_TexCoord; -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; - -void main() -{ - gl_Position = u_ProjModel * u_View * vec4(pos, 1.0); - - v_TexCoord = uv; - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl b/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl deleted file mode 100644 index 2d7f171e5..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl +++ /dev/null @@ -1,20 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos1; -layout (location = 1) in vec2 pos1; -layout (location = 2) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; -uniform float u_TickDelta; - -void main() -{ - if (l_VertexID % 2 != 0) - return; - - vec3 VERTEX_POSITION = mix(pos1, pos2, u_TickDelta); - gl_Position = u_ProjModel * u_View * vec4(VERTEX_POSITION, 1.0); -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl b/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl deleted file mode 100644 index 1d111b6ca..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl +++ /dev/null @@ -1,17 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; - -void main() -{ - if (gl_VertexID % 2 != 0) - return; - - gl_Position = u_ProjModel * u_View * vec4(pos, 1.0); -} \ No newline at end of file diff --git a/src/main/resources/lambda.accesswidener b/src/main/resources/lambda.accesswidener index 484d80718..21abfb4d7 100644 --- a/src/main/resources/lambda.accesswidener +++ b/src/main/resources/lambda.accesswidener @@ -68,10 +68,17 @@ transitive-accessible field net/minecraft/client/render/Camera pos Lnet/minecraf # Renderer transitive-accessible field net/minecraft/client/texture/NativeImage pointer J +transitive-accessible field net/minecraft/client/texture/SpriteContents image Lnet/minecraft/client/texture/NativeImage; transitive-accessible class net/minecraft/client/gui/screen/SplashOverlay$LogoTexture transitive-accessible field com/mojang/blaze3d/systems/RenderSystem$ShapeIndexBuffer indexBuffer Lcom/mojang/blaze3d/buffers/GpuBuffer; transitive-accessible field net/minecraft/client/gl/GlGpuBuffer id I +# BufferBuilder - Custom vertex element support +transitive-accessible method net/minecraft/client/render/BufferBuilder beginElement (Lcom/mojang/blaze3d/vertex/VertexFormatElement;)J +transitive-accessible field net/minecraft/client/render/BufferBuilder offsetsByElementId [I +transitive-accessible field net/minecraft/client/render/BufferBuilder vertexPointer J +transitive-accessible field net/minecraft/client/render/BufferBuilder currentMask I + # Text transitive-accessible field net/minecraft/text/Style color Lnet/minecraft/text/TextColor; transitive-accessible field net/minecraft/text/Style bold Ljava/lang/Boolean; @@ -123,3 +130,22 @@ transitive-accessible method net/minecraft/item/BlockItem getPlacementState (Lne transitive-accessible method net/minecraft/block/AbstractBlock getPickStack (Lnet/minecraft/world/WorldView;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)Lnet/minecraft/item/ItemStack; transitive-accessible field net/minecraft/client/gui/screen/ingame/HandledScreen focusedSlot Lnet/minecraft/screen/slot/Slot; transitive-accessible field net/minecraft/registry/SimpleRegistry frozen Z + +# Render Item +transitive-accessible field net/minecraft/client/render/item/ItemRenderState layers [Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState layerCount I +transitive-accessible field net/minecraft/client/render/item/ItemRenderState displayContext Lnet/minecraft/item/ItemDisplayContext; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState quads Ljava/util/List; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState useLight Z +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState particle Lnet/minecraft/client/texture/Sprite; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState transform Lnet/minecraft/client/render/model/json/Transformation; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState tints [I +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState glint Lnet/minecraft/client/render/item/ItemRenderState$Glint; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState specialModelType Lnet/minecraft/client/render/item/model/special/SpecialModelRenderer; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState renderLayer Lnet/minecraft/client/render/RenderLayer; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState data Ljava/lang/Object; + +# Entity Renderer +transitive-accessible method net/minecraft/client/render/entity/ProjectileEntityRenderer getTexture (Lnet/minecraft/client/render/entity/state/ProjectileEntityRenderState;)Lnet/minecraft/util/Identifier; +transitive-accessible field net/minecraft/client/render/entity/BoatEntityRenderer texture Lnet/minecraft/util/Identifier; +transitive-accessible field net/minecraft/client/render/entity/EndCrystalEntityRenderer TEXTURE Lnet/minecraft/util/Identifier; diff --git a/src/main/resources/lambda.mixins.json b/src/main/resources/lambda.mixins.json index 0326b21f6..96fefd9a1 100644 --- a/src/main/resources/lambda.mixins.json +++ b/src/main/resources/lambda.mixins.json @@ -1,98 +1,97 @@ { - "required": true, - "minVersion": "0.8", - "package": "com.lambda.mixin", - "compatibilityLevel": "JAVA_21", - "client": [ - "CrashReportMixin", - "MinecraftClientMixin", - "baritone.BaritonePlayerContextMixin", - "baritone.LookBehaviourMixin", - "client.sound.SoundSystemMixin", - "entity.ClientPlayerEntityMixin", - "entity.ClientPlayInteractionManagerMixin", - "entity.EntityMixin", - "entity.FireworkRocketEntityMixin", - "entity.HandledScreensMixin", - "entity.LivingEntityMixin", - "entity.PlayerEntityMixin", - "entity.PlayerInventoryMixin", - "input.KeyBindingMixin", - "input.KeyboardMixin", - "input.MouseMixin", - "items.BarrierBlockMixin", - "items.BlockItemMixin", - "items.FilledMapItemMixin", - "network.ClientConnectionMixin", - "network.ClientLoginNetworkMixin", - "network.ClientPlayNetworkHandlerMixin", - "network.HandshakeC2SPacketMixin", - "network.LoginHelloC2SPacketMixin", - "network.LoginKeyC2SPacketMixin", - "render.AbstractTerrainRenderContextMixin", - "render.ArmorFeatureRendererMixin", - "render.BlockMixin", - "render.BlockModelRendererMixin", - "render.BlockRenderManagerMixin", - "render.BossBarHudMixin", - "render.CameraMixin", - "render.CapeFeatureRendererMixin", - "render.ChatHudMixin", - "render.ChatInputSuggestorMixin", - "render.ChatScreenMixin", - "render.ChunkOcclusionDataBuilderMixin", - "render.DrawContextMixin", - "render.ElytraFeatureRendererMixin", - "render.EntityRendererMixin", - "render.FluidRendererMixin", - "render.FogRendererMixin", - "render.GameRendererMixin", - "render.GlStateManagerMixin", - "render.HandledScreenMixin", - "render.HeadFeatureRendererMixin", - "render.HeldItemRendererMixin", - "render.InGameHudMixin", - "render.InGameOverlayRendererMixin", - "render.LightmapTextureManagerMixin", - "render.LivingEntityRendererMixin", - "render.PlayerListHudMixin", - "render.RenderLayersMixin", - "render.ScreenHandlerMixin", - "render.ScreenMixin", - "render.SodiumBlockOcclusionCacheMixin", - "render.SodiumBlockRendererMixin", - "render.SodiumFluidRendererImplMixin", - "render.SodiumLightDataAccessMixin", - "render.SodiumWorldRendererMixin", - "render.SplashOverlayMixin", - "render.SplashOverlayMixin$LogoTextureMixin", - "render.StatusEffectFogModifierMixin", - "render.TooltipComponentMixin", - "render.WeatherRenderingMixin", - "render.WorldBorderRenderingMixin", - "render.WorldRendererMixin", - "render.blockentity.AbstractSignBlockEntityRendererMixin", - "render.blockentity.BeaconBlockEntityRendererMixin", - "render.blockentity.BlockEntityRenderDispatcherMixin", - "render.blockentity.EnchantingTableBlockEntityRendererMixin", - "render.blockentity.MobSpawnerBlockEntityRendererMixin", - "render.particle.BillboardParticleMixin", - "render.particle.ElderGuardianParticleRendererMixin", - "render.particle.ItemPickupParticleRendererMixin", - "world.AbstractBlockMixin", - "world.BlockCollisionSpliteratorMixin", - "world.ClientChunkManagerMixin", - "world.ClientWorldMixin", - "world.DirectionMixin", - "world.StructureTemplateMixin", - "world.WorldMixin" - ], - "injectors": { - "defaultRequire": 0 - }, - "overwrites": { - "conformVisibility": true - }, - "mixinextras": { - "minVersion": "0.5.0"} -} + "required": true, + "minVersion": "0.8", + "package": "com.lambda.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CrashReportMixin", + "MinecraftClientMixin", + "baritone.BaritonePlayerContextMixin", + "baritone.LookBehaviourMixin", + "client.sound.SoundSystemMixin", + "entity.ClientPlayerEntityMixin", + "entity.ClientPlayInteractionManagerMixin", + "entity.EntityMixin", + "entity.FireworkRocketEntityMixin", + "entity.HandledScreensMixin", + "entity.LivingEntityMixin", + "entity.PlayerEntityMixin", + "entity.PlayerInventoryMixin", + "input.KeyBindingMixin", + "input.KeyboardMixin", + "input.MouseMixin", + "items.BlockItemMixin", + "items.FilledMapItemMixin", + "network.ClientConnectionMixin", + "network.ClientLoginNetworkMixin", + "network.ClientPlayNetworkHandlerMixin", + "network.HandshakeC2SPacketMixin", + "network.LoginHelloC2SPacketMixin", + "network.LoginKeyC2SPacketMixin", + "render.AbstractTerrainRenderContextMixin", + "render.ArmorFeatureRendererMixin", + "render.BlockMixin", + "render.BlockModelRendererMixin", + "render.BossBarHudMixin", + "render.CameraMixin", + "render.CapeFeatureRendererMixin", + "render.ChatHudMixin", + "render.ChatInputSuggestorMixin", + "render.ChatScreenMixin", + "render.ChunkOcclusionDataBuilderMixin", + "render.DrawContextMixin", + "render.ElytraFeatureRendererMixin", + "render.EntityRendererMixin", + "render.FluidRendererMixin", + "render.FogRendererMixin", + "render.GameRendererMixin", + "render.GlStateManagerMixin", + "render.HandledScreenMixin", + "render.HeadFeatureRendererMixin", + "render.HeldItemRendererMixin", + "render.InGameHudMixin", + "render.InGameOverlayRendererMixin", + "render.LightmapTextureManagerMixin", + "render.LivingEntityRendererMixin", + "render.PlayerListHudMixin", + "render.RenderLayersMixin", + "render.ScreenHandlerMixin", + "render.ScreenMixin", + "render.SodiumBlockOcclusionCacheMixin", + "render.SodiumBlockRendererMixin", + "render.SodiumFluidRendererImplMixin", + "render.SodiumLightDataAccessMixin", + "render.SodiumWorldRendererMixin", + "render.SplashOverlayMixin", + "render.SplashOverlayMixin$LogoTextureMixin", + "render.StatusEffectFogModifierMixin", + "render.TooltipComponentMixin", + "render.WeatherRenderingMixin", + "render.WorldBorderRenderingMixin", + "render.WorldRendererMixin", + "render.blockentity.AbstractSignBlockEntityRendererMixin", + "render.blockentity.BeaconBlockEntityRendererMixin", + "render.blockentity.BlockEntityRenderDispatcherMixin", + "render.blockentity.EnchantingTableBlockEntityRendererMixin", + "render.blockentity.MobSpawnerBlockEntityRendererMixin", + "render.particle.BillboardParticleMixin", + "render.particle.ElderGuardianParticleRendererMixin", + "render.particle.ItemPickupParticleRendererMixin", + "world.AbstractBlockMixin", + "world.BlockCollisionSpliteratorMixin", + "world.ClientChunkManagerMixin", + "world.ClientWorldMixin", + "world.DirectionMixin", + "world.StructureTemplateMixin", + "world.WorldMixin" + ], + "injectors": { + "defaultRequire": 0 + }, + "overwrites": { + "conformVisibility": true + }, + "mixinextras": { + "minVersion": "0.5.0" + } +} \ No newline at end of file