diff --git a/common/src/main/java/com/seibel/lod/common/wrappers/chunk/ChunkWrapper.java b/common/src/main/java/com/seibel/lod/common/wrappers/chunk/ChunkWrapper.java index 27b3882e0..f349e3595 100644 --- a/common/src/main/java/com/seibel/lod/common/wrappers/chunk/ChunkWrapper.java +++ b/common/src/main/java/com/seibel/lod/common/wrappers/chunk/ChunkWrapper.java @@ -34,6 +34,8 @@ import com.seibel.lod.common.wrappers.block.BiomeWrapper; import com.seibel.lod.common.wrappers.worldGeneration.mimicObject.LightedWorldGenRegion; import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper; +import net.minecraft.client.multiplayer.ClientChunkCache; +import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; #if POST_MC_1_17_1 import net.minecraft.core.QuartPos; @@ -50,6 +52,9 @@ import net.minecraft.world.level.levelgen.Heightmap; import org.jetbrains.annotations.Nullable; import java.util.HashMap; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; public class ChunkWrapper implements IChunkWrapper { @@ -62,9 +67,19 @@ public class ChunkWrapper implements IChunkWrapper private final boolean isDhGeneratedChunk; private final HashMap blockStateByBlockPosCache = new HashMap<>(); - - - + + // Due to vanilla `isClientLightReady()` not designed to be used by non-render thread, that value may return 'true' + // just before the light engine is ticked, (right after all light changes is marked to the engine to be processed). + // To fix this, on client-only mode, we mixin-redirect the `isClientLightReady()` so that after the call, it will + // trigger a synchronous update of this flag here on all chunks that are wrapped. + // + // Note: Using a static weak hash map to store the chunks that need to be updated, as instance of chunk wrapper + // can be duplicated, with same chunk instance. And the data stored here are all temporary, and thus will not be + // visible when a chunk is re-wrapped later. + // (Also, thread safety done via a reader writer lock) + private final static WeakHashMap chunksToUpdateClientLightReady = new WeakHashMap<>(); + private final static ReentrantReadWriteLock weakMapLock = new ReentrantReadWriteLock(); + public ChunkWrapper(ChunkAccess chunk, LevelReader lightSource, @Nullable ILevelWrapper wrappedLevel) { this.chunk = chunk; @@ -75,10 +90,12 @@ public class ChunkWrapper implements IChunkWrapper this.useMcLightingEngine = (Config.Client.Advanced.WorldGenerator.lightingEngine.get() == ELightGenerationMode.MINECRAFT); // TODO is this the best way to differentiate between when we are generating chunks and when MC gave us a chunk? this.isDhGeneratedChunk = (this.lightSource.getClass() == LightedWorldGenRegion.class); + + weakMapLock.writeLock().lock(); + chunksToUpdateClientLightReady.put(chunk, false); + weakMapLock.writeLock().unlock(); } - - //=========// // methods // //=========// @@ -159,12 +176,21 @@ public class ChunkWrapper implements IChunkWrapper #else if (this.chunk instanceof LevelChunk) { - // called when connected to a server (and sometimes when in a singleplayer world) - return ((LevelChunk) this.chunk).isClientLightReady() || this.chunk.isLightCorrect(); + LevelChunk levelChunk = (LevelChunk) this.chunk; + if (levelChunk.getLevel() instanceof ClientLevel) + { + weakMapLock.readLock().lock(); + boolean fixedIsClientLightReady = chunksToUpdateClientLightReady.get(this.chunk); + weakMapLock.readLock().unlock(); + return fixedIsClientLightReady; + } + + // called when in single player or in dedicated server, and the chunk is a level chunk (active) + return this.chunk.isLightCorrect() && levelChunk.loaded; } else { - // called when in a single player world + // called when in a single player world and the chunk is a proto chunk (in world gen, and not active) return this.chunk.isLightCorrect(); } #endif @@ -332,8 +358,38 @@ public class ChunkWrapper implements IChunkWrapper @Override public boolean isStillValid() { return this.wrappedLevel == null || this.wrappedLevel.tryGetChunk(this.chunkPos) == this; } - - + + // Should be called after client light updates are triggered. + private static boolean updateClientLightReady(ChunkAccess chunk, boolean oldValue) { + if (chunk instanceof LevelChunk && ((LevelChunk)chunk).getLevel() instanceof ClientLevel) + { + LevelChunk levelChunk = (LevelChunk)chunk; + ClientChunkCache clientChunkCache = ((ClientLevel)levelChunk.getLevel()).getChunkSource(); + return clientChunkCache.getChunkForLighting(chunk.getPos().x, chunk.getPos().z) != null && levelChunk.isClientLightReady(); + } + else + { + return oldValue; + } + } + + public static void syncedUpdateClientLightStatus() + { + #if PRE_MC_1_18_1 + // TODO: Check what to do in 1.18.1, or in other versions + #else + weakMapLock.writeLock().lock(); + try + { + chunksToUpdateClientLightReady.replaceAll(ChunkWrapper::updateClientLightReady); + } + finally { + weakMapLock.writeLock().unlock(); + } + #endif + } + + //================// // helper methods // diff --git a/common/src/main/java/com/seibel/lod/common/wrappers/worldGeneration/BatchGenerationEnvironment.java b/common/src/main/java/com/seibel/lod/common/wrappers/worldGeneration/BatchGenerationEnvironment.java index 266bb318f..cca8e7093 100644 --- a/common/src/main/java/com/seibel/lod/common/wrappers/worldGeneration/BatchGenerationEnvironment.java +++ b/common/src/main/java/com/seibel/lod/common/wrappers/worldGeneration/BatchGenerationEnvironment.java @@ -416,6 +416,9 @@ public final class BatchGenerationEnvironment extends AbstractBatchGenerationEnv { ChunkAccess target = genChunks.get(offsetX, offsetY); ChunkWrapper wrappedChunk = new ChunkWrapper(target, region, null); + if (target instanceof LevelChunk) { + ((LevelChunk) target).loaded = true; + } if (!wrappedChunk.isLightCorrect()) { throw new RuntimeException("The generated chunk somehow has isLightCorrect() returning false"); @@ -567,6 +570,8 @@ public final class BatchGenerationEnvironment extends AbstractBatchGenerationEnv if (chunk instanceof ProtoChunk) { chunk.setLightCorrect(true); // TODO why are we checking instanceof ProtoChunk? + // TODO: This is due to old times where it may return actual live chunks, which is LevelChunk. + // that though is no longer needed... } #if POST_MC_1_18_1 @@ -575,6 +580,7 @@ public final class BatchGenerationEnvironment extends AbstractBatchGenerationEnv LevelChunk levelChunk = (LevelChunk) chunk; levelChunk.setLightCorrect(true); levelChunk.setClientLightReady(true); + levelChunk.loaded = true; } #endif }); diff --git a/common/src/main/resources/1_18.lod.accesswidener b/common/src/main/resources/1_18.lod.accesswidener index 948446f5a..5357c2b46 100644 --- a/common/src/main/resources/1_18.lod.accesswidener +++ b/common/src/main/resources/1_18.lod.accesswidener @@ -29,6 +29,7 @@ accessible field net/minecraft/world/level/biome/Biome biomeCategory Lnet/minecr accessible method net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator doFill (Lnet/minecraft/world/level/levelgen/blending/Blender;Lnet/minecraft/world/level/StructureFeatureManager;Lnet/minecraft/world/level/chunk/ChunkAccess;II)Lnet/minecraft/world/level/chunk/ChunkAccess; #accessible method net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator doCreateBiomes (Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/blending/Blender;Lnet/minecraft/world/level/StructureFeatureManager;Lnet/minecraft/world/level/chunk/ChunkAccess;)V accessible method net/minecraft/world/level/lighting/LayerLightEngine queueSectionData (JLnet/minecraft/world/level/chunk/DataLayer;Z)V +accessible field net/minecraft/world/level/chunk/LevelChunk loaded Z # lod generation from save file accessible field net/minecraft/server/level/ChunkMap mainThreadExecutor Lnet/minecraft/util/thread/BlockableEventLoop; diff --git a/coreSubProjects b/coreSubProjects index 6832d21a3..9f81194d2 160000 --- a/coreSubProjects +++ b/coreSubProjects @@ -1 +1 @@ -Subproject commit 6832d21a3b42e5a7b8e84a3c213c81ce155a5ed2 +Subproject commit 9f81194d21d6e0f53a10aac8860a2ef1f7534236 diff --git a/fabric/src/main/java/com/seibel/lod/fabric/mixins/client/MixinLevelRenderer.java b/fabric/src/main/java/com/seibel/lod/fabric/mixins/client/MixinLevelRenderer.java index 0335b4814..77df802f7 100644 --- a/fabric/src/main/java/com/seibel/lod/fabric/mixins/client/MixinLevelRenderer.java +++ b/fabric/src/main/java/com/seibel/lod/fabric/mixins/client/MixinLevelRenderer.java @@ -21,15 +21,18 @@ package com.seibel.lod.fabric.mixins.client; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Matrix4f; +import com.seibel.lod.common.wrappers.chunk.ChunkWrapper; import com.seibel.lod.core.config.Config; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.RenderType; +import net.minecraft.world.level.lighting.LevelLightEngine; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** @@ -112,4 +115,24 @@ public class MixinLevelRenderer } } #endif + + @Redirect(method = + "Lnet/minecraft/client/renderer/LevelRenderer;" + + "renderLevel(Lcom/mojang/blaze3d/vertex/PoseStack;" + + "FJZLnet/minecraft/client/Camera;" + + "Lnet/minecraft/client/renderer/GameRenderer;" + + "Lnet/minecraft/client/renderer/LightTexture;" + + "Lcom/mojang/math/Matrix4f;)V" + , + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/lighting/LevelLightEngine;runUpdates(IZZ)I" + )) + private int callAfterRunUpdates(LevelLightEngine light, int pos, boolean isQueueEmpty, boolean updateBlockLight) + { + int r = light.runUpdates(pos, isQueueEmpty, updateBlockLight); + ChunkWrapper.syncedUpdateClientLightStatus(); + return r; + } + }