diff --git a/src/main/java/com/minelittlepony/unicopia/entity/DynamicLightSource.java b/src/main/java/com/minelittlepony/unicopia/entity/DynamicLightSource.java index 112a5087..7c3e412a 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/DynamicLightSource.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/DynamicLightSource.java @@ -2,6 +2,8 @@ package com.minelittlepony.unicopia.entity; import org.jetbrains.annotations.Nullable; +import com.minelittlepony.unicopia.server.world.LightSources; + import net.minecraft.entity.Entity; import net.minecraft.util.math.BlockPos; @@ -22,35 +24,35 @@ public interface DynamicLightSource { @SuppressWarnings("deprecation") void tick() { - if (entity.getWorld().isClient) { - if (entity.isRemoved()) { - remove(); - return; - } + if (entity.isRemoved()) { + remove(); + return; + } - int light = entity.getLightLevel(); + int light = entity.getLightLevel(); - if (light <= 0) { - return; - } + if (light <= 0) { + return; + } - BlockPos currentPos = entity.getBlockPos(); + BlockPos currentPos = entity.getBlockPos(); - if (!currentPos.equals(lastPos) && entity.getWorld().isChunkLoaded(currentPos)) { - try { - if (lastPos != null) { - entity.getWorld().getLightingProvider().checkBlock(lastPos); - } - // TODO: store this in the ether and inject into Chunk#forEachLightSource - //entity.getWorld().getLightingProvider().addLightSource(currentPos, light); - lastPos = currentPos; - } catch (Exception ignored) { } - } + if (!currentPos.equals(lastPos) && entity.getWorld().isChunkLoaded(currentPos)) { + LightSources.get(entity.getWorld()).addLightSource(entity); + + try { + if (lastPos != null) { + entity.getWorld().getLightingProvider().checkBlock(lastPos); + entity.getWorld().getLightingProvider().checkBlock(currentPos); + } + lastPos = currentPos; + } catch (Exception ignored) { } } } void remove() { - if (entity.getWorld().isClient && lastPos != null) { + LightSources.get(entity.getWorld()).removeLightSource(entity); + if (lastPos != null) { try { entity.getWorld().getLightingProvider().checkBlock(lastPos); } catch (Exception ignored) {} diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinChunkBlockLightProvider.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinChunkBlockLightProvider.java new file mode 100644 index 00000000..db7ec5cf --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinChunkBlockLightProvider.java @@ -0,0 +1,99 @@ +package com.minelittlepony.unicopia.mixin; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.minelittlepony.unicopia.server.world.LightSources; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.chunk.light.ChunkBlockLightProvider; +import net.minecraft.world.chunk.light.ChunkLightProvider; +import net.minecraft.world.chunk.light.LightStorage; + +@SuppressWarnings({ "rawtypes", "unchecked" }) +@Mixin(ChunkBlockLightProvider.class) +abstract class MixinChunkBlockLightProvider extends ChunkLightProvider { + @Shadow + private @Final BlockPos.Mutable mutablePos; + + MixinChunkBlockLightProvider() { super(null, null); } + + @Inject(method = "propagateLight", at = @At("TAIL")) + private void onPropagateLight(ChunkPos chunkPos, CallbackInfo info) { + if (chunkProvider.getChunk(chunkPos.x, chunkPos.z) instanceof WorldChunk chunk && chunk.getWorld() instanceof ServerWorld world) { + LightSources.get(world).forEachLightSource(chunkPos, (pos, level) -> { + method_51566(pos.asLong(), ChunkLightProvider.class_8531.method_51573(level, false)); + }); + } + } + + /* + @Inject(method = "getLightSourceLuminance", at = @At("RETURN"), cancellable = true) + private void onGetLightSourceLuminance(long blockPos, BlockState blockState, CallbackInfoReturnable info) { + int x = ChunkSectionPos.getSectionCoord(BlockPos.unpackLongX(blockPos)); + int z = ChunkSectionPos.getSectionCoord(BlockPos.unpackLongZ(blockPos)); + if (chunkProvider.getChunk(x, z) instanceof WorldChunk chunk) { + info.setReturnValue(Math.max(info.getReturnValue(), LightSources.get(chunk.getWorld()).getLuminance(blockPos))); + } + }*/ + + @Inject(method = "method_51529", at = @At("TAIL")) + private void onMethod_51529(long blockPos, CallbackInfo info) { + long sectionPos = ChunkSectionPos.fromBlockPos(blockPos); + if (!((MutableBlockLightStorage)lightStorage).invokeHasSection(sectionPos)) { + return; + } + + int x = ChunkSectionPos.getSectionCoord(BlockPos.unpackLongX(blockPos)); + int z = ChunkSectionPos.getSectionCoord(BlockPos.unpackLongZ(blockPos)); + if (chunkProvider.getChunk(x, z) instanceof WorldChunk chunk) { + int lightSourceLight = LightSources.get(chunk.getWorld()).getLuminance(blockPos); + if (lightSourceLight > 0) { + method_51566(blockPos, ChunkLightProvider.class_8531.method_51573(lightSourceLight, false)); + } + } + } + + @Inject(method = "method_51530", at = @At("TAIL")) + private void onMethod_51530(long blockPos, long flags, CallbackInfo info) { + int x = ChunkSectionPos.getSectionCoord(BlockPos.unpackLongX(blockPos)); + int z = ChunkSectionPos.getSectionCoord(BlockPos.unpackLongZ(blockPos)); + if (chunkProvider.getChunk(x, z) instanceof WorldChunk chunk) { + int lightLevel = ChunkLightProvider.class_8531.getLightLevel(flags); + for (Direction direction : DIRECTIONS) { + int j; + long m; + if (!ChunkLightProvider.class_8531.isDirectionBitSet(flags, direction) + || !((MutableBlockLightStorage)lightStorage).invokeHasSection(ChunkSectionPos.fromBlockPos(m = BlockPos.offset(blockPos, direction))) + || (j = ((MutableBlockLightStorage)lightStorage).invokeGet(m)) == 0) continue; + if (j <= lightLevel - 1) { + int lightSourceLight = LightSources.get(chunk.getWorld()).getLuminance(blockPos); + if (lightSourceLight <= 0) continue; + method_51566(m, ChunkLightProvider.class_8531.method_51573(lightSourceLight, false)); + continue; + } + method_51566(m, ChunkLightProvider.class_8531.method_51579(j, false, direction.getOpposite())); + } + } + } +} + +@Mixin(LightStorage.class) +interface MutableBlockLightStorage { + @Invoker("hasSection") + boolean invokeHasSection(long sectionPos); + @Invoker("get") + int invokeGet(long blockPos); + @Invoker("set") + void invokeSet(long blockPos, int light); +} diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/LightSources.java b/src/main/java/com/minelittlepony/unicopia/server/world/LightSources.java new file mode 100644 index 00000000..5c8f712e --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/server/world/LightSources.java @@ -0,0 +1,121 @@ +package com.minelittlepony.unicopia.server.world; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; + +import com.minelittlepony.unicopia.Unicopia; +import com.minelittlepony.unicopia.entity.DynamicLightSource; + +import it.unimi.dsi.fastutil.objects.AbstractObject2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.PersistentState; +import net.minecraft.world.World; + +public class LightSources extends PersistentState { + private static final Identifier ID = Unicopia.id("light_sources"); + + private final Set lightSourceIds = new HashSet<>(); + private final AbstractObject2IntMap lightSourceClientIds = new Object2IntOpenHashMap<>(); + private volatile boolean empty = true; + + private final World world; + + private final Function entitySupplier; + + public static LightSources get(World world) { + return WorldOverlay.getPersistableStorage(world, ID, LightSources::new, LightSources::new); + } + + LightSources(World world, NbtCompound compound) { + this(world); + } + + LightSources(World world) { + this.world = world; + entitySupplier = world instanceof ServerWorld s ? s::getEntity : id -> world.getEntityById(lightSourceClientIds.getInt(id)); + } + + @Override + public NbtCompound writeNbt(NbtCompound compound) { + return compound; + } + + public void addLightSource(Entity entity) { + synchronized (lightSourceIds) { + lightSourceIds.add(entity.getUuid()); + lightSourceClientIds.put(entity.getUuid(), entity.getId()); + empty = false; + } + } + + public void removeLightSource(Entity entity) { + if (empty) { + return; + } + synchronized (lightSourceIds) { + lightSourceIds.remove(entity.getUuid()); + lightSourceClientIds.removeInt(entity.getUuid()); + empty = lightSourceIds.isEmpty(); + } + } + + public int getLuminance(long blockPos) { + if (empty) { + return 0; + } + final int[] result = {0}; + forEachLightSource((pos, level) -> { + if (pos.asLong() == blockPos) { + result[0] += level; + } + }); + return result[0]; + } + + public void forEachLightSource(ChunkPos chunkPos, LightSourceConsumer consumer) { + forEachLightSource((pos, level) -> { + if (checkPos(chunkPos, pos)) { + consumer.accept(pos, level); + } + }); + } + + public void forEachLightSource(LightSourceConsumer consumer) { + if (empty) { + return; + } + synchronized (lightSourceIds) { + lightSourceIds.removeIf(id -> { + Entity entity = entitySupplier.apply(id); + if (entity instanceof DynamicLightSource source) { + consumer.accept(entity.getBlockPos(), source.getLightLevel()); + return false; + } + return true; + }); + empty = lightSourceIds.isEmpty(); + } + } + + private boolean checkPos(ChunkPos chunkPos, BlockPos pos) { + return world.isInBuildLimit(pos) + && checkPos(pos.getX(), chunkPos.getStartX(), chunkPos.getEndX()) + && checkPos(pos.getZ(), chunkPos.getStartZ(), chunkPos.getEndZ()); + } + + private boolean checkPos(int p, int min, int max) { + return p >= min && p <= max; + } + + public interface LightSourceConsumer { + void accept(BlockPos pos, int light); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java b/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java index 3051c754..91669caf 100644 --- a/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java +++ b/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java @@ -1,6 +1,9 @@ package com.minelittlepony.unicopia.server.world; +import java.lang.ref.WeakReference; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.*; import org.jetbrains.annotations.Nullable; @@ -21,6 +24,7 @@ import net.minecraft.world.PersistentState; import net.minecraft.world.World; public class WorldOverlay extends PersistentState implements Tickable { + private final World world; private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); @@ -39,7 +43,8 @@ public class WorldOverlay extends PersistentState id.toString() ); } - return factory.apply(world); + + return ClientInstance.of(world, id, factory).instance(); } public static WorldOverlay getOverlay(World world, Identifier id, Supplier factory, @Nullable BiConsumer, List> updateSender) { @@ -181,4 +186,26 @@ public class WorldOverlay extends PersistentState public interface State extends NbtSerialisable { boolean tick(); } + + record ClientInstance(WeakReference world, T instance) { + private static final Map> INSTANCES = new HashMap<>(); + + @SuppressWarnings("unchecked") + public static ClientInstance of(World world, Identifier id, Function factory) { + return (ClientInstance)INSTANCES.compute(id, (i, instance) -> { + if (instance == null || !instance.matches(world)) { + return new ClientInstance<>(world, factory); + } + return instance; + }); + } + + public ClientInstance(World world, Function factory) { + this(new WeakReference<>(world), factory.apply(world)); + } + + public boolean matches(World world) { + return this.world().get() == world; + } + } } diff --git a/src/main/resources/unicopia.mixin.json b/src/main/resources/unicopia.mixin.json index 7e19da9e..097d0b86 100644 --- a/src/main/resources/unicopia.mixin.json +++ b/src/main/resources/unicopia.mixin.json @@ -12,6 +12,8 @@ "MixinBlockItem", "MixinBoatEntity", "MixinBrain", + "MixinChunkBlockLightProvider", + "MutableBlockLightStorage", "MixinDamageSource", "MixinFallLocation", "MixinEntity",