diff --git a/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexMode.kt b/src/main/java/com/lambda/graphics/outline/IEntityRenderState.java similarity index 72% rename from src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexMode.kt rename to src/main/java/com/lambda/graphics/outline/IEntityRenderState.java index a08af9096..31d30f52a 100644 --- a/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexMode.kt +++ b/src/main/java/com/lambda/graphics/outline/IEntityRenderState.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 @@ -15,12 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.graphics.buffer.vertex.attributes +package com.lambda.graphics.outline; -import org.lwjgl.opengl.GL11C.GL_LINES -import org.lwjgl.opengl.GL11C.GL_TRIANGLES +/** + * Duck interface for EntityRenderState to store and retrieve entity ID. + */ +public interface IEntityRenderState { + int lambda$getEntityId(); -enum class VertexMode(val mode: Int) { - Lines(GL_LINES), - Triangles(GL_TRIANGLES) + void lambda$setEntityId(int id); } diff --git a/src/main/java/com/lambda/graphics/outline/IWorldRenderer.java b/src/main/java/com/lambda/graphics/outline/IWorldRenderer.java new file mode 100644 index 000000000..1ab5e82e2 --- /dev/null +++ b/src/main/java/com/lambda/graphics/outline/IWorldRenderer.java @@ -0,0 +1,38 @@ +/* + * 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.client.gl.Framebuffer; + +/** + * Mixin interface for WorldRenderer to support custom framebuffer swapping. + * Used by the outline rendering system to redirect entity rendering to custom + * framebuffers. + */ +public interface IWorldRenderer { + /** + * Push the current entity outline framebuffer onto a stack and replace it with + * the given framebuffer. + */ + void lambda$pushEntityOutlineFramebuffer(Framebuffer framebuffer); + + /** + * Pop the previous entity outline framebuffer from the stack and restore it. + */ + void lambda$popEntityOutlineFramebuffer(); +} diff --git a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index a0e5c5b47..039e763b5 100644 --- a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -74,6 +74,7 @@ void closeImGui(CallbackInfo ci) { @WrapMethod(method = "render") void onLoopTick(boolean tick, Operation original) { + com.lambda.graphics.RenderMain.preRender(); EventFlow.post(TickEvent.Render.Pre.INSTANCE); original.call(tick); EventFlow.post(TickEvent.Render.Post.INSTANCE); @@ -120,11 +121,13 @@ void onSound(SoundManager instance, boolean paused, Operation original) { @Inject(at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;)V", shift = At.Shift.AFTER, remap = false), method = "stop") private void onShutdown(CallbackInfo ci) { + com.lambda.graphics.outline.OutlineRenderer.INSTANCE.cleanup(); EventFlow.post(new ClientEvent.Shutdown()); } /** - * Inject after the thread field is set so that {@link ThreadExecutor#getThread} is available + * Inject after the thread field is set so that {@link ThreadExecutor#getThread} + * is available */ @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;thread:Ljava/lang/Thread;", shift = At.Shift.AFTER, ordinal = 0, opcode = Opcodes.PUTFIELD), method = "run") private void onStartup(CallbackInfo ci) { @@ -133,7 +136,8 @@ private void onStartup(CallbackInfo ci) { @Inject(method = "setScreen", at = @At("HEAD")) private void onScreenOpen(@Nullable Screen screen, CallbackInfo ci) { - if (screen == null) return; + if (screen == null) + return; if (screen instanceof ScreenHandlerProvider handledScreen) { EventFlow.post(new InventoryEvent.Open(handledScreen.getScreenHandler())); } @@ -141,7 +145,8 @@ private void onScreenOpen(@Nullable Screen screen, CallbackInfo ci) { @Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;removed()V", shift = At.Shift.AFTER)) private void onScreenRemove(@Nullable Screen screen, CallbackInfo ci) { - if (currentScreen == null) return; + if (currentScreen == null) + return; if (currentScreen instanceof ScreenHandlerProvider handledScreen) { EventFlow.post(new InventoryEvent.Close(handledScreen.getScreenHandler())); } @@ -162,19 +167,22 @@ private void redirectUnPressAll(Operation original) { @WrapWithCondition(method = "doAttack()Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;swingHand(Lnet/minecraft/util/Hand;)V")) private boolean redirectHandSwing(ClientPlayerEntity instance, Hand hand) { - if (this.crosshairTarget == null) return false; + if (this.crosshairTarget == null) + return false; return this.crosshairTarget.getType() != HitResult.Type.BLOCK || PacketMine.INSTANCE.isDisabled(); } @ModifyExpressionValue(method = "doItemUse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;isBreakingBlock()Z")) boolean redirectMultiActon(boolean original) { - if (Interact.INSTANCE.isEnabled() && Interact.getMultiAction()) return false; + if (Interact.INSTANCE.isEnabled() && Interact.getMultiAction()) + return false; return original; } @Inject(method = "doItemUse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;isRiding()Z")) void injectFastPlace(CallbackInfo ci) { - if (!Interact.INSTANCE.isEnabled()) return; + if (!Interact.INSTANCE.isEnabled()) + return; itemUseCooldown = Interact.getPlaceDelay(); } @@ -203,7 +211,8 @@ float getTargetMillisPerTick(float millis, Operation original) { @Inject(method = "updateWindowTitle", at = @At("HEAD"), cancellable = true) void updateWindowTitle(CallbackInfo ci) { - if (!ClickGuiLayout.getSetLambdaWindowTitle()) return; + if (!ClickGuiLayout.getSetLambdaWindowTitle()) + return; WindowUtils.setLambdaTitle(); ci.cancel(); } diff --git a/src/main/java/com/lambda/mixin/entity/EntityMixin.java b/src/main/java/com/lambda/mixin/entity/EntityMixin.java index 06dc75137..d5bd03ea8 100644 --- a/src/main/java/com/lambda/mixin/entity/EntityMixin.java +++ b/src/main/java/com/lambda/mixin/entity/EntityMixin.java @@ -17,6 +17,7 @@ package com.lambda.mixin.entity; +import com.lambda.Lambda; import com.lambda.event.EventFlow; import com.lambda.event.events.EntityEvent; import com.lambda.event.events.PlayerEvent; @@ -151,11 +152,13 @@ private boolean modifyGetFlagGlowing(boolean original) { @WrapWithCondition(method = "changeLookDirection", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setYaw(F)V")) private boolean wrapSetYaw(Entity instance, float yaw) { + if ((Object) this != getMc().player) return true; return RotationManager.getLockYaw() == null; } @WrapWithCondition(method = "changeLookDirection", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setPitch(F)V")) private boolean wrapSetPitch(Entity instance, float yaw) { + if ((Object) this != Lambda.getMc().player) return true; return RotationManager.getLockPitch() == null; } diff --git a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java b/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java deleted file mode 100644 index b4775b72f..000000000 --- a/src/main/java/com/lambda/mixin/items/BarrierBlockMixin.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.mixin.items; - -import com.lambda.module.modules.render.BlockESP; -import com.llamalad7.mixinextras.injector.ModifyReturnValue; -import net.minecraft.block.BarrierBlock; -import net.minecraft.block.BlockRenderType; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(BarrierBlock.class) -public class BarrierBlockMixin { - /** - * Modifies barrier block render type to {@link BlockRenderType#MODEL} when {@link BlockESP} is enabled and {@link BlockESP#getBarrier()} is true - */ - @ModifyReturnValue(method = "getRenderType", at = @At("RETURN")) - private BlockRenderType modifyGetRenderType(BlockRenderType original, BlockState state) { - if (BlockESP.INSTANCE.isEnabled() - && BlockESP.getBarrier() - && state.getBlock() == Blocks.BARRIER - ) return BlockRenderType.MODEL; - return original; - } -} diff --git a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java b/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java deleted file mode 100644 index 49ff48b7f..000000000 --- a/src/main/java/com/lambda/mixin/render/BlockRenderManagerMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.mixin.render; - -import com.lambda.module.modules.render.BlockESP; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import net.minecraft.client.render.block.BlockRenderManager; -import net.minecraft.client.render.model.BlockStateModel; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(BlockRenderManager.class) -public abstract class BlockRenderManagerMixin { - @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) - private void getModel(BlockState state, CallbackInfoReturnable cir) { - if (BlockESP.INSTANCE.isEnabled() - && BlockESP.getBarrier() - && state.getBlock() == Blocks.BARRIER - ) cir.setReturnValue(BlockESP.getModel()); - } -} diff --git a/src/main/java/com/lambda/mixin/render/CameraMixin.java b/src/main/java/com/lambda/mixin/render/CameraMixin.java index f067351b4..93d2f1f19 100644 --- a/src/main/java/com/lambda/mixin/render/CameraMixin.java +++ b/src/main/java/com/lambda/mixin/render/CameraMixin.java @@ -55,19 +55,18 @@ public abstract class CameraMixin { @Inject(method = "update", at = @At("TAIL")) private void onUpdate(World area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickProgress, CallbackInfo ci) { - if (!Freecam.INSTANCE.isEnabled()) return; - - Freecam.updateCam(); + if (Freecam.INSTANCE.isEnabled()) Freecam.updateCam(); } /** * Sets the lock rotation to the active rotation + * *
{@code
      * this.setPos(
-     *     MathHelper.lerp((double)tickDelta, focusedEntity.prevX, focusedEntity.getX()),
-     *     MathHelper.lerp((double)tickDelta, focusedEntity.prevY, focusedEntity.getY()) + (double)MathHelper.lerp(tickDelta, this.lastCameraY, this.cameraY),
-     *     MathHelper.lerp((double)tickDelta, focusedEntity.prevZ, focusedEntity.getZ())
-     *     );
+     *         MathHelper.lerp((double) tickDelta, focusedEntity.prevX, focusedEntity.getX()),
+     *         MathHelper.lerp((double) tickDelta, focusedEntity.prevY, focusedEntity.getY())
+     *                 + (double) MathHelper.lerp(tickDelta, this.lastCameraY, this.cameraY),
+     *         MathHelper.lerp((double) tickDelta, focusedEntity.prevZ, focusedEntity.getZ()));
      * }
*/ @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setPos(DDD)V", shift = At.Shift.AFTER)) @@ -95,13 +94,14 @@ private void onClipToSpace(float distance, CallbackInfoReturnable 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 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 { event -> + if (tracers) tracerBlockPositions.keys.removeIf { it in event.chunk.pos } + } + } + + private fun RenderBuilder.tracer(pair: Pair>) { + val endPoint = worldToScreenNormalized(pair.first) ?: return + val startColor = if (useNaturalColor) pair.second.first else tracerConfig.startColor + val endColor = if (useNaturalColor) pair.second.second else tracerConfig.endColor + screenLineGradient( + 0.5f, 0.5f, + startColor, + endPoint.x, endPoint.y, + endColor, + tracerConfig.width, + tracerConfig.getDashStyle() + ) + } + + private fun RenderBuilder.box(boxes: List, ignoreSides: Int, fillColor: Color, lineColor: Color) { + boxes.forEach { box -> + box(box, outlineConfig) { + hideSides(ignoreSides) + if (fill) fillColor(fillColor) else hideFill() + if (!outline) hideOutline() + else { + outlineColor(lineColor) + outlineConfig.getDashStyle()?.let { lineDashStyle(it) } + outlineMode(blockOutlineMode) + } + } + } + } + + private fun getTracerColors(naturalColor: Color): Pair = + if (useNaturalColor) { + val adjustedNaturalColor = naturalColor.setAlpha(naturalTracerAlpha) + Pair(adjustedNaturalColor, adjustedNaturalColor) + } else Pair(tracerConfig.startColor, tracerConfig.endColor) + + private fun getEntityColor(entity: Entity) = + entityColor(entity).ensureMinBrightness(minimumNaturalBrightness) + + private fun SafeContext.getBlockColor(state: BlockState, pos: BlockPos) = + blockColor(state, pos).ensureMinBrightness(minimumNaturalBrightness) + + /** + * Ensures a color meets the minimum brightness threshold. + * If the color is too dark, scales up the RGB values proportionally. + */ + private fun Color.ensureMinBrightness(minBrightness: Int): Color { + if (minBrightness <= 0) return this + + val brightness = maxOf(red, green, blue) + if (brightness >= minBrightness) return this + if (brightness == 0) return Color(minBrightness, minBrightness, minBrightness, alpha) + + val scale = minBrightness.toFloat() / brightness + return Color( + (red * scale).toInt().coerceIn(0, 255), + (green * scale).toInt().coerceIn(0, 255), + (blue * scale).toInt().coerceIn(0, 255), + alpha + ) + } + + private fun rebuildMesh(ctx: SafeContext, from: Any? = null, to: Any? = null): Unit = chunkedRenderer.rebuild() +} diff --git a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt b/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt deleted file mode 100644 index e20dbe40d..000000000 --- a/src/main/kotlin/com/lambda/module/modules/render/StorageESP.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.module.modules.render - -import com.lambda.context.SafeContext -import com.lambda.graphics.esp.ShapeScope -import com.lambda.graphics.renderer.esp.DirectionMask -import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh -import com.lambda.event.events.onStaticRender -import com.lambda.module.Module -import com.lambda.module.tag.ModuleTag -import com.lambda.util.world.blockEntitySearch -import com.lambda.util.world.entitySearch -import com.lambda.threading.runSafe -import com.lambda.util.NamedEnum -import com.lambda.util.extension.blockColor -import com.lambda.util.math.setAlpha -import net.minecraft.block.entity.BarrelBlockEntity -import net.minecraft.block.entity.BlastFurnaceBlockEntity -import net.minecraft.block.entity.BlockEntity -import net.minecraft.block.entity.BrewingStandBlockEntity -import net.minecraft.block.entity.ChestBlockEntity -import net.minecraft.block.entity.DispenserBlockEntity -import net.minecraft.block.entity.EnderChestBlockEntity -import net.minecraft.block.entity.FurnaceBlockEntity -import net.minecraft.block.entity.HopperBlockEntity -import net.minecraft.block.entity.ShulkerBoxBlockEntity -import net.minecraft.block.entity.SmokerBlockEntity -import net.minecraft.block.entity.TrappedChestBlockEntity -import net.minecraft.entity.Entity -import net.minecraft.entity.decoration.ItemFrameEntity -import net.minecraft.entity.vehicle.AbstractMinecartEntity -import net.minecraft.entity.vehicle.MinecartEntity -import java.awt.Color - -object StorageESP : Module( - name = "StorageESP", - description = "Render storage blocks/entities", - tag = ModuleTag.RENDER, -) { - private val distance by setting("Distance", 64.0, 10.0..256.0, 1.0, "Maximum distance for rendering").group(Group.General) - private var drawFaces: Boolean by setting("Draw Faces", true, "Draw faces of blocks").group(Group.Render) - private var drawEdges: Boolean by setting("Draw Edges", true, "Draw edges of blocks").group(Group.Render) - private val mode by setting("Outline Mode", DirectionMask.OutlineMode.And, "Outline mode").group(Group.Render) - private val mesh by setting("Mesh", true, "Connect similar adjacent blocks").group(Group.Render) - private val useBlockColor by setting("Use Block Color", true, "Use the color of the block instead").group(Group.Color) - private val facesAlpha by setting("Faces Alpha", 0.3, 0.1..1.0, 0.05).group(Group.Color) - private val edgesAlpha by setting("Edges Alpha", 0.3, 0.1..1.0, 0.05).group(Group.Color) - private val outlineWidth by setting("Outline Width", 1.0f, 0.5f..5.0f, 0.5f) { drawEdges }.group(Group.Render) - - // TODO: - // val blockColors by setting("Block Colors", mapOf()) { page == Page.Color - // && - // !useBlockColor } - // val renders by setting("Render Blocks", mapOf()) { page == Page.General - // } - // - // TODO: Create enum of MapColors - - // I used this to extract the colors as rgb format - // > function extract(color) { - // ... console.log((color >> 16) & 0xFF) - // ... console.log((color >> 8) & 0xFF) - // ... console.log(color & 0xFF) - // ... } - - private val barrelColor by setting("Barrel Color", Color(143, 119, 72)) { !useBlockColor }.group(Group.Color) - private val blastFurnaceColor by setting("Blast Furnace Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color) - private val brewingStandColor by setting("Brewing Stand Color", Color(167, 167, 167)) { !useBlockColor }.group(Group.Color) - private val trappedChestColor by setting("Trapped Chest Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color) - private val chestColor by setting("Chest Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color) - private val dispenserColor by setting("Dispenser Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color) - private val enderChestColor by setting("Ender Chest Color", Color(127, 63, 178)) { !useBlockColor }.group(Group.Color) - private val furnaceColor by setting("Furnace Color", Color(153, 153, 153)) { !useBlockColor }.group(Group.Color) - private val hopperColor by setting("Hopper Color", Color(76, 76, 76)) { !useBlockColor }.group(Group.Color) - private val smokerColor by setting("Smoker Color", Color(112, 112, 112)) { !useBlockColor }.group(Group.Color) - private val shulkerColor by setting("Shulker Color", Color(178, 76, 216)) { !useBlockColor }.group(Group.Color) - private val itemFrameColor by setting("Item Frame Color", Color(216, 127, 51)) { !useBlockColor }.group(Group.Color) - private val cartColor by setting("Minecart Color", Color(102, 127, 51)) { !useBlockColor }.group(Group.Color) - - private val entities = setOf( - BarrelBlockEntity::class, - BlastFurnaceBlockEntity::class, - BrewingStandBlockEntity::class, - TrappedChestBlockEntity::class, - ChestBlockEntity::class, - DispenserBlockEntity::class, - EnderChestBlockEntity::class, - FurnaceBlockEntity::class, - HopperBlockEntity::class, - SmokerBlockEntity::class, - ShulkerBoxBlockEntity::class, - AbstractMinecartEntity::class, - ItemFrameEntity::class, - MinecartEntity::class, - ) - - init { - onStaticRender { esp -> - blockEntitySearch(distance) - .filter { it::class in entities } - .forEach { be -> - esp.shapes(be.pos.x.toDouble(), be.pos.y.toDouble(), be.pos.z.toDouble()) { - build(be, excludedSides(be)) - } - } - - val mineCarts = - entitySearch(distance).filter { - it::class in entities - } - val itemFrames = - entitySearch(distance).filter { - it::class in entities - } - (mineCarts + itemFrames).forEach { entity -> - esp.shapes(entity.getX(), entity.getY(), entity.getZ()) { - build(entity, DirectionMask.ALL) - } - } - } - } - - private fun SafeContext.excludedSides(blockEntity: BlockEntity): Int { - val isFullCube = blockEntity.cachedState.isFullCube(world, blockEntity.pos) - return if (mesh && isFullCube) { - buildSideMesh(blockEntity.pos) { neighbor -> - val other = - world.getBlockEntity(neighbor) ?: return@buildSideMesh false - val otherFullCube = other.cachedState.isFullCube(world, other.pos) - val sameType = - blockEntity.cachedState.block == other.cachedState.block - val searchedFor = other::class in entities - - searchedFor && otherFullCube && sameType - } - } else DirectionMask.ALL - } - - private fun ShapeScope.build(block: BlockEntity, sides: Int) = runSafe { - val color = - if (useBlockColor) blockColor(block.cachedState, block.pos) - else block.color ?: return@runSafe - box(block, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth) - } - - private fun ShapeScope.build(entity: Entity, sides: Int) = runSafe { - val color = entity.color ?: return@runSafe - box(entity, color.setAlpha(facesAlpha), color.setAlpha(edgesAlpha), sides, mode, thickness = outlineWidth) - } - - private val BlockEntity?.color - get() = - when (this) { - is BarrelBlockEntity -> barrelColor - is BlastFurnaceBlockEntity -> blastFurnaceColor - is BrewingStandBlockEntity -> brewingStandColor - is TrappedChestBlockEntity -> trappedChestColor - is ChestBlockEntity -> chestColor - is DispenserBlockEntity -> dispenserColor - is EnderChestBlockEntity -> enderChestColor - is FurnaceBlockEntity -> furnaceColor - is HopperBlockEntity -> hopperColor - is SmokerBlockEntity -> smokerColor - is ShulkerBoxBlockEntity -> shulkerColor - else -> null - } - - private val Entity?.color - get() = - when (this) { - is AbstractMinecartEntity -> cartColor - is ItemFrameEntity -> itemFrameColor - else -> null - } - - private enum class Group(override val displayName: String) : NamedEnum { - General("General"), - Render("Render"), - Color("Color") - } -} diff --git a/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt new file mode 100644 index 000000000..deeea03be --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/render/Tracers.kt @@ -0,0 +1,112 @@ +/* + * 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.EntityColorSettings +import com.lambda.config.groups.EntitySelectionSettings +import com.lambda.config.groups.ScreenLineSettings +import com.lambda.friend.FriendManager.isFriend +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer +import com.lambda.graphics.mc.renderer.RendererUtils.worldToScreenNormalized +import com.lambda.module.Module +import com.lambda.module.modules.render.ExtraTab.friendColor +import com.lambda.module.tag.ModuleTag +import com.lambda.util.EntityUtils.EntityGroup +import com.lambda.util.EntityUtils.entityGroup +import com.lambda.util.NamedEnum +import com.lambda.util.extension.prevPos +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.dist +import com.lambda.util.math.lerp +import net.minecraft.client.network.OtherClientPlayerEntity +import org.joml.Vector2f +import org.joml.component1 +import org.joml.component2 +import java.awt.Color + +object Tracers : Module( + name = "Tracers", + description = "Draws lines to entities within the world", + tag = ModuleTag.RENDER +) { + private enum class Group(override val displayName: String) : NamedEnum { + General("General"), + Entities("Entities"), + Colors("Colors"), + LineStyle("Line Style") + } + + private enum class LineGroup(override val displayName: String) : NamedEnum { + Friend("Friend"), + Other("Other") + } + + private val target by setting("Target", TracerMode.Feet).group(Group.General) + private val stem by setting("Stem", true).group(Group.General) + private val entitySettings = EntitySelectionSettings(c = this, baseGroup = arrayOf(Group.Entities)).apply { + applyEdits { + hide(::self, ::blockEntities) + } + } + private val entityColors = EntityColorSettings(c = this, baseGroup = arrayOf(Group.Colors)) + + private val friendLineConfig = ScreenLineSettings("Friend ", this, Group.LineStyle, LineGroup.Friend).apply { + applyEdits { hide(::startColor, ::endColor) } + } + private val otherLineConfig = ScreenLineSettings("Other ", this, Group.LineStyle, LineGroup.Other).apply { + applyEdits { hide(::startColor, ::endColor) } + } + + init { + immediateRenderer("Tracers Immediate Renderer") { safeContext -> + with(safeContext) { + world.entities.forEach { entity -> + if (entity === player) return@forEach + if (!entitySettings.isSelected(entity)) return@forEach + val color = entityColors.getColor(entity) + val lineConfig = if (entity is OtherClientPlayerEntity && entity.isFriend) friendLineConfig else otherLineConfig + val lerpedPos = lerp(mc.tickDelta, entity.prevPos, entity.pos) + val lerpedEyePos = lerpedPos.add(0.0, entity.standingEyeHeight.toDouble(), 0.0) + val targetPos = when(target) { + TracerMode.Feet -> lerpedPos + TracerMode.Middle -> lerpedPos.add(0.0, entity.standingEyeHeight / 2.0, 0.0) + TracerMode.Eyes -> lerpedEyePos + } + val (toX, toY) = worldToScreenNormalized(targetPos) ?: return@forEach + screenLine(0.5f, 0.5f, toX, toY, color, lineConfig.width, lineConfig.getDashStyle()) + if (stem) { + val (lowerX, lowerY) = + if (target == TracerMode.Feet) Vector2f(toX, toY) + else worldToScreenNormalized(lerpedPos) ?: return@forEach + val (upperX, upperY) = + if (target == TracerMode.Eyes) Vector2f(toX, toY) + else worldToScreenNormalized(lerpedEyePos) ?: return@forEach + screenLine(lowerX, lowerY, upperX, upperY, color, lineConfig.width, lineConfig.getDashStyle()) + } + } + } + } + } + + private enum class TracerMode { + Feet, + Middle, + Eyes + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt index 26b7a44e7..963685d61 100644 --- a/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt +++ b/src/main/kotlin/com/lambda/module/modules/render/Zoom.kt @@ -61,10 +61,6 @@ object Zoom : Module( event.cancel() } - listen(alwaysListen = true) { - updateCurrentZoom() - } - onEnable { updateZoomTime() } diff --git a/src/main/kotlin/com/lambda/util/DebugInfoHud.kt b/src/main/kotlin/com/lambda/util/DebugInfoHud.kt index 198eb0a1d..cee004368 100644 --- a/src/main/kotlin/com/lambda/util/DebugInfoHud.kt +++ b/src/main/kotlin/com/lambda/util/DebugInfoHud.kt @@ -23,7 +23,7 @@ import com.lambda.command.CommandRegistry import com.lambda.event.EventFlow import com.lambda.module.ModuleRegistry import com.lambda.util.Formatting.format -import com.lambda.util.extension.tickDelta +import com.lambda.util.extension.tickDeltaF import net.minecraft.util.Formatting import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult @@ -55,7 +55,7 @@ object DebugInfoHud { null -> add("Crosshair Target: None") } - add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDelta)?.format()}") + add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDeltaF)?.format()}") return } diff --git a/src/main/kotlin/com/lambda/util/EntityUtils.kt b/src/main/kotlin/com/lambda/util/EntityUtils.kt index ebecce7da..119706aa3 100644 --- a/src/main/kotlin/com/lambda/util/EntityUtils.kt +++ b/src/main/kotlin/com/lambda/util/EntityUtils.kt @@ -17,11 +17,52 @@ package com.lambda.util +import com.lambda.util.DynamicReflectionSerializer.remappedName import com.lambda.util.math.MathUtils.floorToInt +import com.lambda.util.reflections.scanResult +import io.github.classgraph.ClassInfo +import io.github.classgraph.ClassInfoList +import net.minecraft.block.entity.BlockEntity import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType import net.minecraft.util.math.BlockPos +import kotlin.jvm.java object EntityUtils { + val entities: Collection = scanResult + .getSubclasses(Entity::class.java) + .filter { !it.isAbstract && it.name.startsWith("net.minecraft") } + + val blockEntityMap = createBlockEntityNameMap() + val playerEntityMap = createEntityNameMap("net.minecraft.client.network.") + val bossEntityMap = createEntityNameMap("net.minecraft.entity.boss.") + val decorationEntityMap = createEntityNameMap("net.minecraft.entity.decoration.") + val mobEntityMap = createEntityNameMap("net.minecraft.entity.mob.") + val passiveEntityMap = createEntityNameMap("net.minecraft.entity.passive.") + val projectileEntityMap = createEntityNameMap("net.minecraft.entity.projectile.") + val vehicleEntityMap = createEntityNameMap("net.minecraft.entity.vehicle.") + val miscEntityMap = createEntityNameMap("net.minecraft.entity.", strictDir = true) + + enum class EntityGroup(val nameToDisplayNameMap: Map) { + Player(createEntityNameMap("net.minecraft.client.network.")), + Mob(createEntityNameMap("net.minecraft.entity.mob.")), + Passive(createEntityNameMap("net.minecraft.entity.passive.")), + Vehicle(createEntityNameMap("net.minecraft.entity.vehicle.")), + Projectile(createEntityNameMap("net.minecraft.entity.projectile.")), + Boss(createEntityNameMap("net.minecraft.entity.boss.")), + Decoration(createEntityNameMap("net.minecraft.entity.decoration.")), + Block(createBlockEntityNameMap()), + Misc(createEntityNameMap("net.minecraft.entity.", strictDir = true)) + } + + val Entity.entityGroup get() = entityGroup() + val BlockEntity.entityGroup: EntityGroup get() = entityGroup() + + private fun Any.entityGroup(): EntityGroup { + val simpleName = javaClass.simpleName + return EntityGroup.entries.first { simpleName in it.nameToDisplayNameMap } + } + fun Entity.getPositionsWithinHitboxXZ(minY: Int, maxY: Int): Set { val hitbox = boundingBox val minX = hitbox.minX.floorToInt() @@ -38,4 +79,41 @@ object EntityUtils { } return positions } + + private fun createEntityNameMap(directory: String, strictDir: Boolean = false) = + entities.createNameMap(directory, "Entity", strictDir) + + private fun createBlockEntityNameMap() = + scanResult + .getSubclasses(BlockEntity::class.java) + .filter { !it.isAbstract } + .createNameMap("net.minecraft.block.entity", "BlockEntity") + + fun Collection.createNameMap( + directory: String, + removePattern: String = "", + strictDirectory: Boolean = false + ) = map { + val remappedName = it.name.remappedName + val displayName = remappedName + .substring(remappedName.indexOfLast { it == '.' } + 1) + .replace(removePattern, "") + .fancyFormat() + MappingInfo(it.simpleName, remappedName, displayName) + }.sortedBy { it.displayName.lowercase() } + .filter { info -> + if (strictDirectory) + info.remapped.startsWith(directory) && !info.remapped.substring(directory.length).contains(".") + else info.remapped.startsWith(directory) + } + .associate { it.raw to it.displayName } + + private fun String.fancyFormat() = + replace("$", " - ").replace("(?>() + override val visibility: () -> Boolean = { true } override val locale: Locale = Locale.US override val separator: String = "," override val prefix: String = "(" diff --git a/src/main/kotlin/com/lambda/util/extension/Entity.kt b/src/main/kotlin/com/lambda/util/extension/Entity.kt index c7bc6e583..8d790cddd 100644 --- a/src/main/kotlin/com/lambda/util/extension/Entity.kt +++ b/src/main/kotlin/com/lambda/util/extension/Entity.kt @@ -31,6 +31,9 @@ val Entity.rotation val LivingEntity.fullHealth: Double get() = health + absorptionAmount.toDouble() +val LivingEntity.maxFullHealth: Double + get() = maxHealth + maxAbsorption.toDouble() + var LivingEntity.isElytraFlying get() = isGliding set(value) { diff --git a/src/main/kotlin/com/lambda/util/extension/Mixin.kt b/src/main/kotlin/com/lambda/util/extension/Mixin.kt index 30c210929..7b539430e 100644 --- a/src/main/kotlin/com/lambda/util/extension/Mixin.kt +++ b/src/main/kotlin/com/lambda/util/extension/Mixin.kt @@ -19,8 +19,8 @@ package com.lambda.util.extension import net.minecraft.client.MinecraftClient -val MinecraftClient.partialTicks - get() = tickDelta.toDouble() - val MinecraftClient.tickDelta + get() = tickDeltaF.toDouble() + +val MinecraftClient.tickDeltaF get() = renderTickCounter.getTickProgress(true) diff --git a/src/main/kotlin/com/lambda/util/extension/World.kt b/src/main/kotlin/com/lambda/util/extension/World.kt index a1b2f5124..64c425d5d 100644 --- a/src/main/kotlin/com/lambda/util/extension/World.kt +++ b/src/main/kotlin/com/lambda/util/extension/World.kt @@ -17,20 +17,39 @@ package com.lambda.util.extension +import com.lambda.Lambda.mc import com.lambda.context.SafeContext +import com.lambda.util.extension.paintingColorCache import com.lambda.util.world.FastVector import com.lambda.util.world.toBlockPos import com.lambda.util.world.x import com.lambda.util.world.y import com.lambda.util.world.z +import net.minecraft.block.Block import net.minecraft.block.BlockState import net.minecraft.block.Blocks +import net.minecraft.client.render.entity.EntityRenderer +import net.minecraft.client.render.model.BlockStateManagers +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.Sprite +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.decoration.ItemFrameEntity +import net.minecraft.entity.decoration.painting.PaintingEntity +import net.minecraft.entity.decoration.painting.PaintingVariant import net.minecraft.fluid.FluidState import net.minecraft.fluid.Fluids +import net.minecraft.util.Atlases +import net.minecraft.util.Identifier import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.ColorHelper import net.minecraft.util.shape.VoxelShape import net.minecraft.world.World import java.awt.Color +import java.lang.reflect.Modifier +import java.util.concurrent.ConcurrentHashMap + +private val blockColorCache = ConcurrentHashMap() val SafeContext.worldName: String get() = when { @@ -53,16 +72,291 @@ fun SafeContext.collisionShape(state: BlockState, pos: BlockPos): VoxelShape = fun SafeContext.outlineShape(state: BlockState, pos: BlockPos) = state.getOutlineShape(world, pos).offset(pos) +/** + * Gets the average color of a block from its texture. + * Results are cached by block type for performance. + * Falls back to map color if texture access fails. + */ fun SafeContext.blockColor(state: BlockState, pos: BlockPos): Color { - return when (state.block) { - Blocks.ENDER_CHEST -> Color(0xFF00FF) - Blocks.NETHER_PORTAL -> Color(0xaa00aa) - Blocks.END_PORTAL -> Color(0xFF00FF) - else -> - Color(state.getMapColor(world, pos).color, false) + return blockColorCache.getOrPut(state.block) { + calculateAverageTextureColor(state) ?: Color(state.getMapColor(world, pos).color, false) + } +} + +private val entityColorCache = ConcurrentHashMap, Color>() +private val itemFrameColorCache = ConcurrentHashMap() +private val paintingColorCache = ConcurrentHashMap() + +/** + * Gets the average color of an entity from its texture. + * Results are cached by entity type for performance. + * Falls back to gray if texture access fails. + */ +fun entityColor(entity: Entity): Color { + return when (entity) { + is ItemFrameEntity -> calculateItemFrameColor(entity) ?: Color.LIGHT_GRAY + is PaintingEntity -> paintingColorCache.getOrPut(entity.variant.value()) { calculatePaintingColor(entity) ?: Color.LIGHT_GRAY } + else -> entityColorCache.getOrPut(entity.type) { + calculateEntityTextureColor(entity) ?: Color(128, 128, 128) + } } } +/** + * Calculates the average color from an entity's texture. + * Uses the entity renderer to get the texture identifier, then samples the texture. + * Supports both living and non-living entities using reflection to find texture methods/fields. + */ +private fun calculateEntityTextureColor(entity: Entity): Color? { + return try { + val renderer = mc.entityRenderDispatcher.getRenderer(entity) ?: return null + val textureId = getTextureFromRenderer(entity, renderer) ?: return null + calculateAverageColorFromTexture(textureId) + } catch (_: Exception) { null } +} + +private fun getTextureFromRenderer(entity: Entity, renderer: EntityRenderer<*, *>): Identifier? { + val rendererClass = renderer.javaClass + + val textureFromMethod = tryGetTextureFromMethod(entity, renderer, rendererClass) + if (textureFromMethod != null) return textureFromMethod + + val textureFromField = tryGetTextureFromField(renderer, rendererClass) + if (textureFromField != null) return textureFromField + + return null +} + +/** + * Tries to get texture by calling a getTexture(RenderState) method via reflection. + */ +private fun tryGetTextureFromMethod(entity: Entity, renderer: EntityRenderer<*, *>, rendererClass: Class<*>): Identifier? { + return try { + // Find getTexture method that takes a single parameter + val getTextureMethod = rendererClass.methods.find { method -> + method.name == "getTexture" && + method.parameterCount == 1 && + Identifier::class.java.isAssignableFrom(method.returnType) + } ?: return null + + getTextureMethod.isAccessible = true + + // Create and update render state + val createStateMethod = rendererClass.getMethod("createRenderState") + val state = createStateMethod.invoke(renderer) + + // Try to update the state with entity data + try { + val updateStateMethod = rendererClass.methods.find { + it.name == "updateRenderState" && it.parameterCount == 3 + } + updateStateMethod?.invoke(renderer, entity, state, 0f) + } catch (_: Exception) { /* State update is optional */ } + + getTextureMethod.invoke(renderer, state) as? Identifier + } catch (_: Exception) { null } +} + +/** + * Tries to get texture from an Identifier-typed field via reflection. + * Searches for common field names like 'texture', 'TEXTURE', etc. + */ +private fun tryGetTextureFromField(renderer: EntityRenderer<*, *>, rendererClass: Class<*>): Identifier? { + return try { + // Look through all fields in the class hierarchy for Identifier types + var currentClass: Class<*>? = rendererClass + while (currentClass != null && currentClass != EntityRenderer::class.java) { + for (field in currentClass.declaredFields) { + if (Identifier::class.java.isAssignableFrom(field.type)) { + field.isAccessible = true + val value = field.get(renderer) + if (value is Identifier) return value + + // Also try static fields + if (Modifier.isStatic(field.modifiers)) { + val staticValue = field.get(null) + if (staticValue is Identifier) return staticValue + } + } + } + currentClass = currentClass.superclass + } + null + } catch (_: Exception) { null } +} + +private fun calculateItemFrameColor(entity: ItemFrameEntity): Color? { + return try { + val isGlow = entity.type == EntityType.GLOW_ITEM_FRAME + val hasMap = entity.getMapId(entity.heldItemStack) != null + val blockState = BlockStateManagers.getStateForItemFrame(isGlow, hasMap) + val sprite = mc.blockRenderManager.models.getModelParticleSprite(blockState) + calculateAverageColorFromSprite(sprite) + } catch (_: Exception) { null } +} + +private fun calculatePaintingColor(entity: PaintingEntity): Color? { + return try { + val variant = entity.variant.value() + val paintingAtlas = mc.atlasManager.getAtlasTexture(Atlases.PAINTINGS) + val sprite = paintingAtlas.getSprite(variant.assetId()) + calculateAverageColorFromSprite(sprite) + } catch (_: Exception) { null } +} + +private fun calculateAverageColorFromSprite(sprite: Sprite): Color? { + return try { + val contents = sprite.contents + val image = contents.image + + val width = contents.width + val height = contents.height + + var totalR = 0L + var totalG = 0L + var totalB = 0L + var totalWeight = 0L + + for (y in 0 until height) { + for (x in 0 until width) { + val argb = image.getColorArgb(x, y) + val alpha = ColorHelper.getAlpha(argb) + + if (alpha == 0) continue + + val r = ColorHelper.getRed(argb) + val g = ColorHelper.getGreen(argb) + val b = ColorHelper.getBlue(argb) + + totalR += r.toLong() * alpha + totalG += g.toLong() * alpha + totalB += b.toLong() * alpha + totalWeight += alpha.toLong() + } + } + + if (totalWeight == 0L) return null + + val avgR = (totalR / totalWeight).toInt().coerceIn(0, 255) + val avgG = (totalG / totalWeight).toInt().coerceIn(0, 255) + val avgB = (totalB / totalWeight).toInt().coerceIn(0, 255) + + Color(avgR, avgG, avgB) + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from a texture identifier. + */ +private fun calculateAverageColorFromTexture(textureId: Identifier): Color? { + return try { + // Try to load the texture from the resource manager + val resource = mc.resourceManager.getResource(textureId).orElse(null) ?: run { + // If that fails, try constructing a texture path with .png extension + val pngId = Identifier.of(textureId.namespace, textureId.path + ".png") + mc.resourceManager.getResource(pngId).orElse(null) + } ?: return null + + resource.inputStream.use { inputStream -> + val image = NativeImage.read(inputStream) + + val width = image.width + val height = image.height + + var totalR = 0L + var totalG = 0L + var totalB = 0L + var totalWeight = 0L + + for (y in 0 until height) { + for (x in 0 until width) { + val abgr = image.getColorArgb(x, y) + val alpha = ColorHelper.getAlpha(abgr) + + if (alpha == 0) continue + + val r = ColorHelper.getRed(abgr) + val g = ColorHelper.getGreen(abgr) + val b = ColorHelper.getBlue(abgr) + + totalR += r.toLong() * alpha + totalG += g.toLong() * alpha + totalB += b.toLong() * alpha + totalWeight += alpha.toLong() + } + } + + image.close() + + if (totalWeight == 0L) return null + + val avgR = (totalR / totalWeight).toInt().coerceIn(0, 255) + val avgG = (totalG / totalWeight).toInt().coerceIn(0, 255) + val avgB = (totalB / totalWeight).toInt().coerceIn(0, 255) + + Color(avgR, avgG, avgB) + } + } catch (_: Exception) { null } +} + +/** + * Calculates the average color from a block's particle sprite texture. + * Uses alpha-weighted averaging to ignore transparent pixels. + * Applies block tints (for grass, leaves, water, etc.) using BlockColors. + */ +private fun calculateAverageTextureColor(state: BlockState): Color? { + return try { + val sprite = mc.blockRenderManager.models.getModelParticleSprite(state) + val contents = sprite.contents + val image = contents.image + + val width = contents.width + val height = contents.height + + var totalR = 0L + var totalG = 0L + var totalB = 0L + var totalWeight = 0L + + for (y in 0 until height) { + for (x in 0 until width) { + val argb = image.getColorArgb(x, y) + val alpha = ColorHelper.getAlpha(argb) + + if (alpha == 0) continue + + val r = ColorHelper.getRed(argb) + val g = ColorHelper.getGreen(argb) + val b = ColorHelper.getBlue(argb) + + totalR += r.toLong() * alpha + totalG += g.toLong() * alpha + totalB += b.toLong() * alpha + totalWeight += alpha.toLong() + } + } + + if (totalWeight == 0L) return null + + var avgR = (totalR / totalWeight).toInt().coerceIn(0, 255) + var avgG = (totalG / totalWeight).toInt().coerceIn(0, 255) + var avgB = (totalB / totalWeight).toInt().coerceIn(0, 255) + + val tint = mc.blockColors.getColor(state, null, null, 0) + if (tint != -1) { + val tintR = ColorHelper.getRed(tint) + val tintG = ColorHelper.getGreen(tint) + val tintB = ColorHelper.getBlue(tint) + + avgR = (avgR * tintR / 255).coerceIn(0, 255) + avgG = (avgG * tintG / 255).coerceIn(0, 255) + avgB = (avgB * tintB / 255).coerceIn(0, 255) + } + + Color(avgR, avgG, avgB) + } catch (_: Exception) { null } +} + fun World.getBlockState(x: Int, y: Int, z: Int): BlockState { if (isOutOfHeightLimit(y)) return Blocks.VOID_AIR.defaultState diff --git a/src/main/kotlin/com/lambda/util/math/Color.kt b/src/main/kotlin/com/lambda/util/math/Color.kt index 3c65162d6..5c821be1a 100644 --- a/src/main/kotlin/com/lambda/util/math/Color.kt +++ b/src/main/kotlin/com/lambda/util/math/Color.kt @@ -20,6 +20,9 @@ package com.lambda.util.math import net.minecraft.util.math.Vec3d import java.awt.Color +fun Color.setAlpha(value: Int) = + Color(red, green, blue, value.coerceIn(0, 255)) + fun Color.setAlpha(value: Double) = Color(red, green, blue, (value * 255.0).coerceIn(0.0, 255.0).toInt()) diff --git a/src/main/kotlin/com/lambda/util/reflections/Reflections.kt b/src/main/kotlin/com/lambda/util/reflections/Reflections.kt index c7bcfbeb9..36570a564 100644 --- a/src/main/kotlin/com/lambda/util/reflections/Reflections.kt +++ b/src/main/kotlin/com/lambda/util/reflections/Reflections.kt @@ -37,7 +37,6 @@ val KClass<*>.className: String get() = java.name .substringAfter("${java.packageName}.") .replace('$', '.') - /** * This function returns a instance of subtype [T]. * diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh deleted file mode 100644 index 7727eca9d..000000000 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.fsh +++ /dev/null @@ -1,50 +0,0 @@ -#version 330 - -#moj_import -#moj_import - -in vec4 vertexColor; -noperspective in float v_LineDist; -noperspective in float v_LineWidth; -noperspective in vec2 v_DistPixels; -noperspective in float v_LineLength; -in float sphericalVertexDistance; -in float cylindricalVertexDistance; - -out vec4 fragColor; - -void main() { - // Closest point on the center line segment [0, L] - float closestX = clamp(v_DistPixels.x, 0.0, v_LineLength); - vec2 closestPoint = vec2(closestX, 0.0); - - // Pixel distance from the closest point (Round Capsule SDF) - float dist = length(v_DistPixels - closestPoint); - - // SDF value: distance from the capsule edge - float sdf = dist - (v_LineWidth / 2.0); - - // Ultra-sharp edges (AA transition of 0.3 pixels total) - float alpha; - if (v_LineWidth >= 1.0) { - alpha = smoothstep(0.15, -0.15, sdf); - } else { - // Super thin lines: reduce opacity instead of shrinking width - float transverseAlpha = (1.0 - smoothstep(0.0, 1.0, abs(v_DistPixels.y))) * v_LineWidth; - alpha = transverseAlpha; - } - - // Aggressive fade for tiny segments far away to prevent blobbing - // If a segment is less than 0.8px on screen, fade it out to nothing - float lengthFade = clamp(v_LineLength / 0.8, 0.0, 1.0); - alpha *= lengthFade * lengthFade; // Quadratic falloff for tiny segments - - if (alpha <= 0.0) { - discard; - } - - vec4 color = vertexColor * ColorModulator; - color.a *= alpha; - - fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); -} diff --git a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh b/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh deleted file mode 100644 index 46e84da2a..000000000 --- a/src/main/resources/assets/lambda/shaders/core/advanced_lines.vsh +++ /dev/null @@ -1,74 +0,0 @@ -#version 330 - -#moj_import -#moj_import -#moj_import -#moj_import - -in vec3 Position; -in vec4 Color; -in vec3 Normal; -in float LineWidth; - -out vec4 vertexColor; -noperspective out float v_LineDist; -noperspective out float v_LineWidth; -noperspective out vec2 v_DistPixels; -noperspective out float v_LineLength; -out float sphericalVertexDistance; -out float cylindricalVertexDistance; - -const float VIEW_SHRINK = 1.0 - (1.0 / 256.0); - -void main() { - int vertexIndex = gl_VertexID % 4; - bool isStart = (vertexIndex < 2); - - float actualWidth = max(LineWidth, 0.1); - float padding = 0.5; // AA padding - float halfWidthExtended = actualWidth / 2.0 + padding; - - // Transform start and end - vec4 posStart = ProjMat * ModelViewMat * vec4(isStart ? Position : Position - Normal, 1.0); - vec4 posEnd = ProjMat * ModelViewMat * vec4(isStart ? Position + Normal : Position, 1.0); - - vec3 ndcStart = posStart.xyz / posStart.w; - vec3 ndcEnd = posEnd.xyz / posEnd.w; - - // Screen space coordinates - vec2 screenStart = (ndcStart.xy * 0.5 + 0.5) * ScreenSize; - vec2 screenEnd = (ndcEnd.xy * 0.5 + 0.5) * ScreenSize; - - vec2 delta = screenEnd - screenStart; - float lenPixels = length(delta); - - // Stable direction - vec2 lineDir = (lenPixels > 0.001) ? delta / lenPixels : vec2(1.0, 0.0); - vec2 lineNormal = vec2(-lineDir.y, lineDir.x); - - // Quad vertex layout - float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; - float longitudinalSide = isStart ? -1.0 : 1.0; - - // Expansion in pixels: full radius + padding to contain capsule end - vec2 offsetPixels = lineNormal * side * halfWidthExtended + lineDir * longitudinalSide * halfWidthExtended; - - // Current point NDC - vec3 ndcThis = isStart ? ndcStart : ndcEnd; - float wThis = isStart ? posStart.w : posEnd.w; - - // Convert pixel offset back to NDC - vec2 offsetNDC = (offsetPixels / ScreenSize) * 2.0; - gl_Position = vec4((ndcThis + vec3(offsetNDC, 0.0)) * wThis, wThis); - - vertexColor = Color; - - // Pass coordinates for SDF - v_LineDist = side; - v_DistPixels = vec2(isStart ? -halfWidthExtended : lenPixels + halfWidthExtended, side * halfWidthExtended); - v_LineWidth = actualWidth; - v_LineLength = lenPixels; - - sphericalVertexDistance = fog_spherical_distance(Position); - cylindricalVertexDistance = fog_cylindrical_distance(Position); -} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_id.fsh b/src/main/resources/assets/lambda/shaders/core/outline_id.fsh new file mode 100644 index 000000000..f445edf0b --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_id.fsh @@ -0,0 +1,18 @@ +#version 330 + +#moj_import + +uniform sampler2D Sampler0; + +in vec2 v_TexCoord; +in vec4 v_Color; + +out vec4 fragColor; + +void main() { + float alpha = texture(Sampler0, v_TexCoord).a; + if (alpha < 0.1) discard; + + vec4 baseColor = (ModelOffset.x > 0.5) ? ColorModulator : v_Color; + fragColor = vec4(baseColor.rgb, baseColor.a); +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_id.vsh b/src/main/resources/assets/lambda/shaders/core/outline_id.vsh new file mode 100644 index 000000000..8de213dcd --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_id.vsh @@ -0,0 +1,19 @@ +#version 330 + +#moj_import +#moj_import + +in vec4 Position; +in vec2 UV0; +in vec4 Color; + +out vec2 v_TexCoord; +out vec4 v_Color; + +void main() { + gl_Position = Position; + gl_Position.z = gl_Position.z * 0.9999 - 0.00001 * gl_Position.w; + + v_TexCoord = UV0; + v_Color = Color; +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_silhouette.fsh b/src/main/resources/assets/lambda/shaders/core/outline_silhouette.fsh new file mode 100644 index 000000000..e0bdbceb8 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_silhouette.fsh @@ -0,0 +1,10 @@ +#version 330 +#moj_import + +in vec4 v_Color; +out vec4 fragColor; + +void main() { + vec4 color = v_Color * ColorModulator; + fragColor = vec4(color.rgb, 1.0); +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_silhouette.vsh b/src/main/resources/assets/lambda/shaders/core/outline_silhouette.vsh new file mode 100644 index 000000000..e62c00523 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_silhouette.vsh @@ -0,0 +1,13 @@ +#version 330 +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; + +out vec4 v_Color; + +void main() { + gl_Position = TextureMat * ModelViewMat * vec4(Position, 1.0); + v_Color = Color; +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_silhouette_textured.fsh b/src/main/resources/assets/lambda/shaders/core/outline_silhouette_textured.fsh new file mode 100644 index 000000000..2c241b83e --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_silhouette_textured.fsh @@ -0,0 +1,15 @@ +#version 330 + +uniform sampler2D Sampler0; + +in vec4 v_Color; +in vec2 v_TexCoord; + +out vec4 fragColor; + +void main() { + float alpha = texture(Sampler0, v_TexCoord).a; + if (alpha < 0.1) discard; + + fragColor = vec4(v_Color.rgb, v_Color.a); +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_silhouette_textured.vsh b/src/main/resources/assets/lambda/shaders/core/outline_silhouette_textured.vsh new file mode 100644 index 000000000..263099db6 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_silhouette_textured.vsh @@ -0,0 +1,17 @@ +#version 330 +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; +in vec2 UV0; +in vec2 UV2; + +out vec4 v_Color; +out vec2 v_TexCoord; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position + ModelOffset, 1.0); + v_Color = Color; + v_TexCoord = UV0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_sobel.fsh b/src/main/resources/assets/lambda/shaders/core/outline_sobel.fsh new file mode 100644 index 000000000..f75d33046 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_sobel.fsh @@ -0,0 +1,141 @@ +#version 330 +#moj_import + +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; +uniform sampler2D Sampler2; + +in vec2 v_TexCoord; +out vec4 fragColor; + +float u_Thickness; +float u_GlowIntensity; +float u_GlowRadius; +float u_FillOpacity; + +bool depthTestVisible(vec2 coord) { + float silDepth = texture(Sampler1, coord).r; + float worldDepth = texture(Sampler2, coord).r; + return silDepth <= worldDepth + 0.00001; +} + +bool isVisible(float alpha, vec2 coord) { + if (alpha <= 0.0) return false; + + if (ModelOffset.x > 0.5) { + if (ModelOffset.y > 0.5) return depthTestVisible(coord); + return true; + } + + if (alpha < 0.5) return true; + return depthTestVisible(coord); +} + +bool hasEntityData(vec2 coord) { + return texture(Sampler0, coord).a > 0.0; +} + +vec3 getEntityColor(vec4 sample) { + return sample.rgb; +} + +void main() { + float screenHeight = float(textureSize(Sampler0, 0).y); + u_Thickness = TextureMat[0][0] * screenHeight; + u_GlowIntensity = TextureMat[0][1]; + u_GlowRadius = TextureMat[0][2] * screenHeight; + u_FillOpacity = TextureMat[0][3]; + + if (u_Thickness <= 0.0) u_Thickness = 1.0; + + float outlineAlpha = clamp(u_Thickness, 0.0, 1.0); + float effectiveThickness = max(u_Thickness, 1.0); + + vec2 texelSize = 1.0 / textureSize(Sampler0, 0); + vec4 center = texture(Sampler0, v_TexCoord); + bool centerHasData = center.a > 0.0; + bool centerVisible = centerHasData && isVisible(center.a, v_TexCoord); + + float maxRadius = effectiveThickness + u_GlowRadius; + int maxRadiusInt = int(ceil(maxRadius)); + + float minDistToEdge = maxRadius + 1.0; + + if (centerHasData) { + if (!centerVisible) discard; + + if (u_FillOpacity > 0.0) { + vec3 color; + if (ModelOffset.x > 0.5) color = ColorModulator.rgb; + else color = getEntityColor(center); + fragColor = vec4(color, u_FillOpacity); + return; + } + + discard; + } else { + vec3 nearestColor = vec3(0.0); + bool nearestVisible = false; + + int step = max(1, maxRadiusInt / 12); + int bestDx = 0, bestDy = 0; + + for (int dy = -maxRadiusInt; dy <= maxRadiusInt; dy += step) { + for (int dx = -maxRadiusInt; dx <= maxRadiusInt; dx += step) { + if (dx == 0 && dy == 0) continue; + float dist = length(vec2(float(dx), float(dy))); + if (dist > maxRadius || dist >= minDistToEdge) continue; + + vec2 sampleCoord = v_TexCoord + vec2(float(dx), float(dy)) * texelSize; + vec4 s = texture(Sampler0, sampleCoord); + if (s.a > 0.0) { + minDistToEdge = dist; + bestDx = dx; + bestDy = dy; + nearestVisible = isVisible(s.a, sampleCoord); + if (ModelOffset.x > 0.5) nearestColor = ColorModulator.rgb; + else nearestColor = getEntityColor(s); + } + } + } + + if (step > 1 && minDistToEdge < maxRadius + 1.0) { + int rMin = step; + for (int dy = bestDy - rMin; dy <= bestDy + rMin; dy++) { + for (int dx = bestDx - rMin; dx <= bestDx + rMin; dx++) { + if (dx == 0 && dy == 0) continue; + float dist = length(vec2(float(dx), float(dy))); + if (dist > maxRadius || dist >= minDistToEdge) continue; + + vec2 sampleCoord = v_TexCoord + vec2(float(dx), float(dy)) * texelSize; + vec4 s = texture(Sampler0, sampleCoord); + if (s.a > 0.0) { + minDistToEdge = dist; + nearestVisible = isVisible(s.a, sampleCoord); + if (ModelOffset.x > 0.5) nearestColor = ColorModulator.rgb; + else nearestColor = getEntityColor(s); + } + } + } + } + + if (!nearestVisible) discard; + + if (minDistToEdge <= effectiveThickness) { + fragColor = vec4(nearestColor, outlineAlpha); + return; + } + + if (u_GlowIntensity > 0.0 && u_GlowRadius > 0.0 && minDistToEdge <= maxRadius) { + float glowDist = minDistToEdge - effectiveThickness; + float glowFactor = 1.0 - (glowDist / u_GlowRadius); + glowFactor = clamp(glowFactor, 0.0, 1.0); + glowFactor = glowFactor * glowFactor; + float alpha = glowFactor * u_GlowIntensity * outlineAlpha; + fragColor = vec4(nearestColor, alpha); + return; + } + + discard; + } +} diff --git a/src/main/resources/assets/lambda/shaders/core/outline_sobel.vsh b/src/main/resources/assets/lambda/shaders/core/outline_sobel.vsh new file mode 100644 index 000000000..b1b02bf57 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/outline_sobel.vsh @@ -0,0 +1,11 @@ +#version 330 + +in vec3 Position; +in vec2 UV0; + +out vec2 v_TexCoord; + +void main() { + gl_Position = vec4(Position.xy, 0.0, 1.0); + v_TexCoord = UV0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh b/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh new file mode 100644 index 000000000..23ce9022b --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_faces.fsh @@ -0,0 +1,14 @@ +#version 330 +#moj_import + +in vec4 v_Color; +in float v_Layer; + +out vec4 fragColor; + +void main() { + float alpha = v_Color.a; + if (alpha <= 0.001) discard; + fragColor = v_Color * ColorModulator; + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh b/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh new file mode 100644 index 000000000..07eb32508 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_faces.vsh @@ -0,0 +1,16 @@ +#version 330 +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; +in float Layer; + +out vec4 v_Color; +out float v_Layer; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position.xy, 0.0, 1.0); + v_Color = Color; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_image.fsh b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh new file mode 100644 index 000000000..cb877d267 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_image.fsh @@ -0,0 +1,39 @@ +#version 330 + +#moj_import +#moj_import + +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; + +in vec2 v_TexCoord; +in vec4 v_Color; +in vec4 v_OverlayUV; +in float v_Layer; + +out vec4 fragColor; + +void main() { + vec4 texColor = texture(Sampler0, v_TexCoord); + + vec4 color = texColor * v_Color * ColorModulator; + + if (color.a < 0.004) discard; + + if (v_OverlayUV.z > 0.5) { + float aspectRatio = v_OverlayUV.y; + + vec2 baseUV = vec2(v_TexCoord.x * aspectRatio, v_TexCoord.y); + + vec4 transformedUV = TextureMat * vec4(baseUV, 0.0, 1.0); + + vec4 glint = texture(Sampler1, fract(transformedUV.xy)); + + vec3 layer = glint.rgb * glint.a * 0.75; + color.rgb += (layer * layer); + } + + fragColor = color; + + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_image.vsh b/src/main/resources/assets/lambda/shaders/core/screen_image.vsh new file mode 100644 index 000000000..1ecdfce85 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_image.vsh @@ -0,0 +1,24 @@ +#version 330 + +#moj_import +#moj_import + +in vec3 Position; +in vec2 UV0; +in vec4 Color; +in vec4 OverlayUV; +in float Layer; + +out vec2 v_TexCoord; +out vec4 v_Color; +out vec4 v_OverlayUV; +out float v_Layer; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + v_TexCoord = UV0; + v_Color = Color; + v_OverlayUV = OverlayUV; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh new file mode 100644 index 000000000..094eec4b7 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.fsh @@ -0,0 +1,98 @@ +#version 330 + +#moj_import +#moj_import + +in vec4 v_Color; +in vec2 v_ExpandedPos; +flat in vec2 v_LineStart; +flat in vec2 v_LineEnd; +flat in float v_LineWidth; +flat in float v_SegmentLength; +flat in vec4 v_Dash; +in float v_Layer; + +out vec4 fragColor; + +void main() { + vec2 lineDir = normalize(v_LineEnd - v_LineStart); + vec2 perpDir = vec2(-lineDir.y, lineDir.x); + + vec2 toFragment = v_ExpandedPos - v_LineStart; + float projLength = dot(toFragment, lineDir); + float perpDist = abs(dot(toFragment, perpDir)); + + vec2 dPos_dx = dFdx(v_ExpandedPos); + vec2 dPos_dy = dFdy(v_ExpandedPos); + float pixelSize = (length(dPos_dx) + length(dPos_dy)) * 0.5; + + float dist2D; + if (projLength < 0.0) { + dist2D = length(v_ExpandedPos - v_LineStart); + } else if (projLength > v_SegmentLength) { + dist2D = length(v_ExpandedPos - v_LineEnd); + } else { + dist2D = perpDist; + } + + float screenLineWidth = v_LineWidth / max(pixelSize, 0.0001); + + float minWidth = pixelSize; + float effectiveRadius = max(v_LineWidth * 0.5, minWidth * 0.5); + + float alphaScale = min(screenLineWidth, 1.0); + + float sdf = dist2D - effectiveRadius; + + float aaWidth = pixelSize; + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + alpha *= alphaScale; + + if (alpha < 0.004) { + discard; + } + + float dashLength = v_Dash.x; + float gapLength = v_Dash.y; + float dashOffset = v_Dash.z; + float animationSpeed = v_Dash.w; + + if (dashLength > 0.0) { + float cycleLength = dashLength + gapLength; + + float animatedOffset = dashOffset; + if (animationSpeed != 0.0) { + animatedOffset -= GameTime * animationSpeed * 1200.0; + } + + float dashPos = projLength + animatedOffset * cycleLength; + float posInCycle = mod(dashPos, cycleLength); + + float dashSdf; + if (posInCycle > dashLength) { + float distToGapEnd = cycleLength - posInCycle; + dashSdf = min(posInCycle - dashLength, distToGapEnd); + } else { + float distToDashEnd = dashLength - posInCycle; + float distFromDashStart = posInCycle; + dashSdf = -min(distToDashEnd, distFromDashStart); + } + + float dashAaWidth = fwidth(dashSdf); + float dashAlpha = 1.0 - smoothstep(-dashAaWidth, dashAaWidth, dashSdf); + + if (dashAlpha <= 0.0) { + discard; + } + + alpha *= dashAlpha; + } + + vec4 color = v_Color * ColorModulator; + color.a *= alpha; + + fragColor = color; + + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh new file mode 100644 index 000000000..2172b4be1 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_lines.vsh @@ -0,0 +1,59 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; +in vec2 Direction; +in float LineWidth; +in vec4 Dash; +in float Layer; + +out vec4 v_Color; +out vec2 v_ExpandedPos; +flat out vec2 v_LineStart; +flat out vec2 v_LineEnd; +flat out float v_LineWidth; +flat out float v_SegmentLength; +flat out vec4 v_Dash; +out float v_Layer; + +void main() { + int vertexIndex = gl_VertexID % 4; + bool isStart = (vertexIndex < 2); + float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; + + float segmentLength = length(Direction); + vec2 lineDir = Direction / max(segmentLength, 0.001); + + vec2 lineCenter = isStart ? (Position.xy + Direction * 0.5) : (Position.xy - Direction * 0.5); + + vec2 lineStart = lineCenter - lineDir * (segmentLength * 0.5); + vec2 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); + vec2 thisPoint = isStart ? lineStart : lineEnd; + + vec2 perpDir = vec2(-lineDir.y, lineDir.x); + + float halfWidth = LineWidth / 2.0; + float aaPadding = max(LineWidth * 0.5, 2.0); + float halfWidthPadded = halfWidth + aaPadding; + + vec2 perpOffset = perpDir * side * halfWidthPadded; + float longitudinal = isStart ? -1.0 : 1.0; + vec2 longOffset = lineDir * longitudinal * halfWidthPadded; + + vec2 expandedPos = thisPoint + perpOffset + longOffset; + + gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 0.0, 1.0); + + v_Color = Color; + v_ExpandedPos = expandedPos; + v_LineStart = lineStart; + v_LineEnd = lineEnd; + v_LineWidth = LineWidth; + v_SegmentLength = segmentLength; + v_Dash = Dash; + v_Layer = Layer; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh new file mode 100644 index 000000000..d0cc6b50b --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.fsh @@ -0,0 +1,57 @@ +#version 330 + +#moj_import + +uniform sampler2D Sampler0; + +in vec2 texCoord0; +in vec4 vertexColor; +in vec4 sdfStyleParams; +in float v_Layer; +flat in int v_LayerType; + +out vec4 fragColor; + +void main() { + float OutlineWidth = sdfStyleParams.x; + float GlowRadius = sdfStyleParams.y; + float ShadowSoftness = sdfStyleParams.z; + float SDFThreshold = sdfStyleParams.w; + + vec4 texSample = texture(Sampler0, texCoord0); + float sdfValue = texSample.a; + + float smoothing = fwidth(sdfValue) * 0.5; + + int layerType = v_LayerType; + + float alpha; + + if (layerType == 3) { + alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + } else if (layerType == 2) { + float outlineEdge = SDFThreshold - OutlineWidth; + alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, sdfValue); + float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + alpha = alpha * (1.0 - textMask); + } else if (layerType == 1) { + float glowEdge = (OutlineWidth > 0.001) ? (SDFThreshold - OutlineWidth) : SDFThreshold; + float glowStart = glowEdge - GlowRadius; + float glowEnd = glowEdge; + alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6; + float outlineMask = smoothstep(glowEdge - smoothing, glowEdge + smoothing, sdfValue); + alpha = alpha * (1.0 - outlineMask); + } else { + float shadowStart = SDFThreshold - ShadowSoftness - 0.15; + float shadowEnd = SDFThreshold - 0.1; + alpha = smoothstep(shadowStart, shadowEnd, sdfValue) * 0.5; + } + + vec4 result = vec4(vertexColor.rgb, alpha); + + if (result.a <= 0.001) discard; + + fragColor = result * ColorModulator; + + gl_FragDepth = (1000.0 - v_Layer) / 2000.0; +} diff --git a/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh new file mode 100644 index 000000000..d417e695f --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/screen_sdf_text.vsh @@ -0,0 +1,26 @@ +#version 330 + +#moj_import +#moj_import + +in vec3 Position; +in vec2 UV0; +in vec4 Color; +in vec4 SDFStyle; +in float Layer; + +out vec2 texCoord0; +out vec4 vertexColor; +out vec4 sdfStyleParams; +out float v_Layer; +flat out int v_LayerType; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position.xy, 0.0, 1.0); + + texCoord0 = UV0; + vertexColor = Color; + sdfStyleParams = SDFStyle; + v_Layer = Layer; + v_LayerType = int(Position.z + 0.5); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_faces.fsh b/src/main/resources/assets/lambda/shaders/core/world_faces.fsh new file mode 100644 index 000000000..1a66b7fe3 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_faces.fsh @@ -0,0 +1,17 @@ +#version 330 +#moj_import +#moj_import + +in vec4 v_Color; +in float sphericalVertexDistance; +in float cylindricalVertexDistance; + +out vec4 fragColor; + +void main() { + vec4 color = v_Color * ColorModulator; + if (color.a <= 0.001) discard; + fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, + FogEnvironmentalStart, FogEnvironmentalEnd, + FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_faces.vsh b/src/main/resources/assets/lambda/shaders/core/world_faces.vsh new file mode 100644 index 000000000..dd60b43d3 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_faces.vsh @@ -0,0 +1,21 @@ +#version 330 +#moj_import +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; + +out vec4 v_Color; +out float sphericalVertexDistance; +out float cylindricalVertexDistance; + +void main() { + vec3 worldPos = Position + ModelOffset; + vec4 viewPos = ModelViewMat * vec4(worldPos, 1.0); + gl_Position = ProjMat * viewPos; + + v_Color = Color; + sphericalVertexDistance = fog_spherical_distance(viewPos.xyz); + cylindricalVertexDistance = fog_cylindrical_distance(viewPos.xyz); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_image.fsh b/src/main/resources/assets/lambda/shaders/core/world_image.fsh new file mode 100644 index 000000000..272baa9cf --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_image.fsh @@ -0,0 +1,31 @@ +#version 330 + +#moj_import +#moj_import + +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; + +in vec2 v_TexCoord; +in vec4 v_Color; +in vec4 v_OverlayUV; + +out vec4 fragColor; + +void main() { + vec4 texColor = texture(Sampler0, v_TexCoord); + + vec4 color = texColor * v_Color * ColorModulator; + + if (color.a < 0.004) discard; + + if (v_OverlayUV.z > 0.5) { + vec4 transformedUV = TextureMat * vec4(v_TexCoord, 0.0, 1.0); + vec4 glint = texture(Sampler1, fract(transformedUV.xy)); + + vec3 layer = glint.rgb * glint.a * 0.75; + color.rgb += (layer * layer); + } + + fragColor = color; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_image.vsh b/src/main/resources/assets/lambda/shaders/core/world_image.vsh new file mode 100644 index 000000000..fd44453a3 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_image.vsh @@ -0,0 +1,33 @@ +#version 330 + +#moj_import +#moj_import + +in vec3 Position; +in vec2 UV0; +in vec4 Color; +in vec3 Anchor; +in vec2 BillboardData; +in vec4 OverlayUV; + +out vec2 v_TexCoord; +out vec4 v_Color; +out vec4 v_OverlayUV; + +void main() { + float scale = BillboardData.x; + float billboardFlag = BillboardData.y; + + vec3 anchor = Anchor + ModelOffset; + vec4 mvPos; + if (billboardFlag < 0.5) { + mvPos = ModelViewMat * vec4(anchor, 1.0); + mvPos.xy += Position.xy * scale; + } else mvPos = ModelViewMat * vec4(anchor + Position * scale, 1.0); + + gl_Position = ProjMat * mvPos; + + v_TexCoord = UV0; + v_Color = Color; + v_OverlayUV = OverlayUV; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_lines.fsh b/src/main/resources/assets/lambda/shaders/core/world_lines.fsh new file mode 100644 index 000000000..4d7919e03 --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_lines.fsh @@ -0,0 +1,113 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +in vec4 v_Color; +in vec3 v_WorldPos; +in vec3 v_ExpandedPos; +in vec3 v_Normal; +in vec2 v_LocalPos; +flat in vec3 v_LineCenter; +in float v_LineWidth; +in float v_WorldPixelSize; +flat in float v_SegmentLength; +flat in float v_IsStart; +flat in vec4 v_Dash; +in float sphericalVertexDistance; +in float cylindricalVertexDistance; + +out vec4 fragColor; + +void main() { + float projLength = v_LocalPos.y; + float perpDist = abs(v_LocalPos.x); + + float dist3D; + if (projLength < 0.0) { + dist3D = length(v_LocalPos); + } else if (projLength > v_SegmentLength) { + dist3D = length(v_LocalPos - vec2(0.0, v_SegmentLength)); + } else { + dist3D = perpDist; + } + + vec2 dL_dx = dFdx(v_LocalPos); + vec2 dL_dy = dFdy(v_LocalPos); + + vec2 localGrad; + if (projLength < 0.0) { + localGrad = normalize(v_LocalPos); + } else if (projLength > v_SegmentLength) { + localGrad = normalize(v_LocalPos - vec2(0.0, v_SegmentLength)); + } else { + localGrad = vec2(sign(v_LocalPos.x), 0.0); + } + + float pixelSize = length(localGrad.x * vec2(dL_dx.x, dL_dy.x) + localGrad.y * vec2(dL_dx.y, dL_dy.y)); + + float perpPixelSize = length(vec2(dL_dx.x, dL_dy.x)); + + float screenLineWidth = v_LineWidth / max(perpPixelSize, 0.0001); + + float minWidth = perpPixelSize; + float effectiveRadius = max(v_LineWidth * 0.5, minWidth * 0.5); + + float alphaScale = min(screenLineWidth, 1.0); + + float sdf = dist3D - effectiveRadius; + + float aaWidth = pixelSize; + float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdf); + + alpha *= alphaScale; + + if (alpha < 0.004) { + discard; + } + + float dashLength = v_Dash.x; + float gapLength = v_Dash.y; + float dashOffset = v_Dash.z; + float animationSpeed = v_Dash.w; + + float longPixelSize = length(vec2(dL_dx.y, dL_dy.y)); + + if (dashLength > 0.0) { + float cycleLength = dashLength + gapLength; + + float animatedOffset = dashOffset; + if (animationSpeed != 0.0) { + animatedOffset -= GameTime * animationSpeed * 1200.0; + } + + float dashPos = projLength + animatedOffset * cycleLength; + float posInCycle = mod(dashPos, cycleLength); + + float dashSdf; + if (posInCycle > dashLength) { + float distToGapEnd = cycleLength - posInCycle; + dashSdf = min(posInCycle - dashLength, distToGapEnd); + } else { + float distToDashEnd = dashLength - posInCycle; + float distFromDashStart = posInCycle; + dashSdf = -min(distToDashEnd, distFromDashStart); + } + + float dashAlpha = 1.0 - smoothstep(-longPixelSize, longPixelSize, dashSdf); + + if (dashAlpha <= 0.0) { + discard; + } + + alpha *= dashAlpha; + } + + vec4 color = v_Color * ColorModulator; + color.a *= alpha; + + fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, + FogEnvironmentalStart, FogEnvironmentalEnd, + FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_lines.vsh b/src/main/resources/assets/lambda/shaders/core/world_lines.vsh new file mode 100644 index 000000000..e609a089e --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_lines.vsh @@ -0,0 +1,99 @@ +#version 330 + +#moj_import +#moj_import +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; +in vec3 Normal; +in float LineWidth; +in vec4 Dash; + +out vec4 v_Color; +out vec2 v_TexCoord; +out vec3 v_WorldPos; +out vec3 v_ExpandedPos; +out vec3 v_Normal; +out vec2 v_LocalPos; +flat out vec3 v_LineCenter; +out float v_LineWidth; +out float v_WorldPixelSize; +flat out float v_SegmentLength; +flat out float v_IsStart; +flat out vec4 v_Dash; +out float sphericalVertexDistance; +out float cylindricalVertexDistance; + +void main() { + int vertexIndex = gl_VertexID % 4; + bool isStart = (vertexIndex < 2); + float side = (vertexIndex == 0 || vertexIndex == 3) ? -1.0 : 1.0; + + float segmentLength = length(Normal); + vec3 lineDir = Normal / segmentLength; + + vec3 pos = Position + ModelOffset; + vec3 lineCenter = isStart ? (pos + Normal * 0.5) : (pos - Normal * 0.5); + + vec3 lineStart = lineCenter - lineDir * (segmentLength * 0.5); + vec3 lineEnd = lineCenter + lineDir * (segmentLength * 0.5); + vec3 thisPoint = isStart ? lineStart : lineEnd; + + mat3 rotationInv = transpose(mat3(ModelViewMat)); + vec3 translation = vec3(ModelViewMat[3]); + vec3 cameraPos = rotationInv * (-translation); + + vec3 toCamera = normalize(cameraPos - thisPoint); + + vec3 perpDir = cross(lineDir, toCamera); + if (length(perpDir) < 0.001) { + perpDir = cross(lineDir, vec3(0.0, 1.0, 0.0)); + if (length(perpDir) < 0.001) { + perpDir = cross(lineDir, vec3(1.0, 0.0, 0.0)); + } + } + perpDir = normalize(perpDir); + + vec4 viewPos = ModelViewMat * vec4(thisPoint, 1.0); + float viewDepth = -viewPos.z; + + float tanHalfFov = 1.0 / ProjMat[1][1]; + + float actualLineWidth; + if (LineWidth < 0.0) { + float screenFraction = -LineWidth; + actualLineWidth = screenFraction * 2.0 * viewDepth * tanHalfFov; + } else actualLineWidth = LineWidth; + + float worldPixelSize = (viewDepth * tanHalfFov * 2.0) / ScreenSize.y; + + float halfWidth = actualLineWidth * 0.5; + float aaPadding = max(halfWidth, worldPixelSize * 3.0); + float halfWidthPadded = halfWidth + aaPadding; + + vec3 perpOffset = perpDir * side * halfWidthPadded; + float longitudinal = isStart ? -1.0 : 1.0; + vec3 longOffset = lineDir * longitudinal * halfWidthPadded; + + vec3 expandedPos = thisPoint + perpOffset + longOffset; + + gl_Position = ProjMat * ModelViewMat * vec4(expandedPos, 1.0); + + v_Color = Color; + v_WorldPos = thisPoint; + v_ExpandedPos = expandedPos; + v_Normal = Normal; + v_LocalPos = vec2(side * halfWidthPadded, (isStart ? -halfWidthPadded : segmentLength + halfWidthPadded)); + v_LineCenter = lineCenter; + v_LineWidth = actualLineWidth; + v_WorldPixelSize = worldPixelSize; + v_SegmentLength = segmentLength; + v_IsStart = isStart ? 1.0 : 0.0; + v_Dash = Dash; + v_TexCoord = vec2(0.0); + + sphericalVertexDistance = fog_spherical_distance(viewPos.xyz); + cylindricalVertexDistance = fog_cylindrical_distance(viewPos.xyz); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_model.fsh b/src/main/resources/assets/lambda/shaders/core/world_model.fsh new file mode 100644 index 000000000..44b43ab3c --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_model.fsh @@ -0,0 +1,83 @@ +#version 330 + +#moj_import + +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; +uniform sampler2D Sampler2; +uniform sampler2D Sampler3; + +layout(std140) uniform GlintTransforms { + mat4 GlintMat2; + vec4 UnusedColor; + vec3 UnusedOffset; + mat4 GlintMat; +}; + +in vec2 v_TexCoord; +in vec4 v_Color; +in vec4 v_OverlayUV; +in vec2 v_LightCoord; +in vec2 v_EdgeData; +in vec3 v_Normal; +in vec3 v_LightDir; +in vec3 v_Light1Dir; + +out vec4 fragColor; + +const float GLINT_ALPHA = 0.75; + +void main() { + float mode = floor(v_OverlayUV.z + 0.5); + bool useOverlay = (mod(mode, 2.0) >= 1.0); + bool useAA = (mod(floor(mode / 2.0), 2.0) >= 1.0); + bool useGlint = (mode >= 4.0); + + vec2 texCoord = v_TexCoord; + if (useAA) { + vec2 texSize = vec2(textureSize(Sampler0, 0)); + vec2 pixels = texCoord * texSize; + vec2 t = fract(pixels - 0.5); + vec2 d = fwidth(pixels); + vec2 sharp_t = clamp((t - 0.5) / d + 0.5, 0.0, 1.0); + texCoord = (floor(pixels - 0.5) + sharp_t + 0.5) / texSize; + } + + vec4 texColor = texture(Sampler0, texCoord); + if (texColor.a < 0.1) discard; + + vec4 baseColor = texColor * v_Color * ColorModulator; + + vec3 litColor = baseColor.rgb; + + if (v_OverlayUV.w >= 0.0) { + vec3 n = normalize(v_Normal); + vec3 light0 = normalize(v_LightDir); + vec3 light1 = normalize(v_Light1Dir); + + float d0 = max(0.0, dot(light0, n)); + float d1 = max(0.0, dot(light1, n)); + + float diffuse = min(1.0, (d0 + d1) * 0.6 + 0.4); + + float lightFactor = mix(1.0, diffuse, v_OverlayUV.w); + litColor *= lightFactor; + } else { + litColor *= texture(Sampler2, v_LightCoord).rgb; + } + + if (useOverlay) { + vec4 overlayColor = texture(Sampler1, v_OverlayUV.xy); + litColor = mix(litColor, overlayColor.rgb, overlayColor.a); + } + + if (useGlint) { + vec2 transformedUV = (GlintMat * vec4(v_TexCoord, 0.0, 1.0)).xy; + vec3 glint = texture(Sampler3, fract(transformedUV)).rgb; + + vec3 layer = glint * GLINT_ALPHA; + litColor += (layer * layer); + } + + fragColor = vec4(litColor, baseColor.a); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_model.vsh b/src/main/resources/assets/lambda/shaders/core/world_model.vsh new file mode 100644 index 000000000..bac55a22f --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_model.vsh @@ -0,0 +1,38 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +in vec3 Position; +in vec4 Color; +in vec2 UV0; +in vec4 OverlayUV; +in ivec2 UV2; +in vec3 LightDir; +in vec3 Light1Dir; +in vec3 Normal; +in vec2 EdgeData; + +out vec2 v_TexCoord; +out vec4 v_Color; +out vec4 v_OverlayUV; +out vec2 v_LightCoord; +out vec2 v_EdgeData; +out vec3 v_Normal; +out vec3 v_LightDir; +out vec3 v_Light1Dir; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position + ModelOffset, 1.0); + + v_Color = Color; + v_TexCoord = UV0; + v_OverlayUV = OverlayUV; + v_LightCoord = (vec2(UV2) + 0.5) / 256.0; + v_EdgeData = EdgeData; + + v_Normal = Normal; + v_LightDir = LightDir; + v_Light1Dir = Light1Dir; +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh new file mode 100644 index 000000000..28ca3655a --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.fsh @@ -0,0 +1,61 @@ +#version 330 +#moj_import +#moj_import +#moj_import + +uniform sampler2D Sampler0; + +in vec2 v_TexCoord; +in vec4 v_Color; +in float sphericalVertexDistance; +in float cylindricalVertexDistance; +in vec4 sdfStyleParams; +flat in int v_LayerType; + +out vec4 fragColor; + +void main() { + float OutlineWidth = sdfStyleParams.x; + float GlowRadius = sdfStyleParams.y; + float ShadowSoftness = sdfStyleParams.z; + float SDFThreshold = sdfStyleParams.w; + + vec4 texSample = texture(Sampler0, v_TexCoord); + float sdfValue = texSample.a; + + float smoothing = fwidth(sdfValue) * 0.5; + + int layerType = v_LayerType; + + float alpha; + + if (layerType == 3) { + alpha = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + } else if (layerType == 2) { + float outlineEdge = SDFThreshold - OutlineWidth; + alpha = smoothstep(outlineEdge - smoothing, outlineEdge + smoothing, sdfValue); + float textMask = smoothstep(SDFThreshold - smoothing, SDFThreshold + smoothing, sdfValue); + alpha = alpha * (1.0 - textMask); + } else if (layerType == 1) { + float glowEdge = (OutlineWidth > 0.001) ? (SDFThreshold - OutlineWidth) : SDFThreshold; + float glowStart = glowEdge - GlowRadius; + float glowEnd = glowEdge; + alpha = smoothstep(glowStart, glowEnd, sdfValue) * 0.6; + float outlineMask = smoothstep(glowEdge - smoothing, glowEdge + smoothing, sdfValue); + alpha = alpha * (1.0 - outlineMask); + } else { + float shadowStart = SDFThreshold - ShadowSoftness - 0.15; + float shadowEnd = SDFThreshold - 0.1; + alpha = smoothstep(shadowStart, shadowEnd, sdfValue) * 0.5; + } + + vec4 result = vec4(v_Color.rgb, alpha * v_Color.a); + + if (result.a <= 0.005) discard; + + result *= ColorModulator; + + fragColor = apply_fog(result, sphericalVertexDistance, cylindricalVertexDistance, + FogEnvironmentalStart, FogEnvironmentalEnd, + FogRenderDistanceStart, FogRenderDistanceEnd, FogColor); +} diff --git a/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh new file mode 100644 index 000000000..a2e84b10a --- /dev/null +++ b/src/main/resources/assets/lambda/shaders/core/world_sdf_text.vsh @@ -0,0 +1,57 @@ +#version 330 + +#moj_import +#moj_import +#moj_import + +in vec3 Position; +in vec2 UV0; +in vec4 Color; +in vec3 Anchor; +in vec2 BillboardData; +in vec4 SDFStyle; + +out vec2 v_TexCoord; +out vec4 v_Color; +out float sphericalVertexDistance; +out float cylindricalVertexDistance; +flat out int v_LayerType; +out vec4 sdfStyleParams; + +void main() { + float scale = BillboardData.x; + float billboardFlag = BillboardData.y; + + vec3 worldPos; + vec3 anchor = Anchor + ModelOffset; + + if (billboardFlag == 0.0) { + vec3 right = vec3(ModelViewMat[0][0], ModelViewMat[1][0], ModelViewMat[2][0]); + vec3 up = vec3(ModelViewMat[0][1], ModelViewMat[1][1], ModelViewMat[2][1]); + + float scaledX = Position.x * scale; + float scaledY = Position.y * -scale; + + worldPos = anchor + right * scaledX + up * scaledY; + } else worldPos = anchor + Position * scale; + + vec4 viewPos = ModelViewMat * vec4(worldPos, 1.0); + gl_Position = ProjMat * viewPos; + + int layerType = int(Position.z + 0.5); + v_LayerType = layerType; + float layerOffset; + if (layerType == 3) layerOffset = 0.0004; + else if (layerType == 2) layerOffset = 0.0003; + else if (layerType == 1) layerOffset = 0.0002; + else layerOffset = 0.0001; + + gl_Position.z -= layerOffset * gl_Position.w; + + v_TexCoord = UV0; + v_Color = Color; + sdfStyleParams = SDFStyle; + + sphericalVertexDistance = fog_spherical_distance(viewPos.xyz); + cylindricalVertexDistance = fog_cylindrical_distance(viewPos.xyz); +} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/font.glsl b/src/main/resources/assets/lambda/shaders/fragment/font.glsl deleted file mode 100644 index afb98a7ff..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/font.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 420 - -in vec2 v_TexCoord; -in vec4 v_Color; - -out vec4 color; - -float sdf(float channel, float min, float max) { - return 1.0 - smoothstep(min, max, 1.0 - channel); -} - -void main() -{ - bool isEmoji = v_TexCoord.x < 0.0; - - if (isEmoji) { - vec4 c = texture(u_EmojiTexture, -v_TexCoord); - color = vec4(c.rgb, sdf(c.a, u_SDFMin, u_SDFMax)) * v_Color; - return; - } - - float sdf = sdf(texture(u_FontTexture, v_TexCoord).r, u_SDFMin, u_SDFMax); - color = vec4(1.0, 1.0, 1.0, sdf) * v_Color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/particles.glsl b/src/main/resources/assets/lambda/shaders/fragment/particles.glsl deleted file mode 100644 index 967ef6a91..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/particles.glsl +++ /dev/null @@ -1,12 +0,0 @@ -#version 330 core - -in vec2 v_TexCoord; -in vec4 v_Color; - -out vec4 color; - -void main() -{ - float a = 1.0 - length(v_TexCoord - 0.5) * 2.0; - color = v_Color * vec4(1.0, 1.0, 1.0, a); -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl deleted file mode 100644 index 2eb6b4242..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/pos_color.glsl +++ /dev/null @@ -1,9 +0,0 @@ -#version 420 - -in vec4 v_Color; -out vec4 color; - -void main() -{ - color = v_Color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl deleted file mode 100644 index efac6ba5c..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/pos_tex.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 core - -in vec2 v_TexCoord; -out vec4 color; - -uniform sampler2D u_Texture; - -void main() -{ - color = texture(u_Texture, v_TexCoord); -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl b/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl deleted file mode 100644 index 09f3f6b96..000000000 --- a/src/main/resources/assets/lambda/shaders/fragment/pos_tex_color.glsl +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 core - -in vec2 v_TexCoord; -in vec4 v_Color; - -out vec4 color; - -uniform sampler2D u_Texture; - -void main() -{ - color = texture(u_Texture, v_TexCoord) * v_Color -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/post/sdf.glsl b/src/main/resources/assets/lambda/shaders/post/sdf.glsl deleted file mode 100644 index fa3566da6..000000000 --- a/src/main/resources/assets/lambda/shaders/post/sdf.glsl +++ /dev/null @@ -1,34 +0,0 @@ -attributes { - vec4 pos; - vec2 uv; -}; - -uniforms { - sampler2D u_Texture; # fragment - vec2 u_TexelSize; # fragment -}; - -export { - vec2 v_TexCoord; # uv -}; - -#define SPREAD 4 - -void fragment() { - vec4 colors = vec4(0.0); - vec4 blurWeight = vec4(0.0); - - for (int x = -SPREAD; x <= SPREAD; ++x) { - for (int y = -SPREAD; y <= SPREAD; ++y) { - vec2 offset = vec2(x, y) * u_TexelSize; - - vec4 color = texture(u_Texture, v_TexCoord + offset); - vec4 weight = exp(-color * color); - - colors += color * weight; - blurWeight += weight; - } - } - - color = colors / blurWeight; -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/shared/hsb.glsl b/src/main/resources/assets/lambda/shaders/shared/hsb.glsl deleted file mode 100644 index 25b9eb99d..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/hsb.glsl +++ /dev/null @@ -1,30 +0,0 @@ -vec3 hsb2rgb(vec3 hsb) { - float C = hsb.z * hsb.y; - float X = C * (1.0 - abs(mod(hsb.x / 60.0, 2.0) - 1.0)); - float m = hsb.z - C; - - vec3 rgb; - - if (0.0 <= hsb.x && hsb.x < 60.0) { - rgb = vec3(C, X, 0.0); - } else if (60.0 <= hsb.x && hsb.x < 120.0) { - rgb = vec3(X, C, 0.0); - } else if (120.0 <= hsb.x && hsb.x < 180.0) { - rgb = vec3(0.0, C, X); - } else if (180.0 <= hsb.x && hsb.x < 240.0) { - rgb = vec3(0.0, X, C); - } else if (240.0 <= hsb.x && hsb.x < 300.0) { - rgb = vec3(X, 0.0, C); - } else { - rgb = vec3(C, 0.0, X); - } - - return (rgb + vec3(m)); -}# - -float hue(vec2 uv) { - vec2 centered = uv * 2.0 - 1.0; - float hue = degrees(atan(centered.y, centered.x)) + 180.0; - - return hue; -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/shared/rect.glsl b/src/main/resources/assets/lambda/shaders/shared/rect.glsl deleted file mode 100644 index 4fe1b2220..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/rect.glsl +++ /dev/null @@ -1,45 +0,0 @@ -attributes { - vec4 pos; - vec2 uv; - vec4 color; -}; - -uniforms { - vec2 u_Size; # fragment - - float u_RoundLeftTop; # fragment - float u_RoundLeftBottom; # fragment - float u_RoundRightBottom; # fragment - float u_RoundRightTop; # fragment -}; - -export { - vec2 v_TexCoord; # uv - vec4 v_Color; # color -}; - -#include "shade" - -#define NOISE_GRANULARITY 0.004 -#define SMOOTHING 0.3 - -#define noise getNoise() - -vec4 getNoise() { - // https://shader-tutorial.dev/advanced/color-banding-dithering/ - float random = fract(sin(dot(v_TexCoord, vec2(12.9898, 78.233))) * 43758.5453); - float ofs = mix(-NOISE_GRANULARITY, NOISE_GRANULARITY, random); - return vec4(ofs, ofs, ofs, 0.0); -}# - -float signedDistance(in vec4 r) { - r.xy = (v_TexCoord.x > 0.5) ? r.xy : r.zw; - r.x = (v_TexCoord.y > 0.5) ? r.x : r.y; - - vec2 q = u_Size * (abs(v_TexCoord - 0.5) - 0.5) + r.x; - return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; -}# - -float signedDistance() { - return signedDistance(vec4(u_RoundRightBottom, u_RoundRightTop, u_RoundLeftBottom, u_RoundLeftTop)); -}# diff --git a/src/main/resources/assets/lambda/shaders/shared/sdf.glsl b/src/main/resources/assets/lambda/shaders/shared/sdf.glsl deleted file mode 100644 index 165043f1c..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/sdf.glsl +++ /dev/null @@ -1,3 +0,0 @@ -float sdf(float channel, float min, float max) { - return 1.0 - smoothstep(min, max, 1.0 - channel); -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/shared/shade.glsl b/src/main/resources/assets/lambda/shaders/shared/shade.glsl deleted file mode 100644 index 9371edc22..000000000 --- a/src/main/resources/assets/lambda/shaders/shared/shade.glsl +++ /dev/null @@ -1,22 +0,0 @@ -uniforms { - float u_Shade; # fragment - float u_ShadeTime; # fragment - vec4 u_ShadeColor1; # fragment - vec4 u_ShadeColor2; # fragment - vec2 u_ShadeSize; # fragment -}; - -export { - vec2 v_Position; # gl_Position.xy * 0.5 + 0.5 -}; - -#define shade getShadeColor() - -vec4 getShadeColor() { - if (u_Shade != 1.0) return vec4(1.0); - - vec2 pos = v_Position * u_ShadeSize; - float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5; - - return mix(u_ShadeColor1, u_ShadeColor2, p); -}# \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl b/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl deleted file mode 100644 index aad9c7385..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/box_dynamic.glsl +++ /dev/null @@ -1,17 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos1; -layout (location = 1) in vec3 pos2; -layout (location = 2) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; -uniform float u_TickDelta; - -void main() -{ - gl_Position = u_ProjModel * u_View * vec4(mix(pos1, pos2, u_TickDelta), 1.0); - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl b/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl deleted file mode 100644 index ba73727da..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/box_static.glsl +++ /dev/null @@ -1,15 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; - -void main() -{ - gl_Position = u_ProjModel * u_View * vec4(pos, 1.0); - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/font.glsl b/src/main/resources/assets/lambda/shaders/vertex/font.glsl deleted file mode 100644 index e8a0f3b92..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/font.glsl +++ /dev/null @@ -1,19 +0,0 @@ -#version 330 core - -layout (location = 0) in vec4 pos; -layout (location = 1) in vec2 uv; -layout (location = 2) in vec4 color; // Does this fuck the padding ? - -out vec2 v_TexCoord; -out vec4 v_Color; - -uniform sampler2D u_FontTexture; -uniform sampler2D u_EmojiTexture; -uniform float u_SDFMin; -uniform float u_SDFMin; - -void main() -{ - v_TexCoord = uv; - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/particles.glsl b/src/main/resources/assets/lambda/shaders/vertex/particles.glsl deleted file mode 100644 index 08e0e0a43..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/particles.glsl +++ /dev/null @@ -1,19 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec2 uv; -layout (location = 2) in vec4 color; - -out vec2 v_TexCoord; -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; - -void main() -{ - gl_Position = u_ProjModel * u_View * vec4(pos, 1.0); - - v_TexCoord = uv; - v_Color = color; -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl b/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl deleted file mode 100644 index 2d7f171e5..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/tracer_dynamic.glsl +++ /dev/null @@ -1,20 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos1; -layout (location = 1) in vec2 pos1; -layout (location = 2) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; -uniform float u_TickDelta; - -void main() -{ - if (l_VertexID % 2 != 0) - return; - - vec3 VERTEX_POSITION = mix(pos1, pos2, u_TickDelta); - gl_Position = u_ProjModel * u_View * vec4(VERTEX_POSITION, 1.0); -} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl b/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl deleted file mode 100644 index 1d111b6ca..000000000 --- a/src/main/resources/assets/lambda/shaders/vertex/tracer_static.glsl +++ /dev/null @@ -1,17 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 pos; -layout (location = 1) in vec4 color; - -out vec4 v_Color; - -uniform mat4 u_ProjModel; -uniform mat4 u_View; - -void main() -{ - if (gl_VertexID % 2 != 0) - return; - - gl_Position = u_ProjModel * u_View * vec4(pos, 1.0); -} \ No newline at end of file diff --git a/src/main/resources/assets/minecraft/shaders/include/dynamictransforms.glsl b/src/main/resources/assets/minecraft/shaders/include/dynamictransforms.glsl new file mode 100644 index 000000000..1b804e9d8 --- /dev/null +++ b/src/main/resources/assets/minecraft/shaders/include/dynamictransforms.glsl @@ -0,0 +1,6 @@ +layout(std140) uniform DynamicTransforms { + mat4 ModelViewMat; + vec4 ColorModulator; + vec3 ModelOffset; + mat4 TextureMat; +}; diff --git a/src/main/resources/lambda.accesswidener b/src/main/resources/lambda.accesswidener index 484d80718..3df98906e 100644 --- a/src/main/resources/lambda.accesswidener +++ b/src/main/resources/lambda.accesswidener @@ -33,6 +33,7 @@ transitive-accessible field net/minecraft/entity/Entity GLIDING_FLAG_INDEX I transitive-accessible method net/minecraft/entity/Entity getFlag (I)Z transitive-accessible method net/minecraft/block/ChestBlock getChestType (Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/Direction;)Lnet/minecraft/block/enums/ChestType; transitive-accessible method net/minecraft/block/ChestBlock getNeighborChestDirection (Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/Direction;)Lnet/minecraft/util/math/Direction; +accessible field net/minecraft/client/world/ClientChunkManager$ClientChunkMap loadedChunkCount I # Entity transitive-accessible field net/minecraft/entity/projectile/FireworkRocketEntity shooter Lnet/minecraft/entity/LivingEntity; @@ -68,9 +69,33 @@ transitive-accessible field net/minecraft/client/render/Camera pos Lnet/minecraf # Renderer transitive-accessible field net/minecraft/client/texture/NativeImage pointer J +transitive-accessible field net/minecraft/client/texture/SpriteContents image Lnet/minecraft/client/texture/NativeImage; transitive-accessible class net/minecraft/client/gui/screen/SplashOverlay$LogoTexture transitive-accessible field com/mojang/blaze3d/systems/RenderSystem$ShapeIndexBuffer indexBuffer Lcom/mojang/blaze3d/buffers/GpuBuffer; transitive-accessible field net/minecraft/client/gl/GlGpuBuffer id I +accessible field net/minecraft/client/MinecraftClient entityRenderManager Lnet/minecraft/client/render/entity/EntityRenderManager; +transitive-accessible field net/minecraft/client/render/RenderLayer renderSetup Lnet/minecraft/client/render/RenderSetup; +transitive-accessible field net/minecraft/client/render/RenderSetup layeringTransform Lnet/minecraft/client/render/LayeringTransform; +transitive-accessible field net/minecraft/client/render/LayeringTransform transform Ljava/util/function/Consumer; +transitive-accessible method net/minecraft/client/render/entity/ProjectileEntityRenderer getTexture (Lnet/minecraft/client/render/entity/state/ProjectileEntityRenderState;)Lnet/minecraft/util/Identifier; +transitive-accessible field net/minecraft/client/render/entity/BoatEntityRenderer texture Lnet/minecraft/util/Identifier; +transitive-accessible field net/minecraft/client/render/entity/EndCrystalEntityRenderer TEXTURE Lnet/minecraft/util/Identifier; +transitive-accessible method net/minecraft/client/render/BufferBuilder beginElement (Lcom/mojang/blaze3d/vertex/VertexFormatElement;)J +transitive-accessible field net/minecraft/client/render/BufferBuilder offsetsByElementId [I +transitive-accessible field net/minecraft/client/render/BufferBuilder vertexPointer J +transitive-accessible field net/minecraft/client/render/BufferBuilder currentMask I +transitive-accessible field net/minecraft/client/render/item/ItemRenderState layers [Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState layerCount I +transitive-accessible field net/minecraft/client/render/item/ItemRenderState displayContext Lnet/minecraft/item/ItemDisplayContext; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState quads Ljava/util/List; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState useLight Z +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState particle Lnet/minecraft/client/texture/Sprite; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState transform Lnet/minecraft/client/render/model/json/Transformation; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState tints [I +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState glint Lnet/minecraft/client/render/item/ItemRenderState$Glint; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState specialModelType Lnet/minecraft/client/render/item/model/special/SpecialModelRenderer; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState renderLayer Lnet/minecraft/client/render/RenderLayer; +transitive-accessible field net/minecraft/client/render/item/ItemRenderState$LayerRenderState data Ljava/lang/Object; # Text transitive-accessible field net/minecraft/text/Style color Lnet/minecraft/text/TextColor; diff --git a/src/main/resources/lambda.mixins.json b/src/main/resources/lambda.mixins.json index 0326b21f6..6e57947ca 100644 --- a/src/main/resources/lambda.mixins.json +++ b/src/main/resources/lambda.mixins.json @@ -1,98 +1,98 @@ { - "required": true, - "minVersion": "0.8", - "package": "com.lambda.mixin", - "compatibilityLevel": "JAVA_21", - "client": [ - "CrashReportMixin", - "MinecraftClientMixin", - "baritone.BaritonePlayerContextMixin", - "baritone.LookBehaviourMixin", - "client.sound.SoundSystemMixin", - "entity.ClientPlayerEntityMixin", - "entity.ClientPlayInteractionManagerMixin", - "entity.EntityMixin", - "entity.FireworkRocketEntityMixin", - "entity.HandledScreensMixin", - "entity.LivingEntityMixin", - "entity.PlayerEntityMixin", - "entity.PlayerInventoryMixin", - "input.KeyBindingMixin", - "input.KeyboardMixin", - "input.MouseMixin", - "items.BarrierBlockMixin", - "items.BlockItemMixin", - "items.FilledMapItemMixin", - "network.ClientConnectionMixin", - "network.ClientLoginNetworkMixin", - "network.ClientPlayNetworkHandlerMixin", - "network.HandshakeC2SPacketMixin", - "network.LoginHelloC2SPacketMixin", - "network.LoginKeyC2SPacketMixin", - "render.AbstractTerrainRenderContextMixin", - "render.ArmorFeatureRendererMixin", - "render.BlockMixin", - "render.BlockModelRendererMixin", - "render.BlockRenderManagerMixin", - "render.BossBarHudMixin", - "render.CameraMixin", - "render.CapeFeatureRendererMixin", - "render.ChatHudMixin", - "render.ChatInputSuggestorMixin", - "render.ChatScreenMixin", - "render.ChunkOcclusionDataBuilderMixin", - "render.DrawContextMixin", - "render.ElytraFeatureRendererMixin", - "render.EntityRendererMixin", - "render.FluidRendererMixin", - "render.FogRendererMixin", - "render.GameRendererMixin", - "render.GlStateManagerMixin", - "render.HandledScreenMixin", - "render.HeadFeatureRendererMixin", - "render.HeldItemRendererMixin", - "render.InGameHudMixin", - "render.InGameOverlayRendererMixin", - "render.LightmapTextureManagerMixin", - "render.LivingEntityRendererMixin", - "render.PlayerListHudMixin", - "render.RenderLayersMixin", - "render.ScreenHandlerMixin", - "render.ScreenMixin", - "render.SodiumBlockOcclusionCacheMixin", - "render.SodiumBlockRendererMixin", - "render.SodiumFluidRendererImplMixin", - "render.SodiumLightDataAccessMixin", - "render.SodiumWorldRendererMixin", - "render.SplashOverlayMixin", - "render.SplashOverlayMixin$LogoTextureMixin", - "render.StatusEffectFogModifierMixin", - "render.TooltipComponentMixin", - "render.WeatherRenderingMixin", - "render.WorldBorderRenderingMixin", - "render.WorldRendererMixin", - "render.blockentity.AbstractSignBlockEntityRendererMixin", - "render.blockentity.BeaconBlockEntityRendererMixin", - "render.blockentity.BlockEntityRenderDispatcherMixin", - "render.blockentity.EnchantingTableBlockEntityRendererMixin", - "render.blockentity.MobSpawnerBlockEntityRendererMixin", - "render.particle.BillboardParticleMixin", - "render.particle.ElderGuardianParticleRendererMixin", - "render.particle.ItemPickupParticleRendererMixin", - "world.AbstractBlockMixin", - "world.BlockCollisionSpliteratorMixin", - "world.ClientChunkManagerMixin", - "world.ClientWorldMixin", - "world.DirectionMixin", - "world.StructureTemplateMixin", - "world.WorldMixin" - ], - "injectors": { - "defaultRequire": 0 - }, - "overwrites": { - "conformVisibility": true - }, - "mixinextras": { - "minVersion": "0.5.0"} -} + "required": true, + "minVersion": "0.8", + "package": "com.lambda.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CrashReportMixin", + "MinecraftClientMixin", + "baritone.BaritonePlayerContextMixin", + "baritone.LookBehaviourMixin", + "client.sound.SoundSystemMixin", + "entity.ClientPlayerEntityMixin", + "entity.ClientPlayInteractionManagerMixin", + "entity.EntityMixin", + "entity.FireworkRocketEntityMixin", + "entity.HandledScreensMixin", + "entity.LivingEntityMixin", + "entity.PlayerEntityMixin", + "entity.PlayerInventoryMixin", + "input.KeyBindingMixin", + "input.KeyboardMixin", + "input.MouseMixin", + "items.BlockItemMixin", + "items.FilledMapItemMixin", + "network.ClientConnectionMixin", + "network.ClientLoginNetworkMixin", + "network.ClientPlayNetworkHandlerMixin", + "network.HandshakeC2SPacketMixin", + "network.LoginHelloC2SPacketMixin", + "network.LoginKeyC2SPacketMixin", + "render.AbstractTerrainRenderContextMixin", + "render.ArmorFeatureRendererMixin", + "render.BlockMixin", + "render.BlockModelRendererMixin", + "render.BossBarHudMixin", + "render.CameraMixin", + "render.CapeFeatureRendererMixin", + "render.ChatHudMixin", + "render.ChatInputSuggestorMixin", + "render.ChatScreenMixin", + "render.ChunkOcclusionDataBuilderMixin", + "render.DrawContextMixin", + "render.ElytraFeatureRendererMixin", + "render.EntityRendererMixin", + "render.EntityRenderManagerMixin", + "render.EntityRenderStateMixin", + "render.FluidRendererMixin", + "render.FogRendererMixin", + "render.GameRendererMixin", + "render.HandledScreenMixin", + "render.HeadFeatureRendererMixin", + "render.HeldItemRendererMixin", + "render.InGameHudMixin", + "render.InGameOverlayRendererMixin", + "render.LightmapTextureManagerMixin", + "render.LivingEntityRendererMixin", + "render.PlayerListHudMixin", + "render.RenderLayersMixin", + "render.ScreenHandlerMixin", + "render.ScreenMixin", + "render.SodiumBlockOcclusionCacheMixin", + "render.SodiumBlockRendererMixin", + "render.SodiumFluidRendererImplMixin", + "render.SodiumLightDataAccessMixin", + "render.SodiumWorldRendererMixin", + "render.SplashOverlayMixin", + "render.SplashOverlayMixin$LogoTextureMixin", + "render.StatusEffectFogModifierMixin", + "render.TooltipComponentMixin", + "render.WeatherRenderingMixin", + "render.WorldBorderRenderingMixin", + "render.WorldRendererMixin", + "render.blockentity.AbstractSignBlockEntityRendererMixin", + "render.blockentity.BeaconBlockEntityRendererMixin", + "render.blockentity.BlockEntityRenderDispatcherMixin", + "render.blockentity.EnchantingTableBlockEntityRendererMixin", + "render.blockentity.MobSpawnerBlockEntityRendererMixin", + "render.particle.BillboardParticleMixin", + "render.particle.ElderGuardianParticleRendererMixin", + "render.particle.ItemPickupParticleRendererMixin", + "world.AbstractBlockMixin", + "world.BlockCollisionSpliteratorMixin", + "world.ClientChunkManagerMixin", + "world.ClientWorldMixin", + "world.DirectionMixin", + "world.StructureTemplateMixin", + "world.WorldMixin" + ], + "injectors": { + "defaultRequire": 0 + }, + "overwrites": { + "conformVisibility": true + }, + "mixinextras": { + "minVersion": "0.5.0" + } +} \ No newline at end of file