Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private <S extends BlockEntityRenderState> void wrapRenderQueue(BlockEntityRende
Operation<Void> original) {
BlockPos pos = renderState.pos;

if (pos != null && OutlineManager.INSTANCE.shouldCapture(pos)) {
if (pos != null && OutlineManager.shouldCapture(pos)) {
VertexCapture.INSTANCE.beginCapture(pos);

boolean outlineOnly = !Vec3d.ofCenter(pos).isInRange(cameraState.pos, renderer.getRenderDistance());
Expand Down
48 changes: 14 additions & 34 deletions src/main/java/com/lambda/mixin/world/ClientChunkManagerMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,70 +19,50 @@

import com.lambda.event.EventFlow;
import com.lambda.event.events.WorldEvent;
import com.lambda.module.modules.render.LightLevels;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.world.ClientChunkManager;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.packet.s2c.play.ChunkData;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.world.Heightmap;
import net.minecraft.world.LightType;
import net.minecraft.world.chunk.WorldChunk;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
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;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;

import java.util.Map;
import java.util.function.Consumer;

@Mixin(ClientChunkManager.class)
public class ClientChunkManagerMixin {
@Final
@Shadow
ClientWorld world;

@Inject(method = "loadChunkFromPacket", at = @At("TAIL"))
private void onChunkLoad(
int x, int z, PacketByteBuf buf, Map<Heightmap.Type, long[]> heightmaps, Consumer<ChunkData.BlockEntityVisitor> consumer, CallbackInfoReturnable<WorldChunk> cir
) {
EventFlow.post(new WorldEvent.ChunkEvent.Load(cir.getReturnValue()));
}

@Inject(method = "loadChunkFromPacket", at = @At(value = "NEW", target = "net/minecraft/world/chunk/WorldChunk", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD)
private void onChunkUnload(int x, int z, PacketByteBuf buf, Map<Heightmap.Type, long[]> heightmaps, Consumer<ChunkData.BlockEntityVisitor> consumer, CallbackInfoReturnable<WorldChunk> cir, int i, WorldChunk chunk, ChunkPos chunkPos) {
@Inject(method = "loadChunkFromPacket", at = @At(value = "NEW", target = "net/minecraft/world/chunk/WorldChunk", shift = At.Shift.BEFORE))
private void onChunkUnload(int x, int z, PacketByteBuf buf, Map<Heightmap.Type, long[]> heightmaps, Consumer<ChunkData.BlockEntityVisitor> consumer, CallbackInfoReturnable<WorldChunk> cir, @Local WorldChunk chunk) {
if (chunk != null) {
EventFlow.post(new WorldEvent.ChunkEvent.Unload(chunk));
}
}

@Inject(method = "unload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientChunkManager$ClientChunkMap;unloadChunk(ILnet/minecraft/world/chunk/WorldChunk;)V"), locals = LocalCapture.CAPTURE_FAILHARD)
private void onChunkUnload(ChunkPos pos, CallbackInfo ci, int i, WorldChunk chunk) {
@Inject(method = "unload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientChunkManager$ClientChunkMap;unloadChunk(ILnet/minecraft/world/chunk/WorldChunk;)V"))
private void onChunkUnload(ChunkPos pos, CallbackInfo ci, @Local WorldChunk chunk) {
EventFlow.post(new WorldEvent.ChunkEvent.Unload(chunk));
}

// @Inject(
// method = "updateLoadDistance",
// at = @At(
// value = "INVOKE",
// target = "net/minecraft/client/world/ClientChunkManager$ClientChunkMap.isInRadius(II)Z"
// ),
// locals = LocalCapture.CAPTURE_FAILHARD
// )
// private void onUpdateLoadDistance(
// int loadDistance,
// CallbackInfo ci,
// int oldRadius,
// int newRadius,
// ClientChunkManager.ClientChunkMap clientChunkMap,
// int k,
// WorldChunk oldChunk,
// ChunkPos chunkPos
// ) {
// if (!clientChunkMap.isInRadius(chunkPos.x, chunkPos.z)) {
// EventFlow.post(new WorldEvent.ChunkEvent.Unload(this.world, oldChunk));
// }
// }
@Inject(method = "onLightUpdate", at = @At("RETURN"))
private void injectOnLightUpdate(LightType type, ChunkSectionPos pos, CallbackInfo ci) {
if (LightLevels.INSTANCE.isEnabled()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make an event?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its a pretty niche mixin; doubt any other modules would need it. If the time comes we can always change it

LightLevels.updateChunk(pos.getX(), pos.getZ());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package com.lambda.config.groups

import com.lambda.module.modules.render.ESP.Group
import java.awt.Color

interface EntityColorsConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class WorldLineSettings(

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) { 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()
val screenWidthSetting by c.setting("${prefix}Screen Width", 20, 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
Expand Down
12 changes: 6 additions & 6 deletions src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -381,17 +381,17 @@ class RegionRenderer {
fun hasScreenData(): Boolean = hasScreenData

fun clearData() {
faceVertexBuffer = null
edgeVertexBuffer = null
textVertexBuffer = null
faceVertexBuffer?.close(); faceVertexBuffer = null
edgeVertexBuffer?.close(); edgeVertexBuffer = null
textVertexBuffer?.close(); textVertexBuffer = null
faceIndexCount = 0
edgeIndexCount = 0
textIndexCount = 0
hasWorldData = false

screenFaceVertexBuffer = null
screenEdgeVertexBuffer = null
screenTextVertexBuffer = null
screenFaceVertexBuffer?.close(); screenFaceVertexBuffer = null
screenEdgeVertexBuffer?.close(); screenEdgeVertexBuffer = null
screenTextVertexBuffer?.close(); screenTextVertexBuffer = null
screenFaceIndexCount = 0
screenEdgeIndexCount = 0
screenTextIndexCount = 0
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class RenderBuilder(private val cameraPos: Vec3d, var depthTest: Boolean = false
builder: (BoxBuilder.() -> Unit)? = null
) = boxes(pos, safeContext.blockState(pos), lineConfig, builder)

fun filledQuadGradient(
fun filledQuad(
corner1: Vec3d,
corner2: Vec3d,
corner3: Vec3d,
Expand Down Expand Up @@ -250,10 +250,10 @@ class RenderBuilder(private val cameraPos: Vec3d, var depthTest: Boolean = false
fun circleLine(
center: Vec3d,
radius: Double,
normal: Vec3d = Vec3d(0.0, 1.0, 0.0),
color: Color,
segments: Int = 32,
width: Float = -0.0005f,
normal: Vec3d = Vec3d(0.0, 1.0, 0.0),
segments: Int = 32,
dashStyle: LineDashStyle? = null
) {
val up =
Expand Down
20 changes: 14 additions & 6 deletions src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ChunkedRenderer(
owner: Any,
name: String,
depthTest: SafeContext.() -> Boolean,
pauseUpdates: SafeContext.() -> Boolean,
private val update: RenderBuilder.(ClientWorld, FastVector) -> Unit
) : AbstractRenderer(name, depthTest) {
private val chunkMap = ConcurrentHashMap<Long, ChunkData>()
Expand Down Expand Up @@ -82,9 +83,10 @@ class ChunkedRenderer(
owner.listen<WorldEvent.Player.Leave> { rebuild() }

owner.listenConcurrently<TickEvent.Pre> {
val depth = depthTest()
if (pauseUpdates()) return@listenConcurrently
val queueSize = rebuildQueue.size
val polls = minOf(StyleEditor.rebuildsPerTick, queueSize)
val depth = depthTest()
repeat(polls) { rebuildQueue.poll()?.rebuild(depth) }
}

Expand All @@ -100,13 +102,18 @@ class ChunkedRenderer(
private fun getChunkKey(chunkX: Int, chunkZ: Int) =
(chunkX.toLong() and 0xFFFFFFFFL) or ((chunkZ.toLong() and 0xFFFFFFFFL) shl 32)

context(safeContext: SafeContext)
fun rebuildChunk(x: Int, z: Int) {
safeContext.world.getChunk(x, z)?.chunkData?.markDirty()
}

fun rebuild() {
rebuildQueue.clear()
mc.world?.chunkManager?.chunks?.let { chunks ->
val chunkCount = chunks.loadedChunkCount
(0..chunkCount).forEach { index ->
val chunkCount = chunks.chunks.length()
(0 until chunkCount).forEach { index ->
val chunk = chunks.chunks.get(index) ?: return@forEach
chunkMap.putIfAbsent(chunk.chunkKey, chunk.chunkData)
chunkMap.putIfAbsent(chunk.chunkKey, ChunkData(chunk))
}
}
rebuildQueue.addAll(chunkMap.values)
Expand Down Expand Up @@ -182,11 +189,12 @@ class ChunkedRenderer(
fun Any.chunkedRenderer(
name: String,
depthTest: SafeContext.() -> Boolean = { false },
pauseUpdates: SafeContext.() -> Boolean = { false },
update: RenderBuilder.(ClientWorld, FastVector) -> Unit
) = ChunkedRenderer(this, name, depthTest, update).also { renderer ->
) = ChunkedRenderer(this, name, depthTest, pauseUpdates, update).also { renderer ->
(this as? Module)?.let { module ->
module.onEnable { renderer.rebuild() }
module.onDisable { renderer.rebuild() }
module.onDisable { renderer.clear() }
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/com/lambda/module/modules/render/ESP.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ object ESP : Module(
{ listOf(it.interpolatedBox) }
)
}
val chunkMap = world.chunkManager.chunks
(0 until chunkMap.loadedChunkCount).forEach { chunk ->
chunkMap.chunks.get(chunk)?.blockEntities?.values?.forEach { blockEntity ->
val chunks = world.chunkManager.chunks.chunks
(0 until chunks.length()).forEach { chunk ->
chunks.get(chunk)?.blockEntities?.values?.forEach { blockEntity ->
if (!entitySettings.isSelected(blockEntity)) return@forEach
val color = entityColors.getColor(blockEntity)
drawEsp<BlockEntity>(
Expand Down
152 changes: 152 additions & 0 deletions src/main/kotlin/com/lambda/module/modules/render/LightLevels.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

package com.lambda.module.modules.render

import com.lambda.Lambda.mc
import com.lambda.config.applyEdits
import com.lambda.config.groups.WorldLineSettings
import com.lambda.context.SafeContext
import com.lambda.graphics.mc.LineDashStyle
import com.lambda.graphics.mc.RenderBuilder
import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer
import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.threading.runSafe
import com.lambda.util.BlockUtils.blockState
import com.lambda.util.NamedEnum
import com.lambda.util.math.flooredBlockPos
import com.lambda.util.math.setAlpha
import com.lambda.util.math.vec3d
import com.lambda.util.world.toBlockPos
import net.minecraft.block.Blocks
import net.minecraft.block.SnowBlock
import net.minecraft.registry.tag.BlockTags
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Direction
import net.minecraft.world.LightType
import java.awt.Color

object LightLevels : Module(
name = "LightLevels",
description = "Shows light level. Helpful for mob-proofing areas",
tag = ModuleTag.RENDER
) {
private enum class Group(override val displayName: String) : NamedEnum {
Fill("Fill"),
Line("Line")
}

private val mode: Mode by setting("Mode", Mode.Chunked)
.onValueChange { _, _ -> chunkedRenderer.clear(); refreshChunkedRenderer(this) }
private val minLightLevel by setting("Min Light Level", 0, 0..15).onValueChange(::refreshChunkedRenderer)
private val renderMode by setting("Render Mode", RenderMode.Square).onValueChange(::refreshChunkedRenderer)
private val color by setting("Color", Color.RED).onValueChange(::refreshChunkedRenderer)
private val size by setting("Size", 14, 1..16).onValueChange(::refreshChunkedRenderer)
private val fill by setting("Fill", false) { renderMode == RenderMode.Square }.group(Group.Fill).onValueChange(::refreshChunkedRenderer)
Comment on lines +56 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either put .onValueChange in a newline or not. You are doing both.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case is makes more sense to have them on the same line as it is the same onValueChange repeated for most of the render settings and is small. Unique onValueChange's should be put on a new line as they offer new logic which should be more visible and readable. This is why the onValueChange is on a new line for the mode setting

private val fillAlpha by setting("Fill Alpha", 0.2, 0.0..1.0, 0.01) { renderMode == RenderMode.Square && fill }.group(Group.Fill).onValueChange(::refreshChunkedRenderer)
private val outline by setting("Outline", true) { renderMode == RenderMode.Square }.group(Group.Line).onValueChange(::refreshChunkedRenderer)
private val worldLineConfig = WorldLineSettings(c = this, baseGroup = arrayOf(Group.Line)) { renderMode != RenderMode.Square || outline }.apply {
applyEdits {
hide(::startColor, ::endColor)
settings.forEach { it.onValueChange(::refreshChunkedRenderer) }
}
}
private val depthTest by setting("Depth Test", false, "Shows renders through terrain")
private val horizontalRange by setting("Horizontal Range", 16, 1..32) { mode == Mode.Radius }
private val verticalRange by setting("Vertical Range", 8, 1..32) { mode == Mode.Radius }

private val chunkedRenderer = chunkedRenderer("LightLevels Chunked Renderer", { depthTest }, { mode != Mode.Chunked }) { _, pos ->
runSafe { buildRender(pos.toBlockPos(), worldLineConfig.getDashStyle()) }
}

init {
tickedRenderer("LightLevels Ticked Renderer", { depthTest }) { safeContext ->
if (mode != Mode.Radius) return@tickedRenderer
val playerPos = mc.gameRenderer.camera.pos.flooredBlockPos

val dashStyle = worldLineConfig.getDashStyle()
(playerPos.x - horizontalRange..playerPos.x + horizontalRange).forEach { x ->
(playerPos.z - horizontalRange..playerPos.z + horizontalRange).forEach { z ->
(playerPos.y - verticalRange..playerPos.y + verticalRange).forEach { y ->
with(safeContext) { buildRender(BlockPos(x, y, z), dashStyle) }
}
}
}
}
}

context(safeContext: SafeContext)
private fun RenderBuilder.buildRender(pos: BlockPos, dashStyle: LineDashStyle?) {
val level = safeContext.world.getLightLevel(LightType.BLOCK, pos)
if (level > minLightLevel || !safeContext.hasSpawnPotential(pos)) return

val renderVec = pos.vec3d
val trueSize = (16 - size) / 32.0
val corner1 = renderVec.add(trueSize, 0.05, trueSize)
val corner2 = renderVec.add(1.0 - trueSize, 0.05, trueSize)
val corner3 = renderVec.add(1.0 - trueSize, 0.05, 1.0 - trueSize)
val corner4 = renderVec.add(trueSize, 0.05, 1.0 - trueSize)

when(renderMode) {
RenderMode.Square -> {
if (fill) filledQuad(corner1, corner2, corner3, corner4, color.setAlpha(fillAlpha))
if (outline) polyline(listOf(corner1, corner2, corner3, corner4, corner1), color, worldLineConfig.width, dashStyle)
}
RenderMode.Cross -> {
line(corner1, corner3, color, worldLineConfig.width, dashStyle)
line(corner2, corner4, color, worldLineConfig.width, dashStyle)
}
RenderMode.Circle -> circleLine(renderVec.add(0.5, 0.0, 0.5), (size / 32.0), color, worldLineConfig.width, dashStyle = dashStyle)
}
}

private fun SafeContext.hasSpawnPotential(pos: BlockPos) =
blockState(pos).let { state ->
(!state.block.collidable || (state.block === Blocks.SNOW && state.get(SnowBlock.LAYERS) <= 1)) &&
!state.emitsRedstonePower() &&
state.fluidState.isEmpty &&
!state.isIn(BlockTags.PREVENT_MOB_SPAWNING_INSIDE) &&
pos.down().let {
val underState = blockState(it)
underState.isSideSolidFullSquare(world, it, Direction.UP) &&
!underState.isTransparent &&
underState.block !== Blocks.BEDROCK
}
}

@JvmStatic
fun updateChunk(x: Int, z: Int) = runSafe {
if (mode == Mode.Chunked) chunkedRenderer.rebuildChunk(x, z)
}

private fun refreshChunkedRenderer(ctx: SafeContext, from: Any? = null, to: Any? = null) {
if (mode == Mode.Chunked) chunkedRenderer.rebuild()
}

private enum class Mode {
Chunked,
Radius
}

private enum class RenderMode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe extend NamedEnum and Describable ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its too simple. Its clear that selecting Cross would render crosses, and likewise for the others

Square,
Cross,
Circle
}
}
Loading