1 package me.shedaniel.lightoverlay.common;
3 import com.google.common.base.Suppliers;
4 import com.google.common.collect.Maps;
5 import dev.architectury.injectables.annotations.ExpectPlatform;
6 import it.unimi.dsi.fastutil.longs.Long2ByteMap;
7 import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
8 import net.minecraft.client.Minecraft;
9 import net.minecraft.client.multiplayer.ClientLevel;
10 import net.minecraft.client.player.LocalPlayer;
11 import net.minecraft.core.BlockPos;
12 import net.minecraft.core.Direction;
13 import net.minecraft.core.Holder;
14 import net.minecraft.tags.BlockTags;
15 import net.minecraft.util.Mth;
16 import net.minecraft.world.entity.Entity;
17 import net.minecraft.world.entity.EntityType;
18 import net.minecraft.world.level.BlockGetter;
19 import net.minecraft.world.level.Level;
20 import net.minecraft.world.level.LightLayer;
21 import net.minecraft.world.level.biome.Biome;
22 import net.minecraft.world.level.block.Block;
23 import net.minecraft.world.level.block.state.BlockState;
24 import net.minecraft.world.level.chunk.ChunkStatus;
25 import net.minecraft.world.level.chunk.LevelChunk;
26 import net.minecraft.world.level.lighting.LayerLightEventListener;
27 import net.minecraft.world.phys.shapes.CollisionContext;
28 import net.minecraft.world.phys.shapes.VoxelShape;
29 import org.apache.logging.log4j.LogManager;
30 import sun.misc.Unsafe;
32 import java.lang.reflect.Field;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.ThreadPoolExecutor;
36 import java.util.function.Supplier;
38 public class LightOverlayTicker {
39 private final Minecraft minecraft = Minecraft.getInstance();
40 private long ticks = 0;
41 private static int threadNumber = 0;
42 private static final ThreadPoolExecutor EXECUTOR = (ThreadPoolExecutor) Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
43 Thread thread = new Thread(r, "light-overlay-" + threadNumber++);
44 thread.setDaemon(true);
47 public final Set<CubicChunkPos> POS = Collections.synchronizedSet(new HashSet<>());
48 public final Set<CubicChunkPos> CALCULATING_POS = Collections.synchronizedSet(new HashSet<>());
49 public final Map<CubicChunkPos, Long2ByteMap> CHUNK_MAP = Maps.newConcurrentMap();
50 private static final Supplier<EntityType<Entity>> TESTING_ENTITY_TYPE = Suppliers.memoize(() -> {
53 Field f = Unsafe.class.getDeclaredField("theUnsafe");
54 f.setAccessible(true);
55 Unsafe unsafe = (Unsafe) f.get(null);
57 // instantiate entity type
58 return (EntityType<Entity>) unsafe.allocateInstance(EntityType.class);
59 } catch (Exception e) {
60 throw new RuntimeException(e);
65 public static void populateEntityType(EntityType<Entity> type) {
66 throw new AssertionError();
69 public void queueChunk(CubicChunkPos pos) {
70 if (LightOverlay.enabled && LightOverlay.caching && !CALCULATING_POS.contains(pos)) {
75 public void tick(Minecraft minecraft) {
76 while (LightOverlay.enableOverlay.consumeClick())
77 LightOverlay.enabled = !LightOverlay.enabled;
81 if (minecraft.player == null || !LightOverlay.enabled) {
83 CALCULATING_POS.clear();
84 EXECUTOR.getQueue().clear();
87 LocalPlayer player = minecraft.player;
88 ClientLevel world = minecraft.level;
89 CollisionContext collisionContext = CollisionContext.of(player);
91 if (!LightOverlay.caching) {
92 CALCULATING_POS.clear();
95 BlockPos playerPos = player.blockPosition();
97 LayerLightEventListener block = world.getLightEngine().getLayerListener(LightLayer.BLOCK);
98 LayerLightEventListener sky = LightOverlay.showNumber ? null : world.getLightEngine().getLayerListener(LightLayer.SKY);
99 BlockPos.MutableBlockPos downPos = new BlockPos.MutableBlockPos();
100 Iterable<BlockPos> iterate = BlockPos.betweenClosed(playerPos.getX() - LightOverlay.reach, playerPos.getY() - LightOverlay.reach, playerPos.getZ() - LightOverlay.reach,
101 playerPos.getX() + LightOverlay.reach, playerPos.getY() + LightOverlay.reach, playerPos.getZ() + LightOverlay.reach);
102 Long2ByteMap chunkData = new Long2ByteOpenHashMap();
103 CHUNK_MAP.put(new CubicChunkPos(0, 0, 0), chunkData);
104 for (BlockPos blockPos : iterate) {
105 downPos.set(blockPos.getX(), blockPos.getY() - 1, blockPos.getZ());
106 if (LightOverlay.showNumber) {
107 int level = getCrossLevel(blockPos, downPos, world, block, collisionContext);
109 chunkData.put(blockPos.asLong(), (byte) level);
112 Holder<Biome> biome = !LightOverlay.mushroom ? world.getBiome(blockPos) : null;
113 byte type = getCrossType(blockPos, biome, downPos, world, block, sky, collisionContext);
114 if (type != LightOverlay.CROSS_NONE) {
115 chunkData.put(blockPos.asLong(), type);
120 assert Minecraft.getInstance().level != null;
121 var height = Mth.ceil(Minecraft.getInstance().level.getHeight() / 32.0);
122 var start = Math.floorDiv(Minecraft.getInstance().level.getMinBuildHeight(), 32);
123 int playerPosX = ((int) player.getX()) >> 4;
124 int playerPosY = ((int) player.getY()) >> 5;
125 int playerPosZ = ((int) player.getZ()) >> 4;
126 var chunkRange = LightOverlay.getChunkRange();
127 for (int chunkX = playerPosX - chunkRange; chunkX <= playerPosX + chunkRange; chunkX++) {
128 for (int chunkY = Math.max(playerPosY - Math.max(1, chunkRange >> 1), start); chunkY <= playerPosY + Math.max(1, chunkRange >> 1) && chunkY <= start + height; chunkY++) {
129 for (int chunkZ = playerPosZ - chunkRange; chunkZ <= playerPosZ + chunkRange; chunkZ++) {
130 if (Mth.abs(chunkX - playerPosX) > chunkRange || Mth.abs(chunkY - playerPosY) > chunkRange || Mth.abs(chunkZ - playerPosZ) > chunkRange)
132 CubicChunkPos chunkPos = new CubicChunkPos(chunkX, chunkY, chunkZ);
133 if (!CHUNK_MAP.containsKey(chunkPos))
134 queueChunk(chunkPos);
138 for (int p = 0; p < 3; p++) {
139 if (EXECUTOR.getQueue().size() >= Runtime.getRuntime().availableProcessors()) break;
140 double d1 = Double.MAX_VALUE, d2 = Double.MAX_VALUE, d3 = Double.MAX_VALUE;
141 CubicChunkPos c1 = null, c2 = null, c3 = null;
143 Iterator<CubicChunkPos> iterator = POS.iterator();
144 while (iterator.hasNext()) {
145 CubicChunkPos pos = iterator.next();
146 if (Mth.abs(pos.x - playerPosX) > chunkRange || Mth.abs(pos.y - playerPosY) > Math.max(1, chunkRange >> 1) || Mth.abs(pos.z - playerPosZ) > chunkRange || CALCULATING_POS.contains(pos)) {
149 if (LightOverlay.renderer.isFrustumVisible(pos.getMinBlockX(), pos.getMinBlockY(), pos.getMinBlockZ(), pos.getMaxBlockX(), pos.getMaxBlockY(), pos.getMaxBlockZ())) {
150 int dx = Math.abs(pos.x - playerPosX);
151 int dy = Math.abs(pos.y - playerPosY) << 1;
152 int dz = Math.abs(pos.z - playerPosZ);
153 double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
161 } else if (distance < d2) {
166 } else if (distance < d3) {
174 CubicChunkPos finalC1 = c1;
175 CubicChunkPos finalC2 = c2;
176 CubicChunkPos finalC3 = c3;
177 if (finalC1 != null) {
178 CALCULATING_POS.add(finalC1);
180 if (finalC2 != null) {
181 CALCULATING_POS.add(finalC2);
183 if (finalC3 != null) {
184 CALCULATING_POS.add(finalC3);
188 EXECUTOR.submit(() -> {
189 int playerPosX1 = ((int) minecraft.player.getX()) >> 4;
190 int playerPosY1 = ((int) minecraft.player.getY()) >> 5;
191 int playerPosZ1 = ((int) minecraft.player.getZ()) >> 4;
192 processChunk(finalC1, playerPosX1, playerPosY1, playerPosZ1, collisionContext);
193 if (finalC2 != null) processChunk(finalC2, playerPosX1, playerPosY1, playerPosZ1, collisionContext);
194 if (finalC3 != null) processChunk(finalC3, playerPosX1, playerPosY1, playerPosZ1, collisionContext);
198 if (ticks % 50 == 0) {
199 CHUNK_MAP.entrySet().removeIf(entry -> Mth.abs(entry.getKey().x - playerPosX) > chunkRange * 2 || Mth.abs(entry.getKey().y - playerPosY) > chunkRange * 2 || Mth.abs(entry.getKey().z - playerPosZ) > chunkRange * 2);
203 } catch (Throwable throwable) {
204 LogManager.getLogger().throwing(throwable);
208 private void processChunk(CubicChunkPos pos, int playerPosX, int playerPosY, int playerPosZ, CollisionContext context) {
209 CALCULATING_POS.remove(pos);
210 int chunkRange = LightOverlay.getChunkRange();
211 if (Mth.abs(pos.x - playerPosX) > chunkRange || Mth.abs(pos.y - playerPosY) > Math.max(1, chunkRange >> 1) || Mth.abs(pos.z - playerPosZ) > chunkRange || POS.contains(pos)) {
215 assert minecraft.level != null;
216 calculateChunk(minecraft.level.getChunkSource().getChunk(pos.x, pos.z, ChunkStatus.FULL, false), minecraft.level, pos, context);
217 } catch (Throwable throwable) {
218 LogManager.getLogger().throwing(throwable);
222 private void calculateChunk(LevelChunk chunk, Level world, CubicChunkPos chunkPos, CollisionContext collisionContext) {
223 if (world != null && chunk != null) {
224 Long2ByteMap chunkData = new Long2ByteOpenHashMap();
225 LayerLightEventListener block = world.getLightEngine().getLayerListener(LightLayer.BLOCK);
226 LayerLightEventListener sky = LightOverlay.showNumber ? null : world.getLightEngine().getLayerListener(LightLayer.SKY);
227 for (BlockPos pos : BlockPos.betweenClosed(chunkPos.getMinBlockX(), chunkPos.getMinBlockY(), chunkPos.getMinBlockZ(), chunkPos.getMaxBlockX(), chunkPos.getMaxBlockY(), chunkPos.getMaxBlockZ())) {
228 BlockPos down = pos.below();
229 if (LightOverlay.showNumber) {
230 int level = getCrossLevel(pos, down, chunk, block, collisionContext);
232 chunkData.put(pos.asLong(), (byte) level);
235 Holder<Biome> biome = !LightOverlay.mushroom ? world.getBiome(pos) : null;
236 byte type = getCrossType(pos, biome, down, chunk, block, sky, collisionContext);
237 if (type != LightOverlay.CROSS_NONE) {
238 chunkData.put(pos.asLong(), type);
242 CHUNK_MAP.put(chunkPos, chunkData);
244 CHUNK_MAP.remove(chunkPos);
248 public byte getCrossType(BlockPos pos, Holder<Biome> biome, BlockPos down, BlockGetter world, LayerLightEventListener block, LayerLightEventListener sky, CollisionContext entityContext) {
249 BlockState blockBelowState = world.getBlockState(down);
250 BlockState blockUpperState = world.getBlockState(pos);
251 VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
252 if (!LightOverlay.underwater && !blockUpperState.getFluidState().isEmpty())
253 return LightOverlay.CROSS_NONE;
254 // Check if the outline is full
255 if (Block.isFaceFull(upperCollisionShape, Direction.UP))
256 return LightOverlay.CROSS_NONE;
257 // TODO: Not to hard code no redstone
258 if (blockUpperState.isSignalSource())
259 return LightOverlay.CROSS_NONE;
260 // Check if the collision has a bump
261 if (upperCollisionShape.max(Direction.Axis.Y) > 0)
262 return LightOverlay.CROSS_NONE;
263 if (blockUpperState.is(BlockTags.RAILS))
264 return LightOverlay.CROSS_NONE;
265 // Check block state allow spawning (excludes bedrock and barriers automatically)
266 if (!blockBelowState.isValidSpawn(world, down, TESTING_ENTITY_TYPE.get()))
267 return LightOverlay.CROSS_NONE;
268 if (!LightOverlay.mushroom && isMushroom(biome))
269 return LightOverlay.CROSS_NONE;
270 int blockLightLevel = block.getLightValue(pos);
271 int skyLightLevel = sky.getLightValue(pos);
272 if (blockLightLevel > LightOverlay.higherCrossLevel)
273 return LightOverlay.CROSS_NONE;
274 if (skyLightLevel > LightOverlay.higherCrossLevel)
275 return LightOverlay.higherCross;
276 return LightOverlay.lowerCrossLevel >= 0 && blockLightLevel > LightOverlay.lowerCrossLevel ?
277 LightOverlay.lowerCross : LightOverlay.CROSS_RED;
281 private static boolean isMushroom(Holder<Biome> biome) {
282 throw new AssertionError();
285 public static int getCrossLevel(BlockPos pos, BlockPos down, BlockGetter world, LayerLightEventListener view, CollisionContext collisionContext) {
286 BlockState blockBelowState = world.getBlockState(down);
287 BlockState blockUpperState = world.getBlockState(pos);
288 VoxelShape collisionShape = blockBelowState.getCollisionShape(world, down, collisionContext);
289 VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, collisionContext);
290 if (!LightOverlay.underwater && !blockUpperState.getFluidState().isEmpty())
292 if (!blockBelowState.getFluidState().isEmpty())
294 if (blockBelowState.isAir())
296 if (Block.isFaceFull(upperCollisionShape, Direction.DOWN))
298 return view.getLightValue(pos);