Skip to content

Commit 0d7f7d8

Browse files
authored
LightLevels module (#276)
1 parent ffafb42 commit 0d7f7d8

File tree

14 files changed

+300
-164
lines changed

14 files changed

+300
-164
lines changed

src/main/java/com/lambda/mixin/render/BlockEntityRenderManagerMixin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ private <S extends BlockEntityRenderState> void wrapRenderQueue(BlockEntityRende
4242
Operation<Void> original) {
4343
BlockPos pos = renderState.pos;
4444

45-
if (pos != null && OutlineManager.INSTANCE.shouldCapture(pos)) {
45+
if (pos != null && OutlineManager.shouldCapture(pos)) {
4646
VertexCapture.INSTANCE.beginCapture(pos);
4747

4848
boolean outlineOnly = !Vec3d.ofCenter(pos).isInRange(cameraState.pos, renderer.getRenderDistance());

src/main/java/com/lambda/mixin/world/ClientChunkManagerMixin.java

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,70 +19,50 @@
1919

2020
import com.lambda.event.EventFlow;
2121
import com.lambda.event.events.WorldEvent;
22+
import com.lambda.module.modules.render.LightLevels;
23+
import com.llamalad7.mixinextras.sugar.Local;
2224
import net.minecraft.client.world.ClientChunkManager;
23-
import net.minecraft.client.world.ClientWorld;
2425
import net.minecraft.network.PacketByteBuf;
2526
import net.minecraft.network.packet.s2c.play.ChunkData;
2627
import net.minecraft.util.math.ChunkPos;
28+
import net.minecraft.util.math.ChunkSectionPos;
2729
import net.minecraft.world.Heightmap;
30+
import net.minecraft.world.LightType;
2831
import net.minecraft.world.chunk.WorldChunk;
29-
import org.spongepowered.asm.mixin.Final;
3032
import org.spongepowered.asm.mixin.Mixin;
31-
import org.spongepowered.asm.mixin.Shadow;
3233
import org.spongepowered.asm.mixin.injection.At;
3334
import org.spongepowered.asm.mixin.injection.Inject;
3435
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
3536
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
36-
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
3737

3838
import java.util.Map;
3939
import java.util.function.Consumer;
4040

4141
@Mixin(ClientChunkManager.class)
4242
public class ClientChunkManagerMixin {
43-
@Final
44-
@Shadow
45-
ClientWorld world;
46-
4743
@Inject(method = "loadChunkFromPacket", at = @At("TAIL"))
4844
private void onChunkLoad(
4945
int x, int z, PacketByteBuf buf, Map<Heightmap.Type, long[]> heightmaps, Consumer<ChunkData.BlockEntityVisitor> consumer, CallbackInfoReturnable<WorldChunk> cir
5046
) {
5147
EventFlow.post(new WorldEvent.ChunkEvent.Load(cir.getReturnValue()));
5248
}
5349

54-
@Inject(method = "loadChunkFromPacket", at = @At(value = "NEW", target = "net/minecraft/world/chunk/WorldChunk", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD)
55-
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) {
50+
@Inject(method = "loadChunkFromPacket", at = @At(value = "NEW", target = "net/minecraft/world/chunk/WorldChunk", shift = At.Shift.BEFORE))
51+
private void onChunkUnload(int x, int z, PacketByteBuf buf, Map<Heightmap.Type, long[]> heightmaps, Consumer<ChunkData.BlockEntityVisitor> consumer, CallbackInfoReturnable<WorldChunk> cir, @Local WorldChunk chunk) {
5652
if (chunk != null) {
5753
EventFlow.post(new WorldEvent.ChunkEvent.Unload(chunk));
5854
}
5955
}
6056

61-
@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)
62-
private void onChunkUnload(ChunkPos pos, CallbackInfo ci, int i, WorldChunk chunk) {
57+
@Inject(method = "unload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientChunkManager$ClientChunkMap;unloadChunk(ILnet/minecraft/world/chunk/WorldChunk;)V"))
58+
private void onChunkUnload(ChunkPos pos, CallbackInfo ci, @Local WorldChunk chunk) {
6359
EventFlow.post(new WorldEvent.ChunkEvent.Unload(chunk));
6460
}
6561

66-
// @Inject(
67-
// method = "updateLoadDistance",
68-
// at = @At(
69-
// value = "INVOKE",
70-
// target = "net/minecraft/client/world/ClientChunkManager$ClientChunkMap.isInRadius(II)Z"
71-
// ),
72-
// locals = LocalCapture.CAPTURE_FAILHARD
73-
// )
74-
// private void onUpdateLoadDistance(
75-
// int loadDistance,
76-
// CallbackInfo ci,
77-
// int oldRadius,
78-
// int newRadius,
79-
// ClientChunkManager.ClientChunkMap clientChunkMap,
80-
// int k,
81-
// WorldChunk oldChunk,
82-
// ChunkPos chunkPos
83-
// ) {
84-
// if (!clientChunkMap.isInRadius(chunkPos.x, chunkPos.z)) {
85-
// EventFlow.post(new WorldEvent.ChunkEvent.Unload(this.world, oldChunk));
86-
// }
87-
// }
62+
@Inject(method = "onLightUpdate", at = @At("RETURN"))
63+
private void injectOnLightUpdate(LightType type, ChunkSectionPos pos, CallbackInfo ci) {
64+
if (LightLevels.INSTANCE.isEnabled()) {
65+
LightLevels.updateChunk(pos.getX(), pos.getZ());
66+
}
67+
}
8868
}

src/main/kotlin/com/lambda/config/groups/EntityColorsConfig.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package com.lambda.config.groups
1919

20-
import com.lambda.module.modules.render.ESP.Group
2120
import java.awt.Color
2221

2322
interface EntityColorsConfig {

src/main/kotlin/com/lambda/config/groups/WorldLineSettings.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class WorldLineSettings(
3636

3737
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()
3838
val worldWidthSetting by c.setting("${prefix}Width", 5, 1..50, 1) { visibility() && !distanceScaling }.group(*baseGroup, Group.General).index()
39-
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()
39+
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()
4040

4141
override val width: Float get() =
4242
if (distanceScaling) -screenWidthSetting * 0.00005f

src/main/kotlin/com/lambda/graphics/mc/RegionRenderer.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,17 +381,17 @@ class RegionRenderer {
381381
fun hasScreenData(): Boolean = hasScreenData
382382

383383
fun clearData() {
384-
faceVertexBuffer = null
385-
edgeVertexBuffer = null
386-
textVertexBuffer = null
384+
faceVertexBuffer?.close(); faceVertexBuffer = null
385+
edgeVertexBuffer?.close(); edgeVertexBuffer = null
386+
textVertexBuffer?.close(); textVertexBuffer = null
387387
faceIndexCount = 0
388388
edgeIndexCount = 0
389389
textIndexCount = 0
390390
hasWorldData = false
391391

392-
screenFaceVertexBuffer = null
393-
screenEdgeVertexBuffer = null
394-
screenTextVertexBuffer = null
392+
screenFaceVertexBuffer?.close(); screenFaceVertexBuffer = null
393+
screenEdgeVertexBuffer?.close(); screenEdgeVertexBuffer = null
394+
screenTextVertexBuffer?.close(); screenTextVertexBuffer = null
395395
screenFaceIndexCount = 0
396396
screenEdgeIndexCount = 0
397397
screenTextIndexCount = 0

src/main/kotlin/com/lambda/graphics/mc/RenderBuilder.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class RenderBuilder(private val cameraPos: Vec3d, var depthTest: Boolean = false
134134
builder: (BoxBuilder.() -> Unit)? = null
135135
) = boxes(pos, safeContext.blockState(pos), lineConfig, builder)
136136

137-
fun filledQuadGradient(
137+
fun filledQuad(
138138
corner1: Vec3d,
139139
corner2: Vec3d,
140140
corner3: Vec3d,
@@ -250,10 +250,10 @@ class RenderBuilder(private val cameraPos: Vec3d, var depthTest: Boolean = false
250250
fun circleLine(
251251
center: Vec3d,
252252
radius: Double,
253-
normal: Vec3d = Vec3d(0.0, 1.0, 0.0),
254253
color: Color,
255-
segments: Int = 32,
256254
width: Float = -0.0005f,
255+
normal: Vec3d = Vec3d(0.0, 1.0, 0.0),
256+
segments: Int = 32,
257257
dashStyle: LineDashStyle? = null
258258
) {
259259
val up =

src/main/kotlin/com/lambda/graphics/mc/renderer/ChunkedRenderer.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ChunkedRenderer(
4747
owner: Any,
4848
name: String,
4949
depthTest: SafeContext.() -> Boolean,
50+
pauseUpdates: SafeContext.() -> Boolean,
5051
private val update: RenderBuilder.(ClientWorld, FastVector) -> Unit
5152
) : AbstractRenderer(name, depthTest) {
5253
private val chunkMap = ConcurrentHashMap<Long, ChunkData>()
@@ -82,9 +83,10 @@ class ChunkedRenderer(
8283
owner.listen<WorldEvent.Player.Leave> { rebuild() }
8384

8485
owner.listenConcurrently<TickEvent.Pre> {
85-
val depth = depthTest()
86+
if (pauseUpdates()) return@listenConcurrently
8687
val queueSize = rebuildQueue.size
8788
val polls = minOf(StyleEditor.rebuildsPerTick, queueSize)
89+
val depth = depthTest()
8890
repeat(polls) { rebuildQueue.poll()?.rebuild(depth) }
8991
}
9092

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

105+
context(safeContext: SafeContext)
106+
fun rebuildChunk(x: Int, z: Int) {
107+
safeContext.world.getChunk(x, z)?.chunkData?.markDirty()
108+
}
109+
103110
fun rebuild() {
104111
rebuildQueue.clear()
105112
mc.world?.chunkManager?.chunks?.let { chunks ->
106-
val chunkCount = chunks.loadedChunkCount
107-
(0..chunkCount).forEach { index ->
113+
val chunkCount = chunks.chunks.length()
114+
(0 until chunkCount).forEach { index ->
108115
val chunk = chunks.chunks.get(index) ?: return@forEach
109-
chunkMap.putIfAbsent(chunk.chunkKey, chunk.chunkData)
116+
chunkMap.putIfAbsent(chunk.chunkKey, ChunkData(chunk))
110117
}
111118
}
112119
rebuildQueue.addAll(chunkMap.values)
@@ -182,11 +189,12 @@ class ChunkedRenderer(
182189
fun Any.chunkedRenderer(
183190
name: String,
184191
depthTest: SafeContext.() -> Boolean = { false },
192+
pauseUpdates: SafeContext.() -> Boolean = { false },
185193
update: RenderBuilder.(ClientWorld, FastVector) -> Unit
186-
) = ChunkedRenderer(this, name, depthTest, update).also { renderer ->
194+
) = ChunkedRenderer(this, name, depthTest, pauseUpdates, update).also { renderer ->
187195
(this as? Module)?.let { module ->
188196
module.onEnable { renderer.rebuild() }
189-
module.onDisable { renderer.rebuild() }
197+
module.onDisable { renderer.clear() }
190198
}
191199
}
192200
}

src/main/kotlin/com/lambda/module/modules/render/ESP.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ object ESP : Module(
8181
{ listOf(it.interpolatedBox) }
8282
)
8383
}
84-
val chunkMap = world.chunkManager.chunks
85-
(0 until chunkMap.loadedChunkCount).forEach { chunk ->
86-
chunkMap.chunks.get(chunk)?.blockEntities?.values?.forEach { blockEntity ->
84+
val chunks = world.chunkManager.chunks.chunks
85+
(0 until chunks.length()).forEach { chunk ->
86+
chunks.get(chunk)?.blockEntities?.values?.forEach { blockEntity ->
8787
if (!entitySettings.isSelected(blockEntity)) return@forEach
8888
val color = entityColors.getColor(blockEntity)
8989
drawEsp<BlockEntity>(
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2026 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.module.modules.render
19+
20+
import com.lambda.Lambda.mc
21+
import com.lambda.config.applyEdits
22+
import com.lambda.config.groups.WorldLineSettings
23+
import com.lambda.context.SafeContext
24+
import com.lambda.graphics.mc.LineDashStyle
25+
import com.lambda.graphics.mc.RenderBuilder
26+
import com.lambda.graphics.mc.renderer.ChunkedRenderer.Companion.chunkedRenderer
27+
import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
28+
import com.lambda.module.Module
29+
import com.lambda.module.tag.ModuleTag
30+
import com.lambda.threading.runSafe
31+
import com.lambda.util.BlockUtils.blockState
32+
import com.lambda.util.NamedEnum
33+
import com.lambda.util.math.flooredBlockPos
34+
import com.lambda.util.math.setAlpha
35+
import com.lambda.util.math.vec3d
36+
import com.lambda.util.world.toBlockPos
37+
import net.minecraft.block.Blocks
38+
import net.minecraft.block.SnowBlock
39+
import net.minecraft.registry.tag.BlockTags
40+
import net.minecraft.util.math.BlockPos
41+
import net.minecraft.util.math.Direction
42+
import net.minecraft.world.LightType
43+
import java.awt.Color
44+
45+
object LightLevels : Module(
46+
name = "LightLevels",
47+
description = "Shows light level. Helpful for mob-proofing areas",
48+
tag = ModuleTag.RENDER
49+
) {
50+
private enum class Group(override val displayName: String) : NamedEnum {
51+
Fill("Fill"),
52+
Line("Line")
53+
}
54+
55+
private val mode: Mode by setting("Mode", Mode.Chunked)
56+
.onValueChange { _, _ -> chunkedRenderer.clear(); refreshChunkedRenderer(this) }
57+
private val minLightLevel by setting("Min Light Level", 0, 0..15).onValueChange(::refreshChunkedRenderer)
58+
private val renderMode by setting("Render Mode", RenderMode.Square).onValueChange(::refreshChunkedRenderer)
59+
private val color by setting("Color", Color.RED).onValueChange(::refreshChunkedRenderer)
60+
private val size by setting("Size", 14, 1..16).onValueChange(::refreshChunkedRenderer)
61+
private val fill by setting("Fill", false) { renderMode == RenderMode.Square }.group(Group.Fill).onValueChange(::refreshChunkedRenderer)
62+
private val fillAlpha by setting("Fill Alpha", 0.2, 0.0..1.0, 0.01) { renderMode == RenderMode.Square && fill }.group(Group.Fill).onValueChange(::refreshChunkedRenderer)
63+
private val outline by setting("Outline", true) { renderMode == RenderMode.Square }.group(Group.Line).onValueChange(::refreshChunkedRenderer)
64+
private val worldLineConfig = WorldLineSettings(c = this, baseGroup = arrayOf(Group.Line)) { renderMode != RenderMode.Square || outline }.apply {
65+
applyEdits {
66+
hide(::startColor, ::endColor)
67+
settings.forEach { it.onValueChange(::refreshChunkedRenderer) }
68+
}
69+
}
70+
private val depthTest by setting("Depth Test", false, "Shows renders through terrain")
71+
private val horizontalRange by setting("Horizontal Range", 16, 1..32) { mode == Mode.Radius }
72+
private val verticalRange by setting("Vertical Range", 8, 1..32) { mode == Mode.Radius }
73+
74+
private val chunkedRenderer = chunkedRenderer("LightLevels Chunked Renderer", { depthTest }, { mode != Mode.Chunked }) { _, pos ->
75+
runSafe { buildRender(pos.toBlockPos(), worldLineConfig.getDashStyle()) }
76+
}
77+
78+
init {
79+
tickedRenderer("LightLevels Ticked Renderer", { depthTest }) { safeContext ->
80+
if (mode != Mode.Radius) return@tickedRenderer
81+
val playerPos = mc.gameRenderer.camera.pos.flooredBlockPos
82+
83+
val dashStyle = worldLineConfig.getDashStyle()
84+
(playerPos.x - horizontalRange..playerPos.x + horizontalRange).forEach { x ->
85+
(playerPos.z - horizontalRange..playerPos.z + horizontalRange).forEach { z ->
86+
(playerPos.y - verticalRange..playerPos.y + verticalRange).forEach { y ->
87+
with(safeContext) { buildRender(BlockPos(x, y, z), dashStyle) }
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
context(safeContext: SafeContext)
95+
private fun RenderBuilder.buildRender(pos: BlockPos, dashStyle: LineDashStyle?) {
96+
val level = safeContext.world.getLightLevel(LightType.BLOCK, pos)
97+
if (level > minLightLevel || !safeContext.hasSpawnPotential(pos)) return
98+
99+
val renderVec = pos.vec3d
100+
val trueSize = (16 - size) / 32.0
101+
val corner1 = renderVec.add(trueSize, 0.05, trueSize)
102+
val corner2 = renderVec.add(1.0 - trueSize, 0.05, trueSize)
103+
val corner3 = renderVec.add(1.0 - trueSize, 0.05, 1.0 - trueSize)
104+
val corner4 = renderVec.add(trueSize, 0.05, 1.0 - trueSize)
105+
106+
when(renderMode) {
107+
RenderMode.Square -> {
108+
if (fill) filledQuad(corner1, corner2, corner3, corner4, color.setAlpha(fillAlpha))
109+
if (outline) polyline(listOf(corner1, corner2, corner3, corner4, corner1), color, worldLineConfig.width, dashStyle)
110+
}
111+
RenderMode.Cross -> {
112+
line(corner1, corner3, color, worldLineConfig.width, dashStyle)
113+
line(corner2, corner4, color, worldLineConfig.width, dashStyle)
114+
}
115+
RenderMode.Circle -> circleLine(renderVec.add(0.5, 0.0, 0.5), (size / 32.0), color, worldLineConfig.width, dashStyle = dashStyle)
116+
}
117+
}
118+
119+
private fun SafeContext.hasSpawnPotential(pos: BlockPos) =
120+
blockState(pos).let { state ->
121+
(!state.block.collidable || (state.block === Blocks.SNOW && state.get(SnowBlock.LAYERS) <= 1)) &&
122+
!state.emitsRedstonePower() &&
123+
state.fluidState.isEmpty &&
124+
!state.isIn(BlockTags.PREVENT_MOB_SPAWNING_INSIDE) &&
125+
pos.down().let {
126+
val underState = blockState(it)
127+
underState.isSideSolidFullSquare(world, it, Direction.UP) &&
128+
!underState.isTransparent &&
129+
underState.block !== Blocks.BEDROCK
130+
}
131+
}
132+
133+
@JvmStatic
134+
fun updateChunk(x: Int, z: Int) = runSafe {
135+
if (mode == Mode.Chunked) chunkedRenderer.rebuildChunk(x, z)
136+
}
137+
138+
private fun refreshChunkedRenderer(ctx: SafeContext, from: Any? = null, to: Any? = null) {
139+
if (mode == Mode.Chunked) chunkedRenderer.rebuild()
140+
}
141+
142+
private enum class Mode {
143+
Chunked,
144+
Radius
145+
}
146+
147+
private enum class RenderMode {
148+
Square,
149+
Cross,
150+
Circle
151+
}
152+
}

0 commit comments

Comments
 (0)