]> git.lizzy.rs Git - LightOverlay.git/blob - common/src/main/java/me/shedaniel/lightoverlay/common/LightOverlay.java
Merge pull request #82
[LightOverlay.git] / common / src / main / java / me / shedaniel / lightoverlay / common / LightOverlay.java
1 package me.shedaniel.lightoverlay.common;
2
3 import com.google.common.collect.Maps;
4 import com.mojang.blaze3d.platform.GlStateManager;
5 import com.mojang.blaze3d.platform.InputConstants;
6 import com.mojang.blaze3d.systems.RenderSystem;
7 import com.mojang.blaze3d.vertex.Tesselator;
8 import com.mojang.math.Transformation;
9 import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
10 import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
11 import me.shedaniel.architectury.event.events.GuiEvent;
12 import me.shedaniel.architectury.event.events.client.ClientTickEvent;
13 import me.shedaniel.architectury.platform.Platform;
14 import me.shedaniel.architectury.registry.KeyBindings;
15 import net.minecraft.client.Camera;
16 import net.minecraft.client.KeyMapping;
17 import net.minecraft.client.Minecraft;
18 import net.minecraft.client.gui.Font;
19 import net.minecraft.client.multiplayer.ClientLevel;
20 import net.minecraft.client.player.LocalPlayer;
21 import net.minecraft.client.renderer.MultiBufferSource;
22 import net.minecraft.client.renderer.culling.Frustum;
23 import net.minecraft.core.BlockPos;
24 import net.minecraft.core.Direction;
25 import net.minecraft.resources.ResourceLocation;
26 import net.minecraft.tags.BlockTags;
27 import net.minecraft.util.LazyLoadedValue;
28 import net.minecraft.util.Mth;
29 import net.minecraft.world.entity.Entity;
30 import net.minecraft.world.entity.EntityType;
31 import net.minecraft.world.entity.MobCategory;
32 import net.minecraft.world.level.BlockGetter;
33 import net.minecraft.world.level.ChunkPos;
34 import net.minecraft.world.level.Level;
35 import net.minecraft.world.level.LightLayer;
36 import net.minecraft.world.level.biome.Biome;
37 import net.minecraft.world.level.block.Block;
38 import net.minecraft.world.level.block.state.BlockState;
39 import net.minecraft.world.level.chunk.ChunkBiomeContainer;
40 import net.minecraft.world.level.chunk.ChunkStatus;
41 import net.minecraft.world.level.chunk.LevelChunk;
42 import net.minecraft.world.level.lighting.LayerLightEventListener;
43 import net.minecraft.world.phys.shapes.CollisionContext;
44 import net.minecraft.world.phys.shapes.VoxelShape;
45 import org.apache.logging.log4j.LogManager;
46 import org.lwjgl.opengl.GL11;
47
48 import java.io.File;
49 import java.io.FileInputStream;
50 import java.io.FileOutputStream;
51 import java.io.IOException;
52 import java.lang.invoke.MethodHandle;
53 import java.lang.invoke.MethodHandles;
54 import java.lang.invoke.MethodType;
55 import java.text.DecimalFormat;
56 import java.util.*;
57 import java.util.concurrent.Executors;
58 import java.util.concurrent.ThreadPoolExecutor;
59
60 public class LightOverlay {
61     public static final DecimalFormat FORMAT = new DecimalFormat("#.#");
62     private static final String KEYBIND_CATEGORY = "key.lightoverlay.category";
63     private static final ResourceLocation ENABLE_OVERLAY_KEYBIND = new ResourceLocation("lightoverlay", "enable_overlay");
64     public static int reach = 12;
65     public static int crossLevel = 7;
66     public static int secondaryLevel = -1;
67     public static int lowerCrossLevel = -1;
68     public static int higherCrossLevel = -1;
69     public static boolean caching = false;
70     public static boolean showNumber = false;
71     public static boolean smoothLines = true;
72     public static boolean underwater = false;
73     public static boolean mushroom = false;
74     public static float lineWidth = 1.0F;
75     public static int yellowColor = 0xFFFF00, redColor = 0xFF0000, secondaryColor = 0x0000FF;
76     public static File configFile;
77
78     private static KeyMapping enableOverlay;
79     private static boolean enabled = false;
80     private static final LazyLoadedValue<EntityType<Entity>> TESTING_ENTITY_TYPE = new LazyLoadedValue<>(() ->
81             EntityType.Builder.createNothing(MobCategory.MONSTER).sized(0f, 0f).noSave().build(null));
82     private static int threadNumber = 0;
83     public static Frustum frustum;
84     private static final ThreadPoolExecutor EXECUTOR = (ThreadPoolExecutor) Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
85         Thread thread = new Thread(r, "light-overlay-" + threadNumber++);
86         thread.setDaemon(true);
87         return thread;
88     });
89     private static final Set<ChunkPos> POS = Collections.synchronizedSet(new HashSet<>());
90     private static final Set<ChunkPos> CALCULATING_POS = Collections.synchronizedSet(new HashSet<>());
91     private static final Map<ChunkPos, Long2ReferenceMap<Object>> CHUNK_MAP = Maps.newConcurrentMap();
92     private static final Minecraft CLIENT = Minecraft.getInstance();
93     private static long ticks = 0;
94     
95     public static void register() {
96         // Load Config
97         configFile = new File(Platform.getConfigFolder().toFile(), "lightoverlay.properties");
98         loadConfig(configFile);
99         
100         enableOverlay = createKeyBinding(ENABLE_OVERLAY_KEYBIND, InputConstants.Type.KEYSYM, 296, KEYBIND_CATEGORY);
101         KeyBindings.registerKeyBinding(enableOverlay);
102         
103         registerDebugRenderer(() -> {
104             if (enabled) {
105                 LocalPlayer playerEntity = CLIENT.player;
106                 int playerPosX = ((int) playerEntity.getX()) >> 4;
107                 int playerPosZ = ((int) playerEntity.getZ()) >> 4;
108                 CollisionContext collisionContext = CollisionContext.of(playerEntity);
109                 Level world = CLIENT.level;
110                 BlockPos playerPos = new BlockPos(playerEntity.getX(), playerEntity.getY(), playerEntity.getZ());
111                 Camera camera = CLIENT.gameRenderer.getMainCamera();
112                 
113                 if (showNumber) {
114                     RenderSystem.enableTexture();
115                     RenderSystem.depthMask(true);
116                     BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
117                     BlockPos.MutableBlockPos downMutable = new BlockPos.MutableBlockPos();
118                     for (Map.Entry<ChunkPos, Long2ReferenceMap<Object>> entry : CHUNK_MAP.entrySet()) {
119                         if (caching && (Mth.abs(entry.getKey().x - playerPosX) > getChunkRange() || Mth.abs(entry.getKey().z - playerPosZ) > getChunkRange())) {
120                             continue;
121                         }
122                         for (Long2ReferenceMap.Entry<Object> objectEntry : entry.getValue().long2ReferenceEntrySet()) {
123                             if (objectEntry.getValue() instanceof Byte) {
124                                 mutable.set(BlockPos.getX(objectEntry.getLongKey()), BlockPos.getY(objectEntry.getLongKey()), BlockPos.getZ(objectEntry.getLongKey()));
125                                 if (mutable.closerThan(playerPos, reach)) {
126                                     if (frustum == null || isFrustumVisible(frustum, mutable.getX(), mutable.getY(), mutable.getZ(), mutable.getX() + 1, mutable.getX() + 1, mutable.getX() + 1)) {
127                                         downMutable.set(mutable.getX(), mutable.getY() - 1, mutable.getZ());
128                                         renderLevel(CLIENT, camera, world, mutable, downMutable, (Byte) objectEntry.getValue(), collisionContext);
129                                     }
130                                 }
131                             }
132                         }
133                     }
134                     RenderSystem.enableDepthTest();
135                 } else {
136                     RenderSystem.enableDepthTest();
137                     RenderSystem.disableTexture();
138                     RenderSystem.enableBlend();
139                     RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA);
140                     if (smoothLines) GL11.glEnable(GL11.GL_LINE_SMOOTH);
141                     GL11.glLineWidth(lineWidth);
142                     GL11.glBegin(GL11.GL_LINES);
143                     BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
144                     for (Map.Entry<ChunkPos, Long2ReferenceMap<Object>> entry : CHUNK_MAP.entrySet()) {
145                         if (caching && (Mth.abs(entry.getKey().x - playerPosX) > getChunkRange() || Mth.abs(entry.getKey().z - playerPosZ) > getChunkRange())) {
146                             continue;
147                         }
148                         for (Long2ReferenceMap.Entry<Object> objectEntry : entry.getValue().long2ReferenceEntrySet()) {
149                             if (objectEntry.getValue() instanceof CrossType) {
150                                 mutable.set(BlockPos.getX(objectEntry.getLongKey()), BlockPos.getY(objectEntry.getLongKey()), BlockPos.getZ(objectEntry.getLongKey()));
151                                 if (mutable.closerThan(playerPos, reach)) {
152                                     if (frustum == null || isFrustumVisible(frustum, mutable.getX(), mutable.getY(), mutable.getZ(), mutable.getX() + 1, mutable.getX() + 1, mutable.getX() + 1)) {
153                                         int color = objectEntry.getValue() == CrossType.RED ? redColor : objectEntry.getValue() == CrossType.YELLOW ? yellowColor : secondaryColor;
154                                         renderCross(camera, world, mutable, color, collisionContext);
155                                     }
156                                 }
157                             }
158                         }
159                     }
160                     GL11.glEnd();
161                     RenderSystem.disableBlend();
162                     RenderSystem.enableTexture();
163                     if (smoothLines) GL11.glDisable(GL11.GL_LINE_SMOOTH);
164                 }
165             }
166         });
167     
168         GuiEvent.DEBUG_TEXT_LEFT.register(list -> {
169             if (enabled) {
170                 if (caching) {
171                     list.add(String.format("[Light Overlay] Chunks to queue: %02d", POS.size()));
172                 } else {
173                     list.add("[Light Overlay] Enabled");
174                 }
175             }else {
176                 list.add("[Light Overlay] Disabled");
177             }
178         });
179         ClientTickEvent.CLIENT_POST.register(LightOverlay::tick);
180     }
181     
182     private static void processChunk(ChunkPos pos, int playerPosX, int playerPosZ, CollisionContext context) {
183         CALCULATING_POS.remove(pos);
184         if (Mth.abs(pos.x - playerPosX) > getChunkRange() || Mth.abs(pos.z - playerPosZ) > getChunkRange() || POS.contains(pos)) {
185             return;
186         }
187         try {
188             calculateChunk(CLIENT.level.getChunkSource().getChunk(pos.x, pos.z, ChunkStatus.FULL, false), CLIENT.level, pos, context);
189         } catch (Throwable throwable) {
190             LogManager.getLogger().throwing(throwable);
191         }
192     }
193     
194     public static void queueChunkAndNear(ChunkPos pos) {
195         for (int xOffset = -1; xOffset <= 1; xOffset++) {
196             for (int zOffset = -1; zOffset <= 1; zOffset++) {
197                 queueChunk(new ChunkPos(pos.x + xOffset, pos.z + zOffset));
198             }
199         }
200     }
201     
202     public static void queueChunk(ChunkPos pos) {
203         if (enabled && caching && !CALCULATING_POS.contains(pos)) {
204             POS.add(pos);
205         }
206     }
207     
208     public static int getChunkRange() {
209         return Math.max(Mth.ceil(reach / 16f), 1);
210     }
211     
212     private static void calculateChunk(LevelChunk chunk, Level world, ChunkPos chunkPos, CollisionContext entityContext) {
213         if (world != null && chunk != null) {
214             Long2ReferenceMap<Object> map = new Long2ReferenceOpenHashMap<>();
215             LayerLightEventListener block = world.getLightEngine().getLayerListener(LightLayer.BLOCK);
216             LayerLightEventListener sky = showNumber ? null : world.getLightEngine().getLayerListener(LightLayer.SKY);
217             for (BlockPos pos : BlockPos.betweenClosed(chunkPos.getMinBlockX(), 0, chunkPos.getMinBlockZ(), chunkPos.getMaxBlockX(), 256, chunkPos.getMaxBlockZ())) {
218                 BlockPos down = pos.below();
219                 if (showNumber) {
220                     int level = getCrossLevel(pos, down, chunk, block, entityContext);
221                     if (level >= 0) {
222                         map.put(pos.asLong(), Byte.valueOf((byte) level));
223                     }
224                 } else {
225                     CrossType type = getCrossType(pos, down, chunk, block, sky, entityContext);
226                     if (type != CrossType.NONE) {
227                         map.put(pos.asLong(), type);
228                     }
229                 }
230             }
231             CHUNK_MAP.put(chunkPos, map);
232         } else {
233             CHUNK_MAP.remove(chunkPos);
234         }
235     }
236     
237     public static CrossType getCrossType(BlockPos pos, BlockPos down, BlockGetter world, LayerLightEventListener block, LayerLightEventListener sky, CollisionContext entityContext) {
238         BlockState blockBelowState = world.getBlockState(down);
239         BlockState blockUpperState = world.getBlockState(pos);
240         VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
241         if (!underwater && !blockUpperState.getFluidState().isEmpty())
242             return CrossType.NONE;
243         // Check if the outline is full
244         if (Block.isFaceFull(upperCollisionShape, Direction.UP))
245             return CrossType.NONE;
246         // TODO: Not to hard code no redstone
247         if (blockUpperState.isSignalSource())
248             return CrossType.NONE;
249         // Check if the collision has a bump
250         if (upperCollisionShape.max(Direction.Axis.Y) > 0)
251             return CrossType.NONE;
252         if (blockUpperState.getBlock().is(BlockTags.RAILS))
253             return CrossType.NONE;
254         // Check block state allow spawning (excludes bedrock and barriers automatically)
255         if (!blockBelowState.isValidSpawn(world, down, TESTING_ENTITY_TYPE.get()))
256             return CrossType.NONE;
257         if (!mushroom && CLIENT.level != null && Biome.BiomeCategory.MUSHROOM.equals(CLIENT.level.getBiome(pos).getBiomeCategory()))
258             return CrossType.NONE;
259         int blockLightLevel = block.getLightValue(pos);
260         int skyLightLevel = sky.getLightValue(pos);
261         if (blockLightLevel > higherCrossLevel)
262             return CrossType.NONE;
263         if (skyLightLevel > higherCrossLevel)
264             return CrossType.YELLOW;
265         return lowerCrossLevel >= 0 && blockLightLevel > lowerCrossLevel ? CrossType.SECONDARY : CrossType.RED;
266     }
267     
268     public static int getCrossLevel(BlockPos pos, BlockPos down, BlockGetter world, LayerLightEventListener view, CollisionContext collisionContext) {
269         BlockState blockBelowState = world.getBlockState(down);
270         BlockState blockUpperState = world.getBlockState(pos);
271         VoxelShape collisionShape = blockBelowState.getCollisionShape(world, down, collisionContext);
272         VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, collisionContext);
273         if (!underwater && !blockUpperState.getFluidState().isEmpty())
274             return -1;
275         if (!blockBelowState.getFluidState().isEmpty())
276             return -1;
277         if (blockBelowState.isAir())
278             return -1;
279         if (Block.isFaceFull(upperCollisionShape, Direction.DOWN))
280             return -1;
281         return view.getLightValue(pos);
282     }
283     
284     public static void renderCross(Camera camera, Level world, BlockPos pos, int color, CollisionContext collisionContext) {
285         double d0 = camera.getPosition().x;
286         double d1 = camera.getPosition().y - .005D;
287         VoxelShape upperOutlineShape = world.getBlockState(pos).getShape(world, pos, collisionContext);
288         if (!upperOutlineShape.isEmpty())
289             d1 -= upperOutlineShape.max(Direction.Axis.Y);
290         double d2 = camera.getPosition().z;
291         
292         int red = (color >> 16) & 255;
293         int green = (color >> 8) & 255;
294         int blue = color & 255;
295         int x = pos.getX();
296         int y = pos.getY();
297         int z = pos.getZ();
298         RenderSystem.color4f(red / 255f, green / 255f, blue / 255f, 1f);
299         GL11.glVertex3d(x + .01 - d0, y - d1, z + .01 - d2);
300         GL11.glVertex3d(x - .01 + 1 - d0, y - d1, z - .01 + 1 - d2);
301         GL11.glVertex3d(x - .01 + 1 - d0, y - d1, z + .01 - d2);
302         GL11.glVertex3d(x + .01 - d0, y - d1, z - .01 + 1 - d2);
303     }
304     
305     @SuppressWarnings("deprecation")
306     public static void renderLevel(Minecraft client, Camera camera, Level world, BlockPos pos, BlockPos down, int level, CollisionContext collisionContext) {
307         String text = String.valueOf(level);
308         Font textRenderer_1 = client.font;
309         double double_4 = camera.getPosition().x;
310         double double_5 = camera.getPosition().y;
311         VoxelShape upperOutlineShape = world.getBlockState(down).getShape(world, down, collisionContext);
312         if (!upperOutlineShape.isEmpty())
313             double_5 += 1 - upperOutlineShape.max(Direction.Axis.Y);
314         double double_6 = camera.getPosition().z;
315         RenderSystem.pushMatrix();
316         RenderSystem.translatef((float) (pos.getX() + 0.5f - double_4), (float) (pos.getY() - double_5) + 0.005f, (float) (pos.getZ() + 0.5f - double_6));
317         RenderSystem.rotatef(90, 1, 0, 0);
318         RenderSystem.normal3f(0.0F, 1.0F, 0.0F);
319         float size = 0.07F;
320         RenderSystem.scalef(-size, -size, size);
321         float float_3 = (float) (-textRenderer_1.width(text)) / 2.0F + 0.4f;
322         RenderSystem.enableAlphaTest();
323         MultiBufferSource.BufferSource immediate = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
324         textRenderer_1.drawInBatch(text, float_3, -3.5f, level > higherCrossLevel ? 0xff042404 : (lowerCrossLevel >= 0 && level > lowerCrossLevel ? 0xff0066ff : 0xff731111), false, Transformation.identity().getMatrix(), immediate, false, 0, 15728880);
325         immediate.endBatch();
326         RenderSystem.popMatrix();
327     }
328     
329     public static void loadConfig(File file) {
330         try {
331             redColor = 0xFF0000;
332             yellowColor = 0xFFFF00;
333             secondaryColor = 0x0000FF;
334             if (!file.exists() || !file.canRead())
335                 saveConfig(file);
336             FileInputStream fis = new FileInputStream(file);
337             Properties properties = new Properties();
338             properties.load(fis);
339             fis.close();
340             reach = Integer.parseInt((String) properties.computeIfAbsent("reach", a -> "12"));
341             crossLevel = Integer.parseInt((String) properties.computeIfAbsent("crossLevel", a -> "7"));
342             secondaryLevel = Integer.parseInt((String) properties.computeIfAbsent("secondaryLevel", a -> "-1"));
343             caching = ((String) properties.computeIfAbsent("caching", a -> "false")).equalsIgnoreCase("true");
344             showNumber = ((String) properties.computeIfAbsent("showNumber", a -> "false")).equalsIgnoreCase("true");
345             smoothLines = ((String) properties.computeIfAbsent("smoothLines", a -> "true")).equalsIgnoreCase("true");
346             underwater = ((String) properties.computeIfAbsent("underwater", a -> "false")).equalsIgnoreCase("true");
347             lineWidth = Float.parseFloat((String) properties.computeIfAbsent("lineWidth", a -> "1"));
348             {
349                 int r, g, b;
350                 r = Integer.parseInt((String) properties.computeIfAbsent("yellowColorRed", a -> "255"));
351                 g = Integer.parseInt((String) properties.computeIfAbsent("yellowColorGreen", a -> "255"));
352                 b = Integer.parseInt((String) properties.computeIfAbsent("yellowColorBlue", a -> "0"));
353                 yellowColor = (r << 16) + (g << 8) + b;
354             }
355             {
356                 int r, g, b;
357                 r = Integer.parseInt((String) properties.computeIfAbsent("redColorRed", a -> "255"));
358                 g = Integer.parseInt((String) properties.computeIfAbsent("redColorGreen", a -> "0"));
359                 b = Integer.parseInt((String) properties.computeIfAbsent("redColorBlue", a -> "0"));
360                 redColor = (r << 16) + (g << 8) + b;
361             }
362             {
363                 int r, g, b;
364                 r = Integer.parseInt((String) properties.computeIfAbsent("secondaryColorRed", a -> "0"));
365                 g = Integer.parseInt((String) properties.computeIfAbsent("secondaryColorGreen", a -> "0"));
366                 b = Integer.parseInt((String) properties.computeIfAbsent("secondaryColorBlue", a -> "255"));
367                 secondaryColor = (r << 16) + (g << 8) + b;
368             }
369             saveConfig(file);
370         } catch (Exception e) {
371             e.printStackTrace();
372             reach = 12;
373             crossLevel = 7;
374             secondaryLevel = -1;
375             lineWidth = 1.0F;
376             redColor = 0xFF0000;
377             yellowColor = 0xFFFF00;
378             secondaryColor = 0x0000FF;
379             caching = false;
380             showNumber = false;
381             smoothLines = true;
382             underwater = false;
383             try {
384                 saveConfig(file);
385             } catch (IOException ex) {
386                 ex.printStackTrace();
387             }
388         }
389         if (secondaryLevel >= crossLevel) System.err.println("[Light Overlay] Secondary Level is higher than Cross Level");
390         lowerCrossLevel = Math.min(crossLevel, secondaryLevel);
391         higherCrossLevel = Math.max(crossLevel, secondaryLevel);
392         CHUNK_MAP.clear();
393         POS.clear();
394     }
395     
396     public static void saveConfig(File file) throws IOException {
397         FileOutputStream fos = new FileOutputStream(file, false);
398         fos.write("# Light Overlay Config".getBytes());
399         fos.write("\n".getBytes());
400         fos.write(("reach=" + reach).getBytes());
401         fos.write("\n".getBytes());
402         fos.write(("crossLevel=" + crossLevel).getBytes());
403         fos.write("\n".getBytes());
404         fos.write(("secondaryLevel=" + secondaryLevel).getBytes());
405         fos.write("\n".getBytes());
406         fos.write(("caching=" + caching).getBytes());
407         fos.write("\n".getBytes());
408         fos.write(("showNumber=" + showNumber).getBytes());
409         fos.write("\n".getBytes());
410         fos.write(("smoothLines=" + smoothLines).getBytes());
411         fos.write("\n".getBytes());
412         fos.write(("underwater=" + underwater).getBytes());
413         fos.write("\n".getBytes());
414         fos.write(("lineWidth=" + FORMAT.format(lineWidth)).getBytes());
415         fos.write("\n".getBytes());
416         fos.write(("yellowColorRed=" + ((yellowColor >> 16) & 255)).getBytes());
417         fos.write("\n".getBytes());
418         fos.write(("yellowColorGreen=" + ((yellowColor >> 8) & 255)).getBytes());
419         fos.write("\n".getBytes());
420         fos.write(("yellowColorBlue=" + (yellowColor & 255)).getBytes());
421         fos.write("\n".getBytes());
422         fos.write(("redColorRed=" + ((redColor >> 16) & 255)).getBytes());
423         fos.write("\n".getBytes());
424         fos.write(("redColorGreen=" + ((redColor >> 8) & 255)).getBytes());
425         fos.write("\n".getBytes());
426         fos.write(("redColorBlue=" + (redColor & 255)).getBytes());
427         fos.write("\n".getBytes());
428         fos.write(("secondaryColorRed=" + ((secondaryColor >> 16) & 255)).getBytes());
429         fos.write("\n".getBytes());
430         fos.write(("secondaryColorGreen=" + ((secondaryColor >> 8) & 255)).getBytes());
431         fos.write("\n".getBytes());
432         fos.write(("secondaryColorBlue=" + (secondaryColor & 255)).getBytes());
433         fos.close();
434     }
435     
436     private static KeyMapping createKeyBinding(ResourceLocation id, InputConstants.Type type, int code, String category) {
437         return new KeyMapping("key." + id.getNamespace() + "." + id.getPath(), type, code, category);
438     }
439     
440     private static final LazyLoadedValue<MethodHandle> IS_FRUSTUM_VISIBLE = new LazyLoadedValue<>(() -> {
441         try {
442             return MethodHandles.lookup().findStatic(Class.forName("me.shedaniel.lightoverlay." + Platform.getModLoader() + ".LightOverlayImpl"), "isFrustumVisible",
443                     MethodType.methodType(boolean.class, Frustum.class, double.class, double.class, double.class, double.class, double.class, double.class));
444         } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
445             throw new RuntimeException(e);
446         }
447     });
448     
449     private static boolean isFrustumVisible(Frustum frustum, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
450         try {
451             return (boolean) IS_FRUSTUM_VISIBLE.get().invokeExact(frustum, minX, minY, minZ, maxX, maxY, maxZ);
452         } catch (Throwable throwable) {
453             throw new RuntimeException(throwable);
454         }
455     }
456     
457     private static void registerDebugRenderer(Runnable runnable) {
458         try {
459             Class.forName("me.shedaniel.lightoverlay." + Platform.getModLoader() + ".LightOverlayImpl").getDeclaredField("debugRenderer").set(null, runnable);
460         } catch (Throwable throwable) {
461             throw new RuntimeException(throwable);
462         }
463     }
464     
465     private static void tick(Minecraft minecraft) {
466         while (enableOverlay.consumeClick())
467             enabled = !enabled;
468     
469         try {
470             ticks++;
471             if (CLIENT.player == null || !enabled) {
472                 POS.clear();
473                 CALCULATING_POS.clear();
474                 EXECUTOR.getQueue().clear();
475                 CHUNK_MAP.clear();
476             } else {
477                 LocalPlayer player = CLIENT.player;
478                 ClientLevel world = CLIENT.level;
479                 CollisionContext collisionContext = CollisionContext.of(player);
480             
481                 if (!caching) {
482                     CALCULATING_POS.clear();
483                     POS.clear();
484                     CHUNK_MAP.clear();
485                     BlockPos playerPos = player.blockPosition();
486                     LayerLightEventListener block = world.getLightEngine().getLayerListener(LightLayer.BLOCK);
487                     LayerLightEventListener sky = showNumber ? null : world.getLightEngine().getLayerListener(LightLayer.SKY);
488                     BlockPos.MutableBlockPos downPos = new BlockPos.MutableBlockPos();
489                     Iterable<BlockPos> iterate = BlockPos.betweenClosed(playerPos.getX() - reach, playerPos.getY() - reach, playerPos.getZ() - reach,
490                             playerPos.getX() + reach, playerPos.getY() + reach, playerPos.getZ() + reach);
491                     Long2ReferenceMap<Object> map = new Long2ReferenceOpenHashMap<>();
492                     CHUNK_MAP.put(new ChunkPos(0, 0), map);
493                     for (BlockPos blockPos : iterate) {
494                         downPos.set(blockPos.getX(), blockPos.getY() - 1, blockPos.getZ());
495                         if (showNumber) {
496                             int level = getCrossLevel(blockPos, downPos, world, block, collisionContext);
497                             if (level >= 0) {
498                                 map.put(blockPos.asLong(), Byte.valueOf((byte) level));
499                             }
500                         } else {
501                             CrossType type = getCrossType(blockPos, downPos, world, block, sky, collisionContext);
502                             if (type != CrossType.NONE) {
503                                 map.put(blockPos.asLong(), type);
504                             }
505                         }
506                     }
507                 } else {
508                     int playerPosX = ((int) player.getX()) >> 4;
509                     int playerPosZ = ((int) player.getZ()) >> 4;
510                     for (int chunkX = playerPosX - getChunkRange(); chunkX <= playerPosX + getChunkRange(); chunkX++) {
511                         for (int chunkZ = playerPosZ - getChunkRange(); chunkZ <= playerPosZ + getChunkRange(); chunkZ++) {
512                             if (Mth.abs(chunkX - playerPosX) > getChunkRange() || Mth.abs(chunkZ - playerPosZ) > getChunkRange())
513                                 continue;
514                             ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
515                             if (!CHUNK_MAP.containsKey(chunkPos))
516                                 queueChunk(chunkPos);
517                         }
518                     }
519                     for (int p = 0; p < 3; p++) {
520                         if (EXECUTOR.getQueue().size() >= Runtime.getRuntime().availableProcessors()) break;
521                         double d1 = Double.MAX_VALUE, d2 = Double.MAX_VALUE, d3 = Double.MAX_VALUE;
522                         ChunkPos c1 = null, c2 = null, c3 = null;
523                         synchronized (POS) {
524                             Iterator<ChunkPos> iterator = POS.iterator();
525                             while (iterator.hasNext()) {
526                                 ChunkPos pos = iterator.next();
527                                 if (Mth.abs(pos.x - playerPosX) > getChunkRange() || Mth.abs(pos.z - playerPosZ) > getChunkRange() || CALCULATING_POS.contains(pos)) {
528                                     iterator.remove();
529                                 } else {
530                                     if (isFrustumVisible(frustum, pos.getMinBlockX(), 0, pos.getMinBlockZ(), pos.getMaxBlockX(), 256, pos.getMaxBlockZ())) {
531                                         int i = Math.abs(pos.x - playerPosX);
532                                         int j = Math.abs(pos.z - playerPosZ);
533                                         double distance = Math.sqrt(i * i + j * j);
534                                         if (distance < d1) {
535                                             d3 = d2;
536                                             d2 = d1;
537                                             d1 = distance;
538                                             c3 = c2;
539                                             c2 = c1;
540                                             c1 = pos;
541                                             iterator.remove();
542                                         } else if (distance < d2) {
543                                             d3 = d2;
544                                             d2 = distance;
545                                             c3 = c2;
546                                             c2 = pos;
547                                             iterator.remove();
548                                         } else if (distance < d3) {
549                                             d3 = distance;
550                                             c3 = pos;
551                                             iterator.remove();
552                                         }
553                                     }
554                                 }
555                             }
556                         }
557                         ChunkPos finalC1 = c1;
558                         ChunkPos finalC2 = c2;
559                         ChunkPos finalC3 = c3;
560                         if (finalC1 != null) {
561                             CALCULATING_POS.add(finalC1);
562                             if (finalC2 != null) {
563                                 CALCULATING_POS.add(finalC2);
564                                 if (finalC3 != null) {
565                                     CALCULATING_POS.add(finalC3);
566                                 }
567                             }
568                             EXECUTOR.submit(() -> {
569                                 int playerPosX1 = ((int) CLIENT.player.getX()) >> 4;
570                                 int playerPosZ1 = ((int) CLIENT.player.getZ()) >> 4;
571                                 if (finalC1 != null) processChunk(finalC1, playerPosX1, playerPosZ1, collisionContext);
572                                 if (finalC2 != null) processChunk(finalC2, playerPosX1, playerPosZ1, collisionContext);
573                                 if (finalC3 != null) processChunk(finalC3, playerPosX1, playerPosZ1, collisionContext);
574                             });
575                         }
576                     }
577                     if (ticks % 50 == 0) {
578                         CHUNK_MAP.entrySet().removeIf(pos -> Mth.abs(pos.getKey().x - playerPosX) > getChunkRange() * 2 || Mth.abs(pos.getKey().z - playerPosZ) > getChunkRange() * 2);
579                     }
580                 }
581             }
582         } catch (Throwable throwable) {
583             LogManager.getLogger().throwing(throwable);
584         }
585     }
586     
587     private enum CrossType {
588         YELLOW,
589         RED,
590         SECONDARY,
591         NONE
592     }
593 }