1 package me.shedaniel.lightoverlay.fabric;
3 import com.google.common.collect.Lists;
4 import com.google.common.collect.Maps;
5 import com.mojang.blaze3d.platform.GlStateManager;
6 import com.mojang.blaze3d.systems.RenderSystem;
7 import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
8 import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
9 import me.shedaniel.cloth.api.client.events.v0.ClothClientHooks;
10 import net.fabricmc.api.ClientModInitializer;
11 import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
12 import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
13 import net.fabricmc.loader.api.FabricLoader;
14 import net.minecraft.block.Block;
15 import net.minecraft.block.BlockState;
16 import net.minecraft.client.MinecraftClient;
17 import net.minecraft.client.font.TextRenderer;
18 import net.minecraft.client.network.ClientPlayerEntity;
19 import net.minecraft.client.options.KeyBinding;
20 import net.minecraft.client.render.Camera;
21 import net.minecraft.client.render.Tessellator;
22 import net.minecraft.client.render.VertexConsumerProvider;
23 import net.minecraft.client.util.InputUtil;
24 import net.minecraft.client.util.math.Rotation3;
25 import net.minecraft.client.world.ClientWorld;
26 import net.minecraft.entity.Entity;
27 import net.minecraft.entity.EntityCategory;
28 import net.minecraft.entity.EntityContext;
29 import net.minecraft.entity.EntityType;
30 import net.minecraft.entity.player.PlayerEntity;
31 import net.minecraft.tag.BlockTags;
32 import net.minecraft.util.Identifier;
33 import net.minecraft.util.math.*;
34 import net.minecraft.util.shape.VoxelShape;
35 import net.minecraft.world.BlockView;
36 import net.minecraft.world.LightType;
37 import net.minecraft.world.World;
38 import net.minecraft.world.biome.SpawnSettings;
39 import net.minecraft.world.chunk.ChunkStatus;
40 import net.minecraft.world.chunk.WorldChunk;
41 import net.minecraft.world.chunk.light.ChunkLightingView;
42 import org.apache.logging.log4j.LogManager;
43 import org.lwjgl.opengl.GL11;
46 import java.io.FileInputStream;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.text.DecimalFormat;
51 import java.util.concurrent.Executors;
52 import java.util.concurrent.ThreadPoolExecutor;
54 public class LightOverlay implements ClientModInitializer {
56 static final DecimalFormat FORMAT = new DecimalFormat("#.#");
57 private static final String KEYBIND_CATEGORY = "key.lightoverlay.category";
58 private static final Identifier ENABLE_OVERLAY_KEYBIND = new Identifier("lightoverlay", "enable_overlay");
59 static int reach = 12;
60 static int crossLevel = 7;
61 static int secondaryLevel = -1;
62 static int lowerCrossLevel = -1;
63 static int higherCrossLevel = -1;
64 static boolean caching = false;
65 static boolean showNumber = false;
66 static boolean smoothLines = true;
67 static boolean underwater = false;
68 static float lineWidth = 1.0F;
69 static int yellowColor = 0xFFFF00, redColor = 0xFF0000, secondaryColor = 0x0000FF;
70 static File configFile = new File(FabricLoader.getInstance().getConfigDir().toFile(), "lightoverlay.properties");
71 private static final KeyBinding ENABLE_OVERLAY = createKeyBinding(ENABLE_OVERLAY_KEYBIND, InputUtil.Type.KEYSYM, 296, KEYBIND_CATEGORY);
72 private static boolean enabled = false;
73 private static EntityType<Entity> testingEntityType;
74 private static int threadNumber = 0;
75 private static final ThreadPoolExecutor EXECUTOR = (ThreadPoolExecutor) Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
76 Thread thread = new Thread(r, "light-overlay-" + threadNumber++);
77 thread.setDaemon(true);
80 private static final List<ChunkPos> POS = Lists.newCopyOnWriteArrayList();
81 private static final Map<ChunkPos, Long2ReferenceMap<Object>> CHUNK_MAP = Maps.newConcurrentMap();
82 private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
83 private static long ticks = 0;
86 ClientTickEvents.END_CLIENT_TICK.register(client -> {
89 if (CLIENT.player == null || !enabled) {
96 ClientPlayerEntity player = CLIENT.player;
97 ClientWorld world = CLIENT.world;
98 BlockPos playerPos = player.getSenseCenterPos();
99 EntityContext entityContext = EntityContext.of(player);
100 ChunkLightingView block = world.getLightingProvider().get(LightType.BLOCK);
101 ChunkLightingView sky = showNumber ? null : world.getLightingProvider().get(LightType.SKY);
102 BlockPos.Mutable downPos = new BlockPos.Mutable();
103 Iterable<BlockPos> iterate = BlockPos.iterate(playerPos.getX() - reach, playerPos.getY() - reach, playerPos.getZ() - reach,
104 playerPos.getX() + reach, playerPos.getY() + reach, playerPos.getZ() + reach);
105 Long2ReferenceMap<Object> map = new Long2ReferenceOpenHashMap<>();
106 CHUNK_MAP.put(new ChunkPos(0, 0), map);
107 for (BlockPos blockPos : iterate) {
108 downPos.set(blockPos.getX(), blockPos.getY() - 1, blockPos.getZ());
110 int level = getCrossLevel(blockPos, downPos, world, block, entityContext);
112 map.put(blockPos.asLong(), Integer.valueOf(level));
115 CrossType type = getCrossType(blockPos, downPos, world, block, sky, entityContext);
116 if (type != CrossType.NONE) {
117 map.put(blockPos.asLong(), type);
122 ClientPlayerEntity player = CLIENT.player;
123 ClientWorld world = CLIENT.world;
124 EntityContext entityContext = EntityContext.of(player);
125 Vec3d[] playerPos = {null};
126 int playerPosX = ((int) player.getX()) >> 4;
127 int playerPosZ = ((int) player.getZ()) >> 4;
128 if (ticks % 20 == 0) {
129 for (int chunkX = playerPosX - getChunkRange(); chunkX <= playerPosX + getChunkRange(); chunkX++) {
130 for (int chunkZ = playerPosZ - getChunkRange(); chunkZ <= playerPosZ + getChunkRange(); chunkZ++) {
131 ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
132 if (!CHUNK_MAP.containsKey(chunkPos))
133 queueChunk(chunkPos);
137 POS.removeIf(pos -> MathHelper.abs(pos.x - playerPosX) > getChunkRange() || MathHelper.abs(pos.z - playerPosZ) > getChunkRange());
138 for (int k = 0; k < 2; k++) {
139 if (!POS.isEmpty()) {
140 if (playerPos[0] == null) {
141 playerPos[0] = player.getPos();
143 ChunkPos pos = POS.stream().min(Comparator.comparingDouble(value -> {
144 int i = Math.abs(value.x - playerPosX);
145 int j = Math.abs(value.z - playerPosZ);
146 return i * i + j * j;
149 EXECUTOR.submit(() -> {
151 calculateChunk(world.getChunkManager().getChunk(pos.x, pos.z, ChunkStatus.FULL, false), world, pos, entityContext);
152 } catch (Throwable throwable) {
153 LogManager.getLogger().throwing(throwable);
158 if (ticks % 50 == 0) {
159 CHUNK_MAP.entrySet().removeIf(pos -> MathHelper.abs(pos.getKey().x - playerPosX) > getChunkRange() * 2 || MathHelper.abs(pos.getKey().z - playerPosZ) > getChunkRange() * 2);
163 } catch (Throwable throwable) {
164 LogManager.getLogger().throwing(throwable);
169 public static void queueChunkAndNear(ChunkPos pos) {
170 for (int xOffset = -1; xOffset <= 1; xOffset++) {
171 for (int zOffset = -1; zOffset <= 1; zOffset++) {
172 queueChunk(new ChunkPos(pos.x + xOffset, pos.z + zOffset));
177 public static void queueChunk(ChunkPos pos) {
179 if (!POS.contains(pos))
183 public static int getChunkRange() {
184 return Math.max(MathHelper.ceil(reach / 16f), 1);
187 private static void calculateChunk(WorldChunk chunk, World world, ChunkPos chunkPos, EntityContext entityContext) {
188 if (world != null && chunk != null) {
189 Long2ReferenceMap<Object> map = new Long2ReferenceOpenHashMap<>();
190 ChunkLightingView block = world.getLightingProvider().get(LightType.BLOCK);
191 ChunkLightingView sky = showNumber ? null : world.getLightingProvider().get(LightType.SKY);
192 for (BlockPos pos : BlockPos.iterate(chunkPos.getStartX(), 0, chunkPos.getStartZ(), chunkPos.getEndX(), 256, chunkPos.getEndZ())) {
193 BlockPos down = pos.down();
195 int level = LightOverlay.getCrossLevel(pos, down, chunk, block, entityContext);
197 map.put(pos.asLong(), Integer.valueOf(level));
200 SpawnSettings spawnSettings = world.getBiomeAccess().getBiome(pos).getSpawnSettings();
201 if (spawnSettings.getCreatureSpawnProbability() > 0 && !spawnSettings.getSpawnEntry(EntityCategory.MONSTER).isEmpty()) {
202 CrossType type = LightOverlay.getCrossType(pos, down, chunk, block, sky, entityContext);
203 if (type != CrossType.NONE) {
204 map.put(pos.asLong(), type);
209 CHUNK_MAP.put(chunkPos, map);
211 CHUNK_MAP.remove(chunkPos);
215 public static CrossType getCrossType(BlockPos pos, BlockPos down, BlockView world, ChunkLightingView block, ChunkLightingView sky, EntityContext entityContext) {
216 BlockState blockBelowState = world.getBlockState(down);
217 BlockState blockUpperState = world.getBlockState(pos);
218 VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
219 if (!underwater && !blockUpperState.getFluidState().isEmpty())
220 return CrossType.NONE;
221 // Check if the outline is full
222 if (Block.isFaceFullSquare(upperCollisionShape, Direction.UP))
223 return CrossType.NONE;
224 // TODO: Not to hard code no redstone
225 if (blockUpperState.emitsRedstonePower())
226 return CrossType.NONE;
227 // Check if the collision has a bump
228 if (upperCollisionShape.getMaximum(Direction.Axis.Y) > 0)
229 return CrossType.NONE;
230 if (blockUpperState.getBlock().isIn(BlockTags.RAILS))
231 return CrossType.NONE;
232 // Check block state allow spawning (excludes bedrock and barriers automatically)
233 if (!blockBelowState.allowsSpawning(world, down, testingEntityType))
234 return CrossType.NONE;
235 int blockLightLevel = block.getLightLevel(pos);
236 int skyLightLevel = sky.getLightLevel(pos);
237 if (blockLightLevel > higherCrossLevel)
238 return CrossType.NONE;
239 if (skyLightLevel > higherCrossLevel)
240 return CrossType.YELLOW;
241 return lowerCrossLevel >= 0 && blockLightLevel > lowerCrossLevel ? CrossType.SECONDARY : CrossType.RED;
244 public static int getCrossLevel(BlockPos pos, BlockPos down, BlockView world, ChunkLightingView view, EntityContext entityContext) {
245 BlockState blockBelowState = world.getBlockState(down);
246 BlockState blockUpperState = world.getBlockState(pos);
247 VoxelShape collisionShape = blockBelowState.getCollisionShape(world, down, entityContext);
248 VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
249 if (!underwater && !blockUpperState.getFluidState().isEmpty())
251 if (!blockBelowState.getFluidState().isEmpty())
253 if (blockBelowState.isAir())
255 if (Block.isFaceFullSquare(upperCollisionShape, Direction.DOWN))
257 return view.getLightLevel(pos);
260 public static void renderCross(Camera camera, World world, BlockPos pos, int color, EntityContext entityContext) {
261 double d0 = camera.getPos().x;
262 double d1 = camera.getPos().y - .005D;
263 VoxelShape upperOutlineShape = world.getBlockState(pos).getOutlineShape(world, pos, entityContext);
264 if (!upperOutlineShape.isEmpty())
265 d1 -= upperOutlineShape.getMaximum(Direction.Axis.Y);
266 double d2 = camera.getPos().z;
268 int red = (color >> 16) & 255;
269 int green = (color >> 8) & 255;
270 int blue = color & 255;
274 RenderSystem.color4f(red / 255f, green / 255f, blue / 255f, 1f);
275 GL11.glVertex3d(x + .01 - d0, y - d1, z + .01 - d2);
276 GL11.glVertex3d(x - .01 + 1 - d0, y - d1, z - .01 + 1 - d2);
277 GL11.glVertex3d(x - .01 + 1 - d0, y - d1, z + .01 - d2);
278 GL11.glVertex3d(x + .01 - d0, y - d1, z - .01 + 1 - d2);
281 @SuppressWarnings("deprecation")
282 public static void renderLevel(MinecraftClient client, Camera camera, World world, BlockPos pos, BlockPos down, int level, EntityContext entityContext) {
283 String text = String.valueOf(level);
284 TextRenderer textRenderer_1 = client.textRenderer;
285 double double_4 = camera.getPos().x;
286 double double_5 = camera.getPos().y;
287 VoxelShape upperOutlineShape = world.getBlockState(down).getOutlineShape(world, down, entityContext);
288 if (!upperOutlineShape.isEmpty())
289 double_5 += 1 - upperOutlineShape.getMaximum(Direction.Axis.Y);
290 double double_6 = camera.getPos().z;
291 RenderSystem.pushMatrix();
292 RenderSystem.translatef((float) (pos.getX() + 0.5f - double_4), (float) (pos.getY() - double_5) + 0.005f, (float) (pos.getZ() + 0.5f - double_6));
293 RenderSystem.rotatef(90, 1, 0, 0);
294 RenderSystem.normal3f(0.0F, 1.0F, 0.0F);
296 RenderSystem.scalef(-size, -size, size);
297 float float_3 = (float) (-textRenderer_1.getStringWidth(text)) / 2.0F + 0.4f;
298 RenderSystem.enableAlphaTest();
299 VertexConsumerProvider.Immediate immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().getBuffer());
300 textRenderer_1.draw(text, float_3, -3.5f, level > higherCrossLevel ? 0xff042404 : (lowerCrossLevel >= 0 && level > lowerCrossLevel ? 0xff0066ff : 0xff731111), false, Rotation3.identity().getMatrix(), immediate, false, 0, 15728880);
302 RenderSystem.popMatrix();
305 static void loadConfig(File file) {
308 yellowColor = 0xFFFF00;
309 secondaryColor = 0x0000FF;
310 if (!file.exists() || !file.canRead())
312 FileInputStream fis = new FileInputStream(file);
313 Properties properties = new Properties();
314 properties.load(fis);
316 reach = Integer.parseInt((String) properties.computeIfAbsent("reach", a -> "12"));
317 crossLevel = Integer.parseInt((String) properties.computeIfAbsent("crossLevel", a -> "7"));
318 secondaryLevel = Integer.parseInt((String) properties.computeIfAbsent("secondaryLevel", a -> "-1"));
319 caching = ((String) properties.computeIfAbsent("caching", a -> "false")).equalsIgnoreCase("true");
320 showNumber = ((String) properties.computeIfAbsent("showNumber", a -> "false")).equalsIgnoreCase("true");
321 smoothLines = ((String) properties.computeIfAbsent("smoothLines", a -> "true")).equalsIgnoreCase("true");
322 underwater = ((String) properties.computeIfAbsent("underwater", a -> "false")).equalsIgnoreCase("true");
323 lineWidth = Float.parseFloat((String) properties.computeIfAbsent("lineWidth", a -> "1"));
326 r = Integer.parseInt((String) properties.computeIfAbsent("yellowColorRed", a -> "255"));
327 g = Integer.parseInt((String) properties.computeIfAbsent("yellowColorGreen", a -> "255"));
328 b = Integer.parseInt((String) properties.computeIfAbsent("yellowColorBlue", a -> "0"));
329 yellowColor = (r << 16) + (g << 8) + b;
333 r = Integer.parseInt((String) properties.computeIfAbsent("redColorRed", a -> "255"));
334 g = Integer.parseInt((String) properties.computeIfAbsent("redColorGreen", a -> "0"));
335 b = Integer.parseInt((String) properties.computeIfAbsent("redColorBlue", a -> "0"));
336 redColor = (r << 16) + (g << 8) + b;
340 r = Integer.parseInt((String) properties.computeIfAbsent("secondaryColorRed", a -> "0"));
341 g = Integer.parseInt((String) properties.computeIfAbsent("secondaryColorGreen", a -> "0"));
342 b = Integer.parseInt((String) properties.computeIfAbsent("secondaryColorBlue", a -> "255"));
343 secondaryColor = (r << 16) + (g << 8) + b;
346 } catch (Exception e) {
353 yellowColor = 0xFFFF00;
354 secondaryColor = 0x0000FF;
361 } catch (IOException ex) {
362 ex.printStackTrace();
365 if (secondaryLevel >= crossLevel) System.err.println("[Light Overlay] Secondary Level is higher than Cross Level");
366 lowerCrossLevel = Math.min(crossLevel, secondaryLevel);
367 higherCrossLevel = Math.max(crossLevel, secondaryLevel);
372 static void saveConfig(File file) throws IOException {
373 FileOutputStream fos = new FileOutputStream(file, false);
374 fos.write("# Light Overlay Config".getBytes());
375 fos.write("\n".getBytes());
376 fos.write(("reach=" + reach).getBytes());
377 fos.write("\n".getBytes());
378 fos.write(("crossLevel=" + crossLevel).getBytes());
379 fos.write("\n".getBytes());
380 fos.write(("secondaryLevel=" + secondaryLevel).getBytes());
381 fos.write("\n".getBytes());
382 fos.write(("caching=" + caching).getBytes());
383 fos.write("\n".getBytes());
384 fos.write(("showNumber=" + showNumber).getBytes());
385 fos.write("\n".getBytes());
386 fos.write(("smoothLines=" + smoothLines).getBytes());
387 fos.write("\n".getBytes());
388 fos.write(("underwater=" + underwater).getBytes());
389 fos.write("\n".getBytes());
390 fos.write(("lineWidth=" + FORMAT.format(lineWidth)).getBytes());
391 fos.write("\n".getBytes());
392 fos.write(("yellowColorRed=" + ((yellowColor >> 16) & 255)).getBytes());
393 fos.write("\n".getBytes());
394 fos.write(("yellowColorGreen=" + ((yellowColor >> 8) & 255)).getBytes());
395 fos.write("\n".getBytes());
396 fos.write(("yellowColorBlue=" + (yellowColor & 255)).getBytes());
397 fos.write("\n".getBytes());
398 fos.write(("redColorRed=" + ((redColor >> 16) & 255)).getBytes());
399 fos.write("\n".getBytes());
400 fos.write(("redColorGreen=" + ((redColor >> 8) & 255)).getBytes());
401 fos.write("\n".getBytes());
402 fos.write(("redColorBlue=" + (redColor & 255)).getBytes());
403 fos.write("\n".getBytes());
404 fos.write(("secondaryColorRed=" + ((secondaryColor >> 16) & 255)).getBytes());
405 fos.write("\n".getBytes());
406 fos.write(("secondaryColorGreen=" + ((secondaryColor >> 8) & 255)).getBytes());
407 fos.write("\n".getBytes());
408 fos.write(("secondaryColorBlue=" + (secondaryColor & 255)).getBytes());
412 private static KeyBinding createKeyBinding(Identifier id, InputUtil.Type type, int code, String category) {
413 return KeyBindingHelper.registerKeyBinding(new KeyBinding("key." + id.getNamespace() + "." + id.getPath(), type, code, category));
417 public void onInitializeClient() {
419 loadConfig(configFile);
422 testingEntityType = EntityType.Builder.create(EntityCategory.MONSTER).setDimensions(0f, 0f).disableSaving().build(null);
423 ClientTickEvents.END_CLIENT_TICK.register(minecraftClient -> {
424 while (ENABLE_OVERLAY.wasPressed())
427 ClothClientHooks.DEBUG_RENDER_PRE.register(() -> {
428 if (LightOverlay.enabled) {
429 PlayerEntity playerEntity = CLIENT.player;
430 int playerPosX = ((int) playerEntity.getX()) >> 4;
431 int playerPosZ = ((int) playerEntity.getZ()) >> 4;
432 EntityContext entityContext = EntityContext.of(playerEntity);
433 World world = CLIENT.world;
434 BlockPos playerPos = new BlockPos(playerEntity.getX(), playerEntity.getY(), playerEntity.getZ());
435 Camera camera = CLIENT.gameRenderer.getCamera();
437 RenderSystem.enableTexture();
438 RenderSystem.depthMask(true);
439 BlockPos.Mutable mutable = new BlockPos.Mutable();
440 for (Map.Entry<ChunkPos, Long2ReferenceMap<Object>> entry : CHUNK_MAP.entrySet()) {
441 if (caching && (MathHelper.abs(entry.getKey().x - playerPosX) > getChunkRange() || MathHelper.abs(entry.getKey().z - playerPosZ) > getChunkRange())) {
444 for (Long2ReferenceMap.Entry<Object> objectEntry : entry.getValue().long2ReferenceEntrySet()) {
445 if (objectEntry.getValue() instanceof Integer) {
446 mutable.set(BlockPos.unpackLongX(objectEntry.getLongKey()), BlockPos.unpackLongY(objectEntry.getLongKey()), BlockPos.unpackLongZ(objectEntry.getLongKey()));
447 if (mutable.isWithinDistance(playerPos, reach)) {
448 BlockPos down = mutable.down();
449 LightOverlay.renderLevel(CLIENT, camera, world, mutable, down, (Integer) objectEntry.getValue(), entityContext);
454 RenderSystem.enableDepthTest();
456 RenderSystem.enableDepthTest();
457 RenderSystem.disableTexture();
458 RenderSystem.enableBlend();
459 RenderSystem.blendFunc(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA);
460 if (smoothLines) GL11.glEnable(GL11.GL_LINE_SMOOTH);
461 GL11.glLineWidth(lineWidth);
462 GL11.glBegin(GL11.GL_LINES);
463 BlockPos.Mutable mutable = new BlockPos.Mutable();
464 for (Map.Entry<ChunkPos, Long2ReferenceMap<Object>> entry : CHUNK_MAP.entrySet()) {
465 if (caching && (MathHelper.abs(entry.getKey().x - playerPosX) > getChunkRange() || MathHelper.abs(entry.getKey().z - playerPosZ) > getChunkRange())) {
468 for (Long2ReferenceMap.Entry<Object> objectEntry : entry.getValue().long2ReferenceEntrySet()) {
469 if (objectEntry.getValue() instanceof CrossType) {
470 mutable.set(BlockPos.unpackLongX(objectEntry.getLongKey()), BlockPos.unpackLongY(objectEntry.getLongKey()), BlockPos.unpackLongZ(objectEntry.getLongKey()));
471 if (mutable.isWithinDistance(playerPos, reach)) {
472 int color = objectEntry.getValue() == CrossType.RED ? redColor : objectEntry.getValue() == CrossType.YELLOW ? yellowColor : secondaryColor;
473 LightOverlay.renderCross(camera, world, mutable, color, entityContext);
479 RenderSystem.disableBlend();
480 RenderSystem.enableTexture();
481 if (smoothLines) GL11.glDisable(GL11.GL_LINE_SMOOTH);
487 private enum CrossType {