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