]> git.lizzy.rs Git - LightOverlay.git/blob - common/src/main/java/me/shedaniel/lightoverlay/common/LightOverlayTicker.java
a2539eccaee65dbcc92b4ce340d0609a12ab6774
[LightOverlay.git] / common / src / main / java / me / shedaniel / lightoverlay / common / LightOverlayTicker.java
1 package me.shedaniel.lightoverlay.common;
2
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.biome.Biomes;
23 import net.minecraft.world.level.block.Block;
24 import net.minecraft.world.level.block.state.BlockState;
25 import net.minecraft.world.level.chunk.ChunkStatus;
26 import net.minecraft.world.level.chunk.LevelChunk;
27 import net.minecraft.world.level.lighting.LayerLightEventListener;
28 import net.minecraft.world.phys.shapes.CollisionContext;
29 import net.minecraft.world.phys.shapes.VoxelShape;
30 import org.apache.logging.log4j.LogManager;
31 import sun.misc.Unsafe;
32
33 import java.lang.reflect.Field;
34 import java.util.*;
35 import java.util.concurrent.Executors;
36 import java.util.concurrent.ThreadPoolExecutor;
37 import java.util.function.Supplier;
38
39 public class LightOverlayTicker {
40     private final Minecraft minecraft = Minecraft.getInstance();
41     private long ticks = 0;
42     private static int threadNumber = 0;
43     private static final ThreadPoolExecutor EXECUTOR = (ThreadPoolExecutor) Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
44         Thread thread = new Thread(r, "light-overlay-" + threadNumber++);
45         thread.setDaemon(true);
46         return thread;
47     });
48     public final Set<CubicChunkPos> POS = Collections.synchronizedSet(new HashSet<>());
49     public final Set<CubicChunkPos> CALCULATING_POS = Collections.synchronizedSet(new HashSet<>());
50     public final Map<CubicChunkPos, Long2ByteMap> CHUNK_MAP = Maps.newConcurrentMap();
51     private static final Supplier<EntityType<Entity>> TESTING_ENTITY_TYPE = Suppliers.memoize(() -> {
52         // get unsafe
53         try {
54             Field f = Unsafe.class.getDeclaredField("theUnsafe");
55             f.setAccessible(true);
56             Unsafe unsafe = (Unsafe) f.get(null);
57             
58             // instantiate entity type
59             return (EntityType<Entity>) unsafe.allocateInstance(EntityType.class);
60         } catch (Exception e) {
61             throw new RuntimeException(e);
62         }
63     });
64     
65     @ExpectPlatform
66     public static void populateEntityType(EntityType<Entity> type) {
67         throw new AssertionError();
68     }
69     
70     public void queueChunk(CubicChunkPos pos) {
71         if (LightOverlay.enabled && LightOverlay.caching && !CALCULATING_POS.contains(pos)) {
72             POS.add(pos);
73         }
74     }
75     
76     public void tick(Minecraft minecraft) {
77         while (LightOverlay.enableOverlay.consumeClick())
78             LightOverlay.enabled = !LightOverlay.enabled;
79         
80         try {
81             ticks++;
82             if (minecraft.player == null || !LightOverlay.enabled) {
83                 POS.clear();
84                 CALCULATING_POS.clear();
85                 EXECUTOR.getQueue().clear();
86                 CHUNK_MAP.clear();
87             } else {
88                 LocalPlayer player = minecraft.player;
89                 ClientLevel world = minecraft.level;
90                 CollisionContext collisionContext = CollisionContext.of(player);
91                 
92                 if (!LightOverlay.caching) {
93                     CALCULATING_POS.clear();
94                     POS.clear();
95                     CHUNK_MAP.clear();
96                     BlockPos playerPos = player.blockPosition();
97                     assert world != null;
98                     LayerLightEventListener block = world.getLightEngine().getLayerListener(LightLayer.BLOCK);
99                     LayerLightEventListener sky = LightOverlay.showNumber ? null : world.getLightEngine().getLayerListener(LightLayer.SKY);
100                     BlockPos.MutableBlockPos downPos = new BlockPos.MutableBlockPos();
101                     Iterable<BlockPos> iterate = BlockPos.betweenClosed(playerPos.getX() - LightOverlay.reach, playerPos.getY() - LightOverlay.reach, playerPos.getZ() - LightOverlay.reach,
102                             playerPos.getX() + LightOverlay.reach, playerPos.getY() + LightOverlay.reach, playerPos.getZ() + LightOverlay.reach);
103                     Long2ByteMap chunkData = new Long2ByteOpenHashMap();
104                     CHUNK_MAP.put(new CubicChunkPos(0, 0, 0), chunkData);
105                     for (BlockPos blockPos : iterate) {
106                         downPos.set(blockPos.getX(), blockPos.getY() - 1, blockPos.getZ());
107                         if (LightOverlay.showNumber) {
108                             int level = getCrossLevel(blockPos, downPos, world, block, collisionContext);
109                             if (level >= 0) {
110                                 chunkData.put(blockPos.asLong(), (byte) level);
111                             }
112                         } else {
113                             Holder<Biome> biome = !LightOverlay.mushroom ? world.getBiome(blockPos) : null;
114                             byte type = getCrossType(blockPos, biome, downPos, world, block, sky, collisionContext);
115                             if (type != LightOverlay.CROSS_NONE) {
116                                 chunkData.put(blockPos.asLong(), type);
117                             }
118                         }
119                     }
120                 } else {
121                     assert Minecraft.getInstance().level != null;
122                     var height = Mth.ceil(Minecraft.getInstance().level.getHeight() / 32.0);
123                     var start = Math.floorDiv(Minecraft.getInstance().level.getMinBuildHeight(), 32);
124                     int playerPosX = ((int) player.getX()) >> 4;
125                     int playerPosY = ((int) player.getY()) >> 5;
126                     int playerPosZ = ((int) player.getZ()) >> 4;
127                     var chunkRange = LightOverlay.getChunkRange();
128                     for (int chunkX = playerPosX - chunkRange; chunkX <= playerPosX + chunkRange; chunkX++) {
129                         for (int chunkY = Math.max(playerPosY - Math.max(1, chunkRange >> 1), start); chunkY <= playerPosY + Math.max(1, chunkRange >> 1) && chunkY <= start + height; chunkY++) {
130                             for (int chunkZ = playerPosZ - chunkRange; chunkZ <= playerPosZ + chunkRange; chunkZ++) {
131                                 if (Mth.abs(chunkX - playerPosX) > chunkRange || Mth.abs(chunkY - playerPosY) > chunkRange || Mth.abs(chunkZ - playerPosZ) > chunkRange)
132                                     continue;
133                                 CubicChunkPos chunkPos = new CubicChunkPos(chunkX, chunkY, chunkZ);
134                                 if (!CHUNK_MAP.containsKey(chunkPos))
135                                     queueChunk(chunkPos);
136                             }
137                         }
138                     }
139                     for (int p = 0; p < 3; p++) {
140                         if (EXECUTOR.getQueue().size() >= Runtime.getRuntime().availableProcessors()) break;
141                         double d1 = Double.MAX_VALUE, d2 = Double.MAX_VALUE, d3 = Double.MAX_VALUE;
142                         CubicChunkPos c1 = null, c2 = null, c3 = null;
143                         synchronized (POS) {
144                             Iterator<CubicChunkPos> iterator = POS.iterator();
145                             while (iterator.hasNext()) {
146                                 CubicChunkPos pos = iterator.next();
147                                 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)) {
148                                     iterator.remove();
149                                 } else {
150                                     if (LightOverlay.renderer.isFrustumVisible(pos.getMinBlockX(), pos.getMinBlockY(), pos.getMinBlockZ(), pos.getMaxBlockX(), pos.getMaxBlockY(), pos.getMaxBlockZ())) {
151                                         int dx = Math.abs(pos.x - playerPosX);
152                                         int dy = Math.abs(pos.y - playerPosY) << 1;
153                                         int dz = Math.abs(pos.z - playerPosZ);
154                                         double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
155                                         if (distance < d1) {
156                                             d3 = d2;
157                                             d2 = d1;
158                                             d1 = distance;
159                                             c3 = c2;
160                                             c2 = c1;
161                                             c1 = pos;
162                                         } else if (distance < d2) {
163                                             d3 = d2;
164                                             d2 = distance;
165                                             c3 = c2;
166                                             c2 = pos;
167                                         } else if (distance < d3) {
168                                             d3 = distance;
169                                             c3 = pos;
170                                         }
171                                     }
172                                 }
173                             }
174                         }
175                         CubicChunkPos finalC1 = c1;
176                         CubicChunkPos finalC2 = c2;
177                         CubicChunkPos finalC3 = c3;
178                         if (finalC1 != null) {
179                             CALCULATING_POS.add(finalC1);
180                             POS.remove(finalC1);
181                             if (finalC2 != null) {
182                                 CALCULATING_POS.add(finalC2);
183                                 POS.remove(finalC2);
184                                 if (finalC3 != null) {
185                                     CALCULATING_POS.add(finalC3);
186                                     POS.remove(finalC3);
187                                 }
188                             }
189                             EXECUTOR.submit(() -> {
190                                 int playerPosX1 = ((int) minecraft.player.getX()) >> 4;
191                                 int playerPosY1 = ((int) minecraft.player.getY()) >> 5;
192                                 int playerPosZ1 = ((int) minecraft.player.getZ()) >> 4;
193                                 processChunk(finalC1, playerPosX1, playerPosY1, playerPosZ1, collisionContext);
194                                 if (finalC2 != null) processChunk(finalC2, playerPosX1, playerPosY1, playerPosZ1, collisionContext);
195                                 if (finalC3 != null) processChunk(finalC3, playerPosX1, playerPosY1, playerPosZ1, collisionContext);
196                             });
197                         }
198                     }
199                     if (ticks % 50 == 0) {
200                         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);
201                     }
202                 }
203             }
204         } catch (Throwable throwable) {
205             LogManager.getLogger().throwing(throwable);
206         }
207     }
208     
209     private void processChunk(CubicChunkPos pos, int playerPosX, int playerPosY, int playerPosZ, CollisionContext context) {
210         CALCULATING_POS.remove(pos);
211         int chunkRange = LightOverlay.getChunkRange();
212         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)) {
213             return;
214         }
215         try {
216             assert minecraft.level != null;
217             calculateChunk(minecraft.level.getChunkSource().getChunk(pos.x, pos.z, ChunkStatus.FULL, false), minecraft.level, pos, context);
218         } catch (Throwable throwable) {
219             LogManager.getLogger().throwing(throwable);
220         }
221     }
222     
223     private void calculateChunk(LevelChunk chunk, Level world, CubicChunkPos chunkPos, CollisionContext collisionContext) {
224         if (world != null && chunk != null) {
225             Long2ByteMap chunkData = new Long2ByteOpenHashMap();
226             LayerLightEventListener block = world.getLightEngine().getLayerListener(LightLayer.BLOCK);
227             LayerLightEventListener sky = LightOverlay.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 (LightOverlay.showNumber) {
231                     int level = getCrossLevel(pos, down, chunk, block, collisionContext);
232                     if (level >= 0) {
233                         chunkData.put(pos.asLong(), (byte) level);
234                     }
235                 } else {
236                     Holder<Biome> biome = !LightOverlay.mushroom ? world.getBiome(pos) : null;
237                     byte type = getCrossType(pos, biome, down, chunk, block, sky, collisionContext);
238                     if (type != LightOverlay.CROSS_NONE) {
239                         chunkData.put(pos.asLong(), type);
240                     }
241                 }
242             }
243             CHUNK_MAP.put(chunkPos, chunkData);
244         } else {
245             CHUNK_MAP.remove(chunkPos);
246         }
247     }
248     
249     public byte getCrossType(BlockPos pos, Holder<Biome> biome, BlockPos down, BlockGetter world, LayerLightEventListener block, LayerLightEventListener sky, CollisionContext entityContext) {
250         BlockState blockBelowState = world.getBlockState(down);
251         BlockState blockUpperState = world.getBlockState(pos);
252         VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, entityContext);
253         if (!LightOverlay.underwater && !blockUpperState.getFluidState().isEmpty())
254             return LightOverlay.CROSS_NONE;
255         // Check if the outline is full
256         if (Block.isFaceFull(upperCollisionShape, Direction.UP))
257             return LightOverlay.CROSS_NONE;
258         // TODO: Not to hard code no redstone
259         if (blockUpperState.isSignalSource())
260             return LightOverlay.CROSS_NONE;
261         // Check if the collision has a bump
262         if (upperCollisionShape.max(Direction.Axis.Y) > 0)
263             return LightOverlay.CROSS_NONE;
264         if (blockUpperState.is(BlockTags.RAILS))
265             return LightOverlay.CROSS_NONE;
266         // Check block state allow spawning (excludes bedrock and barriers automatically)
267         if (!blockBelowState.isValidSpawn(world, down, TESTING_ENTITY_TYPE.get()))
268             return LightOverlay.CROSS_NONE;
269         if (!LightOverlay.mushroom && isMushroom(biome))
270             return LightOverlay.CROSS_NONE;
271         int blockLightLevel = block.getLightValue(pos);
272         int skyLightLevel = sky.getLightValue(pos);
273         if (blockLightLevel > LightOverlay.higherCrossLevel)
274             return LightOverlay.CROSS_NONE;
275         if (skyLightLevel > LightOverlay.higherCrossLevel)
276             return LightOverlay.higherCross;
277         return LightOverlay.lowerCrossLevel >= 0 && blockLightLevel > LightOverlay.lowerCrossLevel ?
278                 LightOverlay.lowerCross : LightOverlay.CROSS_RED;
279     }
280     
281     @ExpectPlatform
282     private static boolean isMushroom(Holder<Biome> biome) {
283         throw new AssertionError();
284     }
285     
286     public static int getCrossLevel(BlockPos pos, BlockPos down, BlockGetter world, LayerLightEventListener view, CollisionContext collisionContext) {
287         BlockState blockBelowState = world.getBlockState(down);
288         BlockState blockUpperState = world.getBlockState(pos);
289         VoxelShape collisionShape = blockBelowState.getCollisionShape(world, down, collisionContext);
290         VoxelShape upperCollisionShape = blockUpperState.getCollisionShape(world, pos, collisionContext);
291         if (!LightOverlay.underwater && !blockUpperState.getFluidState().isEmpty())
292             return -1;
293         if (!blockBelowState.getFluidState().isEmpty())
294             return -1;
295         if (blockBelowState.isAir())
296             return -1;
297         if (Block.isFaceFull(upperCollisionShape, Direction.DOWN))
298             return -1;
299         return view.getLightValue(pos);
300     }
301 }