cir) {
/**
* Modifies the third person camera distance
+ *
* {@code
* if (thirdPerson) {
- * if (inverseView) {
- * this.setRotation(this.yaw + 180.0F, -this.pitch);
- * }
+ * if (inverseView) {
+ * this.setRotation(this.yaw + 180.0F, -this.pitch);
+ * }
*
- * this.moveBy(-this.clipToSpace(4.0), 0.0, 0.0);
+ * this.moveBy(-this.clipToSpace(4.0), 0.0, 0.0);
* }
* }
*/
@@ -117,21 +117,35 @@ private float onDistanceUpdate(float distance) {
/**
* Modifies the arguments for setting the camera rotation.
* Mixes into 4 arguments:
- * Experimental Minecart Controller:
+ *
+ * Experimental Minecart Controller:
+ *
+ *
*
* if (experimentalMinecartController.hasCurrentLerpSteps()) {
- * Vec3d vec3d = minecartEntity.getPassengerRidingPos(focusedEntity).subtract(minecartEntity.getPos()).subtract(focusedEntity.getVehicleAttachmentPos(minecartEntity)).add(new Vec3d(0.0, (double)MathHelper.lerp(tickProgress, this.lastCameraY, this.cameraY), 0.0));
+ * Vec3d vec3d = minecartEntity.getPassengerRidingPos(focusedEntity).subtract(minecartEntity.getPos())
+ * .subtract(focusedEntity.getVehicleAttachmentPos(minecartEntity))
+ * .add(new Vec3d(0.0, (double) MathHelper.lerp(tickProgress, this.lastCameraY, this.cameraY), 0.0));
* this.setRotation(focusedEntity.getYaw(tickProgress), focusedEntity.getPitch(tickProgress));
* this.setPos(experimentalMinecartController.getLerpedPosition(tickProgress).add(vec3d));
* break label39;
* }
*
- * Default Camera:
+ *
+ * Default Camera:
+ *
+ *
*
* this.setRotation(focusedEntity.getYaw(tickProgress), focusedEntity.getPitch(tickProgress));
- * this.setPos(MathHelper.lerp((double)tickProgress, focusedEntity.lastX, focusedEntity.getX()), MathHelper.lerp((double)tickProgress, focusedEntity.lastY, focusedEntity.getY()) + (double)MathHelper.lerp(tickProgress, this.lastCameraY, this.cameraY), MathHelper.lerp((double)tickProgress, focusedEntity.lastZ, focusedEntity.getZ()));
+ * this.setPos(MathHelper.lerp((double) tickProgress, focusedEntity.lastX, focusedEntity.getX()),
+ * MathHelper.lerp((double) tickProgress, focusedEntity.lastY, focusedEntity.getY())
+ * + (double) MathHelper.lerp(tickProgress, this.lastCameraY, this.cameraY),
+ * MathHelper.lerp((double) tickProgress, focusedEntity.lastZ, focusedEntity.getZ()));
*
- * Third person camera:
+ *
+ * Third person camera:
+ *
+ *
*
* if (thirdPerson) {
* if (inverseView) {
@@ -140,9 +154,12 @@ private float onDistanceUpdate(float distance) {
* // ...
* }
*
- * When the player is focused on another Living Entity:
+ *
+ * When the player is focused on another Living Entity:
+ *
+ *
*
- * Direction direction = ((LivingEntity)focusedEntity).getSleepingDirection();
+ * Direction direction = ((LivingEntity) focusedEntity).getSleepingDirection();
* this.setRotation(direction != null ? direction.getPositiveHorizontalDegrees() - 180.0F : 0.0F, 0.0F);
* this.moveBy(0.0F, 0.3F, 0.0F);
*
diff --git a/src/main/java/com/lambda/mixin/render/ChatHudMixin.java b/src/main/java/com/lambda/mixin/render/ChatHudMixin.java
index 2b6790df0..b64f481c1 100644
--- a/src/main/java/com/lambda/mixin/render/ChatHudMixin.java
+++ b/src/main/java/com/lambda/mixin/render/ChatHudMixin.java
@@ -29,18 +29,6 @@
@Mixin(ChatHud.class)
public class ChatHudMixin {
- /**
- * Draws emojis at the given chat position
- * {@code
- * context.getMatrices().translate(0.0F, 0.0F, 50.0F);
- * context.drawTextWithShadow(this.client.textRenderer, visible.content(), 0, y, 16777215 + (u << 24));
- * context.getMatrices().pop();
- * }
- */
-// @WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/OrderedText;III)I"))
-// int wrapRenderCall(DrawContext instance, TextRenderer textRenderer, OrderedText text, int x, int y, int color, Operation original) {
-// return original.call(instance, textRenderer, LambdaMoji.INSTANCE.parse(text, x, y, color), 0, y, 16777215 + (color << 24));
-// }
@WrapMethod(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V")
void wrapAddMessage(Text message, MessageSignatureData signatureData, MessageIndicator indicator, Operation original) {
diff --git a/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java b/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java
index f4a556f84..002625ebf 100644
--- a/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java
@@ -33,9 +33,6 @@
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-/**
- * Mixin to override elytra textures with Lambda capes and disable elytra rendering.
- */
@Mixin(ElytraFeatureRenderer.class)
public class ElytraFeatureRendererMixin {
@ModifyReturnValue(method = "getTexture", at = @At("RETURN"))
@@ -46,7 +43,9 @@ private static Identifier injectElytra(Identifier original, BipedEntityRenderSta
var networkHandler = Lambda.getMc().getNetworkHandler();
if (networkHandler == null) return original;
- var entry = playerState.playerName != null ? networkHandler.getPlayerListEntry(playerState.playerName.getString()) : null;
+ var entry = playerState.playerName != null
+ ? networkHandler.getPlayerListEntry(playerState.playerName.getString())
+ : null;
if (entry == null) return original;
var profile = entry.getProfile();
diff --git a/src/main/java/com/lambda/mixin/render/EntityRenderManagerMixin.java b/src/main/java/com/lambda/mixin/render/EntityRenderManagerMixin.java
new file mode 100644
index 000000000..d8a3c43cb
--- /dev/null
+++ b/src/main/java/com/lambda/mixin/render/EntityRenderManagerMixin.java
@@ -0,0 +1,66 @@
+/*
+ * 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.mixin.render;
+
+import com.lambda.graphics.outline.IEntityRenderState;
+import com.lambda.graphics.outline.OutlineManager;
+import com.lambda.graphics.outline.OutlineCapturingQueue;
+import com.lambda.graphics.outline.VertexCapture;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import net.minecraft.client.render.command.OrderedRenderCommandQueue;
+import net.minecraft.client.render.command.OrderedRenderCommandQueueImpl;
+import net.minecraft.client.render.entity.EntityRenderManager;
+import net.minecraft.client.render.entity.EntityRenderer;
+import net.minecraft.client.render.entity.state.EntityRenderState;
+import net.minecraft.client.render.state.CameraRenderState;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.entity.Entity;
+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(EntityRenderManager.class)
+public class EntityRenderManagerMixin {
+
+ @Inject(method = "getAndUpdateRenderState", at = @At("RETURN"))
+ private void captureEntityId(E entity, float tickProgress, CallbackInfoReturnable cir) {
+ EntityRenderState state = cir.getReturnValue();
+ if (state instanceof IEntityRenderState lambdaState) lambdaState.lambda$setEntityId(entity.getId());
+ }
+
+ @WrapOperation(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/EntityRenderer;render(Lnet/minecraft/client/render/entity/state/EntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/command/OrderedRenderCommandQueue;Lnet/minecraft/client/render/state/CameraRenderState;)V"))
+ private void wrapRenderQueue(EntityRenderer, S> renderer, S renderState, MatrixStack matrices, OrderedRenderCommandQueue queue, CameraRenderState cameraState, Operation original) {
+ int entityId = -1;
+ if (renderState instanceof IEntityRenderState lambdaState) {
+ entityId = lambdaState.lambda$getEntityId();
+ }
+
+ if (entityId != -1 && OutlineManager.shouldCapture(entityId)) {
+ VertexCapture.INSTANCE.beginCapture(entityId);
+
+ OrderedRenderCommandQueueImpl wrappedQueue = new OutlineCapturingQueue((OrderedRenderCommandQueueImpl) queue, entityId);
+ original.call(renderer, renderState, matrices, wrappedQueue, cameraState);
+
+ VertexCapture.INSTANCE.endCapture();
+ } else {
+ original.call(renderer, renderState, matrices, queue, cameraState);
+ }
+ }
+}
diff --git a/src/main/java/com/lambda/mixin/render/EntityRenderStateMixin.java b/src/main/java/com/lambda/mixin/render/EntityRenderStateMixin.java
new file mode 100644
index 000000000..05f345b7c
--- /dev/null
+++ b/src/main/java/com/lambda/mixin/render/EntityRenderStateMixin.java
@@ -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.mixin.render;
+
+import com.lambda.graphics.outline.IEntityRenderState;
+import net.minecraft.client.render.entity.state.EntityRenderState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+
+@Mixin(EntityRenderState.class)
+public class EntityRenderStateMixin implements IEntityRenderState {
+ @Unique
+ private int lambda$entityId = -1;
+
+ @Override
+ public int lambda$getEntityId() {
+ return lambda$entityId;
+ }
+
+ @Override
+ public void lambda$setEntityId(int id) {
+ this.lambda$entityId = id;
+ }
+}
diff --git a/src/main/java/com/lambda/mixin/render/EntityRendererMixin.java b/src/main/java/com/lambda/mixin/render/EntityRendererMixin.java
index b71234dce..729d61e9c 100644
--- a/src/main/java/com/lambda/mixin/render/EntityRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/EntityRendererMixin.java
@@ -17,6 +17,7 @@
package com.lambda.mixin.render;
+import com.lambda.graphics.outline.OutlineManager;
import com.lambda.module.modules.render.NoRender;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
@@ -36,6 +37,7 @@ public class EntityRendererMixin {
@Inject(method = "shouldRender(Lnet/minecraft/entity/Entity;Lnet/minecraft/client/render/Frustum;DDD)Z", at = @At("HEAD"), cancellable = true)
private void injectShouldRender(Entity entity, Frustum frustum, double x, double y, double z, CallbackInfoReturnable cir) {
if (NoRender.shouldOmitEntity(entity)) cir.cancel();
+ else if (OutlineManager.shouldCapture(entity.getId())) cir.setReturnValue(true);
}
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
diff --git a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java
index 98cf293e7..1ce56115f 100644
--- a/src/main/java/com/lambda/mixin/render/GameRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/GameRendererMixin.java
@@ -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
@@ -19,8 +19,11 @@
import com.lambda.event.EventFlow;
import com.lambda.event.events.RenderEvent;
-import com.lambda.graphics.RenderMain;
import com.lambda.gui.DearImGui;
+import com.lambda.graphics.RenderMain;
+import com.lambda.graphics.outline.OutlineCapturingQueue;
+import net.minecraft.client.render.command.OrderedRenderCommandQueueImpl;
+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 +43,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 +57,13 @@ 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.render();
+ }
- RenderMain.render3D(positionMatrix, projectionMatrix);
+ @WrapOperation(method = "renderHand", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getEntityRenderCommandQueue()Lnet/minecraft/client/render/command/OrderedRenderCommandQueueImpl;"))
+ private OrderedRenderCommandQueueImpl wrapHandQueue(GameRenderer instance, Operation original) {
+ OrderedRenderCommandQueueImpl queue = original.call(instance);
+ return new OutlineCapturingQueue(queue, -1);
}
@ModifyExpressionValue(method = "renderWorld", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
@@ -69,6 +78,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 +86,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/GlStateManagerMixin.java b/src/main/java/com/lambda/mixin/render/GlStateManagerMixin.java
deleted file mode 100644
index 8c28f2160..000000000
--- a/src/main/java/com/lambda/mixin/render/GlStateManagerMixin.java
+++ /dev/null
@@ -1,65 +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.graphics.gl.GlStateUtils;
-import com.mojang.blaze3d.opengl.GlStateManager;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-
-import static org.lwjgl.opengl.GL11.*;
-
-@Mixin(GlStateManager.class)
-public class GlStateManagerMixin {
- @Inject(method = "_enableDepthTest", at = @At("TAIL"), remap = false)
- private static void depthTestEnable(CallbackInfo ci) {
- GlStateUtils.capSet(GL_DEPTH_TEST, true);
- }
-
- @Inject(method = "_disableDepthTest", at = @At("TAIL"), remap = false)
- private static void depthTestDisable(CallbackInfo ci) {
- GlStateUtils.capSet(GL_DEPTH_TEST, false);
- }
-
- @Inject(method = "_depthMask", at = @At("TAIL"), remap = false)
- private static void depthMask(boolean mask, CallbackInfo ci) {
- GlStateUtils.capSet(GL_DEPTH, mask);
- }
-
- @Inject(method = "_enableBlend", at = @At("TAIL"), remap = false)
- private static void blendEnable(CallbackInfo ci) {
- GlStateUtils.capSet(GL_BLEND, true);
- }
-
- @Inject(method = "_disableBlend", at = @At("TAIL"), remap = false)
- private static void blendDisable(CallbackInfo ci) {
- GlStateUtils.capSet(GL_BLEND, false);
- }
-
- @Inject(method = "_enableCull", at = @At("TAIL"), remap = false)
- private static void cullEnable(CallbackInfo ci) {
- GlStateUtils.capSet(GL_CULL_FACE, true);
- }
-
- @Inject(method = "_disableCull", at = @At("TAIL"), remap = false)
- private static void cullDisable(CallbackInfo ci) {
- GlStateUtils.capSet(GL_CULL_FACE, 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/LightmapTextureManagerMixin.java b/src/main/java/com/lambda/mixin/render/LightmapTextureManagerMixin.java
index 5e663aec8..e10b504d3 100644
--- a/src/main/java/com/lambda/mixin/render/LightmapTextureManagerMixin.java
+++ b/src/main/java/com/lambda/mixin/render/LightmapTextureManagerMixin.java
@@ -33,15 +33,11 @@
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-/**
- * Mixin to override lightmap for Fullbright/XRay and disable darkness effect.
- *
- * Note: In 1.21.11, the lightmap rendering was rewritten to use RenderPass with shaders.
- * We override the texture after normal rendering completes.
- */
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
- @Shadow @Final private GpuTexture glTexture;
+ @Shadow
+ @Final
+ private GpuTexture glTexture;
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/Profiler;pop()V", shift = At.Shift.BEFORE))
private void injectUpdate(float tickProgress, CallbackInfo ci) {
diff --git a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
index 5ce465d21..30d6cc05b 100644
--- a/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/LivingEntityRendererMixin.java
@@ -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
@@ -19,41 +19,30 @@
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;
-// This mixin's purpose is to set the player's pitch the current render pitch to correctly show the rotation
-// regardless of the camera position
@Mixin(LivingEntityRenderer.class)
public class LivingEntityRendererMixin {
- /**
- * Uses the current rotation render pitch
- * {@code
- * float g = MathHelper.lerpAngleDegrees(f, livingEntity.lastHeadYaw, livingEntity.headYaw);
- * livingEntityRenderState.bodyYaw = clampBodyYaw(livingEntity, g, f);
- * livingEntityRenderState.relativeHeadYaw = MathHelper.wrapDegrees(g - livingEntityRenderState.bodyYaw);
- * livingEntityRenderState.pitch = livingEntity.getLerpedPitch(f);
- * livingEntityRenderState.customName = livingEntity.getCustomName();
- * livingEntityRenderState.flipUpsideDown = shouldFlipUpsideDown(livingEntity);
- * if (livingEntityRenderState.flipUpsideDown) {
- * livingEntityRenderState.pitch *= -1.0F;
- * livingEntityRenderState.relativeHeadYaw *= -1.0F;
- * }
- * }
- */
@WrapOperation(method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;getLerpedPitch(F)F"))
private float wrapGetLerpedPitch(LivingEntity livingEntity, float v, Operation original) {
Float headPitch = RotationManager.getHeadPitch();
- if (livingEntity != Lambda.getMc().player || headPitch == null) {
- return original.call(livingEntity, v);
- }
+ if (livingEntity != Lambda.getMc().player || headPitch == null) return original.call(livingEntity, v);
return lerp(v, RotationManager.getPrevServerRotation().getPitchF(), headPitch);
}
+
+ @Inject(method = "hasLabel(Lnet/minecraft/entity/LivingEntity;D)Z", at = @At("HEAD"), cancellable = true)
+ private void injectHasLabel(LivingEntity livingEntity, double d, CallbackInfoReturnable cir) {
+ if (Nametags.INSTANCE.isEnabled() && Nametags.shouldRenderNametag(livingEntity)) cir.setReturnValue(false);
+ }
}
diff --git a/src/main/java/com/lambda/mixin/render/RenderLayersMixin.java b/src/main/java/com/lambda/mixin/render/RenderLayersMixin.java
index 96e786e33..5190ccff1 100644
--- a/src/main/java/com/lambda/mixin/render/RenderLayersMixin.java
+++ b/src/main/java/com/lambda/mixin/render/RenderLayersMixin.java
@@ -27,12 +27,6 @@
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-/**
- * Mixin to make blocks render as translucent for XRay functionality.
- *
- * Note: In 1.21.11, RenderLayers was split - BlockRenderLayers now handles
- * block/fluid layer determination and returns BlockRenderLayer enum instead of RenderLayer.
- */
@Mixin(BlockRenderLayers.class)
public class RenderLayersMixin {
@Inject(method = "getBlockLayer", at = @At("HEAD"), cancellable = true)
diff --git a/src/main/java/com/lambda/mixin/render/WorldRendererMixin.java b/src/main/java/com/lambda/mixin/render/WorldRendererMixin.java
index bfd3189fd..715510850 100644
--- a/src/main/java/com/lambda/mixin/render/WorldRendererMixin.java
+++ b/src/main/java/com/lambda/mixin/render/WorldRendererMixin.java
@@ -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
@@ -17,24 +17,91 @@
package com.lambda.mixin.render;
+import com.lambda.event.EventFlow;
+import com.lambda.event.events.RenderEvent;
+import com.lambda.graphics.RenderMain;
+import com.lambda.graphics.outline.OutlineManager;
+import com.lambda.graphics.outline.IWorldRenderer;
import com.lambda.module.modules.player.Freecam;
import com.lambda.module.modules.render.CameraTweaks;
import com.lambda.module.modules.render.NoRender;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
-import net.minecraft.client.render.Camera;
-import net.minecraft.client.render.WorldRenderer;
+import it.unimi.dsi.fastutil.Stack;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.minecraft.client.gl.Framebuffer;
+import net.minecraft.client.render.*;
+import net.minecraft.client.render.state.WorldRenderState;
+import net.minecraft.client.util.Handle;
+import net.minecraft.client.util.ObjectAllocator;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.effect.StatusEffects;
+import com.mojang.blaze3d.buffers.GpuBufferSlice;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.tick.TickManager;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Matrix4f;
+import org.joml.Vector4f;
+import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+import java.util.Iterator;
@Mixin(WorldRenderer.class)
-public class WorldRendererMixin {
+public abstract class WorldRendererMixin implements IWorldRenderer {
+ @Shadow
+ private Framebuffer entityOutlineFramebuffer;
+
+ @Shadow
+ @Final
+ private DefaultFramebufferSet framebufferSet;
+
+ @Unique
+ private Stack framebufferStack;
+
+ @Unique
+ private Stack> framebufferHandleStack;
+
+ @Unique
+ @Nullable
+ private Entity lambda$currentEntity;
+
+ @Inject(method = "", at = @At("TAIL"))
+ private void onInit(CallbackInfo info) {
+ framebufferStack = new ObjectArrayList<>();
+ framebufferHandleStack = new ObjectArrayList<>();
+ }
+
+ @Override
+ public void lambda$pushEntityOutlineFramebuffer(Framebuffer framebuffer) {
+ framebufferStack.push(this.entityOutlineFramebuffer);
+ this.entityOutlineFramebuffer = framebuffer;
+
+ framebufferHandleStack.push(this.framebufferSet.entityOutlineFramebuffer);
+ this.framebufferSet.entityOutlineFramebuffer = () -> framebuffer;
+ }
+
+ @Override
+ public void lambda$popEntityOutlineFramebuffer() {
+ this.entityOutlineFramebuffer = framebufferStack.pop();
+ this.framebufferSet.entityOutlineFramebuffer = framebufferHandleStack.pop();
+ }
+
+ @Inject(method = "render", at = @At("HEAD"))
+ private void onRender(ObjectAllocator allocator, RenderTickCounter tickCounter, boolean renderBlockOutline, Camera camera, Matrix4f positionMatrix, Matrix4f basicProjectionMatrix, Matrix4f projectionMatrix, GpuBufferSlice fogBuffer, Vector4f fogColor, boolean renderSky, CallbackInfo ci) {
+ RenderMain.updateState(positionMatrix, basicProjectionMatrix, projectionMatrix);
+ EventFlow.post(RenderEvent.PreRenderWorld.INSTANCE);
+ }
+
@Inject(method = "hasBlindnessOrDarkness(Lnet/minecraft/client/render/Camera;)Z", at = @At(value = "HEAD"), cancellable = true)
private void modifyEffectCheck(Camera camera, CallbackInfoReturnable cir) {
Entity entity = camera.getFocusedEntity();
@@ -50,6 +117,18 @@ private boolean renderSetupTerrainModifyArg(boolean spectator) {
return Freecam.INSTANCE.isEnabled() || CameraTweaks.INSTANCE.isEnabled() || spectator;
}
+ @Inject(method = "fillEntityRenderStates", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/EntityRenderManager;shouldRender(Lnet/minecraft/entity/Entity;Lnet/minecraft/client/render/Frustum;DDD)Z", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD)
+ private void injectFillEntityRenderStates(Camera camera, Frustum frustum, RenderTickCounter tickCounter, WorldRenderState renderStates, CallbackInfo ci, Vec3d vec3d, double d, double e, double f, TickManager tickManager, boolean bl, Iterator var14, Entity entity) {
+ this.lambda$currentEntity = entity;
+ }
+
+ @ModifyExpressionValue(method = "fillEntityRenderStates", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;isRenderingReady(Lnet/minecraft/util/math/BlockPos;)Z"))
+ private boolean lambda$bypassIsRenderingReady(boolean original) {
+ if (this.lambda$currentEntity != null && OutlineManager.shouldCapture(this.lambda$currentEntity.getId()))
+ return true;
+ return original;
+ }
+
@ModifyExpressionValue(method = "fillEntityRenderStates", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;isThirdPerson()Z"))
private boolean modifyIsThirdPerson(boolean original) {
return Freecam.INSTANCE.isEnabled() || original;
diff --git a/src/main/java/com/lambda/mixin/render/blockentity/BlockEntityRenderDispatcherMixin.java b/src/main/java/com/lambda/mixin/render/blockentity/BlockEntityRenderDispatcherMixin.java
index b2da13422..2964da03e 100644
--- a/src/main/java/com/lambda/mixin/render/blockentity/BlockEntityRenderDispatcherMixin.java
+++ b/src/main/java/com/lambda/mixin/render/blockentity/BlockEntityRenderDispatcherMixin.java
@@ -28,18 +28,10 @@
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-/**
- * Mixin to disable block entity rendering when NoRender is enabled.
- *
- * Note: In 1.21.11, BlockEntityRenderDispatcher was renamed to BlockEntityRenderManager
- * and uses a render state system. Returning null from getRenderState prevents rendering.
- */
@Mixin(BlockEntityRenderManager.class)
public class BlockEntityRenderDispatcherMixin {
@Inject(method = "getRenderState", at = @At("HEAD"), cancellable = true)
- private void injectGetRenderState(
- E blockEntity, float tickProgress, ModelCommandRenderer.@Nullable CrumblingOverlayCommand crumblingOverlay,
- CallbackInfoReturnable cir) {
+ private void injectGetRenderState(E blockEntity, float tickProgress, ModelCommandRenderer.@Nullable CrumblingOverlayCommand crumblingOverlay, CallbackInfoReturnable cir) {
if (NoRender.shouldOmitBlockEntity(blockEntity)) {
cir.setReturnValue(null);
}
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..9e0156aca 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
@@ -48,13 +48,13 @@ open class AutomationConfig(
Debug("Debug")
}
- override val buildConfig = BuildSettings(this, Group.Build)
- override val breakConfig = BreakSettings(this, Group.Break)
- override val interactConfig = InteractSettings(this, Group.Interact)
- override val rotationConfig = RotationSettings(this, Group.Rotation)
- override val inventoryConfig = InventorySettings(this, Group.Inventory)
- override val hotbarConfig = HotbarSettings(this, Group.Hotbar)
- override val eatConfig = EatSettings(this, Group.Eat)
+ override val buildConfig = BuildSettings(c = this, baseGroup = arrayOf(Group.Build))
+ override val breakConfig = BreakSettings(c = this, baseGroup = arrayOf(Group.Break))
+ override val interactConfig = InteractSettings(c = this, baseGroup = arrayOf(Group.Interact))
+ override val rotationConfig = RotationSettings(c = this, baseGroup = arrayOf(Group.Rotation))
+ override val inventoryConfig = InventorySettings(c = this, baseGroup = arrayOf(Group.Inventory))
+ override val hotbarConfig = HotbarSettings(c = this, baseGroup = arrayOf(Group.Hotbar))
+ override val eatConfig = EatSettings(c = this, baseGroup = arrayOf(Group.Eat))
companion object {
context(module: Module)
@@ -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..f93cb908a 100644
--- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt
@@ -31,8 +31,10 @@ import net.minecraft.block.Block
import java.awt.Color
open class BreakSettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), BreakConfig {
private enum class Group(override val displayName: String) : NamedEnum {
General("General"),
@@ -40,78 +42,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("${prefix}Break Mode", BreakMode.Packet, visibility = visibility).group(*baseGroup, Group.General).index()
+ override val sorter by c.setting("${prefix}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("${prefix}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("${prefix}Double Break", true, "Allows breaking two blocks at once", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val unsafeCancels by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}Break Confirmation", BreakConfirmationMode.BreakThenAwait, "The style of confirmation used when breaking", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val breaksPerTick by c.setting("${prefix}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("${prefix}Ignored Blocks", emptySet(), description = "Blocks that wont be broken", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val avoidLiquids by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}Force Silk Touch", false, "Force silk touch when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val forceFortunePickaxe by c.setting("${prefix}Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val minFortuneLevel by c.setting("${prefix}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("${prefix}Use Wooden Tools", true, "Use wooden tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val useStoneTools by c.setting("${prefix}Use Stone Tools", true, "Use stone tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val useIronTools by c.setting("${prefix}Use Iron Tools", true, "Use iron tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val useDiamondTools by c.setting("${prefix}Use Diamond Tools", true, "Use diamond tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val useGoldTools by c.setting("${prefix}Use Gold Tools", true, "Use gold tools when breaking blocks") { visibility() && swapMode.isEnabled() }.group(*baseGroup, Group.General).index()
+ override val useNetheriteTools by c.setting("${prefix}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("${prefix}Break Sounds", true, "Plays the breaking sounds", visibility = visibility).group(*baseGroup, Group.Cosmetic).index()
+ override val particles by c.setting("${prefix}Particles", true, "Renders the breaking particles", visibility = visibility).group(*baseGroup, Group.Cosmetic).index()
+ override val breakingTexture by c.setting("${prefix}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("${prefix}Renders", true, "Enables the render settings for breaking progress", visibility = visibility).group(*baseGroup, Group.Cosmetic).index()
+ override val animation by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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..7d55aca57 100644
--- a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt
@@ -24,8 +24,10 @@ import com.lambda.util.NamedEnum
import kotlin.math.max
class BuildSettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), BuildConfig {
enum class Group(override val displayName: String) : NamedEnum {
General("General"),
@@ -33,23 +35,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("${prefix}Break", true, "Break blocks", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val interactBlocks by c.setting("${prefix}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("${prefix}Pathing", false, "Path to blocks", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val stayInRange by c.setting("${prefix}Stay In Range", false, "Stay in range of blocks", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val collectDrops by c.setting("${prefix}Collect All Drops", false, "Collect all drops when breaking blocks", visibility = visibility).group(*baseGroup, Group.General).index()
+ override val spleefEntities by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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..f80baefb1 100644
--- a/src/main/kotlin/com/lambda/config/groups/EatSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/EatSettings.kt
@@ -23,24 +23,26 @@ import com.lambda.util.NamedEnum
import net.minecraft.item.Items
class EatSettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum
+ vararg 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("${prefix}Eat On Hunger", true, "Whether to eat when hungry", visibility = visibility).group(*baseGroup).index()
+ override val minFoodLevel by c.setting("${prefix}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("${prefix}Saturated", EatConfig.Saturation.EatSmart, "When to stop eating") { visibility() && eatOnHunger }.group(*baseGroup).index()
+ override val nutritiousFood by c.setting("${prefix}Nutritious Food", nutritiousFoodDefaults, nutritiousFoodDefaults, "Items that are be considered nutritious") { visibility() && eatOnHunger }.group(*baseGroup).index()
+ override val selectionPriority by c.setting("${prefix}Selection Priority", EatConfig.SelectionPriority.MostNutritious, "The priority for selecting food items") { visibility() && eatOnHunger }.group(*baseGroup).index()
+ override val eatOnFire by c.setting("${prefix}Eat On Fire", true, "Whether to eat when on fire", visibility = visibility).group(*baseGroup).index()
+ override val resistanceFood by c.setting("${prefix}Resistance Food", resistanceFoodDefaults, resistanceFoodDefaults, "Items that give Fire Resistance") { visibility() && eatOnFire }.group(*baseGroup).index()
+ override val eatOnDamage by c.setting("${prefix}Eat On Damage", true, "Whether to eat when damaged", visibility = visibility).group(*baseGroup).index()
+ override val minDamage by c.setting("${prefix}Minimum Damage", 10, 0..20, 1, "The minimum damage threshold to trigger eating") { visibility() && eatOnDamage }.group(*baseGroup).index()
+ override val regenerationFood by c.setting("${prefix}Regeneration Food", regenerationFoodDefaults, regenerationFoodDefaults, "Items that give Regeneration") { visibility() && eatOnDamage }.group(*baseGroup).index()
+ override val ignoreBadFood by c.setting("${prefix}Ignore Bad Food", true, "Whether to eat when the food is bad", visibility = visibility).group(*baseGroup).index()
+ override val badFood by c.setting("${prefix}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/EntityColorSettings.kt b/src/main/kotlin/com/lambda/config/groups/EntityColorSettings.kt
new file mode 100644
index 000000000..eaf221756
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/EntityColorSettings.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.config.groups
+
+import com.lambda.config.Configurable
+import com.lambda.config.SettingGroup
+import com.lambda.context.SafeContext
+import com.lambda.friend.FriendManager.isFriend
+import com.lambda.util.EntityUtils
+import com.lambda.util.EntityUtils.entityGroup
+import com.lambda.util.NamedEnum
+import com.lambda.util.extension.entityColor
+import com.lambda.util.math.dist
+import com.lambda.util.math.lerp
+import net.minecraft.client.network.OtherClientPlayerEntity
+import net.minecraft.entity.Entity
+import java.awt.Color
+
+class EntityColorSettings(
+ prefix: String = "",
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : EntityColorsConfig, SettingGroup(c) {
+ override val useNaturalColors by c.setting("${prefix}Use Natural Colors", false, "Uses an average color from the entities texture").group(*baseGroup).index()
+ override val playerColor by c.setting("${prefix}Player Color", Color(255, 50, 50)) { !useNaturalColors }.group(*baseGroup).index()
+ override val playerDistanceGradient by c.setting("${prefix}Player Distance Gradient", true).group(*baseGroup).index()
+ override val playerDistanceColorFar by c.setting("${prefix}Player Far Color", Color.GREEN) { playerDistanceGradient }.group(*baseGroup).index()
+ override val playerDistanceColorClose by c.setting("${prefix}Player Close Color", Color.RED) { playerDistanceGradient }.group(*baseGroup).index()
+ override val separateFriendColor by c.setting("${prefix}Separate Friend Color", true) { useNaturalColors }.group(*baseGroup).index()
+ override val friendColor by c.setting("${prefix}Friend Color", Color(0, 255, 255)) { !useNaturalColors || separateFriendColor }.group(*baseGroup).index()
+ override val mobColor by c.setting("${prefix}Mob Color", Color(255, 70, 50)) { !useNaturalColors }.group(*baseGroup).index()
+ override val passiveColor by c.setting("${prefix}Passive Color", Color(0, 255, 0)) { !useNaturalColors }.group(*baseGroup).index()
+ override val vehicleColor by c.setting("${prefix}Vehicle Color", Color(200, 150, 100)) { !useNaturalColors }.group(*baseGroup).index()
+ override val projectileColor by c.setting("${prefix}Projectile Color", Color(200, 200, 200)) { !useNaturalColors }.group(*baseGroup).index()
+ override val bossColor by c.setting("${prefix}Boss Color", Color(255, 100, 0)) { !useNaturalColors }.group(*baseGroup).index()
+ override val decorationColor by c.setting("${prefix}Decoration Color", Color(100, 100, 255)) { !useNaturalColors }.group(*baseGroup).index()
+ override val blockColor by c.setting("${prefix}Block Color", Color(200, 200, 200)) { !useNaturalColors }.group(*baseGroup).index()
+ override val miscColor by c.setting("${prefix}Misc Color", Color(255, 0, 255)) { !useNaturalColors }.group(*baseGroup).index()
+
+ context(safeContext: SafeContext)
+ fun getColor(entity: Entity): Color {
+ val group = entity.entityGroup
+ return if (useNaturalColors && !hasSpecialCase(entity, group)) entityColor(entity)
+ else when (group) {
+ EntityUtils.EntityGroup.Player ->
+ when {
+ entity is OtherClientPlayerEntity && entity.isFriend && separateFriendColor -> friendColor
+ else ->
+ if (playerDistanceGradient)
+ lerp(
+ entity.dist(safeContext.player) / 60.0,
+ playerDistanceColorClose,
+ playerDistanceColorFar
+ )
+ else playerColor
+ }
+ EntityUtils.EntityGroup.Mob -> mobColor
+ EntityUtils.EntityGroup.Passive -> passiveColor
+ EntityUtils.EntityGroup.Vehicle -> vehicleColor
+ EntityUtils.EntityGroup.Projectile -> projectileColor
+ EntityUtils.EntityGroup.Boss -> bossColor
+ EntityUtils.EntityGroup.Decoration -> decorationColor
+ EntityUtils.EntityGroup.Block -> blockColor
+ EntityUtils.EntityGroup.Misc -> miscColor
+ }
+ }
+
+ private fun hasSpecialCase(entity: Entity, group: EntityUtils.EntityGroup) =
+ group == EntityUtils.EntityGroup.Player &&
+ ((entity is OtherClientPlayerEntity && entity.isFriend && separateFriendColor) || playerDistanceGradient)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/EntityColorsConfig.kt b/src/main/kotlin/com/lambda/config/groups/EntityColorsConfig.kt
new file mode 100644
index 000000000..4f4ce0fb7
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/EntityColorsConfig.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.module.modules.render.ESP.Group
+import java.awt.Color
+
+interface EntityColorsConfig {
+ val useNaturalColors: Boolean
+ val playerColor: Color
+ val playerDistanceGradient: Boolean
+ val playerDistanceColorFar: Color
+ val playerDistanceColorClose: Color
+ val separateFriendColor: Boolean
+ val friendColor: Color
+ val mobColor: Color
+ val passiveColor: Color
+ val vehicleColor: Color
+ val projectileColor: Color
+ val bossColor: Color
+ val decorationColor: Color
+ val blockColor: Color
+ val miscColor: Color
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/graphics/gl/GLObject.kt b/src/main/kotlin/com/lambda/config/groups/EntitySelectionConfig.kt
similarity index 58%
rename from src/main/kotlin/com/lambda/graphics/gl/GLObject.kt
rename to src/main/kotlin/com/lambda/config/groups/EntitySelectionConfig.kt
index edfa95445..c14656e94 100644
--- a/src/main/kotlin/com/lambda/graphics/gl/GLObject.kt
+++ b/src/main/kotlin/com/lambda/config/groups/EntitySelectionConfig.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,8 +15,17 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.gl
+package com.lambda.config.groups
-interface GLObject {
- val gl: Int
-}
+interface EntitySelectionConfig {
+ val self: Boolean
+ val playerEntities: Collection
+ val bossEntities: Collection
+ val decorationEntities: Collection
+ val mobEntities: Collection
+ val passiveEntities: Collection
+ val projectileEntities: Collection
+ val vehicleEntities: Collection
+ val miscEntities: Collection
+ val blockEntities: Collection
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/lambda/config/groups/EntitySelectionSettings.kt b/src/main/kotlin/com/lambda/config/groups/EntitySelectionSettings.kt
new file mode 100644
index 000000000..ab2cfc498
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/EntitySelectionSettings.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.Lambda.mc
+import com.lambda.config.Configurable
+import com.lambda.config.SettingGroup
+import com.lambda.util.EntityUtils.blockEntityMap
+import com.lambda.util.EntityUtils.bossEntityMap
+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 net.minecraft.block.entity.BlockEntity
+import net.minecraft.entity.Entity
+import net.minecraft.entity.SpawnGroup
+
+class EntitySelectionSettings(
+ prefix: String = "",
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : EntitySelectionConfig, SettingGroup(c) {
+ override val self by c.setting("${prefix}Self", false, "Render own player in third person").group(*baseGroup).index()
+ override val playerEntities by c.setting("${prefix}Player Entities", playerEntityMap.values.toSet(), playerEntityMap.values.toSet(), "Player entities to omit from rendering").group(*baseGroup).index()
+ override val mobEntities by c.setting("${prefix}Mob Entities", mobEntityMap.values.toSet(), mobEntityMap.values.toSet(), "Mob entities to omit from rendering").group(*baseGroup).index()
+ override val passiveEntities by c.setting("${prefix}Passive Entities", emptySet(), passiveEntityMap.values.toSet(), "Passive entities to omit from rendering").group(*baseGroup).index()
+ override val vehicleEntities by c.setting("${prefix}Vehicle Entities", emptySet(), vehicleEntityMap.values.toSet(), "Vehicle entities to omit from rendering").group(*baseGroup).index()
+ override val projectileEntities by c.setting("${prefix}Projectile Entities", emptySet(), projectileEntityMap.values.toSet(), "Projectile entities to omit from rendering").group(*baseGroup).index()
+ override val bossEntities by c.setting("${prefix}Boss Entities", emptySet(), bossEntityMap.values.toSet(), "Boss entities to omit from rendering").group(*baseGroup).index()
+ override val decorationEntities by c.setting("${prefix}Decoration Entities", emptySet(), decorationEntityMap.values.toSet(), "Decoration entities to omit from rendering").group(*baseGroup).index()
+ override val blockEntities by c.setting("${prefix}Block Entities", emptySet(), blockEntityMap.values.toSet(), "Block entities to omit from rendering").group(*baseGroup).index()
+ override val miscEntities by c.setting("${prefix}Misc Entities", emptySet(), miscEntityMap.values.toSet(), "Miscellaneous entities to omit from rendering").group(*baseGroup).index()
+
+ fun isSelected(entity: Entity): Boolean {
+ val name = entity::class.simpleName
+ return if (entity == mc.player && !self) false
+ else when (entity.type.spawnGroup) {
+ SpawnGroup.MISC ->
+ miscEntityMap[name] in miscEntities ||
+ playerEntityMap[name] in playerEntities ||
+ projectileEntityMap[name] in projectileEntities ||
+ vehicleEntityMap[name] in vehicleEntities ||
+ decorationEntityMap[name] in decorationEntities ||
+ passiveEntityMap[name] in passiveEntities ||
+ mobEntityMap[name] in mobEntities ||
+ bossEntityMap[name] in bossEntities
+ SpawnGroup.WATER_AMBIENT,
+ SpawnGroup.WATER_CREATURE,
+ SpawnGroup.AMBIENT,
+ SpawnGroup.AXOLOTLS,
+ SpawnGroup.CREATURE,
+ SpawnGroup.UNDERGROUND_WATER_CREATURE -> passiveEntityMap[name] in passiveEntities
+ SpawnGroup.MONSTER -> mobEntityMap[name] in mobEntities
+ }
+ }
+
+ fun isSelected(blockEntity: BlockEntity) =
+ blockEntityMap[blockEntity.javaClass.simpleName] in blockEntities
+}
\ 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..48d20e11b 100644
--- a/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/FormatterSettings.kt
@@ -22,23 +22,25 @@ import com.lambda.config.SettingGroup
import com.lambda.util.NamedEnum
class FormatterSettings(
- c: Configurable,
+ prefix: String = "",
+ 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("${prefix}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("${prefix}Separator", FormatterConfig.TupleSeparator.Comma, "Separator for string serialization of tuple data structures", visibility = visibility).group(*baseGroup).index()
+ val customSep by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}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..48119f6d8 100644
--- a/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt
@@ -25,13 +25,15 @@ import com.lambda.interaction.managers.hotbar.HotbarConfig
import com.lambda.util.NamedEnum
class HotbarSettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum
+ vararg 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("${prefix}Swap Mode", HotbarConfig.SwapMode.Temporary, visibility = visibility).group(*baseGroup).index()
+ override val keepTicks by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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..dfd1a8f6a 100644
--- a/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/InteractSettings.kt
@@ -27,18 +27,20 @@ import com.lambda.interaction.managers.interacting.InteractConfig.InteractConfir
import com.lambda.util.NamedEnum
class InteractSettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum
+ vararg 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("${prefix}Rotate For Interact", true, "Rotate towards block while placing", visibility = visibility).group(*baseGroup).index()
+ override val airPlace by c.setting("${prefix}Air Place", AirPlaceMode.None, "Allows for placing blocks without adjacent faces", visibility = visibility).group(*baseGroup).index()
+ override val axisRotateSetting by c.setting("${prefix}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("${prefix}Interaction Sorter", ActionConfig.SortMode.Tool, "The order in which placements are performed", visibility = visibility).group(*baseGroup).index()
+ override val tickStageMask by c.setting("${prefix}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("${prefix}Interact Confirmation", InteractConfirmationMode.PlaceThenAwait, "Wait for block placement confirmation", visibility = visibility).group(*baseGroup).index()
+ override val interactDelay by c.setting("${prefix}Interact Delay", 0, 0..3, 1, "Tick delay between interacting with another block", visibility = visibility).group(*baseGroup).index()
+ override val interactionsPerTick by c.setting("${prefix}Interactions Per Tick", 1, 1..30, 1, "Maximum instant block places per tick", visibility = visibility).group(*baseGroup).index()
+ override val swing by c.setting("${prefix}Swing On Interact", true, "Swings the players hand when placing", visibility = visibility).group(*baseGroup).index()
+ override val swingType by c.setting("${prefix}Interact Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { visibility() && swing }.group(*baseGroup).index()
+ override val sounds by c.setting("${prefix}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..1f16d1209 100644
--- a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt
@@ -25,8 +25,10 @@ import com.lambda.util.NamedEnum
import com.lambda.util.item.ItemUtils
class InventorySettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
) : SettingGroup(c), InventoryConfig {
enum class Group(override val displayName: String) : NamedEnum {
General("General"),
@@ -34,15 +36,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("${prefix}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("${prefix}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("${prefix}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("${prefix}Swap With Disposables", true, "Swap items with disposable ones", visibility = visibility).group(*baseGroup, Group.Container).index()
+ override val providerPriority by c.setting("${prefix}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("${prefix}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("${prefix}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("${prefix}Access Chests", false, "Allow access to the player's normal chests", visibility = visibility).group(*baseGroup, Group.Access).index()
+ override val accessEnderChest by c.setting("${prefix}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("${prefix}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..30f87205a
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/LineConfig.kt
@@ -0,0 +1,36 @@
+/*
+ * 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
+
+ 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/OutlineSettings.kt b/src/main/kotlin/com/lambda/config/groups/OutlineSettings.kt
new file mode 100644
index 000000000..f5449e2ba
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/OutlineSettings.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.graphics.outline.OutlineStyle
+import com.lambda.util.NamedEnum
+import java.awt.Color
+
+class OutlineSettings(
+ prefix: String = "",
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ override val visibility: () -> Boolean = { true },
+) : SettingGroup(c) {
+ val thicknessSetting by c.setting("${prefix}Line Width", 10, 1..100, 1, "The width of the outline", visibility = visibility).group(*baseGroup).index()
+ val thickness get() = thicknessSetting * 0.00005f
+
+ val glowIntensitySetting by c.setting("${prefix}Glow Intensity", 50, 0..100, 1, "Intensity of the outline glow", visibility = visibility).group(*baseGroup).index()
+ val glowIntensity get() = glowIntensitySetting * 0.01f
+
+ val glowRadiusSetting by c.setting("${prefix}Glow Radius", 20, 0..100, 1, "Radius of the outline glow", visibility = visibility).group(*baseGroup).index()
+ val glowRadius get() = glowRadiusSetting * 0.00005f
+
+ val fill by c.setting("${prefix}Fill", true, "Fill the entity silhouette", visibility = visibility).group(*baseGroup).index()
+
+ val fillOpacitySetting by c.setting("${prefix}Fill Opacity", 40, 0..100, 1, "Opacity of the fill") { visibility() && fill }.group(*baseGroup).index()
+ val fillOpacity get() = fillOpacitySetting * 0.01f
+
+ fun toStyle(color: Color) = OutlineStyle(
+ color = color,
+ thickness = thickness,
+ glowIntensity = glowIntensity,
+ glowRadius = glowRadius,
+ fill = fill,
+ fillOpacity = fillOpacity
+ )
+}
diff --git a/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt b/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt
index 8c6fb1746..8ab3fade3 100644
--- a/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt
+++ b/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt
@@ -32,33 +32,35 @@ import kotlin.math.sqrt
import kotlin.random.Random
class RotationSettings(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum,
+ vararg 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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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("${prefix}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..3ce2470bc
--- /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}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..b25475186
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/ScreenTextSettings.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 {
+ General("General"),
+ 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, Group.General).index()
+ val sizeSetting by c.setting("${prefix}Text Size", 18, 1..50, 1, visibility = visibility).group(*baseGroup, Group.General).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..b897c3c60 100644
--- a/src/main/kotlin/com/lambda/config/groups/Targeting.kt
+++ b/src/main/kotlin/com/lambda/config/groups/Targeting.kt
@@ -19,22 +19,23 @@ package com.lambda.config.groups
import com.lambda.config.Configurable
import com.lambda.config.SettingGroup
+import com.lambda.config.applyEdits
import com.lambda.context.SafeContext
import com.lambda.friend.FriendManager.isFriend
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
import com.lambda.util.world.fastEntitySearch
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.client.network.OtherClientPlayerEntity
+import net.minecraft.client.toast.SystemToast.hide
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.*
/**
@@ -50,83 +51,34 @@ import java.util.*
* @param maxRange The maximum range within which entities can be targeted.
*/
abstract class Targeting(
- private val c: Configurable,
- baseGroup: NamedEnum,
- private val defaultRange: Double,
- private val maxRange: Double,
+ prefix: String = "",
+ c: Configurable,
+ vararg baseGroup: NamedEnum,
+ defaultRange: Double,
+ maxRange: Double,
+ 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("${prefix}Targeting Range", defaultRange, 1.0..maxRange, 0.05, visibility = visibility).group(*baseGroup).index()
+ override val targets = EntitySelectionSettings(c = c, baseGroup = baseGroup).apply {
+ c.applyEdits {
+ hide(::self, ::blockEntities)
+ }
}
+ /**
+ * 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) =
+ targets.isSelected(entity) && (entity !is OtherClientPlayerEntity || !entity.isFriend)
+
/**
* Subclass for targeting entities specifically for combat purposes.
*
@@ -134,21 +86,22 @@ abstract class Targeting(
* @property priority The priority used to determine which entity is targeted when multiple candidates are available.
*/
class Combat(
+ prefix: String = "",
c: Configurable,
- baseGroup: NamedEnum,
+ vararg baseGroup: NamedEnum,
defaultRange: Double = 5.0,
maxRange: Double = 16.0,
- ) : Targeting(c, baseGroup, defaultRange, maxRange) {
-
+ override val visibility: () -> Boolean = { true },
+ ) : Targeting(prefix, c, *baseGroup, defaultRange = defaultRange, maxRange = maxRange, visibility = 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("${prefix}FOV Limit", 180, 5..180, 1) { visibility() && priority == Priority.Fov }.group(*baseGroup).index()
/**
* 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("${prefix}Priority", Priority.Distance, visibility = visibility).group(*baseGroup).index()
/**
* Validates whether a given entity is targetable for combat based on the field of view limit and other settings.
@@ -160,6 +113,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 +132,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..b0a74ce2b 100644
--- a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt
+++ b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt
@@ -19,12 +19,5 @@ package com.lambda.config.groups
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: EntitySelectionConfig
}
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..7d33c2c39
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/TextConfig.kt
@@ -0,0 +1,48 @@
+/*
+ * 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
+
+ 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..3ccda385a
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt
@@ -0,0 +1,58 @@
+/*
+ * 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 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 {
+ General("General"),
+ 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, Group.General).index()
+ val worldWidthSetting by c.setting("${prefix}Width", 5, 1..50, 1, "Line width in world units (blocks)") { visibility() && !distanceScaling }.group(*baseGroup, Group.General).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, Group.General).index()
+
+ override val width: Float get() =
+ if (distanceScaling) -screenWidthSetting * 0.00005f
+ else worldWidthSetting * 0.001f
+
+ 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..ffadc3617
--- /dev/null
+++ b/src/main/kotlin/com/lambda/config/groups/WorldTextSettings.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 {
+ General("General"),
+ 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, Group.General).index()
+ val sizeSetting by c.setting("${prefix}Text Size", 5, 1..50, 1, visibility = visibility).group(*baseGroup, Group.General).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..e8a436fbb 100644
--- a/src/main/kotlin/com/lambda/event/events/RenderEvent.kt
+++ b/src/main/kotlin/com/lambda/event/events/RenderEvent.kt
@@ -17,23 +17,14 @@
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 PreRenderWorld : 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..8fad2c5a0 100644
--- a/src/main/kotlin/com/lambda/graphics/RenderMain.kt
+++ b/src/main/kotlin/com/lambda/graphics/RenderMain.kt
@@ -17,95 +17,45 @@
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 net.minecraft.util.math.Vec3d
+import com.lambda.graphics.mc.renderer.RendererUtils
+import com.lambda.graphics.outline.OutlineIdBuffer
+import com.lambda.graphics.outline.OutlineManager
+import com.lambda.graphics.outline.OutlineRenderer
+import com.lambda.graphics.outline.VertexCapture
import org.joml.Matrix4f
-import org.joml.Vector2f
-import org.joml.Vector4f
object RenderMain {
- @JvmStatic
- val StaticESP = TransientRegionESP("Static")
-
- @JvmStatic
- val DynamicESP = TransientRegionESP("Dynamic")
+ val baseProjectionMatrix = Matrix4f()
+ val worldProjectionMatrix = Matrix4f()
+ val cameraRotationMatrix = Matrix4f()
- val projectionMatrix = Matrix4f()
- val modelViewMatrix
- get() = Matrices.peek()
val projModel: Matrix4f
- get() = Matrix4f(projectionMatrix).mul(modelViewMatrix)
-
- /**
- * Project a world position to screen coordinates. Returns null if the position is behind the
- * camera or off-screen.
- *
- * @param worldPos The world position to project
- * @return Screen coordinates (x, y) in pixels, or null if not visible
- */
- fun worldToScreen(worldPos: Vec3d): Vector2f? {
- val camera = mc.gameRenderer?.camera ?: return null
- val cameraPos = camera.pos
-
- // Camera-relative position
- val relX = (worldPos.x - cameraPos.x).toFloat()
- val relY = (worldPos.y - cameraPos.y).toFloat()
- val relZ = (worldPos.z - cameraPos.z).toFloat()
-
- // Apply projection * modelview matrix
- 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
+ get() = Matrix4f(worldProjectionMatrix).mul(cameraRotationMatrix)
- // Off-screen check (NDC is -1 to 1)
- if (ndcZ < -1 || ndcZ > 1) return null
-
- // 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
-
- return Vector2f(screenX, screenY)
+ @JvmStatic
+ fun preRender() {
+ OutlineManager.clear()
+ VertexCapture.clear()
+ OutlineIdBuffer.beginFrame()
}
- /** Check if a world position is visible on screen. */
- fun isOnScreen(worldPos: Vec3d): Boolean = worldToScreen(worldPos) != null
-
@JvmStatic
- fun render3D(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()
+ fun updateState(camRotMatrix: Matrix4f, basicProjMatrix: Matrix4f, projMatrix: Matrix4f) {
+ baseProjectionMatrix.set(projMatrix)
+ worldProjectionMatrix.set(basicProjMatrix)
+ cameraRotationMatrix.set(camRotMatrix)
}
- init {
- listen {
- StaticESP.clear()
- DynamicESP.clear()
+ @JvmStatic
+ fun render() {
+ RendererUtils.clearXrayDepthBuffer()
- RenderEvent.Upload.post()
+ RenderEvent.RenderWorld.post()
- StaticESP.upload()
- DynamicESP.upload()
- }
+ OutlineRenderer.renderAllIDPasses()
+
+ RenderEvent.RenderScreen.post()
}
}
diff --git a/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt b/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt
deleted file mode 100644
index a03f32e9e..000000000
--- a/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt
+++ /dev/null
@@ -1,243 +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.buffer
-
-import org.lwjgl.opengl.GL46.*
-import java.nio.ByteBuffer
-
-class Buffer private constructor(
- val target: Int,
- var buffer: Int,
-
- /**
- * Specifies a combination of access flags indicating the desired
- * access to the mapping range and must contain one or more of the following:
- *
- * | Flag | Description | Information |
- * |--------------------------------|-----------------------------------------------------|-------------------------------------------------------------------------------------------|
- * | [GL_MAP_READ_BIT] | Allows reading buffer data. | Buffer must be created with this flag. Undefined if not included in access. |
- * | [GL_MAP_WRITE_BIT] | Allows modifying buffer data. | Buffer must be created with this flag. Undefined if not included in access. |
- * | [GL_MAP_PERSISTENT_BIT] | Enables persistent mapping during GL operations. | Requires buffer to be created with [GL_MAP_PERSISTENT_BIT]. |
- * | [GL_MAP_COHERENT_BIT] | Ensures changes are visible to the GPU. | Requires buffer creation with [GL_MAP_PERSISTENT_BIT] or explicit sync. |
- * | [GL_MAP_INVALIDATE_RANGE_BIT] | Discards previous contents of the mapped range. | Cannot be used with [GL_MAP_READ_BIT]. |
- * | [GL_MAP_INVALIDATE_BUFFER_BIT] | Discards previous contents of the entire buffer. | Cannot be used with [GL_MAP_READ_BIT]. |
- * | [GL_MAP_FLUSH_EXPLICIT_BIT] | Requires explicit flushing of modified sub-ranges. | Only valid with [GL_MAP_WRITE_BIT]. Data may be undefined if flushing is skipped. |
- * | [GL_MAP_UNSYNCHRONIZED_BIT] | Skips synchronization before mapping. | May cause data corruption if buffer is accessed concurrently. |
- * | [GL_DYNAMIC_STORAGE_BIT] | Allows updates via [glBufferSubData]. | If omitted, [glBufferSubData] will fail. |
- * | [GL_CLIENT_STORAGE_BIT] | Hints that the buffer should prefer client storage. | Implementation-dependent optimization. |
- *
- * @see Buffer object
- */
- val access: Int,
-) {
- fun bind() = glBindBuffer(target, buffer)
- fun unbind() = glBindBuffer(target, 0)
-
- /**
- * Execute the [block] in a bound context
- */
- fun bind(block: Buffer.() -> Unit) {
- bind()
- block(this)
- unbind()
- }
-
- /**
- * Allocates the buffer with the specified data.
- *
- * @param data The data to put in the new allocated buffer
- *
- * @see glBufferData
- */
- fun allocate(data: ByteBuffer) =
- bind { glBufferData(target, data, GL_DYNAMIC_DRAW) }
-
- /**
- * Allocates the buffer with the specified size.
- *
- * @param data The data to put in the new allocated buffer
- *
- * @see glBufferData
- */
- fun allocate(size: Long) =
- bind { glBufferData(target, size, GL_DYNAMIC_DRAW) }
-
- /**
- * Update the current buffer without re-allocating.
- *
- * @throws [IllegalArgumentException] if the target or usage is invalid
- *
- * @see glBufferSubData
- */
- fun update(offset: Long, data: ByteBuffer) {
- check(offset > -1) { "Cannot have negative buffer offsets" }
- check(access and GL_DYNAMIC_STORAGE_BIT != 0) { "Buffer contents cannot be modified because the buffer was created without the GL_DYNAMIC_STORAGE_BIT set." }
-
- bind { glBufferSubData(target, offset, data) }
- }
-
- /**
- * Update the current buffer without re-allocating.
- *
- * @throws [IllegalArgumentException] if the target or usage is invalid
- *
- * @see glBufferSubData
- */
- fun update(offset: Long, size: Long, data: Long) {
- check(offset > -1) { "Cannot have negative buffer offsets" }
- check(size > -1) { "Cannot have negative sized buffers" }
- check(access and GL_DYNAMIC_STORAGE_BIT != 0) { "Buffer contents cannot be modified because the buffer was created without the GL_DYNAMIC_STORAGE_BIT set." }
-
- bind { nglBufferSubData(target, offset, size, data) }
- }
-
- /**
- * Allocates new storage for the OpenGL buffer using the provided data.
- *
- * This function cannot be called twice for the same buffer.
- *
- * You cannot update the content of the buffer directly unless you are pinning memory
- * or have GL_DYNAMIC_STORAGE_BIT in the access flags.
- *
- * @throws [IllegalArgumentException] if the target or usage is invalid
- *
- * @see glBufferStorage
- */
- fun storage(data: ByteBuffer) = bind { glBufferStorage(target, data, access) }
-
- /**
- * Allocates storage for the buffer object.
- *
- * This function cannot be called twice for the same buffer.
- *
- * You cannot update the content of the buffer directly unless you are pinning memory
- * or have GL_DYNAMIC_STORAGE_BIT in the access flags.
- *
- * @param size The size of the storage buffer
- * @throws [IllegalArgumentException] if the target or usage is invalid
- *
- * @see glBufferStorage
- */
- fun storage(size: Long) {
- check(size > -1) { "Cannot have negative sized buffers" }
- bind { glBufferStorage(target, size, access) }
- }
-
- fun storage(size: Int) = storage(size.toLong())
-
- /**
- * Maps a specified region of the buffer's data store into client memory, processes it using the provided lambda, and
- * then unmaps the buffer.
- *
- * If [access] contains the `GL_MAP_PERSISTENT_BIT` flag, the buffer will not be unmapped.
- *
- * @param size Specifies the length of the range to be mapped.
- * @param offset Specifies the starting offset within the buffer of the range to be mapped.
- * @param block Lambda scope with the mapped buffer passed in
- *
- * @see Direct memory access
- */
- fun map(offset: Long, size: Long, block: (ByteBuffer) -> Unit = {}): ByteBuffer {
- check(offset > -1) { "Cannot have negative buffer offsets" }
- check(size > -1) { "Cannot have negative sized buffers" }
-
- check(offset + size <= glGetBufferParameteri(target, GL_BUFFER_SIZE))
- { "Segmentation fault Size $size + Offset $offset > Buffer ${glGetBufferParameteri(target, GL_BUFFER_SIZE)}." }
-
- check(glGetBufferParameteri(target, GL_BUFFER_MAPPED) == GL_FALSE)
- { "Buffer is already mapped." }
-
- check(access and GL_MAP_WRITE_BIT != 0 || access and GL_MAP_READ_BIT != 0)
- { "Neither GL_MAP_READ_BIT nor GL_MAP_WRITE_BIT is set." }
-
- check((access and GL_MAP_READ_BIT != 0 &&
- (access and GL_MAP_INVALIDATE_RANGE_BIT != 0 ||
- access and GL_MAP_INVALIDATE_BUFFER_BIT != 0 ||
- access and GL_MAP_UNSYNCHRONIZED_BIT != 0)) ||
- access and GL_MAP_WRITE_BIT != 0
- ) { "GL_MAP_READ_BIT is set and any of GL_MAP_INVALIDATE_RANGE_BIT, GL_MAP_INVALIDATE_BUFFER_BIT or GL_MAP_UNSYNCHRONIZED_BIT is set." }
-
- bind()
-
- val sharedRegion = glMapBufferRange(target, offset, size, access)
- check(sharedRegion != null) { "Could not map the buffer" }
-
- block(sharedRegion)
-
- if (access and GL_MAP_PERSISTENT_BIT == 0)
- check(glUnmapBuffer(target)) { "An unknown error occurred due to GPU memory availability of buffer corruption." }
-
- unbind()
-
- return sharedRegion
- }
-
- /**
- * Indicates modifications to a range of a mapped buffer.
- */
- fun flushMappedRange(offset: Long, size: Long) = bind { glFlushMappedBufferRange(target, offset, size) }
-
- /**
- * Copies all or part of one buffer object's data store to the data store of another buffer object.
- */
- // fun copy(dst: Buffer, readOffset: Long, writeOffset: Long, size: Long) {}
-
- /**
- * Deletes the buffer object and creates a new one
- */
- fun orphan() {
- delete()
- create()
- }
-
- /**
- * Deletes the buffer and mark all allocated data as free
- */
- fun delete() {
- glDeleteBuffers(buffer)
- buffer = -69
- }
-
- /**
- * Creates a new buffer
- */
- fun create() {
- check(buffer == -69) { "Cannot create a new buffer if the previous one is not deleted" }
- buffer = glGenBuffers()
- }
-
- init {
- check(access and GL_MAP_COHERENT_BIT == 0 || access and GL_MAP_PERSISTENT_BIT != 0)
- { "GL_MAP_COHERENT_BIT requires GL_MAP_PERSISTENT_BIT flag." }
-
- check(access and GL_MAP_PERSISTENT_BIT == 0 || (access and (GL_MAP_READ_BIT or GL_MAP_WRITE_BIT) != 0))
- { "GL_MAP_PERSISTENT_BIT requires GL_MAP_READ_BIT or GL_MAP_WRITE_BIT." }
- }
-
- companion object {
- /**
- * Creates a buffer.
- *
- * If no [buffer] is provided, one will be created.
- *
- * @see [Buffer.access]
- */
- fun create(target: Int, access: Int, buffer: Int = glGenBuffers(), block: Buffer.() -> Unit = {}) =
- Buffer(target, buffer, access).apply { block() }
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/buffer/DynamicByteBuffer.kt b/src/main/kotlin/com/lambda/graphics/buffer/DynamicByteBuffer.kt
deleted file mode 100644
index 9ee396552..000000000
--- a/src/main/kotlin/com/lambda/graphics/buffer/DynamicByteBuffer.kt
+++ /dev/null
@@ -1,194 +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.buffer
-
-import org.lwjgl.BufferUtils.createByteBuffer
-import org.lwjgl.system.MemoryUtil.memAddress0
-import org.lwjgl.system.MemoryUtil.memCopy
-import org.lwjgl.system.MemoryUtil.memPutByte
-import org.lwjgl.system.MemoryUtil.memPutFloat
-import org.lwjgl.system.MemoryUtil.memPutInt
-import java.awt.Color
-import java.nio.ByteBuffer
-
-/**
- * Dynamically resizable byte buffer designed for efficient vertex building.
- * Automatically grows capacity when needed, and provides
- * convenience methods for common data types used in vertex attributes.
- *
- * @property data The underlying [java.nio.ByteBuffer] storing the vertex data
- * @property capacity Current maximum capacity of the buffer in bytes
- * @property pointer Base memory address of the buffer
- * @property position Current write position in memory address space
- */
-class DynamicByteBuffer private constructor(initialCapacity: Int) {
- var data: ByteBuffer = createByteBuffer(initialCapacity); private set
- var capacity = initialCapacity; private set
-
- var pointer = memAddress0(data); private set
- var position = pointer; private set
-
- /**
- * Gets the total number of bytes written to the buffer since last reset
- */
- val bytesPut get() = position - pointer
-
- /**
- * Resets the write position to the beginning of the buffer while maintaining current capacity,
- * allowing for buffer reuse without reallocation
- */
- fun resetPosition() {
- position = pointer
- }
-
- /**
- * Writes a single byte value at the current position
- * @param value The byte value to write
- */
- fun putByte(value: Byte) {
- require(1)
- memPutByte(position, value)
- position += 1
- }
-
- /**
- * Writes a 4-byte integer value at the current position
- * @param value The integer value to write
- */
- fun putInt(value: Int) {
- require(4)
- memPutInt(position, value)
- position += 4
- }
-
- /**
- * Writes a 4-byte floating point value at the current position
- * @param value The double-precision value to write (will be converted to float)
- */
- fun putFloat(value: Double) {
- require(4)
- memPutFloat(position, value.toFloat())
- position += 4
- }
-
- /**
- * Writes a 2-component vector as two consecutive 4-byte floats
- * @param x X-axis component
- * @param y Y-axis component
- */
- fun putVec2(x: Double, y: Double) {
- require(8)
- memPutFloat(position + 0, x.toFloat())
- memPutFloat(position + 4, y.toFloat())
- position += 8
- }
-
- /**
- * Writes a 3-component vector as three consecutive 4-byte floats
- * @param x X-axis component
- * @param y Y-axis component
- * @param z Z-axis component
- */
- fun putVec3(x: Double, y: Double, z: Double) {
- require(12)
- memPutFloat(position + 0, x.toFloat())
- memPutFloat(position + 4, y.toFloat())
- memPutFloat(position + 8, z.toFloat())
- position += 12
- }
-
- /**
- * Writes a color as four consecutive bytes in RGBA format
- * @param color The color to write
- */
- fun putColor(color: Color) {
- require(4)
- memPutByte(position + 0, color.red.toByte())
- memPutByte(position + 1, color.green.toByte())
- memPutByte(position + 2, color.blue.toByte())
- memPutByte(position + 3, color.alpha.toByte())
- position += 4
- }
-
- /**
- * Ensures the buffer has enough remaining space for the requested number of bytes.
- * Automatically grows the buffer if insufficient space remains.
- * @param size Number of bytes required for the next write operation
- */
- fun require(size: Int) {
- if (capacity - bytesPut > size) return
- grow(capacity * 2)
- }
-
- /**
- * Increases buffer capacity while preserving existing data. New capacity must be greater than current.
- * @param newCapacity New buffer capacity in bytes
- * @throws IllegalArgumentException if newCapacity is not greater than current capacity
- */
- fun grow(newCapacity: Int) {
- check(newCapacity > capacity) {
- "Cannot grow buffer beyond its capacity"
- }
-
- val newBuffer = createByteBuffer(newCapacity)
- val newPointer = memAddress0(newBuffer)
- val offset = position - pointer
-
- memCopy(pointer, newPointer, offset)
-
- data = newBuffer
- pointer = newPointer
- position = newPointer + offset
- capacity = newCapacity
- }
-
- /**
- * Reallocates the buffer with exact new capacity, resetting position and discarding existing data
- * @param newCapacity New buffer capacity in bytes
- */
- fun realloc(newCapacity: Int) {
- if (newCapacity == capacity) {
- resetPosition()
- return
- }
-
- val newBuffer = createByteBuffer(newCapacity)
- val newPointer = memAddress0(newBuffer)
-
- data = newBuffer
- pointer = newPointer
- position = newPointer
- capacity = newCapacity
- }
-
- /**
- * Returns the relative index of the first mismatch between this and the given buffer, otherwise -1 if no mismatch.
- *
- * @see [ByteBuffer.mismatch]
- */
- fun mismatch(other: DynamicByteBuffer) = data.mismatch(other.data)
-
- companion object {
- /**
- * Creates a new DynamicByteBuffer with specified initial capacity
- * @param initialCapacity Starting buffer size in bytes
- */
- fun dynamicByteBuffer(initialCapacity: Int) =
- DynamicByteBuffer(initialCapacity)
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt b/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt
deleted file mode 100644
index a0a5db567..000000000
--- a/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt
+++ /dev/null
@@ -1,68 +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.buffer.vertex
-
-import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
-import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import com.lambda.graphics.pipeline.PersistentBuffer
-import org.lwjgl.opengl.GL30C.GL_UNSIGNED_INT
-import org.lwjgl.opengl.GL30C.glBindVertexArray
-import org.lwjgl.opengl.GL30C.glGenVertexArrays
-import org.lwjgl.opengl.GL32C.glDrawElementsBaseVertex
-
-class VertexArray(
- private val vertexMode: VertexMode,
- private val attributes: VertexAttrib.Group
-) {
- private val vao = glGenVertexArrays()
- private var linkedVBO: PersistentBuffer? = null
-
- fun renderIndices(
- ibo: PersistentBuffer
- ) = linkedVBO?.let { vbo ->
- renderInternal(
- indicesSize = ibo.byteBuffer.bytesPut - ibo.uploadOffset,
- indicesPointer = ibo.byteBuffer.pointer + ibo.uploadOffset,
- verticesOffset = vbo.uploadOffset
- )
- } ?: throw IllegalStateException("Unable to use vertex array without having a VBO linked to it.")
-
- private fun renderInternal(
- indicesSize: Long,
- indicesPointer: Long,
- verticesOffset: Long
- ) {
- glBindVertexArray(vao)
- glDrawElementsBaseVertex(
- vertexMode.mode,
- indicesSize.toInt() / Int.SIZE_BYTES,
- GL_UNSIGNED_INT,
- indicesPointer,
- verticesOffset.toInt() / attributes.stride,
- )
- glBindVertexArray(0)
- }
-
- fun linkVbo(vbo: PersistentBuffer) {
- linkedVBO = vbo
-
- glBindVertexArray(vao)
- vbo.buffer.bind { attributes.link() }
- glBindVertexArray(0)
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt b/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt
deleted file mode 100644
index e1bde0196..000000000
--- a/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt
+++ /dev/null
@@ -1,94 +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.buffer.vertex.attributes
-
-import org.lwjgl.opengl.GL11C.GL_FLOAT
-import org.lwjgl.opengl.GL11C.GL_UNSIGNED_BYTE
-import org.lwjgl.opengl.GL20C.glEnableVertexAttribArray
-import org.lwjgl.opengl.GL20C.glVertexAttribPointer
-import org.lwjgl.opengl.GL33.glVertexAttribDivisor
-
-sealed class VertexAttrib(
- private val componentCount: Int,
- componentSize: Int,
- private val normalized: Boolean,
- private val single: Boolean,
- private val type: Int
-) {
- open class Float(
- normalized: Boolean = false, single: Boolean = false
- ) : VertexAttrib(1, 4, normalized, single, GL_FLOAT) {
- companion object : Float()
- }
-
- open class Vec2(
- normalized: Boolean = false, single: Boolean = false
- ) : VertexAttrib(2, 4, normalized, single, GL_FLOAT) {
- companion object : Vec2()
- }
-
- open class Vec3(
- normalized: Boolean = false, single: Boolean = false
- ) : VertexAttrib(3, 4, normalized, single, GL_FLOAT) {
- companion object : Vec3()
- }
-
- open class Color(
- normalized: Boolean = true, single: Boolean = false
- ) : VertexAttrib(4, 1, normalized, single, GL_UNSIGNED_BYTE) {
- companion object : Color()
- }
-
- val size = componentCount * componentSize
-
- fun link(index: Int, pointer: Long, stride: Int) {
- glEnableVertexAttribArray(index)
- glVertexAttribPointer(index, componentCount, type, normalized, stride, pointer)
- if (single) glVertexAttribDivisor(index, 1)
- }
-
- @Suppress("ClassName")
- open class Group(vararg val attributes: VertexAttrib) {
- // GUI
- object FONT : Group(
- Vec3, Vec2, Color
- )
-
- // WORLD
- object DYNAMIC_RENDERER : Group(
- Vec3, Vec3, Color
- )
-
- object STATIC_RENDERER : Group(
- Vec3, Color
- )
-
- object PARTICLE : Group(
- Vec3, Vec2, Color
- )
-
- val stride = attributes.sumOf { it.size }
-
- fun link() {
- attributes.foldIndexed(0L) { index, pointer, attrib ->
- attrib.link(index, pointer, stride)
- pointer + attrib.size
- }
- }
- }
-}
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/gl/GlStateUtils.kt b/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt
deleted file mode 100644
index c8e57ef5f..000000000
--- a/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt
+++ /dev/null
@@ -1,124 +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.gl
-
-import org.lwjgl.opengl.GL30C.GL_BLEND
-import org.lwjgl.opengl.GL30C.GL_CULL_FACE
-import org.lwjgl.opengl.GL30C.GL_DEPTH_TEST
-import org.lwjgl.opengl.GL30C.GL_LINE_SMOOTH
-import org.lwjgl.opengl.GL30C.GL_ONE_MINUS_SRC_ALPHA
-import org.lwjgl.opengl.GL30C.GL_SRC_ALPHA
-import org.lwjgl.opengl.GL30C.glBlendFunc
-import org.lwjgl.opengl.GL30C.glBlendFuncSeparate
-import org.lwjgl.opengl.GL30C.glDepthMask
-import org.lwjgl.opengl.GL30C.glDisable
-import org.lwjgl.opengl.GL30C.glEnable
-import org.lwjgl.opengl.GL30C.glLineWidth
-
-// ToDo: Migrate particle system so we can remove this
-object GlStateUtils {
- private var depthTestState = true
- private var blendState = false
- private var cullState = true
-
- fun setupGL(block: () -> Unit) {
- val savedDepthTest = depthTestState
- val savedBlend = blendState
- val savedCull = cullState
-
- glDepthMask(false)
- lineSmooth(true)
-
- depthTest(false)
- blend(true)
- cull(false)
-
- block()
-
- glDepthMask(true)
- lineSmooth(false)
-
- depthTest(savedDepthTest)
- blend(savedBlend)
- cull(savedCull)
- }
-
- fun withDepth(maskWrite: Boolean = false, block: () -> Unit) {
- depthTest(true)
- if (maskWrite) glDepthMask(true)
- block()
- if (maskWrite) glDepthMask(false)
- depthTest(false)
- }
-
- fun withFaceCulling(block: () -> Unit) {
- cull(true)
- block()
- cull(false)
- }
-
- fun withLineWidth(width: Double, block: () -> Unit) {
- glLineWidth(width.toFloat())
- block()
- glLineWidth(1f)
- }
-
- fun withBlendFunc(
- sfactorRGB: Int, dfactorRGB: Int,
- sfactorAlpha: Int = sfactorRGB, dfactorAlpha: Int = dfactorRGB,
- block: () -> Unit
- ) {
- glBlendFuncSeparate(sfactorRGB, dfactorRGB, sfactorAlpha, dfactorAlpha)
- block()
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
- }
-
- @JvmStatic
- fun capSet(id: Int, flag: Boolean) {
- val field = when (id) {
- GL_DEPTH_TEST -> ::depthTestState
- GL_BLEND -> ::blendState
- GL_CULL_FACE -> ::cullState
- else -> return
- }
-
- field.set(flag)
- }
-
- private fun blend(flag: Boolean) {
- if (flag) {
- glEnable(GL_BLEND)
- glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
- } else glDisable(GL_BLEND)
- }
-
- private fun cull(flag: Boolean) {
- if (flag) glEnable(GL_CULL_FACE)
- else glDisable(GL_CULL_FACE)
- }
-
- private fun depthTest(flag: Boolean) {
- if (flag) glEnable(GL_DEPTH_TEST)
- else glDisable(GL_DEPTH_TEST)
- }
-
- private fun lineSmooth(flag: Boolean) {
- if (flag) glEnable(GL_LINE_SMOOTH)
- else glDisable(GL_LINE_SMOOTH)
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt
deleted file mode 100644
index 38e5ce03c..000000000
--- a/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt
+++ /dev/null
@@ -1,200 +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.gl
-
-import com.lambda.Lambda.mc
-import net.minecraft.util.math.RotationAxis
-import net.minecraft.util.math.Vec3d
-import org.joml.Matrix4d
-import org.joml.Matrix4f
-
-/**
- * A utility object for managing OpenGL transformation matrices.
- * Provides a stack-based approach to matrix operations such as translation, scaling,
- * and world projection building, with optional support for vertex transformations.
- */
-object Matrices {
- /**
- * A stack of 4x4 transformation matrices.
- */
- private val stack = ArrayDeque(listOf(Matrix4f()))
-
- /**
- * An optional matrix for applying vertex transformations.
- */
- var vertexTransformer: Matrix4d? = null
-
- /**
- * An optional vec3 offset for applying vertex transformations.
- */
- var vertexOffset: Vec3d? = null
-
- /**
- * Executes a block of code within the context of a new matrix.
- * The current matrix is pushed onto the stack before the block executes and popped after the block completes.
- *
- * Push and pop operations are essential for managing hierarchical transformations in OpenGL.
- * - `push`: Saves the current matrix state to allow local transformations.
- * - `block`: Code that uses the modified matrix (ex: rendering)
- * - `pop`: Restores the previous state and effectively reverts any changes.
- *
- * @param block The block of code to execute within the context of the new matrix.
- */
- fun push(block: Matrices.() -> Unit) {
- push()
- block.invoke(this)
- pop()
- }
-
- /**
- * Pushes a copy of the current matrix onto the stack.
- */
- fun push() {
- val entry = stack.last()
- stack.addLast(Matrix4f(entry))
- }
-
- /**
- * Removes the top matrix from the stack.
- *
- * @throws NoSuchElementException If the stack is empty.
- */
- fun pop() {
- stack.removeLast()
- }
-
- /**
- * Applies a translation to the top matrix on the transformation stack
- *
- * @param x The translation amount along the X axis.
- * @param y The translation amount along the Y axis.
- * @param z The translation amount along the Z axis. Defaults to `0.0`.
- */
- fun translate(x: Double, y: Double, z: Double = 0.0) {
- translate(x.toFloat(), y.toFloat(), z.toFloat())
- }
-
- /**
- * Applies a translation to the top matrix on the transformation stack
- *
- * @param x The translation amount along the X axis.
- * @param y The translation amount along the Y axis.
- * @param z The translation amount along the Z axis. Defaults to `0f`.
- */
- fun translate(x: Float, y: Float, z: Float = 0f) {
- stack.last().translate(x, y, z)
- }
-
- /**
- * Scales the current matrix by the given x, y, and z factors.
- *
- * @param x The scaling factor along the X axis.
- * @param y The scaling factor along the Y axis.
- * @param z The scaling factor along the Z axis. Defaults to `1.0`.
- */
- fun scale(x: Double, y: Double, z: Double = 1.0) {
- stack.last().scale(x.toFloat(), y.toFloat(), z.toFloat())
- }
-
- /**
- * Scales the current matrix by the given x, y, and z factors.
- *
- * @param x The scaling factor along the X axis.
- * @param y The scaling factor along the Y axis.
- * @param z The scaling factor along the Z axis. Defaults to `1f`.
- */
- fun scale(x: Float, y: Float, z: Float = 1f) {
- stack.last().scale(x, y, z)
- }
-
- /**
- * Retrieves the current matrix from the stack without removing it.
- *
- * @throws NoSuchElementException if the stack is empty
- * @return The top matrix on the stack
- */
- fun peek(): Matrix4f = stack.last()
-
- /**
- * Resets the matrix stack with a single initial matrix.
- *
- * @param entry The matrix to initialize the stack with.
- */
- fun resetMatrices(entry: Matrix4f) {
- stack.clear()
- stack.add(entry)
- }
-
- /**
- * Temporarily sets a vertex transformation matrix for the duration of a block.
- *
- * @param matrix The transformation matrix to apply to vertices.
- * @param block The block of code to execute with the transformation applied.
- */
- fun withVertexTransform(matrix: Matrix4f, block: () -> Unit) {
- vertexTransformer = Matrix4d(matrix)
- block()
- vertexTransformer = null
- }
-
- /**
- * Applies a temporary vertex offset to mitigate precision issues in matrix operations on large coordinates
- *
- * @param offset the offset to apply to vertices for reducing precision loss
- * @param block the block of code within which the vertex offset is active
- */
- fun withVertexOffset(offset: Vec3d, block: () -> Unit) {
- vertexOffset = offset
- block()
- vertexOffset = null
- }
-
- /**
- * Builds a world projection matrix for a given position, scale, and rotation mode.
- *
- * @param pos The position in world coordinates.
- * @param scale The scaling factor. Defaults to `1.0`.
- * @param mode The rotation mode to apply. Defaults to [ProjRotationMode.ToCamera].
- * @return A [Matrix4f] representing the world projection.
- */
- fun buildWorldProjection(
- pos: Vec3d,
- scale: Double = 1.0,
- mode: ProjRotationMode = ProjRotationMode.ToCamera
- ): Matrix4f =
- Matrix4f().apply {
- val s = 0.025f * scale.toFloat()
-
- val rotation = when (mode) {
- ProjRotationMode.ToCamera -> mc.gameRenderer.camera.rotation
- ProjRotationMode.Up -> RotationAxis.POSITIVE_X.rotationDegrees(90f)
- }
-
- translate(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat())
- rotate(rotation)
- scale(-s, -s, s)
- }
-
- /**
- * Modes for determining the rotation of the world projection.
- */
- enum class ProjRotationMode {
- ToCamera,
- Up
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/gl/Memory.kt b/src/main/kotlin/com/lambda/graphics/gl/Memory.kt
deleted file mode 100644
index dd141b1d8..000000000
--- a/src/main/kotlin/com/lambda/graphics/gl/Memory.kt
+++ /dev/null
@@ -1,38 +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.gl
-
-import java.nio.ByteBuffer
-
-val Int.kilobyte get() = this * 1000
-val Int.megabyte get() = this * 1000 * 1000
-val Int.gigabyte get() = this * 1000 * 1000 * 1000
-
-val Int.kibibyte get() = this * 1024
-val Int.mebibyte get() = this * 1024 * 1024
-val Int.gibibyte get() = this * 1024 * 1024 * 1024
-
-val Long.kilobyte get() = this * 1000
-val Long.megabyte get() = this * 1000 * 1000
-val Long.gigabyte get() = this * 1000 * 1000 * 1000
-
-val Long.kibibyte get() = this * 1024
-val Long.mebibyte get() = this * 1024 * 1024
-val Long.gibibyte get() = this * 1024 * 1024 * 1024
-
-fun ByteBuffer.putTo(dst: ByteBuffer?) { dst?.put(this) }
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..563105130
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/BoxBuilder.kt
@@ -0,0 +1,280 @@
+/*
+ * 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.config.groups.LineConfig
+import com.lambda.graphics.util.DirectionMask
+import net.minecraft.util.math.Direction
+import java.awt.Color
+
+class BoxBuilder(lineConfig: LineConfig?) {
+ var outlineSides: Int = DirectionMask.ALL
+ var fillSides: Int = DirectionMask.ALL
+
+ var outlineMode: DirectionMask.OutlineMode = DirectionMask.OutlineMode.And
+ var lineWidth = lineConfig?.width ?: 0.005f
+
+ var dashStyle: LineDashStyle? = lineConfig?.getDashStyle()
+
+ var fillBottomNorthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var fillBottomNorthEast: Color = lineConfig?.startColor ?: Color.WHITE
+ var fillBottomSouthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var fillBottomSouthEast: Color = lineConfig?.startColor ?: Color.WHITE
+
+ var fillTopNorthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var fillTopNorthEast: Color = lineConfig?.startColor ?: Color.WHITE
+ var fillTopSouthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var fillTopSouthEast: Color = lineConfig?.startColor ?: Color.WHITE
+
+ var outlineBottomNorthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var outlineBottomNorthEast: Color = lineConfig?.startColor ?: Color.WHITE
+ var outlineBottomSouthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var outlineBottomSouthEast: Color = lineConfig?.startColor ?: Color.WHITE
+
+ var outlineTopNorthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var outlineTopNorthEast: Color = lineConfig?.startColor ?: Color.WHITE
+ var outlineTopSouthWest: Color = lineConfig?.startColor ?: Color.WHITE
+ var outlineTopSouthEast: Color = lineConfig?.startColor ?: Color.WHITE
+
+ @RenderDsl
+ fun allColors(color: Color) {
+ outlineColor(color)
+ fillColor(color)
+ }
+
+ @RenderDsl
+ fun colors(fill: Color, outline: Color) {
+ fillColor(fill)
+ outlineColor(outline)
+ }
+
+ @RenderDsl
+ fun fillColor(color: Color) {
+ fillBottomNorthWest = color
+ fillBottomNorthEast = color
+ fillBottomSouthWest = color
+ fillBottomSouthEast = color
+ fillTopNorthWest = color
+ fillTopNorthEast = color
+ fillTopSouthWest = color
+ fillTopSouthEast = color
+ }
+
+ @RenderDsl
+ fun outlineColor(color: Color) {
+ outlineBottomNorthWest = color
+ outlineBottomNorthEast = color
+ outlineBottomSouthWest = color
+ outlineBottomSouthEast = color
+ outlineTopNorthWest = color
+ outlineTopNorthEast = color
+ outlineTopSouthWest = color
+ outlineTopSouthEast = color
+ }
+
+ @RenderDsl
+ fun gradientY(bottom: Color, top: Color) {
+ fillGradientY(bottom, top)
+ outlineGradientY(bottom, top)
+ }
+
+ @RenderDsl
+ fun fillGradientY(bottom: Color, top: Color) {
+ fillBottomNorthWest = bottom
+ fillBottomNorthEast = bottom
+ fillBottomSouthWest = bottom
+ fillBottomSouthEast = bottom
+ fillTopNorthWest = top
+ fillTopNorthEast = top
+ fillTopSouthWest = top
+ fillTopSouthEast = 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)
+ }
+
+ @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
+ }
+
+ @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 lineWidth(lineWidth: Float) {
+ this.lineWidth = lineWidth
+ }
+
+ @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)
+ }
+
+ @RenderDsl
+ fun hideOutline() {
+ outlineSides = DirectionMask.NONE
+ }
+
+ @RenderDsl
+ fun hideFill() {
+ fillSides = DirectionMask.NONE
+ }
+
+ @RenderDsl
+ fun outlineOnly() {
+ outlineSides = DirectionMask.ALL
+ fillSides = DirectionMask.NONE
+ }
+
+ @RenderDsl
+ fun fillOnly() {
+ outlineSides = DirectionMask.NONE
+ fillSides = DirectionMask.ALL
+ }
+
+ @RenderDsl
+ fun showFillSides(vararg directions: Direction) {
+ directions.forEach { fillSides = fillSides or DirectionMask.run { it.mask } }
+ }
+
+ @RenderDsl
+ fun showFillSides(mask: Int) {
+ fillSides = fillSides or mask
+ }
+
+ @RenderDsl
+ fun hideFillSides(vararg directions: Direction) {
+ directions.forEach { fillSides = fillSides and DirectionMask.run { it.mask }.inv() }
+ }
+
+ @RenderDsl
+ fun hideFillSides(mask: Int) {
+ fillSides = fillSides and mask.inv()
+ }
+
+ @RenderDsl
+ fun showOutlineSides(vararg directions: Direction) {
+ directions.forEach { outlineSides = outlineSides or DirectionMask.run { it.mask } }
+ }
+
+ @RenderDsl
+ fun showOutlineSides(mask: Int) {
+ outlineSides = outlineSides or mask
+ }
+
+ @RenderDsl
+ fun hideOutlineSides(vararg directions: Direction) {
+ directions.forEach { outlineSides = outlineSides and DirectionMask.run { it.mask }.inv() }
+ }
+
+ @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..768954a40
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/ItemLighting.kt
@@ -0,0 +1,58 @@
+/*
+ * 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
+
+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 {
+ 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)
+ }
+
+ val NONE = ItemLighting(
+ light0 = Vector3f(0f, 1f, 0f),
+ light1 = Vector3f(0f, 1f, 0f),
+ ambient = 1.0f,
+ strength0 = 0f,
+ strength1 = 0f,
+ respectsUseLight = false
+ )
+
+ 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..5ded6e676
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/ItemOverlay.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.graphics.mc
+
+import net.minecraft.util.Identifier
+
+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 {
+ 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
+ )
+
+ val ENTITY_GLINT = ItemOverlay(
+ texture = Identifier.of("minecraft", "textures/misc/enchanted_glint_armor.png"),
+ scale = 8.0f,
+ speed = 1.0f,
+ angle = 10f,
+ alpha = 0.5f
+ )
+
+ 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..f50357078 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaRenderPipelines.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
@@ -23,97 +23,216 @@ 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
object LambdaRenderPipelines : Loadable {
- override val priority: Int
- get() = 100 // High priority to ensure pipelines are ready early
+ override val priority get() = 100
- /**
- * Base snippet for Lambda ESP rendering. Includes transforms, projection, and a custom
- * per-region uniform.
- */
- private val LAMBDA_ESP_SNIPPET =
- RenderPipeline.builder(RenderPipelines.TRANSFORMS_AND_PROJECTION_SNIPPET).buildSnippet()
+ private val LAMBDA_ESP_SNIPPET = RenderPipeline.builder(RenderPipelines.TRANSFORMS_AND_PROJECTION_SNIPPET).buildSnippet()
- /**
- * Pipeline for ESP lines/outlines.
- * - Uses MC's line rendering with per-vertex line width
- * - No depth write for overlapping
- * - No culling
- */
val ESP_LINES: RenderPipeline =
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"))
+ .withUniform("DynamicTransforms", UniformType.UNIFORM_BUFFER)
+ .withUniform("Fog", UniformType.UNIFORM_BUFFER)
+ .withUniform("Projection", UniformType.UNIFORM_BUFFER)
.withBlend(BlendFunction.TRANSLUCENT)
.withDepthWrite(false)
.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()
)
- /** Pipeline for ESP lines that render through walls. */
- val ESP_LINES_THROUGH: RenderPipeline =
+ val ESP_QUADS: RenderPipeline =
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"))
+ .withLocation(Identifier.of("lambda", "pipeline/esp_quads"))
+ .withVertexShader(Identifier.of("lambda", "core/world_faces"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_faces"))
+ .withUniform("Fog", UniformType.UNIFORM_BUFFER)
.withBlend(BlendFunction.TRANSLUCENT)
.withDepthWrite(false)
- .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(VertexFormats.POSITION_COLOR, VertexFormat.DrawMode.QUADS)
+ .build()
+ )
+
+ val SDF_TEXT: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/sdf_text"))
+ .withVertexShader(Identifier.of("lambda", "core/world_sdf_text"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_sdf_text"))
+ .withUniform("Fog", UniformType.UNIFORM_BUFFER)
+ .withSampler("Sampler0")
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF, VertexFormat.DrawMode.QUADS)
+ .build()
+ )
+
+ 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)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
.withCull(false)
.withVertexFormat(
- VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH,
+ LambdaVertexFormats.SCREEN_FACE_FORMAT,
VertexFormat.DrawMode.QUADS
)
.build()
)
- /**
- * Pipeline for quad-based ESP (compatible with existing shape building). Uses QUADS draw mode
- * which MC converts to triangles internally.
- */
- val ESP_QUADS: RenderPipeline =
+ 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)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_LINE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ val SCREEN_TEXT: RenderPipeline =
RenderPipelines.register(
RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
- .withLocation(Identifier.of("lambda", "pipeline/esp_quads"))
- .withVertexShader(Identifier.ofVanilla("core/position_color"))
- .withFragmentShader(Identifier.ofVanilla("core/position_color"))
+ .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)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ val SCREEN_IMAGE: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_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)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.SCREEN_IMAGE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ val WORLD_IMAGE: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/world_image"))
+ .withVertexShader(Identifier.of("lambda", "core/world_image"))
+ .withFragmentShader(Identifier.of("lambda", "core/world_image"))
+ .withUniform("Fog", UniformType.UNIFORM_BUFFER)
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
.withBlend(BlendFunction.TRANSLUCENT)
.withDepthWrite(false)
.withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
.withCull(false)
.withVertexFormat(
- VertexFormats.POSITION_COLOR,
+ LambdaVertexFormats.WORLD_IMAGE_FORMAT,
+ VertexFormat.DrawMode.QUADS
+ )
+ .build()
+ )
+
+ 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")
+ .withSampler("Sampler1")
+ .withSampler("Sampler2")
+ .withSampler("Sampler3")
+ .withUniform("GlintTransforms", UniformType.UNIFORM_BUFFER)
+ .withUniform("Fog", 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 quad-based ESP that renders through walls. */
- val ESP_QUADS_THROUGH: RenderPipeline =
+ val OUTLINE_SOBEL: RenderPipeline =
RenderPipelines.register(
RenderPipeline.builder(LAMBDA_ESP_SNIPPET)
- .withLocation(Identifier.of("lambda", "pipeline/esp_quads_through"))
- .withVertexShader(Identifier.ofVanilla("core/position_color"))
- .withFragmentShader(Identifier.ofVanilla("core/position_color"))
+ .withLocation(Identifier.of("lambda", "pipeline/outline_sobel"))
+ .withVertexShader(Identifier.of("lambda", "core/outline_sobel"))
+ .withFragmentShader(Identifier.of("lambda", "core/outline_sobel"))
+ .withUniform("DynamicTransforms", UniformType.UNIFORM_BUFFER)
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withSampler("Sampler2")
.withBlend(BlendFunction.TRANSLUCENT)
.withDepthWrite(false)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withCull(false)
.withVertexFormat(
- VertexFormats.POSITION_COLOR,
+ VertexFormats.POSITION_TEXTURE,
VertexFormat.DrawMode.QUADS
)
.build()
)
+
+ val OUTLINE_ID: RenderPipeline =
+ RenderPipelines.register(
+ RenderPipeline.builder(LAMBDA_ESP_SNIPPET, RenderPipelines.GLOBALS_SNIPPET)
+ .withLocation(Identifier.of("lambda", "pipeline/outline_id"))
+ .withVertexShader(Identifier.of("lambda", "core/outline_id"))
+ .withFragmentShader(Identifier.of("lambda", "core/outline_id"))
+ .withSampler("Sampler0")
+ .withoutBlend()
+ .withDepthWrite(true)
+ .withDepthTestFunction(DepthTestFunction.LEQUAL_DEPTH_TEST)
+ .withCull(false)
+ .withVertexFormat(
+ LambdaVertexFormats.OUTLINE_ID_FORMAT,
+ VertexFormat.DrawMode.TRIANGLES
+ )
+ .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..08ab85a66
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/LambdaVertexFormats.kt
@@ -0,0 +1,210 @@
+/*
+ * 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
+
+object LambdaVertexFormats {
+ val NORMAL_FLOAT: VertexFormatElement = VertexFormatElement.register(
+ 30,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.NORMAL,
+ 3
+ )
+
+ val LINE_WIDTH_FLOAT: VertexFormatElement = VertexFormatElement.register(
+ 29,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 1
+ )
+
+ val DASH_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 31,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 4
+ )
+
+ val ANCHOR_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 20,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 3
+ )
+
+ val BILLBOARD_DATA_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 21,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 2
+ )
+
+ 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()
+
+ 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()
+
+ val DIRECTION_2D_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 22,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 2
+ )
+
+ val LAYER_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 24,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 1
+ )
+
+ val SCREEN_FACE_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", VertexFormatElement.POSITION)
+ .add("Color", VertexFormatElement.COLOR)
+ .add("Layer", LAYER_ELEMENT)
+ .build()
+
+ 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()
+
+ val SDF_STYLE_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 23,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 4
+ )
+
+ 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()
+
+ 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()
+
+ val OVERLAY_UV_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 25,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 4
+ )
+
+ 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()
+
+ 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()
+ val EDGE_DATA_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 26,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 2
+ )
+
+ val LIGHT_DIR_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 27,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 3
+ )
+
+ val LIGHT1_DIR_ELEMENT: VertexFormatElement = VertexFormatElement.register(
+ 28,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.GENERIC,
+ 3
+ )
+
+ 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()
+
+ val POSITION_H: VertexFormatElement = VertexFormatElement.register(
+ 19,
+ 0,
+ VertexFormatElement.Type.FLOAT,
+ VertexFormatElement.Usage.POSITION,
+ 4
+ )
+
+ val OUTLINE_ID_FORMAT: VertexFormat = VertexFormat.builder()
+ .add("Position", POSITION_H)
+ .add("UV0", VertexFormatElement.UV0)
+ .add("Color", VertexFormatElement.COLOR)
+ .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..17695d416
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/LineDashStyle.kt
@@ -0,0 +1,97 @@
+/*
+ * 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
+
+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" }
+ }
+
+ val cycleLength: Float get() = dashLength + gapLength
+
+ val dashRatio: Float get() = dashLength / cycleLength
+
+ companion object {
+ val SOLID: LineDashStyle? = null
+
+ fun dotted(size: Float = 0.15f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ fun marchingAnts(
+ dashLength: Float = 0.4f,
+ gapLength: Float = 0.2f,
+ speed: Float = 1f
+ ) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = gapLength,
+ animated = true,
+ animationSpeed = speed
+ )
+
+ fun longDash(dashLength: Float = 0.75f) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = dashLength / 3f
+ )
+
+ fun shortDash(size: Float = 0.3f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ fun screenDotted(size: Float = 0.01f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ fun screenMarchingAnts(
+ dashLength: Float = 0.02f,
+ gapLength: Float = 0.01f,
+ speed: Float = 1f
+ ) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = gapLength,
+ animated = true,
+ animationSpeed = speed
+ )
+
+ fun screenDashed(dashLength: Float = 0.03f, gapLength: Float = 0.015f) = LineDashStyle(
+ dashLength = dashLength,
+ gapLength = gapLength
+ )
+
+ fun screenShortDash(size: Float = 0.015f) = LineDashStyle(
+ dashLength = size,
+ gapLength = size
+ )
+
+ 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..4385192f3 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.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
@@ -18,67 +18,194 @@
package com.lambda.graphics.mc
import com.lambda.Lambda.mc
+import com.lambda.graphics.mc.renderer.RendererUtils
+import com.lambda.graphics.mc.renderer.upload
import com.mojang.blaze3d.buffers.GpuBuffer
import com.mojang.blaze3d.systems.RenderPass
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.VertexFormat
+import org.lwjgl.system.MemoryUtil
import java.util.*
-/**
- * Region-based 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
- * 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 {
private var faceVertexBuffer: GpuBuffer? = null
private var edgeVertexBuffer: GpuBuffer? = null
+ private var textVertexBuffer: GpuBuffer? = null
+
+ private var screenFaceVertexBuffer: GpuBuffer? = null
+ private var screenEdgeVertexBuffer: GpuBuffer? = null
+ private var screenTextVertexBuffer: GpuBuffer? = null
+
+ 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
private var faceIndexCount = 0
private var edgeIndexCount = 0
+ private var textIndexCount = 0
+
+ private var screenFaceIndexCount = 0
+ private var screenEdgeIndexCount = 0
+ private var screenTextIndexCount = 0
+
+ private var outlinedBatches: Map = emptyMap()
- // State tracking
- private var hasData = false
+ private var hasWorldData = false
+ private var hasScreenData = false
- /**
- * Upload collected vertices from an external collector. This must be called on the main/render
- * thread.
- *
- * @param collector The collector containing the geometry to upload
- */
fun upload(collector: RegionVertexCollector) {
- val result = collector.upload()
+ val result = collector.buildWorld()
+ val screenResult = collector.buildScreen()
+
+ fun uploadBatch(batch: BuiltBatch?, label: String, oldBuffer: GpuBuffer?): BufferResult {
+ oldBuffer?.close()
+ if (batch == null) return BufferResult(null, 0)
+
+ val buffer = RenderSystem.getDevice().createBuffer({ label }, GpuBuffer.USAGE_VERTEX or GpuBuffer.USAGE_COPY_DST, batch.buffer.remaining().toLong())
+ buffer.upload(batch.buffer)
+ MemoryUtil.memFree(batch.buffer)
+ return BufferResult(buffer, batch.indexCount)
+ }
+
+ fun uploadTextureBatches(batches: List, label: String, oldBatches: List): List {
+ oldBatches.forEach { it.buffer.close() }
+
+ return batches.map { batch ->
+ val buffer = RenderSystem.getDevice().createBuffer({ label }, GpuBuffer.USAGE_VERTEX or GpuBuffer.USAGE_COPY_DST, batch.buffer.remaining().toLong())
+ buffer.upload(batch.buffer)
+ MemoryUtil.memFree(batch.buffer)
+ TextureBatchResult(batch.textureView, buffer, batch.indexCount, batch.useNearestFilter)
+ }
+ }
+
+ val faceRes = uploadBatch(result.faces, "Lambda World Face Buffer", faceVertexBuffer)
+ faceVertexBuffer = faceRes.buffer
+ faceIndexCount = faceRes.indexCount
+
+ val edgeRes = uploadBatch(result.edges, "Lambda World Edge Buffer", edgeVertexBuffer)
+ edgeVertexBuffer = edgeRes.buffer
+ edgeIndexCount = edgeRes.indexCount
+
+ val textRes = uploadBatch(result.text, "Lambda World Text Buffer", textVertexBuffer)
+ textVertexBuffer = textRes.buffer
+ textIndexCount = textRes.indexCount
+
+ val sFaceRes = uploadBatch(screenResult.faces, "Lambda Screen Face Buffer", screenFaceVertexBuffer)
+ screenFaceVertexBuffer = sFaceRes.buffer
+ screenFaceIndexCount = sFaceRes.indexCount
+
+ val sEdgeRes = uploadBatch(screenResult.edges, "Lambda Screen Edge Buffer", screenEdgeVertexBuffer)
+ screenEdgeVertexBuffer = sEdgeRes.buffer
+ screenEdgeIndexCount = sEdgeRes.indexCount
+
+ val sTextRes = uploadBatch(screenResult.text, "Lambda Screen Text Buffer", screenTextVertexBuffer)
+ screenTextVertexBuffer = sTextRes.buffer
+ screenTextIndexCount = sTextRes.indexCount
+
+ worldImageBatches = uploadTextureBatches(result.images, "Lambda World Image Buffer", worldImageBatches)
+ screenImageBatches = uploadTextureBatches(screenResult.images, "Lambda Screen Image Buffer", screenImageBatches)
+ modelBatches = uploadTextureBatches(result.models, "Lambda World Model Buffer", modelBatches)
+ screenModelBatches = uploadTextureBatches(screenResult.models, "Lambda Screen Model Buffer", screenModelBatches)
+
+ val oldOutlined = outlinedBatches
+ outlinedBatches = result.outlined.mapValues { (id, out) ->
+ val old = oldOutlined[id]
+ OutlinedBatchResult(
+ faces = uploadBatch(out.faces, "Lambda Outlined Face Buffer ($id)", old?.faces?.buffer),
+ edges = uploadBatch(out.edges, "Lambda Outlined Edge Buffer ($id)", old?.edges?.buffer),
+ text = uploadBatch(out.text, "Lambda Outlined Text Buffer ($id)", old?.text?.buffer),
+ models = uploadTextureBatches(out.models, "Lambda Outlined Model Buffer ($id)", old?.models ?: emptyList()),
+ images = uploadTextureBatches(out.images, "Lambda Outlined Image Buffer ($id)", old?.images ?: emptyList())
+ )
+ }
+
+ oldOutlined.forEach { (id, old) ->
+ if (!outlinedBatches.containsKey(id)) {
+ old.faces.buffer?.close()
+ old.edges.buffer?.close()
+ old.text.buffer?.close()
+ old.models.forEach { it.buffer.close() }
+ old.images.forEach { it.buffer.close() }
+ }
+ }
+
+ hasWorldData = faceVertexBuffer != null || edgeVertexBuffer != null || textVertexBuffer != null || worldImageBatches.isNotEmpty() || modelBatches.isNotEmpty() || outlinedBatches.isNotEmpty()
+ hasScreenData = screenFaceVertexBuffer != null || screenEdgeVertexBuffer != null || screenTextVertexBuffer != null || screenImageBatches.isNotEmpty() || screenModelBatches.isNotEmpty()
+ }
+
+ fun getOutlineIds(): Set = outlinedBatches.keys
+
+ fun hasOutlinedData(id: Int): Boolean = outlinedBatches.containsKey(id)
+
+ fun renderOutlinedFaces(renderPass: RenderPass, id: Int) {
+ val batch = outlinedBatches[id] ?: return
+ val vb = batch.faces.buffer ?: return
+ if (batch.faces.indexCount == 0) return
+ renderQuadBuffer(renderPass, vb, batch.faces.indexCount)
+ }
- // Cleanup old buffers
- faceVertexBuffer?.close()
- edgeVertexBuffer?.close()
+ fun renderOutlinedEdges(renderPass: RenderPass, id: Int) {
+ val batch = outlinedBatches[id] ?: return
+ val vb = batch.edges.buffer ?: return
+ if (batch.edges.indexCount == 0) return
+ renderQuadBuffer(renderPass, vb, batch.edges.indexCount)
+ }
+
+ fun renderOutlinedText(renderPass: RenderPass, id: Int) {
+ val batch = outlinedBatches[id] ?: return
+ val vb = batch.text.buffer ?: return
+ if (batch.text.indexCount == 0) return
+ renderQuadBuffer(renderPass, vb, batch.text.indexCount)
+ }
- // Assign new buffers and counts
- faceVertexBuffer = result.faces?.buffer
- faceIndexCount = result.faces?.indexCount ?: 0
+ fun renderOutlinedModels(renderPass: RenderPass, id: Int) {
+ val batch = outlinedBatches[id] ?: return
+ if (batch.models.isEmpty()) return
+ renderModelBatches(renderPass, batch.models)
+ }
- edgeVertexBuffer = result.edges?.buffer
- edgeIndexCount = result.edges?.indexCount ?: 0
+ fun renderOutlinedImages(renderPass: RenderPass, id: Int) {
+ val batch = outlinedBatches[id] ?: return
+ if (batch.images.isEmpty()) return
+ renderImageBatches(renderPass, batch.images)
+ }
- hasData = faceVertexBuffer != null || edgeVertexBuffer != null
+ private fun renderImageBatches(renderPass: RenderPass, batches: List) {
+ 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 batches) {
+ 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)
+ }
+ }
+
+ private fun renderModelBatches(renderPass: RenderPass, batches: List) {
+ 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)
+ val glintTexture = mc.textureManager.getTexture(net.minecraft.client.render.item.ItemRenderer.ITEM_ENCHANTMENT_GLINT)?.glTextureView
+ for (batch in batches) {
+ val sampler = if (batch.useNearestFilter) nearestSampler else linearSampler
+ renderPass.bindTexture("Sampler0", batch.textureView, sampler)
+ 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)
+ }
}
- /**
- * Render faces using the given render pass.
- *
- * @param renderPass The active RenderPass to record commands into
- */
fun renderFaces(renderPass: RenderPass) {
val vb = faceVertexBuffer ?: return
if (faceIndexCount == 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(faceIndexCount)
@@ -86,17 +213,11 @@ class RegionRenderer(val region: RenderRegion) {
renderPass.drawIndexed(0, 0, faceIndexCount, 1)
}
- /**
- * Render edges using the given render pass.
- *
- * @param renderPass The active RenderPass to record commands into
- */
fun renderEdges(renderPass: RenderPass) {
val vb = edgeVertexBuffer ?: return
if (edgeIndexCount == 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(edgeIndexCount)
@@ -104,38 +225,227 @@ class RegionRenderer(val region: RenderRegion) {
renderPass.drawIndexed(0, 0, edgeIndexCount, 1)
}
- /** Clear all geometry data and release GPU resources. */
+ fun renderText(renderPass: RenderPass) {
+ val vb = textVertexBuffer ?: return
+ if (textIndexCount == 0) return
+
+ renderPass.setVertexBuffer(0, vb)
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(textIndexCount)
+
+ renderPass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ renderPass.drawIndexed(0, 0, textIndexCount, 1)
+ }
+
+ fun hasTextData(): Boolean = textVertexBuffer != null && textIndexCount > 0
+
+ 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)
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ fun hasScreenTextData(): Boolean = screenTextVertexBuffer != null && screenTextIndexCount > 0
+
+ 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)
+ }
+ }
+
+ 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)
+
+ 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)
+
+ 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)
+ }
+ }
+
+ fun hasScreenImageData(): Boolean = screenImageBatches.isNotEmpty()
+
+ fun hasScreenModelData(): Boolean = screenModelBatches.isNotEmpty()
+
+ 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)
+ }
+ }
+
+ fun hasWorldImageData(): Boolean = worldImageBatches.isNotEmpty()
+
+ 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)
+
+ 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)
+
+ 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)
+ }
+ }
+
+ fun hasModelData(): Boolean = modelBatches.isNotEmpty()
+
+ fun hasScreenData(): Boolean = hasScreenData
+
fun clearData() {
- faceVertexBuffer?.close()
- edgeVertexBuffer?.close()
faceVertexBuffer = null
edgeVertexBuffer = null
+ textVertexBuffer = null
faceIndexCount = 0
edgeIndexCount = 0
- hasData = false
- }
+ textIndexCount = 0
+ hasWorldData = false
+
+ screenFaceVertexBuffer = null
+ screenEdgeVertexBuffer = null
+ screenTextVertexBuffer = null
+ screenFaceIndexCount = 0
+ screenEdgeIndexCount = 0
+ screenTextIndexCount = 0
- /** Check if this renderer has any data to render. */
- fun hasData(): Boolean = hasData
+ outlinedBatches = emptyMap()
+ screenImageBatches = emptyList()
+ worldImageBatches = emptyList()
+ modelBatches = emptyList()
+ screenModelBatches = emptyList()
- /** Clean up all resources. */
- fun close() {
- clearData()
+ hasScreenData = false
}
+ fun hasData(): Boolean = hasWorldData
+
companion object {
- /** Helper to create a render pass targeting the main framebuffer. */
- fun createRenderPass(label: String): RenderPass? {
+ fun createRenderPass(label: String): RenderPass? = createRenderPass(label, useMcDepth = true)
+
+ fun createRenderPass(label: String, useMcDepth: Boolean): RenderPass? {
val framebuffer = mc.framebuffer ?: return null
+
+ val depthView = if (useMcDepth) {
+ framebuffer.depthAttachmentView
+ } else {
+ RendererUtils.getXrayDepthView()
+ }
+
return RenderSystem.getDevice()
.createCommandEncoder()
.createRenderPass(
{ label },
framebuffer.colorAttachmentView,
OptionalInt.empty(),
- framebuffer.depthAttachmentView,
+ depthView,
OptionalDouble.empty()
)
}
+
+ fun createScreenRenderPassWithDepth(label: String, clearDepth: Boolean = false): RenderPass? {
+ val framebuffer = mc.framebuffer ?: return null
+ val depthView = RendererUtils.getScreenDepthView()
+
+ return RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { label },
+ framebuffer.colorAttachmentView,
+ OptionalInt.empty(),
+ depthView,
+ if (clearDepth) OptionalDouble.of(1.0) else OptionalDouble.empty()
+ )
+ }
+
+ 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..82bc3361b 100644
--- a/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.kt
+++ b/src/main/kotlin/com/lambda/graphics/mc/RegionVertexCollector.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
@@ -18,36 +18,51 @@
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.nio.ByteBuffer
+import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
-/**
- * Thread-safe vertex collector for region-based rendering.
- *
- * Collects vertex data on background threads using thread-safe collections, then writes to MC's
- * BufferBuilder and uploads on the main thread.
- */
class RegionVertexCollector {
val faceVertices = ConcurrentLinkedDeque()
val edgeVertices = ConcurrentLinkedDeque()
+ val textVertices = ConcurrentLinkedDeque()
+
+ val faceVerticesOutlined = ConcurrentHashMap>()
+ val edgeVerticesOutlined = ConcurrentHashMap>()
+ val textVerticesOutlined = ConcurrentHashMap>()
+ val modelVerticesOutlined = ConcurrentHashMap>>()
+ val worldImageVerticesOutlined = ConcurrentHashMap>>()
+
+ 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
+ )
+
+ data class TextVertex(
+ val localX: Float, val localY: Float, val layerType: Int,
+ 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 outlineWidth: Float = 0f,
+ val glowRadius: Float = 0f,
+ val shadowSoftness: Float = 0f,
+ val threshold: Float = 0.5f
)
- /** Edge vertex data (position + color + normal + line width). */
data class EdgeVertex(
val x: Float,
val y: Float,
@@ -59,15 +74,147 @@ class RegionVertexCollector {
val nx: Float,
val ny: Float,
val nz: Float,
- val lineWidth: Float
+ val lineWidth: Float,
+ val dashLength: Float = 0f,
+ val gapLength: Float = 0f,
+ val dashOffset: Float = 0f,
+ val animationSpeed: Float = 0f
+ )
+
+ data class ScreenFaceVertex(
+ val x: Float, val y: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val layer: Float
+ )
+
+ 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,
+ val dashLength: Float = 0f,
+ val gapLength: Float = 0f,
+ val dashOffset: Float = 0f,
+ val animationSpeed: Float = 0f,
+ val layer: Float = 0f
+ )
+
+ data class ScreenTextVertex(
+ val x: Float, val y: Float, val layerType: Int,
+ val u: Float, val v: Float,
+ val r: Int, val g: Int, val b: Int, val a: Int,
+ val outlineWidth: Float = 0f,
+ val glowRadius: Float = 0f,
+ val shadowSoftness: Float = 0f,
+ val threshold: Float = 0.5f,
+ val layer: Float = 0f
+ )
+
+ 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
+ )
+
+ 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
)
- /** 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))
+ 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
+ )
+
+ data class ImageBatchKey(
+ val textureView: GpuTextureView,
+ val useNearestFilter: Boolean
+ )
+
+ private val worldImageBatches = ConcurrentHashMap>()
+ private val screenImageBatches = ConcurrentHashMap>()
+ private val worldModelBatches = ConcurrentHashMap>()
+ private val screenModelBatches = ConcurrentHashMap>()
+
+ fun addScreenImageVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ screenImageBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+
+ fun addWorldImageVertices(
+ texture: GpuTextureView,
+ vertices: List,
+ useNearestFilter: Boolean = false,
+ outlineId: Int? = null
+ ) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ if (outlineId == null) worldImageBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ else {
+ val idMap = worldImageVerticesOutlined.getOrPut(outlineId) { ConcurrentHashMap() }
+ idMap.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+ }
+
+ fun addModelVertices(
+ texture: GpuTextureView,
+ vertices: List,
+ useNearestFilter: Boolean = false,
+ outlineId: Int? = null
+ ) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ if (outlineId == null) worldModelBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ else {
+ val idMap = modelVerticesOutlined.getOrPut(outlineId) { ConcurrentHashMap() }
+ idMap.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+ }
+
+ fun addScreenModelVertices(texture: GpuTextureView, vertices: List, useNearestFilter: Boolean = false) {
+ val key = ImageBatchKey(texture, useNearestFilter)
+ screenModelBatches.getOrPut(key) { ConcurrentLinkedDeque() }.addAll(vertices)
+ }
+
+ fun addFaceVertex(x: Float, y: Float, z: Float, color: Color, outlineId: Int? = null) {
+ val v = FaceVertex(x, y, z, color.red, color.green, color.blue, color.alpha)
+ if (outlineId == null) faceVertices.add(v)
+ else faceVerticesOutlined.getOrPut(outlineId) { ConcurrentLinkedDeque() }.add(v)
+ }
+
+ fun addEdgeVertex(
+ x: Float,
+ y: Float,
+ z: Float,
+ color: Color,
+ nx: Float,
+ ny: Float,
+ nz: Float,
+ lineWidth: Float,
+ outlineId: Int? = null
+ ) {
+ val v = EdgeVertex(x, y, z, color.red, color.green, color.blue, color.alpha, nx, ny, nz, lineWidth)
+ if (outlineId == null) edgeVertices.add(v)
+ else edgeVerticesOutlined.getOrPut(outlineId) { ConcurrentLinkedDeque() }.add(v)
}
- /** Add an edge vertex. */
fun addEdgeVertex(
x: Float,
y: Float,
@@ -76,93 +223,653 @@ class RegionVertexCollector {
nx: Float,
ny: Float,
nz: Float,
- lineWidth: Float
+ lineWidth: Float,
+ dashStyle: LineDashStyle?,
+ outlineId: Int? = null
+ ) {
+ if (dashStyle == null) addEdgeVertex(x, y, z, color, nx, ny, nz, lineWidth, outlineId)
+ else {
+ val v = 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
+ )
+ if (outlineId == null) edgeVertices.add(v)
+ else edgeVerticesOutlined.getOrPut(outlineId) { ConcurrentLinkedDeque() }.add(v)
+ }
+ }
+
+ 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,
+ outlineWidth: Float = 0f,
+ glowRadius: Float = 0f,
+ shadowSoftness: Float = 0f,
+ threshold: Float = 0.5f,
+ outlineId: Int? = null,
+ layerType: Int = 3
) {
- edgeVertices.add(
- EdgeVertex(x, y, z, color.red, color.green, color.blue, color.alpha, nx, ny, nz, lineWidth)
+ val vertex = TextVertex(
+ localX, localY, layerType, u, v, r, g, b, a,
+ anchorX, anchorY, anchorZ, scale, if (billboard) 0f else 1f,
+ outlineWidth, glowRadius, shadowSoftness, threshold
)
+ if (outlineId == null) textVertices.add(vertex)
+ else textVerticesOutlined.getOrPut(outlineId) { ConcurrentLinkedDeque() }.add(vertex)
}
- /**
- * 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
- */
- fun upload(): UploadResult {
- val faces = uploadFaces()
- val edges = uploadEdges()
- return UploadResult(faces, edges)
+ fun addScreenFaceVertex(x: Float, y: Float, color: Color, layer: Float) {
+ screenFaceVertices.add(ScreenFaceVertex(x, y, color.red, color.green, color.blue, color.alpha, layer))
}
- private fun uploadFaces(): BufferResult {
- if (faceVertices.isEmpty()) return BufferResult(null, 0)
+ 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))
+ }
+
+ 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
+ )
+ )
+ }
+ }
+
+ fun buildWorld(): BuiltWorldResult {
+ val faces = buildFaces()
+ val edges = buildEdges()
+ val text = buildText()
+ val models = buildModelBatches()
+ val images = buildWorldImageBatches()
+
+ val outlineIds = faceVerticesOutlined.keys + edgeVerticesOutlined.keys + textVerticesOutlined.keys + modelVerticesOutlined.keys + worldImageVerticesOutlined.keys
+ val outlinedResults = outlineIds.associateWith { id ->
+ BuiltOutlineResult(
+ faces = buildOutlinedFaces(id),
+ edges = buildOutlinedEdges(id),
+ text = buildOutlinedText(id),
+ models = buildOutlinedModelBatches(id),
+ images = buildOutlinedWorldImageBatches(id)
+ )
+ }
+
+ return BuiltWorldResult(faces, edges, text, models, images, outlinedResults)
+ }
+ private fun buildFaces(): BuiltBatch? {
+ if (faceVertices.isEmpty()) return null
val vertices = faceVertices.toList()
faceVertices.clear()
- var result: BufferResult? = null
+ var result: BuiltBatch? = null
BufferAllocator(vertices.size * 16).use { allocator ->
- val builder =
- BufferBuilder(
- allocator,
- VertexFormat.DrawMode.QUADS,
- VertexFormats.POSITION_COLOR
- )
-
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR)
vertices.forEach { v -> builder.vertex(v.x, v.y, v.z).color(v.r, v.g, v.b, v.a) }
-
builder.endNullable()?.let { built ->
- val gpuDevice = RenderSystem.getDevice()
- val buffer =
- gpuDevice.createBuffer(
- { "Lambda ESP Face Buffer" },
- GpuBuffer.USAGE_VERTEX,
- built.buffer
- )
- result = BufferResult(buffer, built.drawParameters.indexCount())
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
built.close()
}
}
- return result ?: BufferResult(null, 0)
+ return result
}
- private fun uploadEdges(): BufferResult {
- if (edgeVertices.isEmpty()) return BufferResult(null, 0)
-
+ private fun buildEdges(): BuiltBatch? {
+ if (edgeVertices.isEmpty()) return null
val vertices = edgeVertices.toList()
edgeVertices.clear()
- var result: BufferResult? = null
- BufferAllocator(vertices.size * 32).use { allocator ->
- val builder =
- BufferBuilder(
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 48).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, 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)
+ 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.LINE_WIDTH_FLOAT).let { p ->
+ if (p != -1L) MemoryUtil.memPutFloat(p, v.lineWidth)
+ }
+ builder.beginElement(LambdaVertexFormats.DASH_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.dashLength); MemoryUtil.memPutFloat(p + 4, v.gapLength); MemoryUtil.memPutFloat(p + 8, v.dashOffset); MemoryUtil.memPutFloat(p + 12, v.animationSpeed)
+ }
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result
+ }
+
+ private fun buildText(): BuiltBatch? {
+ if (textVertices.isEmpty()) return null
+ val vertices = textVertices.toList()
+ textVertices.clear()
+
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 64).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF)
+ vertices.forEach { v ->
+ builder.vertex(v.localX, v.localY, v.layerType.toFloat()).texture(v.u, v.v).color(v.r, v.g, v.b, v.a)
+ builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.anchorX); MemoryUtil.memPutFloat(p + 4, v.anchorY); MemoryUtil.memPutFloat(p + 8, v.anchorZ)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.scale); MemoryUtil.memPutFloat(p + 4, v.billboardFlag)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.outlineWidth); MemoryUtil.memPutFloat(p + 4, v.glowRadius); MemoryUtil.memPutFloat(p + 8, v.shadowSoftness); MemoryUtil.memPutFloat(p + 12, v.threshold)
+ }
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result
+ }
+
+ private fun buildScreenFaces(): BuiltBatch? {
+ if (screenFaceVertices.isEmpty()) return null
+ val vertices = screenFaceVertices.toList()
+ screenFaceVertices.clear()
+
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 24).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, LambdaVertexFormats.SCREEN_FACE_FORMAT)
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, 0f).color(v.r, v.g, v.b, v.a)
+ builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT).let { p ->
+ if (p != -1L) MemoryUtil.memPutFloat(p, v.layer)
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result
+ }
+
+ private fun buildScreenEdges(): BuiltBatch? {
+ if (screenEdgeVertices.isEmpty()) return null
+ val vertices = screenEdgeVertices.toList()
+ screenEdgeVertices.clear()
+
+ var result: BuiltBatch? = null
+ 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)
+ builder.beginElement(LambdaVertexFormats.DIRECTION_2D_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.dx); MemoryUtil.memPutFloat(p + 4, v.dy) }
+ }
+ builder.beginElement(LambdaVertexFormats.LINE_WIDTH_FLOAT).let { p ->
+ if (p != -1L) MemoryUtil.memPutFloat(p, v.lineWidth)
+ }
+ builder.beginElement(LambdaVertexFormats.DASH_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.dashLength); MemoryUtil.memPutFloat(p + 4, v.gapLength); MemoryUtil.memPutFloat(p + 8, v.dashOffset); MemoryUtil.memPutFloat(p + 12, v.animationSpeed)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT).let { p ->
+ if (p != -1L) MemoryUtil.memPutFloat(p, v.layer)
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result
+ }
+
+ private fun buildScreenText(): BuiltBatch? {
+ if (screenTextVertices.isEmpty()) return null
+ val vertices = screenTextVertices.toList()
+ screenTextVertices.clear()
+
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 48).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, LambdaVertexFormats.SCREEN_TEXT_SDF_FORMAT)
+ vertices.forEach { v ->
+ builder.vertex(v.x, v.y, v.layerType.toFloat()).texture(v.u, v.v).color(v.r, v.g, v.b, v.a)
+ builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.outlineWidth); MemoryUtil.memPutFloat(p + 4, v.glowRadius); MemoryUtil.memPutFloat(p + 8, v.shadowSoftness); MemoryUtil.memPutFloat(p + 12, v.threshold)
+ }
+ }
+ builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT).let { p ->
+ if (p != -1L) MemoryUtil.memPutFloat(p, v.layer)
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result
+ }
+
+ fun buildScreen(): BuiltScreenResult {
+ val faces = buildScreenFaces()
+ val edges = buildScreenEdges()
+ val text = buildScreenText()
+ val images = buildScreenImageBatches()
+ val models = buildScreenModelBatches()
+ return BuiltScreenResult(faces, edges, text, images, models)
+ }
+
+ private fun buildScreenImageBatches(): List {
+ if (screenImageBatches.isEmpty()) return emptyList()
+
+ val results = mutableListOf()
+
+ screenImageBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ BufferAllocator(vertices.size * 44).use { allocator ->
+ val builder = BufferBuilder(
allocator,
VertexFormat.DrawMode.QUADS,
- VertexFormats.POSITION_COLOR_NORMAL_LINE_WIDTH
+ 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)
+
+ 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)
+ }
+
+ val layerPointer = builder.beginElement(LambdaVertexFormats.LAYER_ELEMENT)
+ if (layerPointer != -1L) {
+ MemoryUtil.memPutFloat(layerPointer, v.layer)
+ }
+ }
+
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ results.add(BuiltTextureBatch(batchKey.textureView, copy, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ screenImageBatches.clear()
+ return results
+ }
+
+ fun buildWorldImageBatches(): List {
+ if (worldImageBatches.isEmpty()) return emptyList()
+ val results = mutableListOf()
+ worldImageBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ BufferAllocator(vertices.size * 64).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)
+ builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.anchorX); MemoryUtil.memPutFloat(p + 4, v.anchorY); MemoryUtil.memPutFloat(p + 8, v.anchorZ) }
+ }
+ builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.scale); MemoryUtil.memPutFloat(p + 4, v.billboardFlag) }
+ }
+ 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.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ results.add(BuiltTextureBatch(batchKey.textureView, copy, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ worldImageBatches.clear()
+ return results
+ }
+
+ fun buildModelBatches(): List {
+ if (worldModelBatches.isEmpty()) return emptyList()
+ val results = mutableListOf()
+ worldModelBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+ 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 copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ results.add(BuiltTextureBatch(batchKey.textureView, copy, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ worldModelBatches.clear()
+ return results
+ }
+
+ private fun buildScreenModelBatches(): List {
+ if (screenModelBatches.isEmpty()) return emptyList()
+ val results = mutableListOf()
+ screenModelBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+ 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 copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ results.add(BuiltTextureBatch(batchKey.textureView, copy, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ screenModelBatches.clear()
+ return results
+ }
+
+ private fun buildOutlinedFaces(id: Int): BuiltBatch? {
+ val vertices = faceVerticesOutlined[id] ?: return null
+ if (vertices.isEmpty()) return null
+ faceVerticesOutlined.remove(id)
+
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 16).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR)
+ vertices.forEach { v -> builder.vertex(v.x, v.y, v.z).color(v.r, v.g, v.b, v.a) }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
+ }
+ }
+ return result
+ }
+
+ private fun buildOutlinedEdges(id: Int): BuiltBatch? {
+ val vertices = edgeVerticesOutlined[id] ?: return null
+ if (vertices.isEmpty()) return null
+ edgeVerticesOutlined.remove(id)
+
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 48).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, 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)
+ builder.vertex(v.x, v.y, v.z).color(v.r, v.g, v.b, v.a)
+ 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.LINE_WIDTH_FLOAT).let { p ->
+ if (p != -1L) MemoryUtil.memPutFloat(p, v.lineWidth)
+ }
+ builder.beginElement(LambdaVertexFormats.DASH_ELEMENT).let { p ->
+ if (p != -1L) {
+ MemoryUtil.memPutFloat(p, v.dashLength); MemoryUtil.memPutFloat(p + 4, v.gapLength); MemoryUtil.memPutFloat(p + 8, v.dashOffset); MemoryUtil.memPutFloat(p + 12, v.animationSpeed)
+ }
+ }
+ }
+ builder.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
+ built.close()
}
+ }
+ return result
+ }
+
+ private fun buildOutlinedText(id: Int): BuiltBatch? {
+ val vertices = textVerticesOutlined[id] ?: return null
+ if (vertices.isEmpty()) return null
+ textVerticesOutlined.remove(id)
+ var result: BuiltBatch? = null
+ BufferAllocator(vertices.size * 64).use { allocator ->
+ val builder = BufferBuilder(allocator, VertexFormat.DrawMode.QUADS, LambdaVertexFormats.POSITION_TEXTURE_COLOR_ANCHOR_SDF)
+ vertices.forEach { v ->
+ builder.vertex(v.localX, v.localY, v.layerType.toFloat()).texture(v.u, v.v).color(v.r, v.g, v.b, v.a)
+ builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.anchorX); MemoryUtil.memPutFloat(p + 4, v.anchorY); MemoryUtil.memPutFloat(p + 8, v.anchorZ) }
+ }
+ builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.scale); MemoryUtil.memPutFloat(p + 4, v.billboardFlag) }
+ }
+ builder.beginElement(LambdaVertexFormats.SDF_STYLE_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.outlineWidth); MemoryUtil.memPutFloat(p + 4, v.glowRadius); MemoryUtil.memPutFloat(p + 8, v.shadowSoftness); MemoryUtil.memPutFloat(p + 12, v.threshold) }
+ }
+ }
builder.endNullable()?.let { built ->
- val gpuDevice = RenderSystem.getDevice()
- val buffer =
- gpuDevice.createBuffer(
- { "Lambda ESP Edge Buffer" },
- GpuBuffer.USAGE_VERTEX,
- built.buffer
- )
- result = BufferResult(buffer, built.drawParameters.indexCount())
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ result = BuiltBatch(copy, built.drawParameters.indexCount())
built.close()
}
}
- return result ?: BufferResult(null, 0)
+ return result
+ }
+
+ private fun buildOutlinedModelBatches(id: Int): List {
+ val modelBatches = modelVerticesOutlined[id] ?: return emptyList()
+ if (modelBatches.isEmpty()) return emptyList()
+
+ val results = mutableListOf()
+ modelBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ 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) { val l = v.light; MemoryUtil.memPutShort(p, (l and 0xFFFF).toShort()); MemoryUtil.memPutShort(p + 2, (l 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 copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ results.add(BuiltTextureBatch(batchKey.textureView, copy, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ modelVerticesOutlined.remove(id)
+ return results
}
- data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int)
- data class UploadResult(val faces: BufferResult?, val edges: BufferResult?)
+ private fun buildOutlinedWorldImageBatches(id: Int): List {
+ val imageBatches = worldImageVerticesOutlined[id] ?: return emptyList()
+ if (imageBatches.isEmpty()) return emptyList()
+
+ val results = mutableListOf()
+ imageBatches.forEach { (batchKey, vertexDeque) ->
+ val vertices = vertexDeque.toList()
+ vertexDeque.clear()
+ if (vertices.isEmpty()) return@forEach
+
+ 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)
+ builder.beginElement(LambdaVertexFormats.ANCHOR_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.anchorX); MemoryUtil.memPutFloat(p + 4, v.anchorY); MemoryUtil.memPutFloat(p + 8, v.anchorZ) }
+ }
+ builder.beginElement(LambdaVertexFormats.BILLBOARD_DATA_ELEMENT).let { p ->
+ if (p != -1L) { MemoryUtil.memPutFloat(p, v.scale); MemoryUtil.memPutFloat(p + 4, v.billboardFlag) }
+ }
+ 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.endNullable()?.let { built ->
+ val copy = MemoryUtil.memAlloc(built.buffer.remaining())
+ copy.put(built.buffer)
+ copy.flip()
+ results.add(BuiltTextureBatch(batchKey.textureView, copy, built.drawParameters.indexCount(), batchKey.useNearestFilter))
+ built.close()
+ }
+ }
+ }
+ worldImageVerticesOutlined.remove(id)
+ return results
+ }
}
+
+data class BuiltBatch(val buffer: ByteBuffer, val indexCount: Int)
+data class BuiltTextureBatch(val textureView: GpuTextureView, val buffer: ByteBuffer, val indexCount: Int, val useNearestFilter: Boolean = false)
+data class BuiltWorldResult(val faces: BuiltBatch?, val edges: BuiltBatch?, val text: BuiltBatch?, val models: List, val images: List, val outlined: Map)
+data class BuiltScreenResult(val faces: BuiltBatch?, val edges: BuiltBatch?, val text: BuiltBatch?, val images: List, val models: List)
+data class BuiltOutlineResult(val faces: BuiltBatch?, val edges: BuiltBatch?, val text: BuiltBatch?, val models: List, val images: List)
+
+data class TextureBatchResult(
+ val textureView: GpuTextureView,
+ val buffer: GpuBuffer,
+ val indexCount: Int,
+ val useNearestFilter: Boolean = false
+)
+
+data class OutlinedBatchResult(
+ val faces: BufferResult,
+ val edges: BufferResult,
+ val text: BufferResult,
+ val models: List,
+ val images: List
+)
+
+data class BufferResult(val buffer: GpuBuffer?, val indexCount: Int)
+
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..7848ab5f4
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt
@@ -0,0 +1,1524 @@
+/*
+ * 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.config.groups.LineConfig
+import com.lambda.context.SafeContext
+import com.lambda.graphics.outline.OutlineManager
+import com.lambda.graphics.outline.OutlineStyle
+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.entity.Entity
+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
+import kotlin.math.cos
+import kotlin.math.sin
+
+@DslMarker
+annotation class RenderDsl
+
+@Suppress("unused")
+@RenderDsl
+class RenderBuilder(private val cameraPos: Vec3d, var depthTest: Boolean = false) {
+ val collector = RegionVertexCollector()
+
+ var fontAtlas: SDFFontAtlas? = null
+ private set
+
+ private var currentLayer = -800f
+
+ private val layerIncrement = 1f
+
+ 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()
+ )
+ }
+
+ private fun nextLayer(): Float {
+ val layer = currentLayer
+ currentLayer += layerIncrement
+ return layer
+ }
+
+ private var activeOutlineId: Int? = null
+
+ fun withOutline(style: OutlineStyle, block: RenderBuilder.() -> Unit) {
+ val previousId = activeOutlineId
+ activeOutlineId = OutlineManager.registerCustomOutline(style, depthTest = depthTest)
+ try {
+ block()
+ } finally {
+ activeOutlineId = previousId
+ }
+ }
+
+ fun box(
+ box: Box,
+ lineConfig: LineConfig? = null,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) {
+ val boxBuilder = BoxBuilder(lineConfig).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,
+ lineConfig: LineConfig? = null,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) = with(safeContext) {
+ val boxes = state.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) }
+ val boxBuilder = BoxBuilder(lineConfig).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,
+ lineConfig: LineConfig? = null,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) = box(Box(pos), lineConfig, builder)
+
+ context(safeContext: SafeContext)
+ fun boxes(
+ pos: BlockPos,
+ lineConfig: LineConfig? = null,
+ builder: (BoxBuilder.() -> Unit)? = null
+ ) = boxes(pos, safeContext.blockState(pos), lineConfig, 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)
+
+ 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)
+
+ 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)
+ }
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ @JvmName("worldOutline1")
+ fun worldOutline(
+ entity: Entity,
+ style: OutlineStyle
+ ) = OutlineManager.setEntityOutline(entity.id, style, depthTest = depthTest)
+
+ @JvmName("worldOutlines1")
+ fun worldOutlines(
+ entities: Iterable,
+ style: OutlineStyle
+ ) = entities.forEach {
+ OutlineManager.setEntityOutline(it.id, style, depthTest = depthTest)
+ }
+
+ @JvmName("worldOutline2")
+ fun worldOutline(
+ pos: BlockPos,
+ style: OutlineStyle
+ ) = OutlineManager.setBlockOutline(pos, style, depthTest = depthTest)
+
+ @JvmName("worldOutlines2")
+ fun worldOutlines(
+ positions: Iterable,
+ style: OutlineStyle
+ ) = positions.forEach {
+ OutlineManager.setBlockOutline(it, style, depthTest = depthTest)
+ }
+
+ 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
+ ) {
+ 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 = cos(angle) * radius
+ val y = sin(angle) * radius
+ center.add(u.multiply(x)).add(v.multiply(y))
+ }
+
+ polyline(points, color, width, dashStyle)
+ }
+
+ 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
+
+ val anchorX = (pos.x - cameraPos.x).toFloat()
+ val anchorY = (pos.y - cameraPos.y).toFloat()
+ val anchorZ = (pos.z - cameraPos.z).toFloat()
+
+ val textWidth = if (centered) atlas.getStringWidthNormalized(text, 1f) else 0f
+ val startX = -textWidth / 2f
+
+ 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
+
+ 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, shadowColor.alpha,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style, activeOutlineId, 0)
+ }
+
+ if (style.glow != null) {
+ val glowColor = style.glow.color
+ buildTextQuads(atlas, text, startX, 0f,
+ glowColor.red, glowColor.green, glowColor.blue, glowColor.alpha,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style, activeOutlineId, 1)
+ }
+
+ if (style.outline != null) {
+ val outlineColor = style.outline.color
+ buildTextQuads(atlas, text, startX, 0f,
+ outlineColor.red, outlineColor.green, outlineColor.blue, outlineColor.alpha,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style, activeOutlineId, 2)
+ }
+
+ val mainColor = style.color
+ buildTextQuads(atlas, text, startX, 0f,
+ mainColor.red, mainColor.green, mainColor.blue, 255,
+ anchorX, anchorY, anchorZ, size, rotationMatrix, style, activeOutlineId)
+ }
+
+ private val screenWidth get() = mc.window?.scaledWidth?.toFloat() ?: 1920f
+ private val screenHeight get() = mc.window?.scaledHeight?.toFloat() ?: 1080f
+
+ private fun toPixelX(normalizedX: Float): Float = normalizedX * screenWidth
+
+ private fun toPixelY(normalizedY: Float): Float = normalizedY * screenHeight
+
+ private fun toPixelSize(normalizedSize: Float): Float =
+ normalizedSize * screenHeight
+
+ 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)
+ }
+
+ 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)
+
+ 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)
+ }
+
+ 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)
+ }
+
+ fun screenLineGradient(
+ x1: Float, y1: Float, startColor: Color,
+ x2: Float, y2: Float, endColor: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ val px1 = toPixelX(x1)
+ val py1 = toPixelY(y1)
+ val px2 = toPixelX(x2)
+ val py2 = toPixelY(y2)
+ val pixelWidth = toPixelSize(width)
+
+ val dx = px2 - px1
+ val dy = py2 - py1
+
+ val pixelDashStyle = dashStyle?.let {
+ LineDashStyle(
+ dashLength = toPixelSize(it.dashLength),
+ gapLength = toPixelSize(it.gapLength),
+ offset = it.offset,
+ animated = it.animated,
+ animationSpeed = it.animationSpeed
+ )
+ }
+
+ val layer = nextLayer()
+
+ 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)
+ }
+
+ 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)
+
+ 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
+
+ val glintTime = if (hasOverlay) {
+ (net.minecraft.util.Util.getMeasuringTimeMs() / 1000.0f) % 1000f
+ } else 0f
+
+ val aspectRatio = if (hasOverlay && height != 0f) width / height else 1f
+
+ 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)
+ }
+
+ 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 = 1.0f
+ ) {
+ val sprite = model.particleSprite() ?: return
+ val atlas = sprite.atlasId
+
+ 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()
+
+ var overlayFlag = if (overlay != OverlayTexture.DEFAULT_UV) 1.0f else 0.0f
+ if (smartAA) {
+ overlayFlag += 2.0f
+ }
+
+ val random = Random.create()
+ val quads = mutableListOf()
+
+ 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) {
+ val face = quad.face
+ var nx = face?.offsetX?.toFloat() ?: 0f
+ var ny = face?.offsetY?.toFloat() ?: 0f
+ var nz = face?.offsetZ?.toFloat() ?: 0f
+
+ if (face == null) {
+ val v0 = quad.getPosition(0)
+ val v1 = quad.getPosition(1)
+ val v2 = quad.getPosition(2)
+ val e1x = v1.x() - v0.x(); val e1y = v1.y() - v0.y(); val e1z = v1.z() - v0.z()
+ val e2x = v2.x() - v0.x(); val e2y = v2.y() - v0.y(); val e2z = v2.z() - v0.z()
+ nx = e1y * e2z - e1z * e2y
+ ny = e1z * e2x - e1x * e2z
+ nz = e1x * e2y - e1y * e2x
+ val len = Math.sqrt((nx * nx + ny * ny + nz * nz).toDouble()).toFloat()
+ if (len > 0f) { nx /= len; ny /= len; nz /= len }
+ }
+
+ for (i in 0 until 4) {
+ val posVecSrc = quad.getPosition(i)
+
+ vertexPos.set(posVecSrc.x(), posVecSrc.y(), posVecSrc.z())
+
+ if (centered) vertexPos.sub(0.5f, 0.5f, 0.5f)
+
+ vertexPos.mul(scaleVec)
+ rotation?.transform(vertexPos)
+ vertexPos.add(posVec)
+
+ normalVec.set(nx, ny, nz)
+ rotation?.transform(normalVec)
+
+ val packedUV = quad.getTexcoords(i)
+ val u = Float.fromBits((packedUV ushr 32).toInt())
+ val v = Float.fromBits((packedUV and 0xFFFFFFFFL).toInt())
+
+ val edgeX = when(i) {
+ 0 -> 0.0f
+ 1 -> 1.0f
+ 2 -> 1.0f
+ else -> 0.0f
+ }
+ val edgeY = when(i) {
+ 0 -> 0.0f
+ 1 -> 0.0f
+ 2 -> 1.0f
+ else -> 1.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()) {
+ val useNearest = pixelPerfect && !smartAA
+ collector.addModelVertices(textureView, vertices, useNearest, activeOutlineId)
+ }
+ }
+
+ 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)
+
+ val pixelX = toPixelX(x)
+ val pixelY = toPixelY(y)
+ val pixelSize = toPixelSize(size)
+
+ val rot = rotation?.let { eulerToQuaternion(it) }
+
+ 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, activeOutlineId)
+ }
+ }
+
+ val matrixStack = MatrixStack()
+
+ for (i in 0 until state.layerCount) {
+ val layer = state.layers[i]
+
+ 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) {
+ 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() {
+ val p = Vector3f(this.x, this.y, this.z)
+
+ if (flat) p.z = 0f
+
+ posTransform(p)
+
+ 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) {
+ 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
+
+ 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) {}
+ }
+
+
+ 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
+
+ val anchorX = (pos.x - cameraPos.x).toFloat()
+ val anchorY = (pos.y - cameraPos.y).toFloat()
+ val anchorZ = (pos.z - cameraPos.z).toFloat()
+
+ val halfWidth = size * ratio / 2f
+ val halfHeight = size / 2f
+
+ val overlayFlag = if (hasOverlay) 1f else 0f
+ val billboardFlag = if (rotation == null) 0f else 1f
+
+ val gx0 = 0f
+ val gx1 = 1f
+ val gy0 = 0f
+ val gy1 = 1f
+
+ val x0 = -halfWidth / size
+ val x1 = halfWidth / size
+ val y0 = -halfHeight / size
+ val y1 = halfHeight / size
+
+ val vertices = if (rotation == null) {
+ 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 {
+ 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, activeOutlineId)
+ }
+
+ fun screenImage(
+ texture: Identifier,
+ x: Float, y: Float,
+ width: Float, height: Float,
+ tint: Color = Color.WHITE,
+ hasOverlay: Boolean = false,
+ pixelPerfect: Boolean = true
+ ) {
+ val imageEntry = LambdaImageAtlas.loadMCTexture(texture) ?: return
+ screenImage(imageEntry, x, y, width, height, tint, hasOverlay, pixelPerfect)
+ }
+
+ 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
+ ) {
+ val imageEntry = LambdaImageAtlas.loadMCTexture(texture) ?: return
+ val ratio = aspectRatio ?: imageEntry.aspectRatio
+ worldImage(imageEntry, pos, size, tint, hasOverlay, ratio, rotation, pixelPerfect)
+ }
+
+ 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
+
+ val pixelX = toPixelX(x)
+ val pixelY = toPixelY(y)
+
+ val targetPixelHeight = toPixelSize(size)
+
+ val pixelSize = targetPixelHeight * atlas.baseSize / atlas.ascent
+
+ val normalizedTextWidth = if (centered) atlas.getStringWidthNormalized(text, size) else 0f
+ val textWidth = normalizedTextWidth * screenWidth
+ val startX = -textWidth / 2f
+
+ if (style.shadow != null) {
+ val shadowColor = style.shadow.color
+ val offsetX = style.shadow.offsetX * pixelSize
+ val offsetY = -style.shadow.offsetY * pixelSize
+ val layer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX + offsetX, offsetY,
+ shadowColor.red, shadowColor.green, shadowColor.blue, shadowColor.alpha,
+ pixelX, pixelY, pixelSize, style, layer, 0)
+ }
+
+ if (style.glow != null) {
+ val glowColor = style.glow.color
+ val layer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX, 0f,
+ glowColor.red, glowColor.green, glowColor.blue, glowColor.alpha,
+ pixelX, pixelY, pixelSize, style, layer, 1)
+ }
+
+ if (style.outline != null) {
+ val outlineColor = style.outline.color
+ val layer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX, 0f,
+ outlineColor.red, outlineColor.green, outlineColor.blue, outlineColor.alpha,
+ pixelX, pixelY, pixelSize, style, layer, 2)
+ }
+
+ val mainColor = style.color
+ val mainLayer = nextLayer()
+ buildScreenTextQuads(atlas, text, startX, 0f,
+ mainColor.red, mainColor.green, mainColor.blue, mainColor.alpha,
+ pixelX, pixelY, pixelSize, style, mainLayer, 3)
+ }
+
+ private fun buildScreenTextQuads(
+ atlas: SDFFontAtlas,
+ text: String,
+ startX: Float,
+ startY: Float,
+ r: Int, g: Int, b: Int, a: Int,
+ anchorX: Float, anchorY: Float,
+ pixelSize: Float,
+ style: SDFStyle,
+ layer: Float,
+ layerType: Int = 3
+ ) {
+ val outlineWidth = style.outline?.width ?: 0f
+ val glowRadius = style.glow?.radius ?: 0f
+ val shadowSoftness = style.shadow?.softness ?: 0f
+ val threshold = 0.5f
+
+ var penX = 0f
+
+ for (char in text) {
+ val glyph = atlas.getGlyph(char.code) ?: continue
+
+ val localX0 = penX + glyph.bearingX
+ val localY1 = glyph.bearingY
+
+ val localX1 = localX0 + glyph.width / atlas.baseSize
+ val localY0 = localY1 - glyph.height / atlas.baseSize
+
+ val x0 = anchorX + startX + localX0 * pixelSize
+ val y0 = anchorY + startY + localY0 * pixelSize
+ val x1 = anchorX + startX + localX1 * pixelSize
+ val y1 = anchorY + startY + localY1 * pixelSize
+
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x0, y0, layerType, glyph.u0, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x1, y0, layerType, glyph.u1, glyph.v1, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x1, y1, layerType, glyph.u1, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+ collector.screenTextVertices.add(RegionVertexCollector.ScreenTextVertex(
+ x0, y1, layerType, glyph.u0, glyph.v0, r, g, b, a, outlineWidth, glowRadius, shadowSoftness, threshold, layer))
+
+ penX += glyph.advance
+ }
+ }
+
+ 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,
+ outlineId: Int? = null,
+ layerType: Int = 3
+ ) {
+ val outlineWidth = style.outline?.width ?: 0f
+ val glowRadius = style.glow?.radius ?: 0f
+ val shadowSoftness = style.shadow?.softness ?: 0f
+ val threshold = 0.5f
+
+ 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) {
+ collector.addTextVertex(x0, y1, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, true, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ collector.addTextVertex(x1, y1, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, true, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ collector.addTextVertex(x1, y0, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, true, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ collector.addTextVertex(x0, y0, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, true, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ } else {
+ val p0 = transformPoint(rotationMatrix, x0, -y1, 0f)
+ val p1 = transformPoint(rotationMatrix, x1, -y1, 0f)
+ val p2 = transformPoint(rotationMatrix, x1, -y0, 0f)
+ val p3 = transformPoint(rotationMatrix, x0, -y0, 0f)
+
+ collector.addTextVertex(p0.x, p0.y, glyph.u0, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, false, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ collector.addTextVertex(p1.x, p1.y, glyph.u1, glyph.v1, r, g, b, a, anchorX, anchorY, anchorZ, scale, false, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ collector.addTextVertex(p2.x, p2.y, glyph.u1, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, false, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ collector.addTextVertex(p3.x, p3.y, glyph.u0, glyph.v0, r, g, b, a, anchorX, anchorY, anchorZ, scale, false, outlineWidth, glowRadius, shadowSoftness, threshold, outlineId, layerType)
+ }
+
+ penX += glyph.advance
+ }
+ }
+
+ private fun BoxBuilder.boxFaces(box: Box) {
+ if (fillSides.hasDirection(DirectionMask.EAST)) {
+ 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)) {
+ 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)) {
+ 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)) {
+ 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)) {
+ 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)) {
+ 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)
+
+ 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
+ )
+ }
+
+ 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
+ )
+ }
+
+ 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
+ )
+ }
+ }
+
+ private fun line(
+ x1: Double, y1: Double, z1: Double,
+ x2: Double, y2: Double, z2: Double,
+ color1: Color,
+ color2: Color,
+ width: Float,
+ dashStyle: LineDashStyle? = null
+ ) {
+ 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()
+
+ val dx = rx2 - rx1
+ val dy = ry2 - ry1
+ val dz = rz2 - rz1
+
+ collector.addEdgeVertex(rx1, ry1, rz1, color1, dx, dy, dz, width, dashStyle, activeOutlineId)
+ collector.addEdgeVertex(rx1, ry1, rz1, color1, dx, dy, dz, width, dashStyle, activeOutlineId)
+ collector.addEdgeVertex(rx2, ry2, rz2, color2, dx, dy, dz, width, dashStyle, activeOutlineId)
+ collector.addEdgeVertex(rx2, ry2, rz2, color2, dx, dy, dz, width, dashStyle, activeOutlineId)
+ }
+
+ 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)
+ }
+
+ 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, activeOutlineId)
+ }
+
+ data class SDFOutline(
+ val color: Color = Color.BLACK,
+ val width: Float = 0.1f
+ )
+
+ data class SDFGlow(
+ val color: Color = Color(0, 200, 255, 180),
+ val radius: Float = 0.2f
+ )
+
+ data class SDFShadow(
+ val color: Color = Color(0, 0, 0, 180),
+ val offset: Float = 0.05f,
+ val angle: Float = 135f,
+ val softness: Float = 0f
+ ) {
+ val offsetX: Float get() = offset * cos(Math.toRadians(angle.toDouble())).toFloat()
+ val offsetY: Float get() = offset * sin(Math.toRadians(angle.toDouble())).toFloat()
+ }
+
+ data class SDFStyle(
+ var color: Color = Color.WHITE,
+ val outline: SDFOutline? = null,
+ val glow: SDFGlow? = null,
+ val shadow: SDFShadow? = SDFShadow()
+ )
+}
\ 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..fb10722e6
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/AbstractRenderer.kt
@@ -0,0 +1,342 @@
+/*
+ * 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.outline.OutlineManager
+import com.lambda.graphics.outline.OutlineRenderer
+import com.lambda.graphics.outline.OutlineStyle
+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 class AbstractRenderer(val name: String, var depthTest: SafeContext.() -> Boolean) {
+ protected abstract fun getRendererTransforms(): List>
+
+ protected abstract fun getScreenRenderers(): List
+
+ protected abstract val currentFontAtlas: SDFFontAtlas?
+
+ fun SafeContext.render() {
+ val chunks = getRendererTransforms()
+ if (chunks.isEmpty()) return
+
+ val depth = depthTest()
+
+ RegionRenderer.createRenderPass("$name Faces", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.facesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderFaces(pass)
+ }
+ }
+
+ RegionRenderer.createRenderPass("$name Edges", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.edgesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderEdges(pass)
+ }
+ }
+
+ 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.textPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.bindTexture("Sampler0", textureView, sampler)
+ textChunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderText(pass)
+ }
+ }
+ }
+ }
+
+ val imageChunks = chunks.filter { (renderer, _) -> renderer.hasWorldImageData() }
+ if (imageChunks.isNotEmpty()) {
+ RendererUtils.ensureGlintTextureLoaded()
+
+ RegionRenderer.createRenderPass("$name World Images", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.worldImagePipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+
+ RendererUtils.bindGlintTexture(pass, "Sampler1")
+
+ imageChunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderWorldImages(pass)
+ }
+ }
+ }
+
+ val modelChunks = chunks.filter { (renderer, _) -> renderer.hasModelData() }
+ if (modelChunks.isNotEmpty()) {
+ RendererUtils.ensureGlintTextureLoaded()
+
+ val glintUniform = RendererUtils.createGlintUniform(8.0f)
+
+ RegionRenderer.createRenderPass("$name World Models", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.modelPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+
+ RendererUtils.bindOverlayTexture(pass, "Sampler1")
+ RendererUtils.bindLightmapTexture(pass, "Sampler2")
+ RendererUtils.bindGlintTexture(pass, "Sampler3")
+
+ pass.setUniform("GlintTransforms", glintUniform)
+
+ modelChunks.forEach { (renderer, transform) ->
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderModels(pass)
+ }
+ }
+ }
+
+ val outlinedIds = chunks.flatMap { it.first.getOutlineIds() }.toSet()
+ if (outlinedIds.isNotEmpty()) {
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+
+ RendererUtils.ensureGlintTextureLoaded()
+ val glintUniformL = RendererUtils.createGlintUniform(8.0f)
+
+ val depth = depthTest()
+ outlinedIds.forEach { id ->
+ val style = OutlineManager.getOutlineStyle(id) ?: OutlineStyle.DEFAULT
+
+ RegionRenderer.createRenderPass("$name Outlined Draw $id", depth)?.use { pass ->
+ pass.setPipeline(RendererUtils.facesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedFaces(pass, id)
+ }
+ }
+
+ pass.setPipeline(RendererUtils.edgesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedEdges(pass, id)
+ }
+ }
+
+ val atlasD = currentFontAtlas
+ if (atlasD != null && atlasD.textureView != null) {
+ pass.setPipeline(RendererUtils.textPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.bindTexture("Sampler0", atlasD.textureView!!, atlasD.sampler ?: nearestSampler)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedText(pass, id)
+ }
+ }
+ }
+
+ pass.setPipeline(RendererUtils.worldImagePipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ RendererUtils.bindGlintTexture(pass, "Sampler1")
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedImages(pass, id)
+ }
+ }
+
+ pass.setPipeline(RendererUtils.modelPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ RendererUtils.bindOverlayTexture(pass, "Sampler1")
+ RendererUtils.bindLightmapTexture(pass, "Sampler2")
+ RendererUtils.bindGlintTexture(pass, "Sampler3")
+ pass.setUniform("GlintTransforms", glintUniformL)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedModels(pass, id)
+ }
+ }
+ }
+
+ OutlineRenderer.beginGroupPass("$name Group $id")
+ val groupTarget = OutlineRenderer.getGroupView() ?: return@forEach
+ val groupDepthView = OutlineRenderer.getGroupDepthView() ?: return@forEach
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "$name Outline Group $id - Draw" },
+ groupTarget,
+ java.util.OptionalInt.empty(),
+ groupDepthView,
+ java.util.OptionalDouble.empty()
+ )?.use { pass ->
+ pass.setPipeline(RendererUtils.facesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedFaces(pass, id)
+ }
+ }
+
+ pass.setPipeline(RendererUtils.edgesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedEdges(pass, id)
+ }
+ }
+
+ val atlasT = currentFontAtlas
+ if (atlasT != null && atlasT.textureView != null) {
+ pass.setPipeline(RendererUtils.textPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.bindTexture("Sampler0", atlasT.textureView!!, atlasT.sampler ?: nearestSampler)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedText(pass, id)
+ }
+ }
+ }
+
+ pass.setPipeline(RendererUtils.worldImagePipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ RendererUtils.bindGlintTexture(pass, "Sampler1")
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedImages(pass, id)
+ }
+ }
+
+ pass.setPipeline(RendererUtils.modelPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ RendererUtils.bindOverlayTexture(pass, "Sampler1")
+ RendererUtils.bindLightmapTexture(pass, "Sampler2")
+ RendererUtils.bindGlintTexture(pass, "Sampler3")
+ pass.setUniform("GlintTransforms", glintUniformL)
+ chunks.forEach { (renderer, transform) ->
+ if (renderer.hasOutlinedData(id)) {
+ pass.setUniform("DynamicTransforms", transform)
+ renderer.renderOutlinedModels(pass, id)
+ }
+ }
+ }
+
+ OutlineRenderer.endGroupPass(style, depth)
+ }
+ }
+ }
+
+ fun renderScreen() {
+ val renderers = getScreenRenderers()
+
+ RendererUtils.withScreenContext {
+ val dynamicTransform = RendererUtils.createScreenDynamicTransform()
+
+ RendererUtils.clearScreenDepthBuffer()
+
+ fun getScreenPass(label: String): RenderPass? =
+ RegionRenderer.createScreenRenderPassWithDepth(label, clearDepth = false)
+
+ val modelRenderers = renderers.filter { it.hasScreenModelData() }
+ if (modelRenderers.isNotEmpty()) {
+ RendererUtils.ensureGlintTextureLoaded()
+
+ val glintUniform = RendererUtils.createGlintUniform(8.0f)
+
+ getScreenPass("$name Screen Models")?.use { pass ->
+ pass.setPipeline(RendererUtils.modelPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ pass.setUniform("GlintTransforms", glintUniform)
+
+ RendererUtils.bindOverlayTexture(pass, "Sampler1")
+ RendererUtils.bindLightmapTexture(pass, "Sampler2")
+ RendererUtils.bindGlintTexture(pass, "Sampler3")
+
+ modelRenderers.forEach { it.renderScreenModels(pass) }
+ }
+ }
+
+ getScreenPass("$name Screen Faces")?.use { pass ->
+ pass.setPipeline(RendererUtils.screenFacesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ renderers.forEach { it.renderScreenFaces(pass) }
+ }
+
+ getScreenPass("$name Screen Edges")?.use { pass ->
+ pass.setPipeline(RendererUtils.screenEdgesPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ renderers.forEach { it.renderScreenEdges(pass) }
+ }
+
+ 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.screenTextPipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ pass.bindTexture("Sampler0", textureView, sampler)
+ textRenderers.forEach { it.renderScreenText(pass) }
+ }
+ }
+ }
+
+ val imageRenderers = renderers.filter { it.hasScreenImageData() }
+ if (imageRenderers.isNotEmpty()) {
+ RendererUtils.ensureGlintTextureLoaded()
+
+ val glintTransform = RendererUtils.createScreenDynamicTransformWithGlint()
+
+ getScreenPass("$name Screen Images")?.use { pass ->
+ pass.setPipeline(RendererUtils.screenImagePipeline)
+ RenderSystem.bindDefaultUniforms(pass)
+ pass.setUniform("DynamicTransforms", glintTransform)
+
+ RendererUtils.bindGlintTexture(pass, "Sampler1")
+
+ 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..555219ad7
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.Module
+import com.lambda.module.modules.client.StyleEditor
+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.client.world.ClientWorld
+import net.minecraft.util.math.Vec3d
+import net.minecraft.world.chunk.WorldChunk
+import org.joml.Vector3f
+import org.joml.Vector4f
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedDeque
+
+class ChunkedRenderer(
+ owner: Any,
+ name: String,
+ depthTest: SafeContext.() -> Boolean,
+ private val update: RenderBuilder.(ClientWorld, 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>()
+
+ 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.listen { rebuild() }
+
+ owner.listenConcurrently {
+ val depth = depthTest()
+ val queueSize = rebuildQueue.size
+ val polls = minOf(StyleEditor.rebuildsPerTick, queueSize)
+ repeat(polls) { rebuildQueue.poll()?.rebuild(depth) }
+ }
+
+ 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)
+
+ fun rebuild() {
+ rebuildQueue.clear()
+ mc.world?.chunkManager?.chunks?.let { chunks ->
+ val chunkCount = chunks.loadedChunkCount
+ (0..chunkCount).forEach { index ->
+ val chunk = chunks.chunks.get(index) ?: return@forEach
+ chunkMap.putIfAbsent(chunk.chunkKey, chunk.chunkData)
+ }
+ }
+ rebuildQueue.addAll(chunkMap.values)
+ }
+
+ fun clear() {
+ chunkMap.values.forEach { it.clearData() }
+ chunkMap.clear()
+ rebuildQueue.clear()
+ uploadQueue.clear()
+ }
+
+ 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 glintMatrix = RendererUtils.createGlintTransform(0.25f)
+
+ return activeChunks.map { chunkData ->
+ val offsetX = (chunkData.originX - cameraPos.x).toFloat()
+ val offsetY = (chunkData.originY - cameraPos.y).toFloat()
+ val offsetZ = (chunkData.originZ - cameraPos.z).toFloat()
+
+ val dynamicTransform = RenderSystem.getDynamicUniforms()
+ .write(
+ RenderMain.cameraRotationMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(offsetX, offsetY, offsetZ),
+ glintMatrix
+ )
+
+ chunkData.renderer to dynamicTransform
+ }
+ }
+
+ override fun getScreenRenderers() =
+ chunkMap.values
+ .filter { it.renderer.hasScreenData() }
+ .map { it.renderer }
+
+ private inner class ChunkData(val chunk: WorldChunk) {
+ val originX = (chunk.pos.x shl 4).toDouble()
+ val originY = chunk.bottomY.toDouble()
+ val originZ = (chunk.pos.z shl 4).toDouble()
+
+ val renderer = RegionRenderer()
+
+ fun markDirty() {
+ if (!rebuildQueue.contains(this)) rebuildQueue.add(this)
+ }
+
+ fun rebuild(depthTest: Boolean) {
+ val chunkOriginVec = Vec3d(originX, originY, originZ)
+ val scope = RenderBuilder(chunkOriginVec, depthTest = depthTest)
+
+ 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 as? ClientWorld ?: continue, 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.(ClientWorld, FastVector) -> Unit
+ ) = ChunkedRenderer(this, name, depthTest, update).also { renderer ->
+ (this as? Module)?.let { module ->
+ module.onEnable { renderer.rebuild() }
+ module.onDisable { renderer.rebuild() }
+ }
+ }
+ }
+}
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..5f889ab66
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/ImmediateRenderer.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.Vector3f
+import org.joml.Vector4f
+
+class ImmediateRenderer(
+ owner: Any,
+ name: String,
+ depthTest: SafeContext.() -> Boolean,
+ update: RenderBuilder.(SafeContext) -> Unit
+) : AbstractRenderer(name, depthTest) {
+ private val renderer = RegionRenderer()
+
+ private var _currentFontAtlas: SDFFontAtlas? = null
+ override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas
+
+ init {
+ owner.listen {
+ val context = SafeContext.create() ?: return@listen
+ val depth = depthTest(context)
+ val renderBuilder = RenderBuilder(mc.gameRenderer.camera.pos, depthTest = depth).also {
+ it.update(context)
+ }
+ upload(renderBuilder)
+ }
+
+ owner.listen { render() }
+ owner.listen { renderScreen() }
+ }
+
+ fun upload(renderBuilder: RenderBuilder) {
+ renderer.upload(renderBuilder.collector)
+ _currentFontAtlas = renderBuilder.fontAtlas
+ }
+
+ override fun getRendererTransforms(): List> {
+ if (!renderer.hasData()) return emptyList()
+
+ val dynamicTransform = RenderSystem.getDynamicUniforms()
+ .write(
+ RenderMain.cameraRotationMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ RendererUtils.createGlintTransform(0.125f)
+ )
+
+ return listOf(renderer to dynamicTransform)
+ }
+
+ 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..7db300810
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/RendererUtils.kt
@@ -0,0 +1,319 @@
+/*
+ * 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.graphics.RenderMain.projModel
+import com.lambda.graphics.mc.LambdaRenderPipelines
+import com.lambda.graphics.texture.LambdaImageAtlas
+import com.mojang.blaze3d.buffers.GpuBuffer
+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.render.ProjectionMatrix2
+import net.minecraft.util.math.Vec3d
+import org.joml.Matrix4f
+import org.joml.Vector2f
+import org.joml.Vector3f
+import org.joml.Vector4f
+import java.nio.ByteBuffer
+import kotlin.math.abs
+
+object RendererUtils {
+ private val screenProjectionMatrix = ProjectionMatrix2("lambda_screen", -1000f, 1000f, false)
+
+ val facesPipeline: RenderPipeline get() = LambdaRenderPipelines.ESP_QUADS
+ val edgesPipeline: RenderPipeline get() = LambdaRenderPipelines.ESP_LINES
+ val textPipeline: RenderPipeline get() = LambdaRenderPipelines.SDF_TEXT
+ val worldImagePipeline: RenderPipeline get() = LambdaRenderPipelines.WORLD_IMAGE
+ val modelPipeline: RenderPipeline get() = LambdaRenderPipelines.WORLD_MODEL
+
+ val screenFacesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_FACES
+ val screenEdgesPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_LINES
+ val screenTextPipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_TEXT
+ val screenImagePipeline: RenderPipeline get() = LambdaRenderPipelines.SCREEN_IMAGE
+
+ private var glintTextureView: com.mojang.blaze3d.textures.GpuTextureView? = null
+ private var glintSampler: net.minecraft.client.gl.GpuSampler? = null
+ private var glintTextureLoaded = false
+
+ 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
+
+ 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
+
+ fun createScreenDynamicTransform(): GpuBufferSlice {
+ val identityMatrix = Matrix4f()
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ identityMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ identityMatrix
+ )
+ }
+
+ fun createScreenDynamicTransformWithGlint(): GpuBufferSlice {
+ val identityMatrix = Matrix4f()
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ identityMatrix,
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ createGlintTransform(0.125f)
+ )
+ }
+
+ fun createGlintTransform(scale: Float): Matrix4f {
+ val glintSpeed = mc.options?.glintSpeed?.value ?: 0.5
+ val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong()
+
+ val scrollX = (time % 110000L) / 110000.0f
+ val scrollY = (time % 30000L) / 30000.0f
+
+ val matrix = Matrix4f()
+ matrix.translation(-scrollX, scrollY, 0f)
+ matrix.rotateZ((Math.PI / 18.0).toFloat())
+ matrix.scale(scale)
+
+ return matrix
+ }
+
+ fun createGlintUniform(scale: Float): GpuBufferSlice {
+ val glintSpeed = mc.options?.glintSpeed?.value ?: 0.5
+ val time = (net.minecraft.util.Util.getMeasuringTimeMs() * glintSpeed * 8.0).toLong()
+
+ 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())
+ .scale(scale)
+
+ return RenderSystem.getDynamicUniforms()
+ .write(
+ Matrix4f(),
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ mat1
+ )
+ }
+
+ fun withScreenContext(block: () -> Unit) {
+ val window = mc.window ?: return
+ val width = window.scaledWidth.toFloat()
+ val height = window.scaledHeight.toFloat()
+
+ RenderSystem.backupProjectionMatrix()
+
+ screenProjectionMatrix.set(width, height).let { slice ->
+ RenderSystem.setProjectionMatrix(slice, ProjectionType.ORTHOGRAPHIC)
+ }
+
+ RenderSystem.getModelViewStack().pushMatrix().identity()
+
+ try {
+ block()
+ } finally {
+ RenderSystem.getModelViewStack().popMatrix()
+ RenderSystem.restoreProjectionMatrix()
+ }
+ }
+
+ fun ensureGlintTextureLoaded() {
+ 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
+ }
+
+ glintSampler = RenderSystem.getSamplerCache().getRepeated(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ }
+
+ fun bindGlintTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) {
+ val view = glintTextureView ?: return
+ val sampler = glintSampler ?: return
+ pass.bindTexture(samplerName, view, sampler)
+ }
+
+ fun bindOverlayTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) {
+ val overlay = mc.gameRenderer.overlayTexture ?: return
+ val view = overlay.textureView ?: return
+ val sampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ pass.bindTexture(samplerName, view, sampler)
+ }
+
+ fun bindLightmapTexture(pass: com.mojang.blaze3d.systems.RenderPass, samplerName: String) {
+ val lightmap = mc.gameRenderer.lightmapTextureManager ?: return
+ val view = lightmap.glTextureView ?: return
+ val sampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.LINEAR)
+ pass.bindTexture(samplerName, view, sampler)
+ }
+
+ fun getXrayDepthView(): com.mojang.blaze3d.textures.GpuTextureView? {
+ val framebuffer = mc.framebuffer ?: return null
+ val width = framebuffer.textureWidth
+ val height = framebuffer.textureHeight
+
+ if (xrayDepthTexture == null || xrayDepthWidth != width || xrayDepthHeight != height) {
+ xrayDepthView?.close()
+ xrayDepthTexture?.close()
+
+ val gpuDevice = RenderSystem.getDevice()
+ xrayDepthTexture = gpuDevice.createTexture(
+ { "Lambda Xray Depth Buffer" },
+ 15,
+ com.mojang.blaze3d.textures.TextureFormat.DEPTH32,
+ width,
+ height,
+ 1,
+ 1
+ )
+ xrayDepthView = gpuDevice.createTextureView(xrayDepthTexture)
+ xrayDepthWidth = width
+ xrayDepthHeight = height
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Clear Xray Depth" },
+ framebuffer.colorAttachmentView,
+ java.util.OptionalInt.empty(),
+ xrayDepthView,
+ java.util.OptionalDouble.of(1.0)
+ )?.close()
+ }
+
+ return xrayDepthView
+ }
+
+ fun clearXrayDepthBuffer() {
+ val depthView = getXrayDepthView() ?: return
+ val framebuffer = mc.framebuffer ?: return
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Clear Xray Depth" },
+ framebuffer.colorAttachmentView,
+ java.util.OptionalInt.empty(),
+ depthView,
+ java.util.OptionalDouble.of(1.0)
+ )?.close()
+ }
+
+ fun getScreenDepthView(): com.mojang.blaze3d.textures.GpuTextureView? {
+ val framebuffer = mc.framebuffer ?: return null
+ val width = framebuffer.textureWidth
+ val height = framebuffer.textureHeight
+
+ if (screenDepthTexture == null || screenDepthWidth != width || screenDepthHeight != height) {
+ screenDepthView?.close()
+ screenDepthTexture?.close()
+
+ val gpuDevice = RenderSystem.getDevice()
+ screenDepthTexture = gpuDevice.createTexture(
+ { "Lambda Screen Depth Buffer" },
+ 15,
+ com.mojang.blaze3d.textures.TextureFormat.DEPTH32,
+ width,
+ height,
+ 1,
+ 1
+ )
+ screenDepthView = gpuDevice.createTextureView(screenDepthTexture)
+ screenDepthWidth = width
+ screenDepthHeight = height
+ }
+
+ return screenDepthView
+ }
+
+ 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()
+ }
+
+ fun worldToScreenNormalized(worldPos: Vec3d): Vector2f? {
+ val camera = mc.gameRenderer?.camera ?: return null
+ val cameraPos = camera.pos
+
+ 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)
+
+ val isBehind = vec.w < 0
+ val w = if (abs(vec.w) < 0.001f) 0.001f else abs(vec.w)
+
+ var ndcX = vec.x / w
+ var ndcY = vec.y / w
+
+ if (isBehind) {
+ val len = kotlin.math.sqrt(ndcX * ndcX + ndcY * ndcY)
+ if (len > 0.0001f) {
+ ndcX = (ndcX / len) * 3f
+ ndcY = (ndcY / len) * 3f
+ } else ndcY = -3f
+ }
+
+ val normalizedX = (ndcX + 1f) * 0.5f
+ val normalizedY = (ndcY + 1f) * 0.5f
+
+ return Vector2f(normalizedX, normalizedY)
+ }
+
+ fun isOnScreen(worldPos: Vec3d): Boolean {
+ val camera = mc.gameRenderer?.camera ?: return false
+ val cameraPos = camera.pos
+
+ 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
+ }
+}
+
+fun GpuBuffer.upload(data: ByteBuffer) =
+ RenderSystem.getDevice().createCommandEncoder().writeToBuffer(this.slice(), data)
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..b33333d15
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/mc/renderer/TickedRenderer.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.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
+
+
+class TickedRenderer(
+ owner: Any,
+ name: String,
+ depthTest: SafeContext.() -> Boolean,
+ update: RenderBuilder.(SafeContext) -> Unit
+) : AbstractRenderer(name, depthTest) {
+ private val renderer = RegionRenderer()
+
+ private var tickCameraPos: Vec3d? = null
+
+ private var _currentFontAtlas: SDFFontAtlas? = null
+ override val currentFontAtlas: SDFFontAtlas? get() = _currentFontAtlas
+
+ init {
+ owner.listen {
+ val depth = depthTest()
+ clear()
+ tickCameraPos = mc.gameRenderer.camera.pos
+ val renderBuilder = RenderBuilder(tickCameraPos ?: return@listen, depthTest = depth).also {
+ it.update(this)
+ }
+ upload(renderBuilder)
+ }
+
+ owner.listen { render() }
+ owner.listen { renderScreen() }
+ }
+
+ fun clear() {
+ renderer.clearData()
+ tickCameraPos = null
+ }
+
+ fun upload(renderBuilder: RenderBuilder) {
+ renderer.upload(renderBuilder.collector)
+ _currentFontAtlas = renderBuilder.fontAtlas
+ }
+
+ override fun getRendererTransforms(): List> {
+ val currentCameraPos = mc.gameRenderer?.camera?.pos ?: return emptyList()
+ val tickCamera = tickCameraPos ?: return emptyList()
+ if (!renderer.hasData()) return emptyList()
+
+ val deltaX = (tickCamera.x - currentCameraPos.x).toFloat()
+ val deltaY = (tickCamera.y - currentCameraPos.y).toFloat()
+ val deltaZ = (tickCamera.z - currentCameraPos.z).toFloat()
+
+ val modelView = Matrix4f(RenderMain.cameraRotationMatrix).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)
+ }
+
+ 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/outline/OutlineCapturingQueue.kt b/src/main/kotlin/com/lambda/graphics/outline/OutlineCapturingQueue.kt
new file mode 100644
index 000000000..a22e87c9c
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/outline/OutlineCapturingQueue.kt
@@ -0,0 +1,261 @@
+/*
+ * 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.outline
+
+import com.lambda.graphics.RenderMain
+import net.minecraft.client.model.Model
+import net.minecraft.client.model.ModelPart
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.VertexConsumer
+import net.minecraft.client.render.model.BakedQuad
+import net.minecraft.client.render.state.CameraRenderState
+import net.minecraft.client.texture.Sprite
+import net.minecraft.client.util.math.MatrixStack
+import net.minecraft.item.ItemDisplayContext
+import net.minecraft.util.math.Vec3d
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+import com.mojang.blaze3d.textures.GpuTextureView
+import net.minecraft.client.render.command.BatchingRenderCommandQueue
+import net.minecraft.client.render.command.OrderedRenderCommandQueue
+import net.minecraft.client.render.command.OrderedRenderCommandQueueImpl
+import net.minecraft.client.render.command.ModelCommandRenderer
+import net.minecraft.text.OrderedText
+import net.minecraft.text.Text
+import org.joml.Quaternionf
+
+class OutlineCapturingQueue(
+ private val delegate: OrderedRenderCommandQueueImpl,
+ private val entityId: Int
+) : OrderedRenderCommandQueueImpl() {
+
+ override fun getBatchingQueue(i: Int): BatchingRenderCommandQueue {
+ return OutlineCapturingBatchingQueue(delegate.getBatchingQueue(i), this)
+ }
+
+ override fun clear() = delegate.clear()
+ override fun onNextFrame() = delegate.onNextFrame()
+ override fun getBatchingQueues() = delegate.batchingQueues
+
+ private inner class OutlineCapturingBatchingQueue(
+ private val batchedDelegate: BatchingRenderCommandQueue,
+ parent: OrderedRenderCommandQueueImpl
+ ) : BatchingRenderCommandQueue(parent) {
+
+ private fun getTextureView(renderLayer: RenderLayer): GpuTextureView? {
+ val outlineLayer = renderLayer.getAffectedOutline().orElse(null) ?: renderLayer
+ val setup = outlineLayer.renderSetup ?: return null
+ val textures = setup.resolveTextures()
+ return textures["Sampler0"]?.textureView ?: textures.values.firstOrNull()?.textureView
+ }
+
+ private fun getClipTransform(renderLayer: RenderLayer): Matrix4f {
+ val proj =
+ if (entityId == -1) RenderMain.baseProjectionMatrix
+ else RenderMain.worldProjectionMatrix
+ val camRot = RenderMain.cameraRotationMatrix
+ val result = Matrix4f(proj).mul(camRot)
+
+ val transform = renderLayer.renderSetup?.layeringTransform?.transform
+ if (transform != null) {
+ val stack = org.joml.Matrix4fStack(1)
+ stack.set(result)
+ transform.accept(stack)
+ result.set(stack)
+ }
+ return result
+ }
+
+ override fun submitShadowPieces(matrices: MatrixStack, radius: Float, pieces: List) =
+ batchedDelegate.submitShadowPieces(matrices, radius, pieces)
+
+ override fun submitLabel(matrices: MatrixStack, pos: Vec3d?, y: Int, label: Text, ns: Boolean, l: Int, dist: Double, cam: CameraRenderState) =
+ batchedDelegate.submitLabel(matrices, pos, y, label, ns, l, dist, cam)
+
+ override fun submitText(matrices: MatrixStack, x: Float, y: Float, text: OrderedText, ds: Boolean, lt: net.minecraft.client.font.TextRenderer.TextLayerType, l: Int, c: Int, bc: Int, oc: Int) =
+ batchedDelegate.submitText(matrices, x, y, text, ds, lt, l, c, bc, oc)
+
+ override fun submitFire(matrices: MatrixStack, state: net.minecraft.client.render.entity.state.EntityRenderState, rot: Quaternionf) =
+ batchedDelegate.submitFire(matrices, state, rot)
+
+ override fun submitLeash(matrices: MatrixStack, data: net.minecraft.client.render.entity.state.EntityRenderState.LeashData) =
+ batchedDelegate.submitLeash(matrices, data)
+
+ override fun submitModel(
+ model: Model,
+ state: S,
+ matrices: MatrixStack,
+ renderLayer: RenderLayer,
+ light: Int,
+ overlay: Int,
+ tintedColor: Int,
+ sprite: Sprite?,
+ outlineColor: Int,
+ crumblingOverlay: ModelCommandRenderer.CrumblingOverlayCommand?
+ ) {
+ if (renderLayer.isOutline || renderLayer.affectedOutline.isPresent) {
+ VertexCapture.setActiveTexture(getTextureView(renderLayer))
+ val consumer = CapturingConsumer(renderLayer)
+ model.setAngles(state)
+ model.render(matrices, consumer, light, overlay, tintedColor)
+ consumer.flush()
+ }
+ batchedDelegate.submitModel(model, state, matrices, renderLayer, light, overlay, tintedColor, sprite, outlineColor, crumblingOverlay)
+ }
+
+ override fun submitModelPart(
+ part: ModelPart,
+ matrices: MatrixStack,
+ renderLayer: RenderLayer,
+ light: Int,
+ overlay: Int,
+ sprite: Sprite?,
+ sheeted: Boolean,
+ hasGlint: Boolean,
+ tintedColor: Int,
+ crumblingOverlay: ModelCommandRenderer.CrumblingOverlayCommand?,
+ i: Int
+ ) {
+ if (renderLayer.isOutline || renderLayer.getAffectedOutline().isPresent) {
+ VertexCapture.setActiveTexture(getTextureView(renderLayer))
+ val consumer = CapturingConsumer(renderLayer)
+ part.render(matrices, consumer, light, overlay, tintedColor)
+ consumer.flush()
+ }
+ batchedDelegate.submitModelPart(part, matrices, renderLayer, light, overlay, sprite, sheeted, hasGlint, tintedColor, crumblingOverlay, i)
+ }
+
+ override fun submitBlock(matrices: MatrixStack, state: net.minecraft.block.BlockState, light: Int, overlay: Int, outlineColor: Int) =
+ batchedDelegate.submitBlock(matrices, state, light, overlay, outlineColor)
+
+ override fun submitMovingBlock(matrices: MatrixStack, state: net.minecraft.client.render.block.MovingBlockRenderState) =
+ batchedDelegate.submitMovingBlock(matrices, state)
+
+ override fun submitBlockStateModel(matrices: MatrixStack, layer: RenderLayer, model: net.minecraft.client.render.model.BlockStateModel, r: Float, g: Float, b: Float, l: Int, o: Int, oc: Int) =
+ batchedDelegate.submitBlockStateModel(matrices, layer, model, r, g, b, l, o, oc)
+
+ override fun submitItem(
+ matrices: MatrixStack,
+ displayContext: ItemDisplayContext,
+ light: Int,
+ overlay: Int,
+ outlineColors: Int,
+ tintLayers: IntArray,
+ quads: List,
+ renderLayer: RenderLayer,
+ glintType: net.minecraft.client.render.item.ItemRenderState.Glint
+ ) {
+ if (renderLayer.isOutline || renderLayer.getAffectedOutline().isPresent) {
+ VertexCapture.setActiveTexture(getTextureView(renderLayer))
+ val combined = getClipTransform(renderLayer).mul(matrices.peek().positionMatrix)
+ val posVec = Vector4f()
+ val normVec = Vector3f()
+
+ for (quad in quads) {
+ val nxSrc = quad.face.offsetX.toFloat()
+ val nySrc = quad.face.offsetY.toFloat()
+ val nzSrc = quad.face.offsetZ.toFloat()
+ combined.transformDirection(nxSrc, nySrc, nzSrc, normVec)
+
+ for (vIdx in 0 until 4) {
+ val posSrc = quad.getPosition(vIdx)
+ combined.transform(posSrc.x(), posSrc.y(), posSrc.z(), 1.0f, posVec)
+ val packedUV = quad.getTexcoords(vIdx)
+ val u = java.lang.Float.intBitsToFloat((packedUV shr 32).toInt())
+ val v = java.lang.Float.intBitsToFloat(packedUV.toInt())
+ VertexCapture.captureVertex(posVec.x, posVec.y, posVec.z, posVec.w, normVec.x, normVec.y, normVec.z, u, v)
+ }
+ }
+ }
+ batchedDelegate.submitItem(matrices, displayContext, light, overlay, outlineColors, tintLayers, quads, renderLayer, glintType)
+ }
+
+ override fun submitCustom(matrices: MatrixStack, layer: RenderLayer, renderer: OrderedRenderCommandQueue.Custom) =
+ batchedDelegate.submitCustom(matrices, layer, renderer)
+
+ override fun submitCustom(renderer: OrderedRenderCommandQueue.LayeredCustom) =
+ batchedDelegate.submitCustom(renderer)
+
+ override fun getShadowPiecesCommands() = batchedDelegate.shadowPiecesCommands
+ override fun getFireCommands() = batchedDelegate.fireCommands
+ override fun getLabelCommands() = batchedDelegate.labelCommands
+ override fun getTextCommands() = batchedDelegate.textCommands
+ override fun getLeashCommands() = batchedDelegate.leashCommands
+ override fun getBlockCommands() = batchedDelegate.blockCommands
+ override fun getMovingBlockCommands() = batchedDelegate.movingBlockCommands
+ override fun getBlockStateModelCommands() = batchedDelegate.blockStateModelCommands
+ override fun getModelPartCommands() = batchedDelegate.modelPartCommands
+ override fun getItemCommands() = batchedDelegate.itemCommands
+ override fun getLayeredCustomCommands() = batchedDelegate.layeredCustomCommands
+ override fun getModelCommands() = batchedDelegate.modelCommands
+ override fun getCustomCommands() = batchedDelegate.customCommands
+ override fun hasCommands() = batchedDelegate.hasCommands()
+ override fun clear() = batchedDelegate.clear()
+ override fun onNextFrame() = batchedDelegate.onNextFrame()
+
+ private inner class CapturingConsumer(renderLayer: RenderLayer) : VertexConsumer {
+ private val combined = getClipTransform(renderLayer)
+ private val posVec = Vector4f()
+ private var isCapturingFull = false
+ private var hasVertex = false
+ private var curX = 0f; private var curY = 0f; private var curZ = 0f; private var curW = 1.0f
+ private var curU = 0f; private var curV = 0f
+ private var curNX = 0f; private var curNY = 0f; private var curNZ = 0f
+
+ private fun commit() {
+ if (hasVertex) {
+ VertexCapture.captureVertex(curX, curY, curZ, curW, curNX, curNY, curNZ, curU, curV)
+ hasVertex = false
+ }
+ }
+
+ fun flush() = commit()
+
+ override fun vertex(x: Float, y: Float, z: Float): VertexConsumer {
+ if (!isCapturingFull) {
+ commit()
+ combined.transform(x, y, z, 1.0f, posVec)
+ curX = posVec.x; curY = posVec.y; curZ = posVec.z; curW = posVec.w
+ curU = 0f; curV = 0f; curNX = 0f; curNY = 0f; curNZ = 1f
+ hasVertex = true
+ }
+ return this
+ }
+
+ override fun color(argb: Int): VertexConsumer = this
+ override fun color(r: Int, g: Int, b: Int, a: Int): VertexConsumer = this
+ override fun texture(u: Float, v: Float): VertexConsumer { curU = u; curV = v; return this }
+ override fun overlay(u: Int, v: Int): VertexConsumer = this
+ override fun light(u: Int, v: Int): VertexConsumer = this
+ override fun lineWidth(width: Float): VertexConsumer = this
+ override fun normal(nx: Float, ny: Float, nz: Float): VertexConsumer { curNX = nx; curNY = ny; curNZ = nz; return this }
+
+ override fun vertex(x: Float, y: Float, z: Float, color: Int, u: Float, v: Float, overlay: Int, light: Int, nx: Float, ny: Float, nz: Float) {
+ isCapturingFull = true
+ try {
+ commit()
+ combined.transform(x, y, z, 1.0f, posVec)
+ VertexCapture.captureVertex(posVec.x, posVec.y, posVec.z, posVec.w, nx, ny, nz, u, v)
+ } finally {
+ isCapturingFull = false
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/outline/OutlineIdBuffer.kt b/src/main/kotlin/com/lambda/graphics/outline/OutlineIdBuffer.kt
new file mode 100644
index 000000000..193793eaa
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/outline/OutlineIdBuffer.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.outline
+
+import com.lambda.Lambda.mc
+import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.textures.GpuTexture
+import com.mojang.blaze3d.textures.GpuTextureView
+import com.mojang.blaze3d.textures.TextureFormat
+import java.util.OptionalDouble
+import java.util.OptionalInt
+
+object OutlineIdBuffer {
+ private var idTexture: GpuTexture? = null
+ private var idTextureView: GpuTextureView? = null
+
+ private var depthTexture: GpuTexture? = null
+ private var depthTextureView: GpuTextureView? = null
+
+ private var bufferWidth = 0
+ private var bufferHeight = 0
+
+ var hasData = false
+ private set
+
+ fun ensureBuffer(): Boolean {
+ val framebuffer = mc.framebuffer ?: return false
+ val width = framebuffer.textureWidth
+ val height = framebuffer.textureHeight
+
+ if (idTexture == null || bufferWidth != width || bufferHeight != height) {
+ cleanup()
+
+ val gpuDevice = RenderSystem.getDevice()
+
+ idTexture = gpuDevice.createTexture(
+ { "Lambda Entity ID Buffer" },
+ 15,
+ TextureFormat.RGBA8,
+ width,
+ height,
+ 1,
+ 1
+ )
+ idTextureView = gpuDevice.createTextureView(idTexture)
+
+ depthTexture = gpuDevice.createTexture(
+ { "Lambda Entity ID Depth Buffer" },
+ 15,
+ TextureFormat.DEPTH32,
+ width,
+ height,
+ 1,
+ 1
+ )
+ depthTextureView = gpuDevice.createTextureView(depthTexture)
+
+ bufferWidth = width
+ bufferHeight = height
+ }
+
+ return true
+ }
+
+ fun beginFrame() {
+ if (!ensureBuffer()) return
+ hasData = false
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Clear Entity ID Buffer" },
+ idTextureView,
+ OptionalInt.of(0x00000000),
+ depthTextureView,
+ OptionalDouble.of(1.0)
+ )?.close()
+ }
+
+ fun markHasData() {
+ hasData = true
+ }
+
+ fun getMcDepthView(): GpuTextureView? = mc.framebuffer?.depthAttachmentView
+
+ fun getDepthView(): GpuTextureView? = depthTextureView
+
+ fun getSilhouetteDepthView(): GpuTextureView? = depthTextureView
+
+ fun getTextureView(): GpuTextureView? = idTextureView
+
+ fun getWidth(): Int = bufferWidth
+ fun getHeight(): Int = bufferHeight
+
+ fun isReady(): Boolean = idTexture != null && idTextureView != null
+
+ fun cleanup() {
+ idTextureView?.close()
+ idTexture?.close()
+ depthTextureView?.close()
+ depthTexture?.close()
+
+ idTextureView = null
+ idTexture = null
+ depthTextureView = null
+ depthTexture = null
+ bufferWidth = 0
+ bufferHeight = 0
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/outline/OutlineIdPassRenderer.kt b/src/main/kotlin/com/lambda/graphics/outline/OutlineIdPassRenderer.kt
new file mode 100644
index 000000000..f706fe008
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/outline/OutlineIdPassRenderer.kt
@@ -0,0 +1,232 @@
+/*
+ * 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.outline
+
+import com.lambda.Lambda.mc
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.mc.LambdaRenderPipelines
+import com.lambda.graphics.mc.renderer.upload
+import com.mojang.blaze3d.buffers.GpuBuffer
+import com.mojang.blaze3d.systems.RenderSystem
+import net.minecraft.client.render.VertexFormats
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+import org.lwjgl.system.MemoryUtil
+import java.nio.ByteOrder
+import java.util.OptionalDouble
+import java.util.OptionalInt
+
+object OutlineIdPassRenderer {
+ private val vertexSize = 28
+
+ private class DrawBatch(val textureView: com.mojang.blaze3d.textures.GpuTextureView?, val vertexCount: Int, val vertexOffset: Int)
+
+ private var depthTestedVertexBuffer: GpuBuffer? = null
+ private val depthTestedBatches = mutableListOf()
+
+ private var xrayVertexBuffer: GpuBuffer? = null
+ private val xrayBatches = mutableListOf()
+
+ fun render(entityIds: Set, useMcDepth: Boolean) {
+ if (!OutlineIdBuffer.ensureBuffer()) return
+
+ val colorView = OutlineIdBuffer.getTextureView() ?: return
+
+ val outlines = if (useMcDepth) OutlineManager.getDepthTestedStyles() else OutlineManager.getXrayStyles()
+ val filteredOutlines = outlines.filter { (id, _) -> id in entityIds }
+
+ if (filteredOutlines.isNotEmpty()) {
+ buildVertexBuffer(filteredOutlines, isDepthTested = useMcDepth)
+ } else {
+ if (useMcDepth) depthTestedBatches.clear() else xrayBatches.clear()
+ }
+
+ val vbo = if (useMcDepth) depthTestedVertexBuffer else xrayVertexBuffer
+ val batches = if (useMcDepth) depthTestedBatches else xrayBatches
+
+ if (vbo != null && batches.isNotEmpty()) {
+ OutlineIdBuffer.markHasData()
+ renderPass(colorView, vbo, batches)
+ }
+ }
+
+ fun buildVertexBuffer(entities: Map, isDepthTested: Boolean) {
+ val geometries = entities.mapValues { (id, _) -> VertexCapture.getEntityGeometries(id) }
+ buildVertexBufferInternal(geometries, styles = entities, isDepthTested = isDepthTested, transform = null)
+ }
+
+ private fun buildVertexBufferInternal(
+ allGeometries: Map>,
+ styles: Map,
+ isDepthTested: Boolean,
+ transform: Matrix4f? = null
+ ) {
+ val targetBatches = if (isDepthTested) depthTestedBatches else xrayBatches
+ targetBatches.clear()
+
+ val textureGroups = mutableMapOf>>()
+ var totalTriVertices = 0
+
+ for ((id, geometries) in allGeometries) {
+ val style = styles[id] ?: continue
+ for (geometry in geometries) {
+ if (!geometry.isEmpty()) {
+ textureGroups.getOrPut(geometry.textureView) { mutableListOf() }.add(geometry to style)
+ totalTriVertices += (geometry.getVertices().size / 4) * 6
+ }
+ }
+ }
+
+ if (totalTriVertices == 0) return
+
+ val buffer = MemoryUtil.memAlloc(totalTriVertices * vertexSize).order(ByteOrder.nativeOrder())
+ val posVec = Vector4f()
+
+ try {
+ var currentVertexOffset = 0
+
+ for ((textureView, group) in textureGroups) {
+ val batchStartVertex = currentVertexOffset
+ var batchVertexCount = 0
+
+ for ((geometry, style) in group) {
+ val capturedVerts = geometry.getVertices()
+ val color = style.color
+ var alpha =
+ if (style.fill) (style.fillOpacity * 125f).toInt().coerceIn(2, 125)
+ else 1
+
+ if (isDepthTested) alpha = alpha or 128
+
+ val packedColor = (alpha shl 24) or
+ ((color.blue and 0xFF) shl 16) or
+ ((color.green and 0xFF) shl 8) or
+ (color.red and 0xFF)
+
+ val quadCount = capturedVerts.size / 4
+ for (q in 0 until quadCount) {
+ val baseIdx = q * 4
+ val quadVerts = arrayOf(
+ capturedVerts[baseIdx],
+ capturedVerts[baseIdx + 1],
+ capturedVerts[baseIdx + 2],
+ capturedVerts[baseIdx + 3]
+ )
+
+ val transformed = if (transform != null) {
+ quadVerts.map { v ->
+ transform.transform(v.x, v.y, v.z, v.w, posVec)
+ CapturedVertex(posVec.x, posVec.y, posVec.z, posVec.w, v.nx, v.ny, v.nz, v.u, v.v)
+ }
+ } else quadVerts.toList()
+
+ val v0 = transformed[0]
+ val v1 = transformed[1]
+ val v2 = transformed[2]
+ val v3 = transformed[3]
+
+ buffer.putFloat(v0.x).putFloat(v0.y).putFloat(v0.z).putFloat(v0.w).putFloat(v0.u).putFloat(v0.v).putInt(packedColor)
+ buffer.putFloat(v1.x).putFloat(v1.y).putFloat(v1.z).putFloat(v1.w).putFloat(v1.u).putFloat(v1.v).putInt(packedColor)
+ buffer.putFloat(v2.x).putFloat(v2.y).putFloat(v2.z).putFloat(v2.w).putFloat(v2.u).putFloat(v2.v).putInt(packedColor)
+
+ buffer.putFloat(v0.x).putFloat(v0.y).putFloat(v0.z).putFloat(v0.w).putFloat(v0.u).putFloat(v0.v).putInt(packedColor)
+ buffer.putFloat(v2.x).putFloat(v2.y).putFloat(v2.z).putFloat(v2.w).putFloat(v2.u).putFloat(v2.v).putInt(packedColor)
+ buffer.putFloat(v3.x).putFloat(v3.y).putFloat(v3.z).putFloat(v3.w).putFloat(v3.u).putFloat(v3.v).putInt(packedColor)
+
+ currentVertexOffset += 6
+ batchVertexCount += 6
+ }
+ }
+
+ if (batchVertexCount > 0) {
+ targetBatches.add(DrawBatch(textureView, batchVertexCount, batchStartVertex))
+ }
+ }
+
+ buffer.flip()
+ if (isDepthTested) {
+ depthTestedVertexBuffer?.close()
+ val vbo = RenderSystem.getDevice().createBuffer({ "Lambda Outline ID (Depth)" }, GpuBuffer.USAGE_VERTEX or GpuBuffer.USAGE_COPY_DST, buffer.remaining().toLong())
+ vbo.upload(buffer)
+ depthTestedVertexBuffer = vbo
+ } else {
+ xrayVertexBuffer?.close()
+ val vbo = RenderSystem.getDevice().createBuffer({ "Lambda Outline ID (Xray)" }, GpuBuffer.USAGE_VERTEX or GpuBuffer.USAGE_COPY_DST, buffer.remaining().toLong())
+ vbo.upload(buffer)
+ xrayVertexBuffer = vbo
+ }
+ } finally {
+ MemoryUtil.memFree(buffer)
+ }
+ }
+
+ private fun renderPass(
+ colorView: com.mojang.blaze3d.textures.GpuTextureView,
+ vertexBuffer: GpuBuffer,
+ batches: List
+ ) {
+ val depthView = OutlineIdBuffer.getSilhouetteDepthView() ?: return
+ if (batches.isEmpty()) return
+
+ val blockAtlas = mc.textureManager.getTexture(net.minecraft.util.Identifier.ofVanilla("textures/atlas/blocks.png"))
+ val whiteTexture = blockAtlas.glTextureView
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+
+ val idUniform = RenderSystem.getDynamicUniforms().write(
+ Matrix4f(),
+ Vector4f(1f, 1f, 1f, 1f),
+ Vector3f(0f, 0f, 0f),
+ Matrix4f()
+ )
+
+ val renderPass = RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Outline ID Pass (Internal Silhouette)" },
+ colorView,
+ OptionalInt.empty(),
+ depthView,
+ OptionalDouble.empty()
+ ) ?: return
+
+ renderPass.use { renderPass ->
+ renderPass.setPipeline(LambdaRenderPipelines.OUTLINE_ID)
+
+ renderPass.setUniform("DynamicTransforms", idUniform)
+
+ renderPass.setVertexBuffer(0, vertexBuffer)
+
+ for (batch in batches) {
+ val textureView = batch.textureView ?: whiteTexture
+ renderPass.bindTexture("Sampler0", textureView, nearestSampler)
+ renderPass.draw(batch.vertexOffset, batch.vertexCount)
+ }
+ }
+ }
+
+ fun cleanup() {
+ depthTestedVertexBuffer?.close()
+ depthTestedVertexBuffer = null
+ depthTestedBatches.clear()
+
+ xrayVertexBuffer?.close()
+ xrayVertexBuffer = null
+ xrayBatches.clear()
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/outline/OutlineManager.kt b/src/main/kotlin/com/lambda/graphics/outline/OutlineManager.kt
new file mode 100644
index 000000000..efdd05110
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/outline/OutlineManager.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.outline
+
+import net.minecraft.util.math.BlockPos
+
+object OutlineManager {
+ private val depthTestedEntityOutlines = mutableMapOf()
+ private val xrayEntityOutlines = mutableMapOf()
+
+ private val blockOutlines = mutableMapOf()
+
+ private val depthTestedCustomOutlines = mutableMapOf()
+ private val xrayCustomOutlines = mutableMapOf()
+ private var nextCustomId = 1_000_000
+
+ fun setEntityOutline(entityId: Int, style: OutlineStyle?, depthTest: Boolean = true) {
+ if (style != null) {
+ if (depthTest) {
+ depthTestedEntityOutlines[entityId] = style
+ xrayEntityOutlines.remove(entityId)
+ } else {
+ xrayEntityOutlines[entityId] = style
+ depthTestedEntityOutlines.remove(entityId)
+ }
+ } else {
+ depthTestedEntityOutlines.remove(entityId)
+ xrayEntityOutlines.remove(entityId)
+ }
+ }
+
+ fun registerCustomOutline(style: OutlineStyle, depthTest: Boolean = true): Int {
+ val id = nextCustomId++
+ if (depthTest) {
+ depthTestedCustomOutlines[id] = style
+ } else {
+ xrayCustomOutlines[id] = style
+ }
+ return id
+ }
+
+ fun getOutlineStyle(id: Int): OutlineStyle? {
+ return depthTestedEntityOutlines[id] ?: xrayEntityOutlines[id] ?:
+ depthTestedCustomOutlines[id] ?: xrayCustomOutlines[id]
+ }
+
+ fun getEntityOutline(entityId: Int): OutlineStyle? =
+ depthTestedEntityOutlines[entityId] ?: xrayEntityOutlines[entityId]
+
+ fun hasEntityOutlines(): Boolean = depthTestedEntityOutlines.isNotEmpty() || xrayEntityOutlines.isNotEmpty()
+
+ @JvmStatic
+ fun shouldCapture(entityId: Int): Boolean =
+ depthTestedEntityOutlines.containsKey(entityId) || xrayEntityOutlines.containsKey(entityId)
+
+ fun getEntityOutlines(): Map> {
+ val all = mutableMapOf>()
+ depthTestedEntityOutlines.forEach { (id, style) -> all[id] = style to true }
+ xrayEntityOutlines.forEach { (id, style) -> all[id] = style to false }
+ return all
+ }
+
+ fun getDepthTestedEntityIds(): Set = depthTestedEntityOutlines.keys
+ fun getXrayEntityIds(): Set = xrayEntityOutlines.keys
+
+ fun getDepthTestedStyles(): Map = depthTestedEntityOutlines
+ fun getXrayStyles(): Map = xrayEntityOutlines
+
+ fun getCustomOutlines(): Map> {
+ val all = mutableMapOf>()
+ depthTestedCustomOutlines.forEach { (id, style) -> all[id] = style to true }
+ xrayCustomOutlines.forEach { (id, style) -> all[id] = style to false }
+ return all
+ }
+
+ fun getDepthTestedCustomStyles(): Map = depthTestedCustomOutlines
+ fun getXrayCustomStyles(): Map = xrayCustomOutlines
+
+ fun setBlockOutline(pos: BlockPos, style: OutlineStyle?, depthTest: Boolean = true) {
+ if (style != null) {
+ blockOutlines[pos] = style
+ } else {
+ blockOutlines.remove(pos)
+ }
+ }
+
+ fun getBlockOutline(pos: BlockPos): OutlineStyle? = blockOutlines[pos]
+
+ fun hasBlockOutlines(): Boolean = blockOutlines.isNotEmpty()
+
+ fun getBlockOutlines(): Map = blockOutlines
+
+ fun clear() {
+ depthTestedEntityOutlines.clear()
+ xrayEntityOutlines.clear()
+ blockOutlines.clear()
+ depthTestedCustomOutlines.clear()
+ xrayCustomOutlines.clear()
+ }
+
+ fun clearEntities() {
+ depthTestedEntityOutlines.clear()
+ xrayEntityOutlines.clear()
+ }
+
+ fun clearBlocks() {
+ blockOutlines.clear()
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/outline/OutlineRenderer.kt b/src/main/kotlin/com/lambda/graphics/outline/OutlineRenderer.kt
new file mode 100644
index 000000000..bdccbf378
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/outline/OutlineRenderer.kt
@@ -0,0 +1,287 @@
+/*
+ * 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.outline
+
+import com.lambda.Lambda.mc
+import com.lambda.graphics.mc.LambdaRenderPipelines
+import com.mojang.blaze3d.buffers.GpuBuffer
+import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.textures.GpuTexture
+import com.mojang.blaze3d.textures.GpuTextureView
+import com.mojang.blaze3d.textures.TextureFormat
+import com.mojang.blaze3d.vertex.VertexFormat
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector4f
+import org.lwjgl.system.MemoryUtil
+import java.util.OptionalDouble
+import java.util.OptionalInt
+
+object OutlineRenderer {
+ private var silhouetteTexture: GpuTexture? = null
+ private var silhouetteView: GpuTextureView? = null
+ private var silhouetteDepthTexture: GpuTexture? = null
+ private var silhouetteDepthView: GpuTextureView? = null
+ private var silhouetteWidth = 0
+ private var silhouetteHeight = 0
+
+ private var silhouetteVertexBuffer: GpuBuffer? = null
+
+ private var fullscreenQuadBuffer: GpuBuffer? = null
+
+ fun getGroupView(): GpuTextureView? = silhouetteView
+
+ fun getGroupDepthView(): GpuTextureView? = silhouetteDepthView
+
+ fun beginGroupPass(name: String) {
+ if (!ensureSilhouetteFBO()) return
+ val colorView = silhouetteView ?: return
+ val depthView = silhouetteDepthView ?: return
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda Begin Group Pass: $name" },
+ colorView,
+ OptionalInt.of(0),
+ depthView,
+ OptionalDouble.of(1.0)
+ )?.close()
+ }
+
+ fun endGroupPass(style: OutlineStyle, depthTest: Boolean = false) {
+ val groupView = silhouetteView ?: return
+ val framebuffer = mc.framebuffer ?: return
+
+ ensureFullscreenQuad()
+ val quadBuffer = fullscreenQuadBuffer ?: return
+
+ val outlineColor = Vector4f(style.color.red / 255f, style.color.green / 255f, style.color.blue / 255f, 1.0f)
+ val styleMat = buildStyleMatrix(style)
+ val dynamicTransform = RenderSystem.getDynamicUniforms().write(
+ Matrix4f(),
+ outlineColor,
+ Vector3f(1f, if (depthTest) 1f else 0f, 0f),
+ styleMat
+ )
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { "Lambda End Group Pass" },
+ framebuffer.colorAttachmentView,
+ OptionalInt.empty(),
+ null,
+ OptionalDouble.empty()
+ )?.use { pass ->
+ pass.setPipeline(LambdaRenderPipelines.OUTLINE_SOBEL)
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+ pass.bindTexture("Sampler0", groupView, nearestSampler)
+
+ val silDepth = silhouetteDepthView
+ if (silDepth != null) pass.bindTexture("Sampler1", silDepth, nearestSampler)
+
+ val worldDepth = framebuffer.depthAttachmentView
+ if (worldDepth != null) pass.bindTexture("Sampler2", worldDepth, nearestSampler)
+
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ pass.setVertexBuffer(0, quadBuffer)
+
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(4)
+ pass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ pass.drawIndexed(0, 0, 6, 1)
+ }
+ }
+ private fun ensureSilhouetteFBO(): Boolean {
+ val framebuffer = mc.framebuffer ?: return false
+ val width = framebuffer.textureWidth
+ val height = framebuffer.textureHeight
+
+ if (silhouetteTexture == null || silhouetteWidth != width || silhouetteHeight != height) {
+ silhouetteView?.close()
+ silhouetteTexture?.close()
+ silhouetteDepthView?.close()
+ silhouetteDepthTexture?.close()
+
+ val gpuDevice = RenderSystem.getDevice()
+
+ silhouetteTexture = gpuDevice.createTexture(
+ { "Lambda Outline Silhouette" },
+ 15,
+ TextureFormat.RGBA8,
+ width,
+ height,
+ 1,
+ 1
+ )
+ silhouetteView = gpuDevice.createTextureView(silhouetteTexture)
+
+ silhouetteDepthTexture = gpuDevice.createTexture(
+ { "Lambda Outline Silhouette Depth" },
+ 15,
+ TextureFormat.DEPTH32,
+ width,
+ height,
+ 1,
+ 1
+ )
+ silhouetteDepthView = gpuDevice.createTextureView(silhouetteDepthTexture)
+
+ silhouetteWidth = width
+ silhouetteHeight = height
+ }
+
+ return true
+ }
+
+ private fun ensureFullscreenQuad() {
+ if (fullscreenQuadBuffer != null) return
+
+ val vertexSize = 20
+ val buffer = MemoryUtil.memAlloc(4 * vertexSize)
+ try {
+ buffer.putFloat(-1f).putFloat(1f).putFloat(0f).putFloat(0f).putFloat(1f)
+ buffer.putFloat(-1f).putFloat(-1f).putFloat(0f).putFloat(0f).putFloat(0f)
+ buffer.putFloat(1f).putFloat(-1f).putFloat(0f).putFloat(1f).putFloat(0f)
+ buffer.putFloat(1f).putFloat(1f).putFloat(0f).putFloat(1f).putFloat(1f)
+ buffer.flip()
+
+ fullscreenQuadBuffer = RenderSystem.getDevice().createBuffer(
+ { "Lambda Outline Fullscreen Quad" },
+ GpuBuffer.USAGE_VERTEX,
+ buffer
+ )
+ } finally {
+ MemoryUtil.memFree(buffer)
+ }
+ }
+
+ private data class StyleKey(
+ val thickness: Float,
+ val glowIntensity: Float,
+ val glowRadius: Float,
+ val fill: Boolean,
+ val fillOpacity: Float
+ )
+
+ private fun OutlineStyle.toKey() = StyleKey(thickness, glowIntensity, glowRadius, fill, fillOpacity)
+
+ fun renderAllIDPasses() {
+ val depthTestedStyles = OutlineManager.getDepthTestedStyles()
+ val xrayStyles = OutlineManager.getXrayStyles()
+
+ val depthTestedGroups = depthTestedStyles.entries.groupBy({ it.value.toKey() }, { it.key })
+ val xrayGroups = xrayStyles.entries.groupBy({ it.value.toKey() }, { it.key })
+
+ val allStyleKeys = (depthTestedGroups.keys + xrayGroups.keys).distinct()
+
+ for (styleKey in allStyleKeys) {
+ OutlineIdBuffer.beginFrame()
+
+ val depthTestedIds = depthTestedGroups[styleKey]
+ val xrayIds = xrayGroups[styleKey]
+
+ if (!depthTestedIds.isNullOrEmpty()) {
+ OutlineIdPassRenderer.render(depthTestedIds.toSet(), useMcDepth = true)
+ }
+ if (!xrayIds.isNullOrEmpty()) {
+ OutlineIdPassRenderer.render(xrayIds.toSet(), useMcDepth = false)
+ }
+
+ if (OutlineIdBuffer.hasData) {
+ val representativeId = depthTestedIds?.firstOrNull() ?: xrayIds?.firstOrNull() ?: continue
+ val representativeStyle = OutlineManager.getOutlineStyle(representativeId) ?: continue
+ applyEdgeDetection(representativeStyle)
+ }
+ }
+ }
+
+ private fun applyEdgeDetection(style: OutlineStyle = OutlineStyle.DEFAULT) {
+ val idBufferView = OutlineIdBuffer.getTextureView() ?: return
+ applySobel(idBufferView, "Lambda Global Outline Sobel Pass", style)
+ }
+
+ private fun buildStyleMatrix(style: OutlineStyle): Matrix4f {
+ val mat = Matrix4f()
+ mat.m00(style.thickness)
+ mat.m01(style.glowIntensity)
+ mat.m02(style.glowRadius)
+ mat.m03(if (style.fill) style.fillOpacity else 0f)
+ return mat
+ }
+
+ private fun applySobel(textureView: GpuTextureView, label: String, style: OutlineStyle = OutlineStyle.DEFAULT) {
+ val framebuffer = mc.framebuffer ?: return
+
+ ensureFullscreenQuad()
+ val quadBuffer = fullscreenQuadBuffer ?: return
+
+ val styleMat = buildStyleMatrix(style)
+ val dynamicTransform = RenderSystem.getDynamicUniforms().write(
+ Matrix4f(), Vector4f(1f, 1f, 1f, 1f), Vector3f(0f, 0f, 0f), styleMat
+ )
+
+ RenderSystem.getDevice()
+ .createCommandEncoder()
+ .createRenderPass(
+ { label },
+ framebuffer.colorAttachmentView,
+ OptionalInt.empty(),
+ null,
+ OptionalDouble.empty()
+ )?.use { pass ->
+ pass.setPipeline(LambdaRenderPipelines.OUTLINE_SOBEL)
+ val nearestSampler = RenderSystem.getSamplerCache().get(com.mojang.blaze3d.textures.FilterMode.NEAREST)
+ pass.bindTexture("Sampler0", textureView, nearestSampler)
+
+ val silDepth = OutlineIdBuffer.getSilhouetteDepthView()
+ if (silDepth != null) pass.bindTexture("Sampler1", silDepth, nearestSampler)
+
+ val worldDepth = OutlineIdBuffer.getMcDepthView()
+ if (worldDepth != null) pass.bindTexture("Sampler2", worldDepth, nearestSampler)
+ pass.setUniform("DynamicTransforms", dynamicTransform)
+ pass.setVertexBuffer(0, quadBuffer)
+ val shapeIndexBuffer = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS)
+ val indexBuffer = shapeIndexBuffer.getIndexBuffer(4)
+ pass.setIndexBuffer(indexBuffer, shapeIndexBuffer.indexType)
+ pass.drawIndexed(0, 0, 6, 1)
+ }
+ }
+
+ fun cleanup() {
+ silhouetteView?.close()
+ silhouetteTexture?.close()
+ silhouetteDepthView?.close()
+ silhouetteDepthTexture?.close()
+ silhouetteVertexBuffer?.close()
+ fullscreenQuadBuffer?.close()
+
+ silhouetteView = null
+ silhouetteTexture = null
+ silhouetteDepthView = null
+ silhouetteDepthTexture = null
+ silhouetteVertexBuffer = null
+ fullscreenQuadBuffer = null
+ silhouetteWidth = 0
+ silhouetteHeight = 0
+
+ OutlineIdBuffer.cleanup()
+ OutlineIdPassRenderer.cleanup()
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/shader/ShaderType.kt b/src/main/kotlin/com/lambda/graphics/outline/OutlineStyle.kt
similarity index 63%
rename from src/main/kotlin/com/lambda/graphics/shader/ShaderType.kt
rename to src/main/kotlin/com/lambda/graphics/outline/OutlineStyle.kt
index 80b97a341..499775f7a 100644
--- a/src/main/kotlin/com/lambda/graphics/shader/ShaderType.kt
+++ b/src/main/kotlin/com/lambda/graphics/outline/OutlineStyle.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,13 +15,19 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.shader
+package com.lambda.graphics.outline
-import com.lambda.graphics.gl.GLObject
-import org.lwjgl.opengl.GL20C.GL_FRAGMENT_SHADER
-import org.lwjgl.opengl.GL20C.GL_VERTEX_SHADER
+import java.awt.Color
-enum class ShaderType(override val gl: Int) : GLObject {
- FragmentShader(GL_FRAGMENT_SHADER),
- VertexShader(GL_VERTEX_SHADER)
+data class OutlineStyle(
+ val color: Color,
+ val thickness: Float = 0.0005f,
+ val glowIntensity: Float = 0.5f,
+ val glowRadius: Float = 0.001f,
+ val fill: Boolean = true,
+ val fillOpacity: Float = 0.4f
+) {
+ companion object {
+ val DEFAULT = OutlineStyle(Color.WHITE)
+ }
}
diff --git a/src/main/kotlin/com/lambda/graphics/outline/VertexCapture.kt b/src/main/kotlin/com/lambda/graphics/outline/VertexCapture.kt
new file mode 100644
index 000000000..6e0542aa3
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/outline/VertexCapture.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.outline
+
+import com.mojang.blaze3d.textures.GpuTextureView
+import kotlin.collections.find
+
+object VertexCapture {
+ private val entityGeometries = mutableMapOf>()
+
+ private var capturingEntityId: Int? = null
+
+ private var currentTextureView: GpuTextureView? = null
+
+ private var currentGeometry: CapturedGeometry? = null
+
+ fun beginCapture(entityId: Int) {
+ capturingEntityId = entityId
+ entityGeometries.getOrPut(entityId) { mutableListOf() }.clear()
+ currentTextureView = null
+ currentGeometry = null
+ }
+
+ fun setActiveTexture(textureView: GpuTextureView?) {
+ val entityId = capturingEntityId ?: return
+
+ val list = entityGeometries[entityId] ?: return
+
+ if (textureView != currentTextureView || currentGeometry == null) {
+ currentTextureView = textureView
+ currentGeometry = list.find { it.textureView == textureView }
+
+ if (currentGeometry == null) {
+ val newGeom = CapturedGeometry(textureView)
+ list.add(newGeom)
+ currentGeometry = newGeom
+ }
+ }
+ }
+
+ fun endCapture() {
+ val entityId = capturingEntityId ?: return
+ entityGeometries[entityId]?.removeAll { it.isEmpty() }
+ if (entityGeometries[entityId]?.isEmpty() == true) entityGeometries.remove(entityId)
+
+ capturingEntityId = null
+ currentTextureView = null
+ currentGeometry = null
+ }
+
+ fun isCapturing(): Boolean = capturingEntityId != null
+ fun isCapturing(entityId: Int): Boolean = capturingEntityId == entityId
+
+ fun captureVertex(x: Float, y: Float, z: Float, w: Float, nx: Float, ny: Float, nz: Float, u: Float = 0f, v: Float = 0f) {
+ currentGeometry?.addVertex(x, y, z, w, nx, ny, nz, u, v)
+ }
+
+ fun getEntityGeometries(entityId: Int): List = entityGeometries[entityId] ?: emptyList()
+
+ fun hasEntityGeometry(): Boolean = entityGeometries.isNotEmpty()
+ fun getCapturedEntityIds(): Set = entityGeometries.keys
+
+ fun clear() {
+ entityGeometries.clear()
+ capturingEntityId = null
+ currentTextureView = null
+ currentGeometry = null
+ }
+}
+
+data class CapturedVertex(
+ val x: Float,
+ val y: Float,
+ val z: Float,
+ val w: Float = 1.0f,
+ val nx: Float = 0f,
+ val ny: Float = 0f,
+ val nz: Float = 1f,
+ val u: Float = 0f,
+ val v: Float = 0f
+)
+
+class CapturedGeometry(val textureView: GpuTextureView?) {
+ private val vertices = ArrayList()
+
+ fun addVertex(x: Float, y: Float, z: Float, w: Float, nx: Float, ny: Float, nz: Float, u: Float = 0f, v: Float = 0f) {
+ vertices.add(CapturedVertex(x, y, z, w, nx, ny, nz, u, v))
+ }
+
+ fun getVertices(): List = vertices
+
+ fun isEmpty(): Boolean = vertices.isEmpty()
+
+ fun size(): Int = vertices.size
+
+ fun clear() {
+ vertices.clear()
+ }
+}
diff --git a/src/main/kotlin/com/lambda/graphics/pipeline/PersistentBuffer.kt b/src/main/kotlin/com/lambda/graphics/pipeline/PersistentBuffer.kt
deleted file mode 100644
index c221a9e8d..000000000
--- a/src/main/kotlin/com/lambda/graphics/pipeline/PersistentBuffer.kt
+++ /dev/null
@@ -1,91 +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.pipeline
-
-import com.lambda.graphics.buffer.Buffer
-import com.lambda.graphics.buffer.DynamicByteBuffer.Companion.dynamicByteBuffer
-import com.lambda.graphics.gl.kibibyte
-import org.lwjgl.opengl.GL30.GL_MAP_WRITE_BIT
-import org.lwjgl.opengl.GL44.GL_DYNAMIC_STORAGE_BIT
-import org.lwjgl.system.MemoryUtil.memCopy
-
-/**
- * Represents a persistent dynamic coherent buffer for fast opengl rendering purposes
- */
-class PersistentBuffer(
- target: Int, stride: Int, initialSize: Int = 1.kibibyte
-) {
- /**
- * Resizable byte buffer that stores all data used last frame
- */
- val byteBuffer = dynamicByteBuffer(stride * initialSize)
-
- /**
- * Represents a OpenGl Object that store unformatted memory
- */
- val buffer = Buffer.create(target, GL_MAP_WRITE_BIT or GL_DYNAMIC_STORAGE_BIT) { allocate(byteBuffer.capacity.toLong()) }
-
- /**
- * Data that has passed through the buffer within previous frame
- */
- private val snapshot = dynamicByteBuffer(1)
- private var snapshotData = 0L
-
- private var glSize = 0
-
- var uploadOffset = 0L
-
- fun upload() {
- val dataStart = byteBuffer.pointer + uploadOffset
- val dataCount = byteBuffer.bytesPut - uploadOffset
- if (dataCount <= 0) return
-
- if (glSize != byteBuffer.capacity) {
- glSize = byteBuffer.capacity
- buffer.allocate(byteBuffer.data)
- snapshot.realloc(byteBuffer.capacity)
- snapshotData = 0
- return
- }
-
- if (snapshotData > 0 && snapshot.capacity >= byteBuffer.bytesPut) {
- if (snapshot.mismatch(byteBuffer) >= 0) return
- }
-
- buffer.update(uploadOffset, dataCount, dataStart)
- }
-
- fun end() {
- uploadOffset = byteBuffer.bytesPut
- }
-
- fun sync() {
- memCopy(byteBuffer.pointer, snapshot.pointer, byteBuffer.bytesPut)
- snapshotData = byteBuffer.bytesPut
-
- byteBuffer.resetPosition()
- uploadOffset = 0
- }
-
- fun clear() {
- snapshot.resetPosition()
- byteBuffer.resetPosition()
- uploadOffset = 0
- snapshotData = 0
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/pipeline/VertexBuilder.kt b/src/main/kotlin/com/lambda/graphics/pipeline/VertexBuilder.kt
deleted file mode 100644
index de2869eae..000000000
--- a/src/main/kotlin/com/lambda/graphics/pipeline/VertexBuilder.kt
+++ /dev/null
@@ -1,287 +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.pipeline
-
-import com.lambda.graphics.buffer.DynamicByteBuffer
-import com.lambda.graphics.gl.Matrices
-import org.joml.Vector4d
-import java.util.concurrent.ConcurrentLinkedDeque
-
-/**
- * A builder class for constructing vertex buffer objects (VBOs) with associated vertex attributes and indices.
- * Provides a DSL-like syntax for defining vertices and their attributes in a type-safe manner.
- */
-@Suppress("DuplicatedCode")
-class VertexBuilder(
- private val direct: VertexPipeline? = null
-) {
- val vertices = ConcurrentLinkedDeque()
- val indices = ConcurrentLinkedDeque()
-
- private var verticesCounter = 0
-
- /**
- * Adds multiple indices to the index buffer
- * @param indices The indices to add to the element array buffer
- */
- fun build(vararg indices: Int) {
- direct?.let {
- indices.forEach { i ->
- it.indices.putInt(i)
- }
-
- return
- }
-
- indices.forEach { this.indices += it }
- }
-
- /**
- * Adds rectangle indices (2 triangles) using 4 vertices
- */
- fun buildQuad(
- index1: Int, index2: Int, index3: Int, index4: Int
- ) {
- direct?.let {
- it.indices.putInt(index1)
- it.indices.putInt(index2)
- it.indices.putInt(index3)
- it.indices.putInt(index3)
- it.indices.putInt(index4)
- it.indices.putInt(index1)
- return
- }
-
- this.indices += index1
- this.indices += index2
- this.indices += index3
- this.indices += index3
- this.indices += index4
- this.indices += index1
- }
-
- /**
- * Adds line indices between 2 vertices
- */
- fun buildLine(
- index1: Int, index2: Int,
- ) {
- direct?.let {
- it.indices.putInt(index1)
- it.indices.putInt(index2)
- return
- }
-
- this.indices += index1
- this.indices += index2
- }
-
- /**
- * Adds triangle indices using 3 vertices
- */
- fun buildTriangle(
- index1: Int, index2: Int, index3: Int
- ) {
- direct?.let {
- it.indices.putInt(index1)
- it.indices.putInt(index2)
- it.indices.putInt(index3)
- return
- }
-
- this.indices += index1
- this.indices += index2
- this.indices += index3
- }
-
- /**
- * Creates a collection of indices from varargs
- * @return List of provided indices for element array buffer
- */
- fun collect(vararg indices: Int) =
- indices
-
- /**
- * Creates a new vertex with specified attributes
- * @param block Configuration lambda for defining vertex attributes
- * @return Index of the created vertex in the vertex array
- */
- fun vertex(block: Vertex.() -> Unit): Int {
- Vertex(this).apply(block)
- return verticesCounter++
- }
-
- /**
- * Uploads constructed vertex data to a rendering pipeline
- * @param pipeline The target pipeline for vertex and index data
- */
- fun uploadTo(pipeline: VertexPipeline) {
- check(direct == null) {
- "Builder is already associated with a rendering pipeline. Cannot upload data again."
- }
-
- uploadVertices(pipeline.vertices)
- uploadIndices(pipeline.indices)
- }
-
- fun uploadVertices(buffer: DynamicByteBuffer) {
- vertices.forEach { attribute ->
- attribute.upload(buffer)
- }
- }
-
- fun uploadIndices(buffer: DynamicByteBuffer) {
- indices.forEach {
- buffer.putInt(it)
- }
- }
-
- /**
- * Represents a single vertex with multiple attributes.
- * Attributes are stored in the order they're declared, which must match shader layout.
- */
- class Vertex(private val builder: VertexBuilder) {
-
- /**
- * Adds a single-precision floating point attribute
- * @param value The scalar value to add
- */
- fun float(value: Double): Vertex {
- builder.direct?.let {
- it.vertices.putFloat(value)
- return this
- }
-
- builder.vertices.add(Attribute.Float(value))
- return this
- }
-
- /**
- * Adds a 2-component vector attribute
- * @param x X-axis component
- * @param y Y-axis component
- */
- fun vec2(x: Double, y: Double): Vertex {
- builder.direct?.let {
- it.vertices.putVec2(x, y)
- return this
- }
-
- builder.vertices.add(Attribute.Vec2(x, y))
- return this
- }
-
- /**
- * Adds a matrix-transformed 2-component vector
- * @param x X-axis component
- * @param y Y-axis component
- */
- fun vec2m(x: Double, y: Double) =
- Matrices.vertexTransformer?.let { mat ->
- val vec = Vector4d(x, y, 0.0, 1.0).apply(mat::transform)
- vec2(vec.x, vec.y)
- } ?: vec2(x, y)
-
- /**
- * Adds a 3-component vector attribute
- * @param x X-axis component
- * @param y Y-axis component
- * @param z Z-axis component
- */
- fun vec3(x: Double, y: Double, z: Double): Vertex {
- builder.direct?.let {
- it.vertices.putVec3(x, y, z)
- return this
- }
-
- builder.vertices.add(Attribute.Vec3(x, y, z))
- return this
- }
-
- /**
- * Adds a matrix-transformed 3-component vector attribute
- * @param x X-axis component
- * @param y Y-axis component
- * @param z Z-axis component (defaults to 0.0)
- */
- fun vec3m(x: Double, y: Double, z: Double = 0.0) =
- Matrices.vertexTransformer?.let { mat ->
- val vec = Vector4d(x, y, z, 1.0).apply(mat::transform)
- vec3(vec.x, vec.y, vec.z)
- } ?: vec3(x, y, z)
-
- /**
- * Adds a color attribute in RGBA format
- * @param color Color value using AWT Color class
- */
- fun color(color: java.awt.Color): Vertex {
- builder.direct?.let {
- it.vertices.putColor(color)
- return this
- }
-
- builder.vertices.add(Attribute.Color(color))
- return this
- }
- }
-
- /**
- * Sealed hierarchy representing different vertex attribute types.
- * Each attribute knows how to upload itself to a [DynamicByteBuffer].
- *
- * @property upload Lambda that handles writing the attribute data to a buffer
- */
- sealed class Attribute(
- val upload: DynamicByteBuffer.() -> Unit
- ) {
- /**
- * Single-precision floating point attribute
- * @property value Scalar floating point value
- */
- data class Float(
- var value: Double
- ) : Attribute({ putFloat(value) })
-
- /**
- * 2-component vector attribute
- * @property x X-axis component
- * @property y Y-axis component
- */
- data class Vec2(
- var x: Double, var y: Double
- ) : Attribute({ putVec2(x, y) })
-
- /**
- * 3-component vector attribute
- * @property x X-axis component
- * @property y Y-axis component
- * @property z Z-axis component
- */
- data class Vec3(
- var x: Double, var y: Double, var z: Double
- ) : Attribute({ putVec3(x, y, z) })
-
- /**
- * Color attribute in RGBA format
- * @property color Color value using AWT Color class
- */
- data class Color(
- var color: java.awt.Color
- ) : Attribute({ putColor(color) })
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/pipeline/VertexPipeline.kt b/src/main/kotlin/com/lambda/graphics/pipeline/VertexPipeline.kt
deleted file mode 100644
index ba91e6c33..000000000
--- a/src/main/kotlin/com/lambda/graphics/pipeline/VertexPipeline.kt
+++ /dev/null
@@ -1,148 +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.pipeline
-
-import com.lambda.graphics.buffer.vertex.VertexArray
-import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
-import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import org.lwjgl.opengl.GL32C.GL_ARRAY_BUFFER
-import org.lwjgl.opengl.GL32C.GL_ELEMENT_ARRAY_BUFFER
-
-/**
- * A GPU vertex processing pipeline that manages Vertex Array Objects (VAO) and associated buffers.
- * Handles vertex data storage, attribute binding, and rendering operations.
- *
- * @property vertexMode The primitive type used for rendering (e.g., triangles, lines)
- * @property attributes Group of vertex attributes defining the data layout
- *
- * @see VertexMode for vertex configuration
- * @see VertexAttrib.Group for attribute configuration
- * @see PersistentBuffer for buffer management
- */
-class VertexPipeline(
- private val vertexMode: VertexMode,
- private val attributes: VertexAttrib.Group
-) {
- private val vao = VertexArray(vertexMode, attributes)
-
- private val vbo = PersistentBuffer(GL_ARRAY_BUFFER, attributes.stride)
- private val ibo = PersistentBuffer(GL_ELEMENT_ARRAY_BUFFER, Int.SIZE_BYTES)
-
- init {
- vao.linkVbo(vbo)
- }
-
- /**
- * Direct access to the vertex buffer's underlying byte storage
- */
- val vertices get() = vbo.byteBuffer
-
- /**
- * Direct access to the index buffer's underlying byte storage
- */
- val indices get() = ibo.byteBuffer
-
- /**
- * Submits a draw call to the GPU using currently uploaded data
- * Binds VAO and issues glDrawElementsBaseVertex command
- */
- fun render() = vao.renderIndices(ibo)
-
- /**
- * Builds and renders data constructed by [VertexBuilder]
- *
- * It is recommended to use this method for direct data transfer
- * to avoid the overhead caused by [VertexBuilder]
- *
- * Uploads buffered data to GPU memory
- *
- * Note: only one vertex builder could be built and uploaded within 1 batch
- */
- fun immediate(block: VertexBuilder.() -> Unit) {
- VertexBuilder(this).apply(block)
- uploadInternal(); render(); clear()
- }
-
- /**
- * Builds data constructed by [VertexBuilder]
- *
- * It is recommended to use this method for direct data transfer
- * to avoid the overhead caused by [VertexBuilder]
- *
- * Uploads buffered data to GPU memory
- *
- * Note: only one vertex builder could be built and uploaded within 1 batch
- */
- fun upload(block: VertexBuilder.() -> Unit) {
- VertexBuilder(this).apply(block)
- uploadInternal()
- }
-
- /**
- * Builds data constructed by [VertexBuilder]
- *
- * Uploads buffered data to GPU memory
- *
- * Note: only one vertex builder could be built and uploaded within 1 batch
- */
- fun upload(builder: VertexBuilder) {
- builder.uploadTo(this)
- uploadInternal()
- }
-
- /**
- * Creates a [VertexBuilder]
- *
- * Note: only one vertex builder could be built and uploaded within 1 batch
- */
- fun build(block: VertexBuilder.() -> Unit = {}) =
- VertexBuilder().apply(block)
-
- /**
- * Uploads buffered data to GPU memory
- */
- private fun uploadInternal() {
- vbo.upload()
- ibo.upload()
- }
-
- /**
- * Finalizes the current draw batch and prepares for new data
- */
- fun end() {
- vbo.end()
- ibo.end()
- }
-
- /**
- * Synchronizes buffer states between frames
- * Should be called at the end of each frame
- */
- fun sync() {
- vbo.sync()
- ibo.sync()
- }
-
- /**
- * Resets both vertex and index buffers
- */
- fun clear() {
- vbo.clear()
- ibo.clear()
- }
-}
diff --git a/src/main/kotlin/com/lambda/graphics/shader/Shader.kt b/src/main/kotlin/com/lambda/graphics/shader/Shader.kt
deleted file mode 100644
index 6a4ae1e06..000000000
--- a/src/main/kotlin/com/lambda/graphics/shader/Shader.kt
+++ /dev/null
@@ -1,99 +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.shader
-
-import com.lambda.Lambda.mc
-import com.lambda.graphics.RenderMain
-import com.lambda.graphics.shader.ShaderUtils.createShaderProgram
-import com.lambda.graphics.shader.ShaderUtils.loadShader
-import com.lambda.graphics.shader.ShaderUtils.uniformMatrix
-import com.lambda.util.LambdaResource
-import com.lambda.util.math.Vec2d
-import com.lambda.util.stream
-import com.lambda.util.text
-import it.unimi.dsi.fastutil.objects.Object2IntMap
-import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
-import net.minecraft.util.math.Vec3d
-import org.joml.Matrix4f
-import org.lwjgl.opengl.GL20C.glGetUniformLocation
-import org.lwjgl.opengl.GL20C.glUniform1f
-import org.lwjgl.opengl.GL20C.glUniform1i
-import org.lwjgl.opengl.GL20C.glUniform2f
-import org.lwjgl.opengl.GL20C.glUniform3f
-import org.lwjgl.opengl.GL20C.glUniform4f
-import org.lwjgl.opengl.GL20C.glUseProgram
-import java.awt.Color
-
-class Shader(vertex: LambdaResource, fragment: LambdaResource) {
- private val uniformCache: Object2IntMap = Object2IntOpenHashMap()
-
- private val id: Int = createShaderProgram(
- loadShader(ShaderType.VertexShader, vertex.text),
- loadShader(ShaderType.FragmentShader, fragment.text)
- )
-
- fun use() {
- glUseProgram(id)
- set("u_ProjModel", RenderMain.projModel)
-
- val x = mc.gameRenderer.camera.pos.x.toFloat()
- val y = mc.gameRenderer.camera.pos.y.toFloat()
- val z = mc.gameRenderer.camera.pos.z.toFloat()
-
- val view = Matrix4f()
- .translation(-x, -y, -z)
-
- set("u_View", view)
- }
-
- private fun loc(name: String) =
- if (uniformCache.containsKey(name))
- uniformCache.getInt(name)
- else
- glGetUniformLocation(id, name).let { location ->
- uniformCache.put(name, location)
- location
- }
-
- operator fun set(name: String, v: Boolean) =
- glUniform1i(loc(name), if (v) 1 else 0)
-
- operator fun set(name: String, v: Int) =
- glUniform1i(loc(name), v)
-
- operator fun set(name: String, v: Double) =
- glUniform1f(loc(name), v.toFloat())
-
- operator fun set(name: String, vec: Vec2d) =
- glUniform2f(loc(name), vec.x.toFloat(), vec.y.toFloat())
-
- operator fun set(name: String, vec: Vec3d) =
- glUniform3f(loc(name), vec.x.toFloat(), vec.y.toFloat(), vec.z.toFloat())
-
- operator fun set(name: String, color: Color) =
- glUniform4f(
- loc(name),
- color.red / 255f,
- color.green / 255f,
- color.blue / 255f,
- color.alpha / 255f
- )
-
- operator fun set(name: String, mat: Matrix4f) =
- uniformMatrix(loc(name), mat)
-}
diff --git a/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt b/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt
deleted file mode 100644
index 91389eeb5..000000000
--- a/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt
+++ /dev/null
@@ -1,110 +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.shader
-
-import com.mojang.blaze3d.opengl.GlStateManager
-import org.joml.Matrix4f
-import org.lwjgl.BufferUtils
-import org.lwjgl.opengl.GL30C.GL_COMPILE_STATUS
-import org.lwjgl.opengl.GL30C.GL_FALSE
-import org.lwjgl.opengl.GL30C.GL_LINK_STATUS
-import org.lwjgl.opengl.GL30C.glAttachShader
-import org.lwjgl.opengl.GL30C.glCompileShader
-import org.lwjgl.opengl.GL30C.glCreateProgram
-import org.lwjgl.opengl.GL30C.glCreateShader
-import org.lwjgl.opengl.GL30C.glDeleteShader
-import org.lwjgl.opengl.GL30C.glGetProgramInfoLog
-import org.lwjgl.opengl.GL30C.glGetProgrami
-import org.lwjgl.opengl.GL30C.glGetShaderInfoLog
-import org.lwjgl.opengl.GL30C.glGetShaderi
-import org.lwjgl.opengl.GL30C.glLinkProgram
-import org.lwjgl.opengl.GL30C.glUniformMatrix4fv
-
-object ShaderUtils {
- private val matrixBuffer = BufferUtils.createFloatBuffer(4 * 4)
- private const val shaderInfoLogLength = 512
-
- fun loadShader(type: ShaderType, text: String): Int {
- // Create new shader object
- val shader = glCreateShader(type.gl)
-
- // Attach source code and compile it
- GlStateManager.glShaderSource(shader, text)
- val error = compileShader(shader)
-
- // Handle error
- error?.let { err ->
- val builder = StringBuilder()
- .append("Failed to compile ${type.name} shader").appendLine()
- .append("Compiler output:").appendLine()
- .append(err)
- .appendLine().appendLine("CODE:")
- .append(text)
-
- throw RuntimeException(builder.toString())
- }
-
- return shader
- }
-
- fun createShaderProgram(vararg shaders: Int): Int {
- // Create new shader program
- val program = glCreateProgram()
- val error = linkProgram(program, shaders)
-
- // Handle error
- error?.let { err ->
- val builder = StringBuilder()
- .append("Failed to link shader program").appendLine()
- .append("Output:").appendLine()
- .append(err)
-
- throw RuntimeException(builder.toString())
- }
-
- shaders.forEach(::glDeleteShader)
-
- return program
- }
-
- private fun compileShader(shader: Int): String? {
- glCompileShader(shader)
- val status = glGetShaderi(shader, GL_COMPILE_STATUS)
-
- return if (status != GL_FALSE) null
- else glGetShaderInfoLog(shader, shaderInfoLogLength)
- }
-
- private fun linkProgram(program: Int, shaders: IntArray): String? {
- shaders.forEach {
- glAttachShader(program, it)
- }
-
- glLinkProgram(program)
-
- val status = glGetProgrami(program, GL_LINK_STATUS)
-
- return if (status != GL_FALSE) null
- else glGetProgramInfoLog(program, shaderInfoLogLength)
- }
-
- fun uniformMatrix(location: Int, v: Matrix4f) {
- v.get(matrixBuffer)
- glUniformMatrix4fv(location, false, matrixBuffer)
- }
-}
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..302ea4da9
--- /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
+ // In ABGR, it's (A << 24 | B << 16 | G << 8 | R)
+ val abgr = (sdfValue shl 24) or 0x00FFFFFF
+ 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..5784d5c94
--- /dev/null
+++ b/src/main/kotlin/com/lambda/graphics/texture/LambdaImageAtlas.kt
@@ -0,0 +1,266 @@
+/*
+ * 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
+
+object LambdaImageAtlas {
+ data class ImageEntry(
+ val textureView: GpuTextureView,
+ val u0: Float,
+ val v0: Float,
+ val u1: Float,
+ val v1: Float,
+ val width: Int,
+ val height: Int
+ ) {
+ val aspectRatio: Float get() = if (height != 0) width.toFloat() / height.toFloat() else 1f
+
+ val isFullTexture: Boolean get() = u0 == 0f && v0 == 0f && u1 == 1f && v1 == 1f
+ }
+
+ private val mcTextureCache = ConcurrentHashMap()
+
+ private var glintEntry: ImageEntry? = null
+
+ private var missingEntry: ImageEntry? = null
+
+ private val pendingLoadQueue = java.util.concurrent.ConcurrentLinkedQueue()
+
+ fun loadMCTexture(id: Identifier): ImageEntry? {
+ mcTextureCache[id]?.let { return it }
+
+ if (!RenderSystem.isOnRenderThread()) {
+ pendingLoadQueue.add(id)
+ return null
+ }
+
+ 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
+
+ ImageEntry(
+ textureView = gpuTextureView,
+ u0 = 0f, v0 = 0f,
+ u1 = 1f, v1 = 1f,
+ width = 256,
+ height = 256
+ )
+ }
+ }
+
+ fun processPendingLoads() {
+ if (!RenderSystem.isOnRenderThread()) return
+
+ var id = pendingLoadQueue.poll()
+ while (id != null) {
+ 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()
+ }
+ }
+
+ fun getGlintTexture(): ImageEntry? {
+ if (glintEntry != null) return glintEntry
+
+ val id = Identifier.ofVanilla("textures/misc/enchanted_glint_item.png")
+ glintEntry = loadMCTexture(id)
+ return glintEntry
+ }
+
+ 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
+ }
+
+ fun getItemSprite(stack: ItemStack): ImageEntry? {
+ if (stack.isEmpty) return getMissingTexture()
+
+ RenderSystem.assertOnRenderThread()
+
+ val itemId = net.minecraft.registry.Registries.ITEM.getId(stack.item)
+ return loadItemTexture(itemId) ?: getMissingTexture()
+ }
+
+ fun loadItemTexture(itemId: Identifier): ImageEntry? {
+ RenderSystem.assertOnRenderThread()
+
+ val itemTextureId = Identifier.of(itemId.namespace, "textures/item/${itemId.path}.png")
+ val itemEntry = loadMCTexture(itemTextureId)
+ if (itemEntry != null) return itemEntry
+
+ val blockTextureId = Identifier.of(itemId.namespace, "textures/block/${itemId.path}.png")
+ return loadMCTexture(blockTextureId)
+ }
+
+ fun hasGlint(stack: ItemStack): Boolean {
+ return stack.hasGlint()
+ }
+
+ 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
+ )
+ }
+
+ private val uploadedTextureCache = ConcurrentHashMap()
+ private var nextTextureId = 0
+
+ data class UploadedTexture(
+ val id: String,
+ val texture: net.minecraft.client.texture.NativeImageBackedTexture,
+ val entry: ImageEntry
+ )
+
+ fun upload(image: BufferedImage, name: String? = null): ImageEntry? {
+ RenderSystem.assertOnRenderThread()
+
+ val cacheKey = name ?: "uploaded_${nextTextureId++}"
+
+ uploadedTextureCache[cacheKey]?.let { return it.entry }
+
+ 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)
+ 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)
+ }
+ }
+
+ 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 = image.width,
+ height = image.height
+ )
+
+ uploadedTextureCache[cacheKey] = UploadedTexture(cacheKey, nativeTexture, entry)
+ return entry
+ }
+
+ fun upload(buffer: ByteBuffer, width: Int, height: Int, name: String? = null): ImageEntry? {
+ RenderSystem.assertOnRenderThread()
+
+ val cacheKey = name ?: "uploaded_${nextTextureId++}"
+
+ uploadedTextureCache[cacheKey]?.let { return it.entry }
+
+ 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
+ val abgr = (a shl 24) or (b shl 16) or (g shl 8) or r
+ nativeImage.setColor(x, y, abgr)
+ }
+ }
+
+ 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
+ }
+
+ fun removeUploaded(name: String) {
+ uploadedTextureCache.remove(name)?.let { uploaded ->
+ mc.textureManager.destroyTexture(Identifier.of("lambda", "uploaded/$name"))
+ }
+ }
+
+ 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/gui/snap/SnapManager.kt b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt
index 0eb81b35d..3a1f0a410 100644
--- a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt
+++ b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt
@@ -1,9 +1,25 @@
+/*
+ * 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.gui.snap
import com.lambda.core.Loadable
import com.lambda.event.events.GuiEvent
import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.gui.components.ClickGuiLayout
import com.lambda.gui.components.ClickGuiLayout.gridSize
import com.lambda.gui.components.ClickGuiLayout.snapDistanceElement
import com.lambda.gui.components.ClickGuiLayout.snapDistanceGrid
diff --git a/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt b/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt
index 0efcda8b6..8a45cce10 100644
--- a/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt
+++ b/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt
@@ -71,7 +71,7 @@ object BaritoneManager : Configurable(LambdaConfig), Automated by AutomationConf
Schematic("Schematic")
}
- override val rotationConfig = RotationSettings(this@BaritoneManager, Group.Rotation)
+ override val rotationConfig = RotationSettings(c = this, baseGroup = arrayOf(Group.Rotation))
init {
// ToDo: Dont actually save the settings as its duplicate data
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 b528ab03c..fa6bae195 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..967c7b983 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) {
+ 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/Module.kt b/src/main/kotlin/com/lambda/module/Module.kt
index 0066648b3..73b5bb658 100644
--- a/src/main/kotlin/com/lambda/module/Module.kt
+++ b/src/main/kotlin/com/lambda/module/Module.kt
@@ -161,37 +161,37 @@ abstract class Module(
isEnabled = !isEnabled
}
- protected fun onEnable(block: SafeContext.() -> Unit) {
+ fun onEnable(block: SafeContext.() -> Unit) {
isEnabledSetting.onValueChange { from, to ->
if (!from && to) block()
}
}
- protected fun onDisable(block: SafeContext.() -> Unit) {
+ fun onDisable(block: SafeContext.() -> Unit) {
isEnabledSetting.onValueChange { from, to ->
if (from && !to) block()
}
}
- protected fun onToggle(block: SafeContext.(to: Boolean) -> Unit) {
+ fun onToggle(block: SafeContext.(to: Boolean) -> Unit) {
isEnabledSetting.onValueChange { from, to ->
if (from != to) block(to)
}
}
- protected fun onEnableUnsafe(block: () -> Unit) {
+ fun onEnableUnsafe(block: () -> Unit) {
isEnabledSetting.onValueChangeUnsafe { from, to ->
if (!from && to) block()
}
}
- protected fun onDisableUnsafe(block: () -> Unit) {
+ fun onDisableUnsafe(block: () -> Unit) {
isEnabledSetting.onValueChangeUnsafe { from, to ->
if (from && !to) block()
}
}
- protected fun onToggleUnsafe(block: (to: Boolean) -> Unit) {
+ fun onToggleUnsafe(block: (to: Boolean) -> Unit) {
isEnabledSetting.onValueChangeUnsafe { from, to ->
if (from != to) block(to)
}
diff --git a/src/main/kotlin/com/lambda/module/hud/Coordinates.kt b/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
index c137b7ab2..55be87284 100644
--- a/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
+++ b/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
@@ -43,7 +43,7 @@ object Coordinates : HudModule(
private val showDimension by setting("Show Dimension", true)
- private val formatter = FormatterSettings(this, Group.CurrentDimension).apply {
+ private val formatter = FormatterSettings(c = this, baseGroup = arrayOf(Group.CurrentDimension)).apply {
applyEdits {
::timeFormat.edit { hide() }
}
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/hud/Rotation.kt b/src/main/kotlin/com/lambda/module/hud/Rotation.kt
index bced69631..f07f7b4ec 100644
--- a/src/main/kotlin/com/lambda/module/hud/Rotation.kt
+++ b/src/main/kotlin/com/lambda/module/hud/Rotation.kt
@@ -32,7 +32,7 @@ object Rotation : HudModule(
description = "Show your rotation",
tag = ModuleTag.HUD,
) {
- private val formatter = FormatterSettings(this).apply {
+ private val formatter = FormatterSettings(c = this).apply {
applyEdits {
::timeFormat.edit { hide() }
}
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/chat/ChatTimestamp.kt b/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt
index d28caeaa0..be7488a27 100644
--- a/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt
+++ b/src/main/kotlin/com/lambda/module/modules/chat/ChatTimestamp.kt
@@ -46,7 +46,7 @@ object ChatTimestamp : Module(
val javaColor: Color get() = Color(color.colorValue!! and 16777215)
- val formatter = FormatterSettings(this).apply { applyEdits { hide(::localeEnum, ::sep, ::customSep, ::group, ::floatingPrecision); editTyped(::timeFormat) { defaultValue(FormatterConfig.Time.IsoLocalTime) } } }
+ val formatter = FormatterSettings(c = this).apply { applyEdits { hide(::localeEnum, ::sep, ::customSep, ::group, ::floatingPrecision); editTyped(::timeFormat) { defaultValue(FormatterConfig.Time.IsoLocalTime) } } }
private val currentTime get() =
ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault())
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/combat/CrystalAura.kt b/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt
index 6f85dce46..ba0781fe8 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt
@@ -110,7 +110,7 @@ object CrystalAura : Module(
private val packetLifetime by setting("Packet Lifetime", 500L, 50L..1000L) { prediction.onPlace }.group(Group.Prediction)
/* Targeting */
- private val targeting = Targeting.Combat(this, Group.Targeting, 10.0)
+ private val targeting = Targeting.Combat(c = this, baseGroup = arrayOf(Group.Targeting), defaultRange = 10.0)
private val blueprint = mutableMapOf()
private var activeOpportunity: Opportunity? = null
diff --git a/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt b/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt
index 1cdf18cd0..3fd1dc856 100644
--- a/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt
+++ b/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt
@@ -57,7 +57,7 @@ object KillAura : Module(
private val hitDelay2 by setting("Hit Delay 2", 6.0, 0.0..20.0, 1.0) { attackMode == AttackMode.Delay }.group(Group.General)
// Targeting
- private val targeting = Targeting.Combat(this, Group.Targeting)
+ private val targeting = Targeting.Combat(c = this, baseGroup = arrayOf(Group.Targeting))
val target: LivingEntity?
get() = targeting.target()
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..434752e9b 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)) {
+ 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..76f3926cf 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) {
+ 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)) {
+ 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..2c533dc03
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RendererTestModule.kt
@@ -0,0 +1,445 @@
+/*
+ * 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.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+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.graphics.outline.OutlineManager
+import com.lambda.graphics.outline.OutlineStyle
+import com.lambda.graphics.outline.VertexCapture
+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.entity.mob.HostileEntity
+import net.minecraft.entity.passive.PassiveEntity
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.Box
+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
+
+object ChunkedRendererTest : Module(
+ name = "ChunkedRendererTest",
+ description = "Test module for ChunkedRenderer",
+ tag = ModuleTag.DEBUG,
+) {
+ var updated = false
+
+ init {
+ chunkedRenderer("ChunkedRendererTest", depthTest = { false }) { world, pos ->
+ runSafe {
+ if (updated) return@chunkedRenderer
+ if (player.chunkPos != ChunkPos(pos.toBlockPos())) return@chunkedRenderer
+ updated = true
+
+ 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()
+ )
+ )
+
+ screenRectGradient(
+ 0.02f, 0.1f, 0.15f, 0.05f,
+ Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW
+ )
+
+ screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180))
+
+ screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts())
+
+ screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f)
+
+ screenText(
+ "Screen Space Text!",
+ 0.02f,
+ 0.30f,
+ size = 0.025f,
+ style = SDFStyle(
+ color = Color.WHITE,
+ outline = SDFOutline(),
+ shadow = SDFShadow()
+ )
+ )
+
+ screenText(
+ "Centered Screen Text",
+ 0.5f,
+ 0.05f,
+ size = 0.03f,
+ style = SDFStyle(
+ color = Color.YELLOW,
+ glow = SDFGlow(Color(255, 200, 0, 150)),
+ shadow = SDFShadow()
+ ),
+ centered = true
+ )
+
+ 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
+ )
+
+ 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
+ )
+
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ pos = worldImagePos.offset(Direction.EAST, 2.0),
+ size = 0.8f,
+ tint = Color.WHITE,
+ hasOverlay = true
+ )
+
+ 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),
+ pixelPerfect = true
+ )
+ }
+ }
+ }
+
+ listen {
+ updated = false
+ }
+ }
+}
+
+object TickedRendererTest : Module(
+ name = "TickedRendererTest",
+ description = "Test module for TickedRenderer",
+ 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()
+ )
+ )
+
+ screenRectGradient(
+ 0.02f, 0.1f, 0.15f, 0.05f,
+ Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW
+ )
+
+ screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180))
+
+ screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts())
+
+ screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f)
+
+ screenText(
+ "Screen Space Text!",
+ 0.02f,
+ 0.30f,
+ size = 0.025f,
+ style = SDFStyle(
+ color = Color.WHITE,
+ outline = SDFOutline(),
+ shadow = SDFShadow()
+ )
+ )
+
+ screenText(
+ "Centered Screen Text",
+ 0.5f,
+ 0.05f,
+ size = 0.03f,
+ style = SDFStyle(
+ color = Color.YELLOW,
+ glow = SDFGlow(Color(255, 200, 0, 150)),
+ shadow = SDFShadow()
+ ),
+ centered = true
+ )
+
+ 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
+ )
+
+ 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
+ )
+
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ pos = worldImagePos.offset(Direction.EAST, 2.0),
+ size = 0.8f,
+ tint = Color.WHITE,
+ hasOverlay = true
+ )
+
+ val goldState = net.minecraft.block.Blocks.GOLD_BLOCK.defaultState
+ val goldModel = mc.bakedModelManager.blockModels.getModel(goldState)
+ val goldRandom = 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),
+ pixelPerfect = true
+ )
+ }
+ }
+ }
+ }
+}
+
+object ImmediateRendererTest : Module(
+ name = "ImmediateRendererTest",
+ description = "Test module for ImmediateRenderer",
+ 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()
+ )
+ )
+
+ screenRectGradient(
+ 0.02f, 0.1f, 0.15f, 0.05f,
+ Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW
+ )
+
+ screenRect(0.02f, 0.17f, 0.1f, 0.03f, Color(50, 50, 200, 180))
+
+ screenLine(0.02f, 0.22f, 0.17f, 0.25f, Color.CYAN, 0.003f, dashStyle = screenMarchingAnts())
+
+ screenLineGradient(0.02f, 0.27f, Color.MAGENTA, 0.17f, 0.27f, Color.ORANGE, 0.004f)
+
+ screenText(
+ "Screen Space Text!",
+ 0.02f,
+ 0.30f,
+ size = 0.025f, // 2.5% of screen
+ style = SDFStyle(
+ color = Color.WHITE,
+ outline = SDFOutline(),
+ shadow = SDFShadow()
+ )
+ )
+
+ screenText(
+ "Centered Screen Text",
+ 0.5f,
+ 0.05f,
+ size = 0.03f,
+ style = SDFStyle(
+ color = Color.YELLOW,
+ glow = SDFGlow(Color(255, 200, 0, 150)),
+ shadow = SDFShadow()
+ ),
+ centered = true
+ )
+
+ 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
+ )
+
+ 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
+ )
+
+ worldImage(
+ texture = Identifier.ofVanilla("textures/item/netherite_sword.png"),
+ pos = worldImagePos.offset(Direction.EAST, 2.0),
+ size = 0.8f,
+ tint = Color.WHITE,
+ hasOverlay = true
+ )
+
+ 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
+ )
+ }
+
+ worldGuiItem(
+ stack = ItemStack(Items.NETHERITE_SWORD),
+ pos = startPos.offset(Direction.NORTH, 2.0).add(0.0, 1.5, 0.0),
+ scale = 0.5f,
+ )
+
+ 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,
+ )
+
+ screenGuiItem(
+ stack = ItemStack(Items.DIAMOND_PICKAXE),
+ x = 0.5f, y = 0.4f,
+ size = 0.08f,
+ )
+
+ val lightTime = (System.currentTimeMillis() % 4000L / 4000f) * 360f
+
+ worldGuiItem(
+ stack = ItemStack(Items.GRASS_BLOCK),
+ pos = startPos.offset(Direction.NORTH, 3.0),
+ scale = 0.5f,
+ flat = true,
+ overlay = ItemOverlay.ENCHANT_GLINT
+ )
+
+ worldGuiItem(
+ stack = ItemStack(Items.GOLDEN_APPLE),
+ pos = startPos.offset(Direction.NORTH, 3.0).offset(Direction.EAST, 1.0),
+ scale = 0.5f,
+ )
+
+ 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
+ )
+
+ withOutline(OutlineStyle(Color.CYAN)) {
+ box(Box(startPos.add(3.0, 1.0, 3.0), startPos.add(4.0, 2.0, 4.0))) {
+ fillColor(Color(255, 255, 255, 60))
+ hideOutline()
+ }
+
+ worldText(
+ "Outlined Text in Immediate!",
+ startPos.add(3.5, 2.5, 3.5),
+ size = 0.4f,
+ style = SDFStyle(color = Color.WHITE)
+ )
+ }
+
+ worldOutlines(world.entities, OutlineStyle(Color.RED))
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt
index acbe687f1..394192561 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt
@@ -31,7 +31,7 @@ object RotationTest : Module(
name = "RotationTest",
tag = ModuleTag.DEBUG,
) {
- override val rotationConfig = RotationSettings(this, AutomationConfig.Group.Rotation)
+ override val rotationConfig = RotationSettings(c = this, baseGroup = arrayOf(AutomationConfig.Group.Rotation))
var hitPos: HitResult? = null
init {
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/debug/SilentSwap.kt b/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt
index 5d9f56e3d..8e63c9890 100644
--- a/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt
+++ b/src/main/kotlin/com/lambda/module/modules/debug/SilentSwap.kt
@@ -35,7 +35,7 @@ object SilentSwap : Module(
Hotbar("Hotbar")
}
- override val hotbarConfig = HotbarSettings(this, Group.Hotbar)
+ override val hotbarConfig = HotbarSettings(c = this, baseGroup = arrayOf(Group.Hotbar))
init {
listen {
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..7bac671a9 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) {
+ 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..81dab2d9c 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) {
+ colors(color.setAlpha(0.3), color)
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/movement/ElytraFly.kt b/src/main/kotlin/com/lambda/module/modules/movement/ElytraFly.kt
index 07089d20f..576bb95c3 100644
--- a/src/main/kotlin/com/lambda/module/modules/movement/ElytraFly.kt
+++ b/src/main/kotlin/com/lambda/module/modules/movement/ElytraFly.kt
@@ -138,7 +138,7 @@ object ElytraFly : Module(
if (mc.options.rightKey.isPressed) vec = vec.add(Vec3d.fromPolar(0f, yaw + 90f))
if (mc.options.jumpKey.isPressed) vec = vec.add(Vec3d(0.0, 1.0, 0.0))
if (mc.options.sneakKey.isPressed) vec = vec.add(Vec3d(0.0, -1.0, 0.0))
- if (vec === Vec3d.ZERO && player.hasFirework) {
+ if (vec.lengthSquared() < 1e-4 && player.hasFirework) {
if (flipFlop) {
flipFlop = false
rotationRequest { rotation(0f, 0f) }
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..7fcd4f1ec 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",
@@ -62,8 +63,6 @@ object AirPlace : Module(
private var distance by setting("Distance", 4.0, 1.0..7.0, 1.0).group(Group.General)
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 var placementPos: BlockPos? = null
private var placementState: BlockState? = null
private val pendingInteractions = ConcurrentLinkedQueue()
@@ -106,14 +105,12 @@ 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) { hideFill() }
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt b/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt
index 5039ebcf5..595b119e0 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/AntiAim.kt
@@ -68,7 +68,7 @@ object AntiAim : Module(
private val yawSpeed by setting("Yaw Speed", 30, 1..90, 1, "Yaw rotation degrees per tick", "°") { yaw != YawMode.None }.group(Group.General)
private val pitchSpeed by setting("Pitch Speed", 30, 1..90, 1, "Pitch rotation degrees per tick", "°") { pitch != PitchMode.None }.group(Group.General)
- override val rotationConfig = RotationSettings(this, Group.Rotation)
+ override val rotationConfig = RotationSettings(c = this, baseGroup = arrayOf(Group.Rotation))
private var currentYaw = 0.0f
private var currentPitch = 0.0f
diff --git a/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt b/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt
index c0c041a53..868910419 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt
@@ -35,6 +35,7 @@ object Nuker : Module(
name = "Nuker",
description = "Breaks blocks around you",
tag = ModuleTag.PLAYER,
+ autoDisable = true
) {
private val height by setting("Height", 6, 1..8, 1)
private val width by setting("Width", 6, 1..8, 1)
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..0d1a64e90 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
@@ -35,6 +35,7 @@ import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.threading.runSafeAutomated
import com.lambda.util.BlockUtils.blockState
+import com.lambda.util.ChatUtils.colors
import com.lambda.util.Describable
import com.lambda.util.NamedEnum
import com.lambda.util.math.distSq
@@ -71,6 +72,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 +175,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) {
+ 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) {
+ colors(color, color.setAlpha(1.0))
+ }
}
}
}
diff --git a/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt b/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt
index bc6967292..ae46619dd 100644
--- a/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt
+++ b/src/main/kotlin/com/lambda/module/modules/player/RotationLock.kt
@@ -45,7 +45,7 @@ object RotationLock : Module(
private val pitchStep by setting("Pitch Step", 45.0, 1.0..90.0, 1.0) { pitchMode == Mode.Snap }.group(Group.General)
private val customPitch by setting("Custom Pitch", 0.0, -90.0..90.0, 1.0) { pitchMode == Mode.Custom }.group(Group.General)
- override val rotationConfig = RotationSettings(this, Group.Rotation).apply {
+ override val rotationConfig = RotationSettings(c = this, baseGroup = arrayOf(Group.Rotation)).apply {
applyEdits {
::rotationMode.edit { defaultValue(RotationMode.Lock) }
}
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..bec4c7a9a 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)) {
+ 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..1a98f73a1
--- /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) {
+ 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/ESP.kt b/src/main/kotlin/com/lambda/module/modules/render/ESP.kt
new file mode 100644
index 000000000..5d58f8fa4
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/ESP.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.EntityColorSettings
+import com.lambda.config.groups.EntitySelectionSettings
+import com.lambda.config.groups.OutlineSettings
+import com.lambda.config.groups.ScreenLineSettings
+import com.lambda.config.groups.WorldLineSettings
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+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.NamedEnum
+import com.lambda.util.math.setAlpha
+import fi.dy.masa.malilib.render.RenderUtils.depthTest
+import net.minecraft.client.network.ClientPlayerEntity
+
+object ESP : Module(
+ name = "ESP",
+ description = "Highlight entities with smooth interpolated rendering",
+ tag = ModuleTag.RENDER
+) {
+ private enum class Group(override val displayName: String): NamedEnum {
+ General("General"),
+ Shader("Shader"),
+ Box("Box"),
+// Frame("Frame"),
+ Entities("Entities"),
+ Colors("Colors")
+ }
+
+ private val mode by setting("Mode", EspMode.Shader).group(Group.General)
+ private val depthTest by setting("Depth Test", false, "Blend ESP renders into the world").group(Group.General)
+
+ private val outlineStyle = OutlineSettings(c = this, baseGroup = arrayOf(Group.Shader)) { mode == EspMode.Shader }
+
+ private var drawFilled: Boolean by setting("Box Fill", true, "Fill entity boxes") { mode == EspMode.Box }.group(Group.Box)
+ .onValueChange { _, to -> if (!to && !drawOutline) drawOutline = true }
+ private var drawOutline: Boolean by setting("Box Outline", true, "Draw box outlines") { mode == EspMode.Box }.group(Group.Box)
+ .onValueChange { _, to -> if (!to && !drawFilled) drawFilled = true }
+ private val boxOutlineSettings = WorldLineSettings(c = this, baseGroup = arrayOf(Group.Box)) { mode == EspMode.Box && drawOutline }.apply {
+ applyEdits {
+ hide(::startColor, ::endColor)
+ }
+ }
+ private val fillAlpha by setting("Filled Alpha", 0.2, 0.0..1.0, 0.05) { mode == EspMode.Box && drawFilled }.group(Group.Box)
+ private val outlineAlpha by setting("Outline Alpha", 0.8, 0.0..1.0, 0.05) { mode == EspMode.Box && drawOutline }.group(Group.Box)
+
+ private val entitySettings = EntitySelectionSettings(c = this, baseGroup = arrayOf(Group.Entities))
+ private val entityColors = EntityColorSettings(c = this, baseGroup = arrayOf(Group.Colors))
+
+ init {
+ immediateRenderer("EntityESP Immediate Renderer", depthTest = { depthTest }) { safeContext ->
+ with(safeContext) {
+ world.entities.forEach { entity ->
+ if (!entitySettings.isSelected(entity)) return@forEach
+ val entityColor = entityColors.getColor(entity)
+ when (mode) {
+ EspMode.Shader -> worldOutline(entity, outlineStyle.toStyle(entityColor))
+ EspMode.Box -> {
+ box(entity.interpolatedBox, boxOutlineSettings) {
+ if (!drawFilled) hideFill()
+ else if (!drawOutline) hideOutline()
+ colors(entityColor.setAlpha(fillAlpha), entityColor.setAlpha(outlineAlpha))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private enum class EspMode {
+ Shader,
+ Box,
+ //ToDo: Implement
+// Frame
+ }
+}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt b/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt
deleted file mode 100644
index 506a98a2f..000000000
--- a/src/main/kotlin/com/lambda/module/modules/render/EntityESP.kt
+++ /dev/null
@@ -1,347 +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.context.SafeContext
-import com.lambda.event.events.GuiEvent
-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.InterpolatedRegionESP
-import com.lambda.graphics.mc.TransientRegionESP
-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 imgui.ImGui
-import net.minecraft.entity.Entity
-import net.minecraft.entity.ItemEntity
-import net.minecraft.entity.LivingEntity
-import net.minecraft.entity.decoration.ArmorStandEntity
-import net.minecraft.entity.decoration.EndCrystalEntity
-import net.minecraft.entity.mob.HostileEntity
-import net.minecraft.entity.mob.MobEntity
-import net.minecraft.entity.passive.AnimalEntity
-import net.minecraft.entity.passive.PassiveEntity
-import net.minecraft.entity.player.PlayerEntity
-import net.minecraft.entity.projectile.ProjectileEntity
-import net.minecraft.entity.vehicle.AbstractMinecartEntity
-import net.minecraft.entity.vehicle.BoatEntity
-import net.minecraft.util.math.Vec3d
-import java.awt.Color
-
-object EntityESP : Module(
- name = "EntityESP",
- 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 fun SafeContext.shouldRender(entity: Entity): Boolean {
- if (entity == player && !self) return false
- if (entity is LivingEntity && !entity.isAlive) return false
- return when (entity) {
- is PlayerEntity -> players
- is HostileEntity -> hostiles
- is AnimalEntity -> passives
- is PassiveEntity -> passives
- is MobEntity -> neutrals
- is ItemEntity -> items
- is ProjectileEntity -> projectiles
- is BoatEntity -> vehicles
- is AbstractMinecartEntity -> vehicles
- is EndCrystalEntity -> crystals
- is ArmorStandEntity -> armorStands
- else -> false
- }
- }
-
- private fun getEntityColor(entity: Entity): Color {
- return when (entity) {
- is PlayerEntity -> playerColor
- is HostileEntity -> hostileColor
- is AnimalEntity -> passiveColor
- is PassiveEntity -> passiveColor
- is MobEntity -> neutralColor
- is ItemEntity -> itemColor
- is ProjectileEntity -> projectileColor
- is BoatEntity -> vehicleColor
- is AbstractMinecartEntity -> vehicleColor
- is EndCrystalEntity -> crystalColor
- 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..332a461aa
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/Nametags.kt
@@ -0,0 +1,253 @@
+/*
+ * 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.mc.RenderBuilder
+import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer
+import com.lambda.graphics.mc.renderer.RendererUtils.worldToScreenNormalized
+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
+import kotlin.math.max
+
+//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"),
+ Background("Background"),
+ 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 background by setting("Background", true).group(Group.Background)
+ private val backgroundColor by setting("Background Color", Color(0, 0, 0, 60)) { background }.group(Group.Background)
+ private val backgroundSize by setting("Background Size", 1.0f, 1.0f..2.0f, 0.01f) { background }.group(Group.Background)
+ 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", true).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
+ var trueBGSizeX = 0f
+ var trueBGSizeY = 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
+ trueBGSizeY = (backgroundSize * 0.005f) * 0.5f
+ trueBGSizeX = trueBGSizeY * 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 nameWidth = getDefaultFont().getStringWidthNormalized(nameText, textSize)
+ val box = entity.interpolatedBox
+ val boxCenter = box.center
+ var (anchorX, anchorY) =
+ worldToScreenNormalized(Vec3d(boxCenter.x, box.maxY + yOffset, boxCenter.z))
+ ?: return@forEach
+
+ val halfNameWidth = nameWidth / 2
+
+ if (entity !is LivingEntity) {
+ if (background) {
+ screenRect(anchorX - halfNameWidth - trueBGSizeX, anchorY - trueBGSizeY, nameWidth + (trueBGSizeX * 2), textSize + (trueBGSizeY * 2), backgroundColor)
+ }
+ screenText(nameText, anchorX, anchorY + (textSize / 2f), textSize, style = textStyle, centered = true)
+ return@forEach
+ }
+
+ 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 * 0.5f)
+
+ val itemNameText = if (itemName) entity.mainHandStack.name.string else ""
+ val itemNameSize = if (itemName) textSize * itemNameScale else 0f
+
+ if (background) {
+ anchorY += trueBGSizeY
+ val maxWidth =
+ if (itemName) max(getDefaultFont().getStringWidthNormalized(itemNameText, itemNameSize), combinedWidth)
+ else nameWidth
+ screenRect(nameX - trueBGSizeX, anchorY - trueBGSizeY, maxWidth + (trueBGSizeX * 2), textSize + itemNameSize + trueSpacingY + (trueBGSizeY * 2), backgroundColor)
+ }
+
+ if (itemName && !entity.mainHandStack.isEmpty) {
+ screenText(itemNameText, anchorX, anchorY, itemNameSize, centered = true)
+ anchorY += (itemNameSize * 1.1f) + trueSpacingY
+ }
+ 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 (background) anchorY += trueBGSizeY
+
+ 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 * 0.5f) + 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 * 0.5f)
+ 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 * 0.5f), 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 * 0.5f
+ 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..4212549a4 100644
--- a/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
+++ b/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
@@ -17,12 +17,21 @@
package com.lambda.module.modules.render
+import com.lambda.config.groups.EntitySelectionSettings
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 +43,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"),
@@ -86,15 +82,7 @@ object NoRender : Module(
// RenderLayer.getArmorEntityGlint(), RenderLayer.getGlint(), RenderLayer.getGlintTranslucent(), RenderLayer.getEntityGlint()
// @JvmStatic val noEnchantmentGlint by setting("No Enchantment Glint", false).group(Group.Entity)
// @JvmStatic val noDeadEntities by setting("No Dead Entities", false).group(Group.Entity)
- private val playerEntities by setting("Player Entities", emptySet(), playerEntityMap.values.toSet(), "Player entities to omit from rendering").group(Group.Entity)
- private val bossEntities by setting("Boss Entities", emptySet(), bossEntityMap.values.toSet(), "Boss entities to omit from rendering").group(Group.Entity)
- private val decorationEntities by setting("Decoration Entities", emptySet(), decorationEntityMap.values.toSet(), "Decoration entities to omit from rendering").group(Group.Entity)
- private val mobEntities by setting("Mob Entities", emptySet(), mobEntityMap.values.toSet(), "Mob entities to omit from rendering").group(Group.Entity)
- private val passiveEntities by setting("Passive Entities", emptySet(), passiveEntityMap.values.toSet(), "Passive entities to omit from rendering").group(Group.Entity)
- private val projectileEntities by setting("Projectile Entities", emptySet(), projectileEntityMap.values.toSet(), "Projectile entities to omit from rendering").group(Group.Entity)
- private val vehicleEntities by setting("Vehicle Entities", emptySet(), vehicleEntityMap.values.toSet(), "Vehicle entities to omit from rendering").group(Group.Entity)
- private val miscEntities by setting("Misc Entities", emptySet(), miscEntityMap.values.toSet(), "Miscellaneous entities to omit from rendering").group(Group.Entity)
- private val blockEntities by setting("Block Entities", emptySet(), blockEntityMap.values.toSet(), "Block entities to omit from rendering").group(Group.Entity)
+ private val entitySettings = EntitySelectionSettings(c = this, baseGroup = arrayOf(Group.Entity))
@JvmStatic val noTerrainFog by setting("No Terrain Fog", false).group(Group.World)
@JvmStatic val noSignText by setting("No Sign Text", false).group(Group.World)
@@ -112,37 +100,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("(?
- miscEntityMap[simpleName] in miscEntities ||
- playerEntityMap[simpleName] in playerEntities ||
- projectileEntityMap[simpleName] in projectileEntities ||
- vehicleEntityMap[simpleName] in vehicleEntities ||
- decorationEntityMap[simpleName] in decorationEntities ||
- passiveEntityMap[simpleName] in passiveEntities ||
- mobEntityMap[simpleName] in mobEntities ||
- bossEntityMap[simpleName] in bossEntities
- SpawnGroup.WATER_AMBIENT,
- SpawnGroup.WATER_CREATURE,
- SpawnGroup.AMBIENT,
- SpawnGroup.AXOLOTLS,
- SpawnGroup.CREATURE,
- SpawnGroup.UNDERGROUND_WATER_CREATURE -> passiveEntityMap[simpleName] in passiveEntities
- SpawnGroup.MONSTER -> mobEntityMap[simpleName] in mobEntities
- }
- }
+ fun shouldOmitEntity(entity: Entity): Boolean = entitySettings.isSelected(entity)
@JvmStatic
- fun shouldOmitBlockEntity(blockEntity: BlockEntity) =
- isEnabled && blockEntityMap[blockEntity.javaClass.simpleName] in blockEntities
-
- private data class MappingInfo(
- val raw: String,
- val remapped: String,
- val displayName: String
- )
+ fun shouldOmitBlockEntity(blockEntity: BlockEntity) = entitySettings.isSelected(blockEntity)
}
diff --git a/src/main/kotlin/com/lambda/module/modules/render/Particles.kt b/src/main/kotlin/com/lambda/module/modules/render/Particles.kt
deleted file mode 100644
index a50eb92a9..000000000
--- a/src/main/kotlin/com/lambda/module/modules/render/Particles.kt
+++ /dev/null
@@ -1,220 +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.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..1dd60427e
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/Search.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.mc.renderer.RendererUtils.worldToScreenNormalized
+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", true, "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