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 me.shedaniel.cloth.hooks.ClothClientHooks;
8 import net.fabricmc.api.ClientModInitializer;
9 import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding;
10 import net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry;
11 import net.fabricmc.fabric.api.event.client.ClientTickCallback;
12 import net.fabricmc.loader.api.FabricLoader;
13 import net.minecraft.block.Block;
14 import net.minecraft.block.BlockState;
15 import net.minecraft.client.MinecraftClient;
16 import net.minecraft.client.font.TextRenderer;
17 import net.minecraft.client.network.ClientPlayerEntity;
18 import net.minecraft.client.render.*;
19 import net.minecraft.client.util.InputUtil;
20 import net.minecraft.client.util.math.Rotation3;
21 import net.minecraft.client.world.ClientWorld;
22 import net.minecraft.entity.Entity;
23 import net.minecraft.entity.EntityCategory;
24 import net.minecraft.entity.EntityContext;
25 import net.minecraft.entity.EntityType;
26 import net.minecraft.entity.player.PlayerEntity;
27 import net.minecraft.tag.BlockTags;
28 import net.minecraft.text.TranslatableText;
29 import net.minecraft.util.Identifier;
30 import net.minecraft.util.math.*;
31 import net.minecraft.util.shape.VoxelShape;
32 import net.minecraft.world.BlockView;
33 import net.minecraft.world.LightType;
34 import net.minecraft.world.World;
35 import net.minecraft.world.biome.Biome;
36 import net.minecraft.world.chunk.ChunkStatus;
37 import net.minecraft.world.chunk.WorldChunk;
38 import net.minecraft.world.chunk.light.ChunkLightingView;
39 import org.apache.logging.log4j.LogManager;
40 import org.lwjgl.opengl.GL11;
43 import java.io.FileInputStream;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.text.DecimalFormat;
48 import java.util.concurrent.ExecutorService;
49 import java.util.concurrent.Executors;
51 public class LightOverlay implements ClientModInitializer {
52 static final DecimalFormat FORMAT = new DecimalFormat("#.#");
53 private static final String KEYBIND_CATEGORY = "key.lightoverlay.category";
54 private static final Identifier ENABLE_OVERLAY_KEYBIND = new Identifier("lightoverlay", "enable_overlay");
55 private static final Identifier INCREASE_REACH_KEYBIND = new Identifier("lightoverlay", "increase_reach");
56 private static final Identifier DECREASE_REACH_KEYBIND = new Identifier("lightoverlay", "decrease_reach");
57 private static final Identifier INCREASE_LINE_WIDTH_KEYBIND = new Identifier("lightoverlay", "increase_line_width");
58 private static final Identifier DECREASE_LINE_WIDTH_KEYBIND = new Identifier("lightoverlay", "decrease_line_width");
59 static int reach = 12;
60 static int crossLevel = 7;
61 static boolean showNumber = false;
62 static boolean smoothLines = true;
63 static boolean underwater = false;
64 static float lineWidth = 1.0F;
65 static int yellowColor = 0xFFFF00, redColor = 0xFF0000;
66 static File configFile = new File(FabricLoader.getInstance().getConfigDirectory(), "lightoverlay.properties");
67 private static FabricKeyBinding enableOverlay, increaseReach, decreaseReach, increaseLineWidth, decreaseLineWidth;
68 private static boolean enabled = false;
69 private static EntityType<Entity> testingEntityType;
70 private static int threadNumber = 0;
71 private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
72 Thread thread = new Thread(r, "light-overlay-" + threadNumber++);
73 thread.setDaemon(true);
76 private static final List<ChunkPos> POS = Lists.newCopyOnWriteArrayList();
77 private static final Map<ChunkPos, Map<Long, Object>> CHUNK_MAP = Maps.newConcurrentMap();
78 private static long ticks = 0;
81 ClientTickCallback.EVENT.register(client -> {
84 if (MinecraftClient.getInstance().player == null || !enabled) {
88 ClientPlayerEntity player = MinecraftClient.getInstance().player;
89 ClientWorld world = MinecraftClient.getInstance().world;
90 EntityContext entityContext = EntityContext.of(player);
91 Vec3d[] playerPos = {null};
92 int playerPosX = ((int) player.getX()) >> 4;
93 int playerPosZ = ((int) player.getZ()) >> 4;
94 if (ticks % 20 == 0) {
95 for (int chunkX = playerPosX - getChunkRange(); chunkX <= playerPosX + getChunkRange(); chunkX++) {
96 for (int chunkZ = playerPosZ - getChunkRange(); chunkZ <= playerPosZ + getChunkRange(); chunkZ++) {
97 ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
98 if (!CHUNK_MAP.containsKey(chunkPos))
103 if (!POS.isEmpty()) {
104 if (playerPos[0] == null) {
105 playerPos[0] = player.getPos();
107 ChunkPos pos = POS.stream().min(Comparator.comparingDouble(value -> value.toBlockPos(8, 0, 8).getSquaredDistance(playerPos[0].x, 0, playerPos[0].z, false))).get();
108 EXECUTOR.submit(() -> {
109 if (MathHelper.abs(pos.x - playerPosX) <= getChunkRange() && MathHelper.abs(pos.z - playerPosZ) <= getChunkRange()) {
110 calculateChunk(world.getChunkManager().getChunk(pos.x, pos.z, ChunkStatus.FULL, false), world, pos, entityContext);
112 CHUNK_MAP.remove(pos);
117 Iterator<Map.Entry<ChunkPos, Map<Long, Object>>> chunkMapIterator = CHUNK_MAP.entrySet().iterator();
118 while (chunkMapIterator.hasNext()) {
119 Map.Entry<ChunkPos, Map<Long, Object>> pos = chunkMapIterator.next();
120 if (MathHelper.abs(pos.getKey().x - playerPosX) > getChunkRange() * 2 || MathHelper.abs(pos.getKey().z - playerPosZ) > getChunkRange() * 2) {
121 chunkMapIterator.remove();
125 } catch (Exception e) {
126 LogManager.getLogger().throwing(e);
131 public static void queueChunkAndNear(ChunkPos pos) {
132 for (int xOffset = -1; xOffset <= 1; xOffset++) {
133 for (int zOffset = -1; zOffset <= 1; zOffset++) {
134 queueChunk(new ChunkPos(pos.x + xOffset, pos.z + zOffset));
139 public static void queueChunk(ChunkPos pos) {
140 if (!POS.contains(pos))
144 public static int getChunkRange() {
145 return Math.max(MathHelper.ceil(reach / 16f), 1);
148 private static void calculateChunk(WorldChunk chunk, World world, ChunkPos chunkPos, EntityContext entityContext) {
149 Map<Long, Object> map = Maps.newHashMap();
151 ChunkLightingView block = chunk.getLightingProvider().get(LightType.BLOCK);
152 ChunkLightingView sky = showNumber ? null : chunk.getLightingProvider().get(LightType.SKY);
153 for (BlockPos pos : BlockPos.iterate(chunkPos.getStartX(), 0, chunkPos.getStartZ(), chunkPos.getEndX(), 256, chunkPos.getEndZ())) {
154 BlockPos down = pos.down();
156 int level = LightOverlay.getCrossLevel(pos, down, chunk, block, entityContext);
158 map.put(pos.asLong(), level);
161 Biome biome = world.getBiomeAccess().getBiome(pos);
162 if (biome.getMaxSpawnLimit() > 0 && !biome.getEntitySpawnList(EntityCategory.MONSTER).isEmpty()) {
163 CrossType type = LightOverlay.getCrossType(pos, down, chunk, block, sky, entityContext);
164 if (type != CrossType.NONE) {
165 map.put(pos.asLong(), type);
171 CHUNK_MAP.put(chunkPos, map);
174 public static CrossType getCrossType(BlockPos pos, BlockPos down, BlockView world, ChunkLightingView block, ChunkLightingView sky, EntityContext entityContext) {
175 BlockState blockBelowState = world.getBlockState(down);
176 BlockState blockUpperState = world.getBlockState(pos);
177 VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
178 if (!underwater && !blockUpperState.getFluidState().isEmpty())
179 return CrossType.NONE;
180 // Check if the outline is full
181 if (Block.isFaceFullSquare(upperCollisionShape, Direction.UP))
182 return CrossType.NONE;
183 // TODO: Not to hard code no redstone
184 if (blockUpperState.emitsRedstonePower())
185 return CrossType.NONE;
186 // Check if the collision has a bump
187 if (upperCollisionShape.getMaximum(Direction.Axis.Y) > 0)
188 return CrossType.NONE;
189 if (blockUpperState.getBlock().matches(BlockTags.RAILS))
190 return CrossType.NONE;
191 // Check block state allow spawning (excludes bedrock and barriers automatically)
192 if (!blockBelowState.allowsSpawning(world, down, testingEntityType))
193 return CrossType.NONE;
194 if (block.getLightLevel(pos) > crossLevel)
195 return CrossType.NONE;
196 if (sky.getLightLevel(pos) > crossLevel)
197 return CrossType.YELLOW;
198 return CrossType.RED;
201 public static int getCrossLevel(BlockPos pos, BlockPos down, BlockView world, ChunkLightingView view, EntityContext entityContext) {
202 BlockState blockBelowState = world.getBlockState(down);
203 BlockState blockUpperState = world.getBlockState(pos);
204 VoxelShape collisionShape = blockBelowState.getCollisionShape(world, down, entityContext);
205 VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
206 if (!underwater && !blockUpperState.getFluidState().isEmpty())
208 if (!blockBelowState.getFluidState().isEmpty())
210 if (blockBelowState.isAir())
212 if (Block.isFaceFullSquare(upperCollisionShape, Direction.DOWN))
214 return view.getLightLevel(pos);
217 public static void renderCross(Tessellator tessellator, BufferBuilder buffer, Camera camera, World world, BlockPos pos, int color, EntityContext entityContext) {
218 double d0 = camera.getPos().x;
219 double d1 = camera.getPos().y - .005D;
220 VoxelShape upperOutlineShape = world.getBlockState(pos).getOutlineShape(world, pos, entityContext);
221 if (!upperOutlineShape.isEmpty())
222 d1 -= upperOutlineShape.getMaximum(Direction.Axis.Y);
223 double d2 = camera.getPos().z;
225 buffer.begin(1, VertexFormats.POSITION_COLOR);
226 int red = (color >> 16) & 255;
227 int green = (color >> 8) & 255;
228 int blue = color & 255;
229 buffer.vertex(pos.getX() + .01 - d0, pos.getY() - d1, pos.getZ() + .01 - d2).color(red, green, blue, 255).next();
230 buffer.vertex(pos.getX() - .01 + 1 - d0, pos.getY() - d1, pos.getZ() - .01 + 1 - d2).color(red, green, blue, 255).next();
231 buffer.vertex(pos.getX() - .01 + 1 - d0, pos.getY() - d1, pos.getZ() + .01 - d2).color(red, green, blue, 255).next();
232 buffer.vertex(pos.getX() + .01 - d0, pos.getY() - d1, pos.getZ() - .01 + 1 - d2).color(red, green, blue, 255).next();
236 public static void renderLevel(MinecraftClient client, Camera camera, World world, BlockPos pos, BlockPos down, int level, EntityContext entityContext) {
237 String string_1 = String.valueOf(level);
238 TextRenderer textRenderer_1 = client.textRenderer;
239 double double_4 = camera.getPos().x;
240 double double_5 = camera.getPos().y;
241 VoxelShape upperOutlineShape = world.getBlockState(down).getOutlineShape(world, down, entityContext);
242 if (!upperOutlineShape.isEmpty())
243 double_5 += 1 - upperOutlineShape.getMaximum(Direction.Axis.Y);
244 double double_6 = camera.getPos().z;
245 RenderSystem.pushMatrix();
246 RenderSystem.translatef((float) (pos.getX() + 0.5f - double_4), (float) (pos.getY() - double_5) + 0.005f, (float) (pos.getZ() + 0.5f - double_6));
247 RenderSystem.rotatef(90, 1, 0, 0);
248 RenderSystem.normal3f(0.0F, 1.0F, 0.0F);
250 RenderSystem.scalef(-size, -size, size);
251 float float_3 = (float) (-textRenderer_1.getStringWidth(string_1)) / 2.0F + 0.4f;
252 RenderSystem.enableAlphaTest();
253 VertexConsumerProvider.Immediate vertexConsumerProvider$Immediate_1 = VertexConsumerProvider.immediate(Tessellator.getInstance().getBuffer());
254 textRenderer_1.draw(string_1, float_3, -3.5f, level > crossLevel ? 0xff042404 : 0xff731111, false, Rotation3.identity().getMatrix(), vertexConsumerProvider$Immediate_1, false, 0, 15728880);
255 vertexConsumerProvider$Immediate_1.draw();
256 RenderSystem.popMatrix();
259 static void loadConfig(File file) {
262 yellowColor = 0xFFFF00;
263 if (!file.exists() || !file.canRead())
265 FileInputStream fis = new FileInputStream(file);
266 Properties properties = new Properties();
267 properties.load(fis);
269 reach = Integer.parseInt((String) properties.computeIfAbsent("reach", a -> "12"));
270 crossLevel = Integer.parseInt((String) properties.computeIfAbsent("crossLevel", a -> "7"));
271 showNumber = ((String) properties.computeIfAbsent("showNumber", a -> "false")).equalsIgnoreCase("true");
272 smoothLines = ((String) properties.computeIfAbsent("smoothLines", a -> "true")).equalsIgnoreCase("true");
273 underwater = ((String) properties.computeIfAbsent("underwater", a -> "false")).equalsIgnoreCase("true");
274 lineWidth = Float.parseFloat((String) properties.computeIfAbsent("lineWidth", a -> "1"));
277 r = Integer.parseInt((String) properties.computeIfAbsent("yellowColorRed", a -> "255"));
278 g = Integer.parseInt((String) properties.computeIfAbsent("yellowColorGreen", a -> "255"));
279 b = Integer.parseInt((String) properties.computeIfAbsent("yellowColorBlue", a -> "0"));
280 yellowColor = (r << 16) + (g << 8) + b;
284 r = Integer.parseInt((String) properties.computeIfAbsent("redColorRed", a -> "255"));
285 g = Integer.parseInt((String) properties.computeIfAbsent("redColorGreen", a -> "0"));
286 b = Integer.parseInt((String) properties.computeIfAbsent("redColorBlue", a -> "0"));
287 redColor = (r << 16) + (g << 8) + b;
290 } catch (Exception e) {
296 yellowColor = 0xFFFF00;
302 } catch (IOException ex) {
303 ex.printStackTrace();
310 static void saveConfig(File file) throws IOException {
311 FileOutputStream fos = new FileOutputStream(file, false);
312 fos.write("# Light Overlay Config".getBytes());
313 fos.write("\n".getBytes());
314 fos.write(("reach=" + reach).getBytes());
315 fos.write("\n".getBytes());
316 fos.write(("crossLevel=" + crossLevel).getBytes());
317 fos.write("\n".getBytes());
318 fos.write(("showNumber=" + showNumber).getBytes());
319 fos.write("\n".getBytes());
320 fos.write(("smoothLines=" + smoothLines).getBytes());
321 fos.write("\n".getBytes());
322 fos.write(("underwater=" + underwater).getBytes());
323 fos.write("\n".getBytes());
324 fos.write(("lineWidth=" + FORMAT.format(lineWidth)).getBytes());
325 fos.write("\n".getBytes());
326 fos.write(("yellowColorRed=" + ((yellowColor >> 16) & 255)).getBytes());
327 fos.write("\n".getBytes());
328 fos.write(("yellowColorGreen=" + ((yellowColor >> 8) & 255)).getBytes());
329 fos.write("\n".getBytes());
330 fos.write(("yellowColorBlue=" + (yellowColor & 255)).getBytes());
331 fos.write("\n".getBytes());
332 fos.write(("redColorRed=" + ((redColor >> 16) & 255)).getBytes());
333 fos.write("\n".getBytes());
334 fos.write(("redColorGreen=" + ((redColor >> 8) & 255)).getBytes());
335 fos.write("\n".getBytes());
336 fos.write(("redColorBlue=" + (redColor & 255)).getBytes());
341 public void onInitializeClient() {
343 loadConfig(configFile);
346 testingEntityType = EntityType.Builder.create(EntityCategory.MONSTER).setDimensions(0f, 0f).disableSaving().build(null);
347 MinecraftClient client = MinecraftClient.getInstance();
348 KeyBindingRegistry.INSTANCE.addCategory(KEYBIND_CATEGORY);
349 KeyBindingRegistry.INSTANCE.register(enableOverlay = FabricKeyBinding.Builder.create(ENABLE_OVERLAY_KEYBIND, InputUtil.Type.KEYSYM, 296, KEYBIND_CATEGORY).build());
350 KeyBindingRegistry.INSTANCE.register(increaseReach = FabricKeyBinding.Builder.create(INCREASE_REACH_KEYBIND, InputUtil.Type.KEYSYM, -1, KEYBIND_CATEGORY).build());
351 KeyBindingRegistry.INSTANCE.register(decreaseReach = FabricKeyBinding.Builder.create(DECREASE_REACH_KEYBIND, InputUtil.Type.KEYSYM, -1, KEYBIND_CATEGORY).build());
352 KeyBindingRegistry.INSTANCE.register(increaseLineWidth = FabricKeyBinding.Builder.create(INCREASE_LINE_WIDTH_KEYBIND, InputUtil.Type.KEYSYM, -1, KEYBIND_CATEGORY).build());
353 KeyBindingRegistry.INSTANCE.register(decreaseLineWidth = FabricKeyBinding.Builder.create(DECREASE_LINE_WIDTH_KEYBIND, InputUtil.Type.KEYSYM, -1, KEYBIND_CATEGORY).build());
354 ClothClientHooks.HANDLE_INPUT.register(minecraftClient -> {
355 while (enableOverlay.wasPressed())
357 while (increaseReach.wasPressed()) {
361 saveConfig(configFile);
362 } catch (IOException e) {
365 client.player.addChatMessage(new TranslatableText("text.lightoverlay.current_reach", reach), false);
367 while (decreaseReach.wasPressed()) {
371 saveConfig(configFile);
372 } catch (IOException e) {
375 client.player.addChatMessage(new TranslatableText("text.lightoverlay.current_reach", reach), false);
377 while (increaseLineWidth.wasPressed()) {
381 saveConfig(configFile);
382 } catch (IOException e) {
385 client.player.addChatMessage(new TranslatableText("text.lightoverlay.current_line_width", FORMAT.format(lineWidth)), false);
387 while (decreaseLineWidth.wasPressed()) {
391 saveConfig(configFile);
392 } catch (IOException e) {
395 client.player.addChatMessage(new TranslatableText("text.lightoverlay.current_line_width", FORMAT.format(lineWidth)), false);
398 ClothClientHooks.DEBUG_RENDER_PRE.register(() -> {
399 if (LightOverlay.enabled) {
400 PlayerEntity playerEntity = client.player;
401 int playerPosX = ((int) playerEntity.getX()) >> 4;
402 int playerPosZ = ((int) playerEntity.getZ()) >> 4;
403 EntityContext entityContext = EntityContext.of(playerEntity);
404 World world = client.world;
405 BlockPos playerPos = new BlockPos(playerEntity.getX(), playerEntity.getY(), playerEntity.getZ());
406 Camera camera = MinecraftClient.getInstance().gameRenderer.getCamera();
408 RenderSystem.enableTexture();
409 RenderSystem.depthMask(true);
410 BlockPos.Mutable mutable = new BlockPos.Mutable();
411 for (Map.Entry<ChunkPos, Map<Long, Object>> entry : CHUNK_MAP.entrySet()) {
412 if (MathHelper.abs(entry.getKey().x - playerPosX) > getChunkRange() || MathHelper.abs(entry.getKey().z - playerPosZ) > getChunkRange()) {
415 for (Map.Entry<Long, Object> objectEntry : entry.getValue().entrySet()) {
416 if (objectEntry.getValue() instanceof Integer) {
417 mutable.set(BlockPos.unpackLongX(objectEntry.getKey()), BlockPos.unpackLongY(objectEntry.getKey()), BlockPos.unpackLongZ(objectEntry.getKey()));
418 if (mutable.isWithinDistance(playerPos, reach)) {
419 BlockPos down = mutable.down();
420 LightOverlay.renderLevel(client, camera, world, mutable, down, (Integer) objectEntry.getValue(), entityContext);
425 RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
426 RenderSystem.enableDepthTest();
428 RenderSystem.enableDepthTest();
429 RenderSystem.disableTexture();
430 RenderSystem.enableBlend();
431 RenderSystem.blendFunc(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA);
432 RenderSystem.disableLighting();
433 if (smoothLines) GL11.glEnable(GL11.GL_LINE_SMOOTH);
434 RenderSystem.lineWidth(lineWidth);
435 Tessellator tessellator = Tessellator.getInstance();
436 BufferBuilder buffer = tessellator.getBuffer();
437 BlockPos.Mutable mutable = new BlockPos.Mutable();
438 for (Map.Entry<ChunkPos, Map<Long, Object>> entry : CHUNK_MAP.entrySet()) {
439 if (MathHelper.abs(entry.getKey().x - playerPosX) > getChunkRange() || MathHelper.abs(entry.getKey().z - playerPosZ) > getChunkRange()) {
442 for (Map.Entry<Long, Object> objectEntry : entry.getValue().entrySet()) {
443 if (objectEntry.getValue() instanceof CrossType) {
444 mutable.set(BlockPos.unpackLongX(objectEntry.getKey()), BlockPos.unpackLongY(objectEntry.getKey()), BlockPos.unpackLongZ(objectEntry.getKey()));
445 if (mutable.isWithinDistance(playerPos, reach)) {
446 BlockPos down = mutable.down();
447 int color = objectEntry.getValue() == CrossType.RED ? redColor : yellowColor;
448 LightOverlay.renderCross(tessellator, buffer, camera, world, mutable, color, entityContext);
453 RenderSystem.disableBlend();
454 RenderSystem.enableTexture();
455 if (smoothLines) GL11.glDisable(GL11.GL_LINE_SMOOTH);
461 private enum CrossType {