diff --git a/src/datagen/java/com/github/elenterius/biomancy/datagen/loot/ModBlockLoot.java b/src/datagen/java/com/github/elenterius/biomancy/datagen/loot/ModBlockLoot.java index 432cd9b74..d75e8edbe 100644 --- a/src/datagen/java/com/github/elenterius/biomancy/datagen/loot/ModBlockLoot.java +++ b/src/datagen/java/com/github/elenterius/biomancy/datagen/loot/ModBlockLoot.java @@ -61,6 +61,7 @@ protected static LootTable.Builder createPrimordialCradleTable(Block block) { return LootTable.lootTable().withPool(applyExplosionCondition(block, LootPool.lootPool().setRolls(ConstantValue.exactly(1)) .add(LootItem.lootTableItem(block) .apply(CopyNbtFunction.copyData(ContextNbtProvider.BLOCK_ENTITY).copy("PrimalEnergy", "BlockEntityTag.PrimalEnergy")) + .apply(CopyNbtFunction.copyData(ContextNbtProvider.BLOCK_ENTITY).copy("ProcGenValues", "BlockEntityTag.ProcGenValues")) .apply(CopyNbtFunction.copyData(ContextNbtProvider.BLOCK_ENTITY).copy("SacrificeHandler", "BlockEntityTag.SacrificeHandler")) ))); } diff --git a/src/main/java/com/github/elenterius/biomancy/block/cradle/PrimordialCradleBlockEntity.java b/src/main/java/com/github/elenterius/biomancy/block/cradle/PrimordialCradleBlockEntity.java index cd0fd789c..b8d152b03 100644 --- a/src/main/java/com/github/elenterius/biomancy/block/cradle/PrimordialCradleBlockEntity.java +++ b/src/main/java/com/github/elenterius/biomancy/block/cradle/PrimordialCradleBlockEntity.java @@ -9,9 +9,9 @@ import com.github.elenterius.biomancy.network.ModNetworkHandler; import com.github.elenterius.biomancy.tribute.Tribute; import com.github.elenterius.biomancy.util.SoundUtil; +import com.github.elenterius.biomancy.util.shape.Shape; import com.github.elenterius.biomancy.world.PrimordialEcosystem; -import com.github.elenterius.biomancy.world.ShapeManager; -import com.github.elenterius.biomancy.world.ShapeTicket; +import com.github.elenterius.biomancy.world.RegionManager; import com.github.elenterius.biomancy.world.mound.MoundGenerator; import com.github.elenterius.biomancy.world.mound.MoundShape; import com.google.common.math.IntMath; @@ -46,10 +46,13 @@ public class PrimordialCradleBlockEntity extends SimpleSyncedBlockEntity implements PrimalEnergyHandler, IAnimatable, ISyncableAnimation { - public static final int DURATION_TICKS = 20 * 4; public static final String SACRIFICE_SYNC_KEY = "SyncSacrificeHandler"; public static final String SACRIFICE_KEY = "SacrificeHandler"; public static final String PRIMAL_ENERGY_KEY = "PrimalEnergy"; + public static final String PROC_GEN_VALUES_KEY = "ProcGenValues"; + + public static final int DURATION_TICKS = 20 * 4; + protected static final AnimationBuilder IDLE_ANIM = new AnimationBuilder().addAnimation("cradle.idle"); protected static final AnimationBuilder SPIKE_ANIM = new AnimationBuilder().addAnimation("cradle.spike"); private final AnimationFactory animationFactory = GeckoLibUtil.createFactory(this); @@ -57,9 +60,7 @@ public class PrimordialCradleBlockEntity extends SimpleSyncedBlockEntity impleme private boolean playAttackAnimation = false; private long ticks; private int primalEnergy; - - @Nullable - private ShapeTicket shapeTicket; + private @Nullable MoundShape.ProcGenValues procGenValues; public PrimordialCradleBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.PRIMORDIAL_CRADLE.get(), pos, state); @@ -78,24 +79,25 @@ public static void serverTick(Level level, BlockPos pos, BlockState state, Primo @Override public void onLoad() { super.onLoad(); - if (level != null && !level.isClientSide) { - if (shapeTicket == null) { - long seed = Mth.getSeed(worldPosition); //TODO: make unique per cradle - MoundShape moundShape = MoundGenerator.constructShape(level, worldPosition, seed); - shapeTicket = ShapeManager.addShapeTicket(level, moundShape); - } - else { - shapeTicket.validate(); + if (level instanceof ServerLevel serverLevel && !serverLevel.isClientSide) { + Shape shape = RegionManager.getOrCreateShapeRegion(serverLevel, worldPosition, () -> { + if (procGenValues != null) { + return MoundGenerator.constructShape(worldPosition, procGenValues); + } + return MoundGenerator.constructShape(level, worldPosition, level.random.nextLong()); + }); + if (shape instanceof MoundShape moundShape) { + procGenValues = moundShape.getProcGenValues(); } } } @Override public void setRemoved() { - super.setRemoved(); - if (shapeTicket != null) { - shapeTicket.invalidate(); + if (level instanceof ServerLevel serverLevel && !serverLevel.isClientSide) { + RegionManager.remove(serverLevel, worldPosition); } + super.setRemoved(); } /** @@ -331,6 +333,12 @@ protected void saveAdditional(CompoundTag tag) { super.saveAdditional(tag); tag.put(SACRIFICE_KEY, sacrificeHandler.serializeNBT()); tag.putInt(PRIMAL_ENERGY_KEY, primalEnergy); + + if (procGenValues != null) { + CompoundTag tagProcGen = new CompoundTag(); + procGenValues.writeTo(tagProcGen); + tag.put(PROC_GEN_VALUES_KEY, tagProcGen); + } } @Override @@ -349,6 +357,10 @@ else if (tag.contains(SACRIFICE_SYNC_KEY)) { } primalEnergy = tag.getInt(PRIMAL_ENERGY_KEY); + + if (tag.contains(PROC_GEN_VALUES_KEY)) { + procGenValues = MoundShape.ProcGenValues.readFrom(tag.getCompound(PROC_GEN_VALUES_KEY)); + } } @Override diff --git a/src/main/java/com/github/elenterius/biomancy/block/veins/FleshVeinsBlock.java b/src/main/java/com/github/elenterius/biomancy/block/veins/FleshVeinsBlock.java index a788a8ba9..3e893e9ab 100644 --- a/src/main/java/com/github/elenterius/biomancy/block/veins/FleshVeinsBlock.java +++ b/src/main/java/com/github/elenterius/biomancy/block/veins/FleshVeinsBlock.java @@ -15,7 +15,7 @@ import com.github.elenterius.biomancy.util.random.CellularNoise; import com.github.elenterius.biomancy.util.shape.Shape; import com.github.elenterius.biomancy.world.PrimordialEcosystem; -import com.github.elenterius.biomancy.world.ShapeManager; +import com.github.elenterius.biomancy.world.RegionManager; import com.github.elenterius.biomancy.world.mound.MoundChamber; import com.github.elenterius.biomancy.world.mound.MoundShape; import net.minecraft.core.BlockPos; @@ -132,19 +132,20 @@ else if (PrimordialEcosystem.FULL_FLESH_BLOCKS.contains(stateRelative.getBlock() return destroyBlockAndConvertIntoEnergy(level, posRelative, energyHandler, 30); //TODO: this might interfere with future room content generation } - BlockPos posBelow2 = pos.relative(axisDirection, 2); - BlockState stateBelow2 = level.getBlockState(posBelow2); + BlockPos posRelative2 = pos.relative(axisDirection, 2); + BlockState stateRelative2 = level.getBlockState(posRelative2); + boolean isFleshBlock = PrimordialEcosystem.FULL_FLESH_BLOCKS.contains(stateRelative2.getBlock()); - if (axisDirection == Direction.UP && PrimordialEcosystem.FULL_FLESH_BLOCKS.contains(stateBelow2.getBlock()) && LevelUtil.getMaxBrightness(level, pos) < 5) { + if (isFleshBlock && axisDirection == Direction.UP && LevelUtil.getMaxBrightness(level, pos) < 5) { return level.setBlock(posRelative, ModBlocks.BLOOMLIGHT.get().defaultBlockState(), Block.UPDATE_CLIENTS); } - posRelative = posBelow2; - stateRelative = stateBelow2; - if (PrimordialEcosystem.isReplaceable(stateRelative)) { + if (PrimordialEcosystem.isReplaceable(stateRelative2) && stateRelative2.isCollisionShapeFullBlock(level, posRelative2)) { BlockState replacementState = level.random.nextFloat() < nearBoundingCenterPct ? ModBlocks.PRIMAL_FLESH.get().defaultBlockState() : ModBlocks.MALIGNANT_FLESH.get().defaultBlockState(); - return level.setBlock(posRelative, replacementState, Block.UPDATE_CLIENTS); + return level.setBlock(posRelative2, replacementState, Block.UPDATE_CLIENTS); } + + //TODO: place membranes as "doors" } } @@ -492,7 +493,7 @@ public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource MoundChamber chamber = null; float nearBoundingCenterPct = 0; - if (ShapeManager.getShape(level, pos) instanceof MoundShape moundShape) { + if (RegionManager.getClosestShape(level, pos) instanceof MoundShape moundShape) { BlockPos origin = moundShape.getOrigin(); BlockEntity existingBlockEntity = level.getExistingBlockEntity(origin); if (existingBlockEntity instanceof PrimalEnergyHandler peh) { diff --git a/src/main/java/com/github/elenterius/biomancy/util/Bit32Set.java b/src/main/java/com/github/elenterius/biomancy/util/Bit32Set.java index 57b8c9a64..00d8183e1 100644 --- a/src/main/java/com/github/elenterius/biomancy/util/Bit32Set.java +++ b/src/main/java/com/github/elenterius/biomancy/util/Bit32Set.java @@ -1,5 +1,7 @@ package com.github.elenterius.biomancy.util; +import com.github.elenterius.biomancy.util.serialization.IntegerSerializable; + public class Bit32Set implements IntegerSerializable { private int bits; diff --git a/src/main/java/com/github/elenterius/biomancy/util/IntegerSerializable.java b/src/main/java/com/github/elenterius/biomancy/util/serialization/IntegerSerializable.java similarity index 67% rename from src/main/java/com/github/elenterius/biomancy/util/IntegerSerializable.java rename to src/main/java/com/github/elenterius/biomancy/util/serialization/IntegerSerializable.java index c5f56e2a5..fc1caafce 100644 --- a/src/main/java/com/github/elenterius/biomancy/util/IntegerSerializable.java +++ b/src/main/java/com/github/elenterius/biomancy/util/serialization/IntegerSerializable.java @@ -1,4 +1,4 @@ -package com.github.elenterius.biomancy.util; +package com.github.elenterius.biomancy.util.serialization; public interface IntegerSerializable { int serializeToInteger(); diff --git a/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializable.java b/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializable.java new file mode 100644 index 000000000..e9cc82be3 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializable.java @@ -0,0 +1,5 @@ +package com.github.elenterius.biomancy.util.serialization; + +public interface NBTSerializable { + NBTSerializer getNBTSerializer(); +} diff --git a/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializer.java b/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializer.java new file mode 100644 index 000000000..c3629c20e --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializer.java @@ -0,0 +1,16 @@ +package com.github.elenterius.biomancy.util.serialization; + +import net.minecraft.nbt.CompoundTag; + +public interface NBTSerializer { + String id(); + + CompoundTag serializeNBT(T t); + + T deserializeNBT(CompoundTag tag); + + @FunctionalInterface + interface Factory { + NBTSerializer create(String id); + } +} diff --git a/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializers.java b/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializers.java new file mode 100644 index 000000000..7b4edf7da --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/util/serialization/NBTSerializers.java @@ -0,0 +1,47 @@ +package com.github.elenterius.biomancy.util.serialization; + +import com.github.elenterius.biomancy.util.shape.OctantEllipsoidShape; +import com.github.elenterius.biomancy.util.shape.Shape; +import com.github.elenterius.biomancy.util.shape.SphereShape; +import com.github.elenterius.biomancy.world.Region; +import com.github.elenterius.biomancy.world.ShapeRegion; +import com.github.elenterius.biomancy.world.mound.MoundShape; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public final class NBTSerializers { + private static final Map> SERIALIZERS = new HashMap<>(); + public static final NBTSerializer SPHERE_SERIALIZER = registerShape("sphere", SphereShape.Serializer::new); + public static final NBTSerializer OCTANT_ELLIPSOID_SERIALIZER = registerShape("octant_ellipsoid", OctantEllipsoidShape.Serializer::new); + public static final NBTSerializer MOUND_SERIALIZER = registerShape("mound", MoundShape.Serializer::new); + public static final NBTSerializer SHAPE_REGION_SERIALIZER = registerRegion("shape_region", ShapeRegion.Serializer::new); + + private NBTSerializers() {} + + public static NBTSerializer register(String id, NBTSerializer.Factory factory) { + NBTSerializer serializer = factory.create(id); + SERIALIZERS.put(id, serializer); + return serializer; + } + + public static NBTSerializer registerShape(String id, NBTSerializer.Factory factory) { + NBTSerializer serializer = factory.create(id); + SERIALIZERS.put(id, serializer); + //noinspection unchecked + return (NBTSerializer) serializer; + } + + public static NBTSerializer registerRegion(String id, NBTSerializer.Factory factory) { + NBTSerializer serializer = factory.create(id); + SERIALIZERS.put(id, serializer); + //noinspection unchecked + return (NBTSerializer) serializer; + } + + public static @Nullable NBTSerializer get(String id) { + return SERIALIZERS.get(id); + } + +} diff --git a/src/main/java/com/github/elenterius/biomancy/util/serialization/package-info.java b/src/main/java/com/github/elenterius/biomancy/util/serialization/package-info.java new file mode 100644 index 000000000..8144fc5c3 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/util/serialization/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package com.github.elenterius.biomancy.util.serialization; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/com/github/elenterius/biomancy/util/shape/OctantEllipsoidShape.java b/src/main/java/com/github/elenterius/biomancy/util/shape/OctantEllipsoidShape.java index f2a6f8dc6..e23269c7a 100644 --- a/src/main/java/com/github/elenterius/biomancy/util/shape/OctantEllipsoidShape.java +++ b/src/main/java/com/github/elenterius/biomancy/util/shape/OctantEllipsoidShape.java @@ -1,5 +1,8 @@ package com.github.elenterius.biomancy.util.shape; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; +import com.github.elenterius.biomancy.util.serialization.NBTSerializers; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; @@ -70,4 +73,44 @@ public double getRadius() { return radius; } + @Override + public NBTSerializer getNBTSerializer() { + return NBTSerializers.OCTANT_ELLIPSOID_SERIALIZER; + } + + public record Serializer(String id) implements NBTSerializer { + + @Override + public CompoundTag serializeNBT(OctantEllipsoidShape shape) { + CompoundTag tag = new CompoundTag(); + + tag.putDouble("X", shape.origin.x); + tag.putDouble("Y", shape.origin.y); + tag.putDouble("Z", shape.origin.z); + + tag.putFloat("A+", shape.aPos); + tag.putFloat("B+", shape.bPos); + tag.putFloat("C+", shape.cPos); + tag.putFloat("A-", shape.aNeg); + tag.putFloat("B-", shape.bNeg); + tag.putFloat("C-", shape.cNeg); + return tag; + } + + @Override + public OctantEllipsoidShape deserializeNBT(CompoundTag tag) { + double x = tag.getDouble("X"); + double y = tag.getDouble("Y"); + double z = tag.getDouble("Z"); + + float aPos = tag.getFloat("A+"); + float bPos = tag.getFloat("B+"); + float cPos = tag.getFloat("C+"); + float aNeg = tag.getFloat("A-"); + float bNeg = tag.getFloat("B-"); + float cNeg = tag.getFloat("C-"); + + return new OctantEllipsoidShape(x, y, z, aPos, bPos, cPos, aNeg, bNeg, cNeg); + } + } } diff --git a/src/main/java/com/github/elenterius/biomancy/util/shape/Shape.java b/src/main/java/com/github/elenterius/biomancy/util/shape/Shape.java index 0e0e56d59..b203813c2 100644 --- a/src/main/java/com/github/elenterius/biomancy/util/shape/Shape.java +++ b/src/main/java/com/github/elenterius/biomancy/util/shape/Shape.java @@ -1,9 +1,12 @@ package com.github.elenterius.biomancy.util.shape; +import com.github.elenterius.biomancy.util.serialization.NBTSerializable; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; -public interface Shape { +public interface Shape extends NBTSerializable { boolean contains(double x, double y, double z); @@ -16,4 +19,45 @@ public interface Shape { interface Sphere extends Shape { double getRadius(); } + + Shape EMPTY = new Shape() { + static final Serializer SERIALIZER = new Serializer("empty"); + + @Override + public boolean contains(double x, double y, double z) { + return false; + } + + @Override + public Vec3 getCenter() { + return Vec3.ZERO; + } + + @Override + public double distanceToSqr(double x, double y, double z) { + return Double.MAX_VALUE; + } + + @Override + public AABB getAABB() { + return AABB.unitCubeFromLowerCorner(Vec3.ZERO); + } + + @Override + public NBTSerializer getNBTSerializer() { + return SERIALIZER; + } + + public record Serializer(String id) implements NBTSerializer { + @Override + public CompoundTag serializeNBT(Shape shape) { + return new CompoundTag(); + } + + @Override + public Shape deserializeNBT(CompoundTag tag) { + return Shape.EMPTY; + } + } + }; } diff --git a/src/main/java/com/github/elenterius/biomancy/util/shape/ShapeMap.java b/src/main/java/com/github/elenterius/biomancy/util/shape/ShapeHierarchy.java similarity index 96% rename from src/main/java/com/github/elenterius/biomancy/util/shape/ShapeMap.java rename to src/main/java/com/github/elenterius/biomancy/util/shape/ShapeHierarchy.java index 94106ae0c..7df81d1c5 100644 --- a/src/main/java/com/github/elenterius/biomancy/util/shape/ShapeMap.java +++ b/src/main/java/com/github/elenterius/biomancy/util/shape/ShapeHierarchy.java @@ -11,17 +11,16 @@ import java.util.HashSet; import java.util.Set; -/** - * spatial hash map based on a 16x16x16 grid - * - * @param - */ -public class ShapeMap { +public class ShapeHierarchy { + + /** + * spatial hash map based on a 16x16x16 cell grid + */ protected final Long2ObjectMap> sections = new Long2ObjectOpenHashMap<>(); protected final AABB aabb; - public ShapeMap(Iterable shapes) { + public ShapeHierarchy(Iterable shapes) { double aabbMinX = Double.MAX_VALUE; double aabbMinY = Double.MAX_VALUE; double aabbMinZ = Double.MAX_VALUE; diff --git a/src/main/java/com/github/elenterius/biomancy/util/shape/SphereShape.java b/src/main/java/com/github/elenterius/biomancy/util/shape/SphereShape.java index 90ea79e31..843ae6da8 100644 --- a/src/main/java/com/github/elenterius/biomancy/util/shape/SphereShape.java +++ b/src/main/java/com/github/elenterius/biomancy/util/shape/SphereShape.java @@ -1,5 +1,8 @@ package com.github.elenterius.biomancy.util.shape; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; +import com.github.elenterius.biomancy.util.serialization.NBTSerializers; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; @@ -43,4 +46,29 @@ public double getRadius() { return radius; } + @Override + public NBTSerializer getNBTSerializer() { + return NBTSerializers.SPHERE_SERIALIZER; + } + + public record Serializer(String id) implements NBTSerializer { + @Override + public CompoundTag serializeNBT(SphereShape shape) { + CompoundTag tag = new CompoundTag(); + tag.putFloat("Radius", shape.radius); + tag.putDouble("X", shape.origin.x); + tag.putDouble("Y", shape.origin.y); + tag.putDouble("Z", shape.origin.z); + return tag; + } + + @Override + public SphereShape deserializeNBT(CompoundTag tag) { + float radius = tag.getFloat("Radius"); + double x = tag.getDouble("X"); + double y = tag.getDouble("Y"); + double z = tag.getDouble("Z"); + return new SphereShape(x, y, z, radius); + } + } } diff --git a/src/main/java/com/github/elenterius/biomancy/util/shape/package-info.java b/src/main/java/com/github/elenterius/biomancy/util/shape/package-info.java new file mode 100644 index 000000000..aaf23b603 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/util/shape/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package com.github.elenterius.biomancy.util.shape; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/com/github/elenterius/biomancy/world/ChunkPosConsumer.java b/src/main/java/com/github/elenterius/biomancy/world/ChunkPosConsumer.java new file mode 100644 index 000000000..aa04e1e76 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/ChunkPosConsumer.java @@ -0,0 +1,6 @@ +package com.github.elenterius.biomancy.world; + +@FunctionalInterface +public interface ChunkPosConsumer { + void accept(int chunkX, int chunkZ); +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/Region.java b/src/main/java/com/github/elenterius/biomancy/world/Region.java new file mode 100644 index 000000000..3b9e4c9bb --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/Region.java @@ -0,0 +1,59 @@ +package com.github.elenterius.biomancy.world; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.Vec3; + +public abstract class Region { + + protected final BlockPos origin; + protected boolean isValid = false; + + protected Region(BlockPos origin) { + this.origin = origin; + } + + public abstract SABB getSABB(); + + public BlockPos getRegionOrigin() { + return origin; + } + + public final long getId() { + return origin.asLong(); + } + + abstract boolean contains(double x, double y, double z); + + abstract boolean contains(int x, int y, int z); + + boolean contains(BlockPos pos) { + return contains(pos.getX(), pos.getY(), pos.getZ()); + } + + abstract double distanceToSqr(double x, double y, double z); + + public boolean contains(Vec3 pos) { + return contains(pos.x, pos.y, pos.z); + } + + public boolean isValid() { + return isValid; + } + + public void setValid(boolean flag) { + isValid = flag; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof Region otherRegion)) return false; + return getId() == otherRegion.getId(); + } + + @Override + public int hashCode() { + return Long.hashCode(getId()); + } + +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/RegionManager.java b/src/main/java/com/github/elenterius/biomancy/world/RegionManager.java new file mode 100644 index 000000000..a2f09e629 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/RegionManager.java @@ -0,0 +1,73 @@ +package com.github.elenterius.biomancy.world; + +import com.github.elenterius.biomancy.BiomancyMod; +import com.github.elenterius.biomancy.util.shape.Shape; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.event.level.ChunkEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.function.Supplier; + +@Mod.EventBusSubscriber(modid = BiomancyMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE) +public final class RegionManager { + + private RegionManager() {} + + @SubscribeEvent + public static void onChunkUnload(ChunkEvent.Unload event) { + if (!event.getLevel().isClientSide() && event.getLevel() instanceof ServerLevel serverLevel) { + RegionStore regionStore = RegionStore.loadOrGet(serverLevel); + regionStore.unloadChunk(event.getChunk()); + } + } + + @SubscribeEvent + public static void onChunkLoad(ChunkEvent.Load event) { + if (!event.getLevel().isClientSide() && event.getLevel() instanceof ServerLevel serverLevel) { + RegionStore regionStore = RegionStore.loadOrGet(serverLevel); + regionStore.loadChunk(event.getChunk()); + } + } + + public static @Nullable Shape getOrCreateShapeRegion(ServerLevel level, BlockPos regionOrigin, Supplier supplier) { + RegionStore regionStore = RegionStore.loadOrGet(level); + Region region = regionStore.getOrCreate(regionOrigin, pos -> new ShapeRegion(pos, supplier.get())); + return region instanceof ShapeRegion shapeRegion ? shapeRegion.getShape() : null; + } + + public static void remove(ServerLevel level, BlockPos regionOrigin) { + RegionStore regionStore = RegionStore.loadOrGet(level); + regionStore.remove(regionOrigin); + } + + @Nullable + public static Shape getClosestShape(ServerLevel level, BlockPos blockPos) { + RegionStore regionStore = RegionStore.loadOrGet(level); + + Vec3 position = Vec3.atCenterOf(blockPos); + double minDistSqr = Double.MAX_VALUE; + Shape closestShape = null; + + Set regions = regionStore.getRegionsAt(blockPos); + if (regions == null) return null; + + for (Region region : regions) { + if (region.contains(position) && region instanceof ShapeRegion shapeRegion) { + Shape shape = shapeRegion.getShape(); + double distSqr = shape.distanceToSqr(position.x, position.y, position.z); + if (distSqr < minDistSqr) { + closestShape = shape; + minDistSqr = distSqr; + } + } + } + + return closestShape; + } + +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/RegionSectionMap.java b/src/main/java/com/github/elenterius/biomancy/world/RegionSectionMap.java new file mode 100644 index 000000000..b83a401fd --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/RegionSectionMap.java @@ -0,0 +1,276 @@ +package com.github.elenterius.biomancy.world; + +import com.github.elenterius.biomancy.util.serialization.NBTSerializable; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; +import com.github.elenterius.biomancy.util.serialization.NBTSerializers; +import it.unimi.dsi.fastutil.ints.IntArraySet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.level.ChunkPos; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +public class RegionSectionMap { + + /** + * spatial hash map based on a 16x16x16 cell grid + */ + protected final Long2ObjectMap> sections = new Long2ObjectOpenHashMap<>(); + protected final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + protected final Long2ObjectMap loadedRegions = new Long2ObjectOpenHashMap<>(); + + public RegionSectionMap() {} + + public RegionSectionMap(Iterable regions) { + for (Region region : regions) { + add(region); + } + } + + public boolean add(Region region) { + if (loadedRegions.containsKey(region.getId())) return false; + + SABB sectionRegion = region.getSABB(); + + sectionRegion.forEachSection((sectionX, sectionY, sectionZ) -> { + long sectionKey = SectionPos.asLong(sectionX, sectionY, sectionZ); + sections.computeIfAbsent(sectionKey, k -> new HashSet<>()).add(region); + + long chunkKey = ChunkPos.asLong(sectionX, sectionZ); + chunks.computeIfAbsent(chunkKey, k -> new IntArraySet()).add(sectionY); + }); + + loadedRegions.put(region.getId(), region); + return true; + } + + public boolean remove(long regionId) { + Region region = loadedRegions.get(regionId); + if (region == null) return false; + + return remove(region); + } + + public boolean remove(Region region) { + if (!loadedRegions.containsKey(region.getId())) return false; + + SABB sectionRange = region.getSABB(); + + sectionRange.forEachSection((sectionX, sectionY, sectionZ) -> { + long sectionKey = SectionPos.asLong(sectionX, sectionY, sectionZ); + + Set section = sections.get(sectionKey); + if (section != null) { + section.remove(region); + + if (section.isEmpty()) { + sections.remove(sectionKey); + removeChunkSection(sectionX, sectionY, sectionZ); + } + } + else { + removeChunkSection(sectionX, sectionY, sectionZ); + } + }); + + loadedRegions.remove(region.getId()); + return true; + } + + public void removeSectionRange(SABB sectionRange) { + Set removedRegions = new HashSet<>(); + + sectionRange.forEachSection((sectionX, sectionY, sectionZ) -> { + long sectionKey = SectionPos.asLong(sectionX, sectionY, sectionZ); + Set removedValues = sections.remove(sectionKey); + if (removedValues != null) { + removedRegions.addAll(removedValues); + } + removeChunkSection(sectionX, sectionY, sectionZ); + }); + + removeUnloadedRegions(removedRegions); + } + + public void removeSection(long sectionKey) { + Set removedRegions = sections.remove(sectionKey); + removeChunkSection(SectionPos.x(sectionKey), SectionPos.y(sectionKey), SectionPos.z(sectionKey)); + if (removedRegions == null) return; + + removeUnloadedRegions(removedRegions); + } + + public void removeChunk(long chunkKey) { + IntSet chunkSections = chunks.remove(chunkKey); + if (chunkSections == null) return; + + int sectionX = ChunkPos.getX(chunkKey); + int sectionZ = ChunkPos.getZ(chunkKey); + + Set removedRegions = new HashSet<>(); + + for (int sectionY : chunkSections) { + long sectionKey = SectionPos.asLong(sectionX, sectionY, sectionZ); + Set removedValues = sections.remove(sectionKey); + if (removedValues != null) { + removedRegions.addAll(removedValues); + } + } + + removeUnloadedRegions(removedRegions); + } + + private void removeChunkSection(int sectionX, int sectionY, int sectionZ) { + long chunkKey = ChunkPos.asLong(sectionX, sectionZ); + + IntSet chunkSections = chunks.get(chunkKey); + if (chunkSections != null) { + chunkSections.remove(sectionY); + if (chunkSections.isEmpty()) { + chunks.remove(chunkKey); + } + } + } + + private void removeUnloadedRegions(Set regions) { + for (Region region : regions) { + SABB sectionRange = region.getSABB(); + + boolean isLoaded = sectionRange.stream() + .map(pos -> sections.get(pos.asLong())) + .anyMatch(section -> section != null && section.contains(region)); + + if (!isLoaded) { + loadedRegions.remove(region); + } + } + } + + public boolean has(Region region) { + return loadedRegions.containsKey(region.getId()); + } + + + protected @Nullable Set getSection(int sectionX, int sectionY, int sectionZ) { + long sectionKey = SectionPos.asLong(sectionX, sectionY, sectionZ); + return sections.get(sectionKey); + } + + protected @Nullable Set getSectionAt(double x, double y, double z) { + int sectionX = SectionPos.blockToSectionCoord(x); + int sectionY = SectionPos.blockToSectionCoord(y); + int sectionZ = SectionPos.blockToSectionCoord(z); + return getSection(sectionX, sectionY, sectionZ); + } + + protected @Nullable Set getSectionAt(int x, int y, int z) { + int sectionX = SectionPos.blockToSectionCoord(x); + int sectionY = SectionPos.blockToSectionCoord(y); + int sectionZ = SectionPos.blockToSectionCoord(z); + return getSection(sectionX, sectionY, sectionZ); + } + + protected @Nullable Set getSectionAt(BlockPos pos) { + int sectionX = SectionPos.blockToSectionCoord(pos.getX()); + int sectionY = SectionPos.blockToSectionCoord(pos.getY()); + int sectionZ = SectionPos.blockToSectionCoord(pos.getZ()); + return getSection(sectionX, sectionY, sectionZ); + } + + public @Nullable Region getClosestContainingAt(double x, double y, double z) { + Region closestRegion = null; + + Set section = getSectionAt(x, y, z); + if (section != null) { + double minDistSqr = Double.MAX_VALUE; + for (Region region : section) { + if (region.contains(x, y, z)) { + double distSqr = region.distanceToSqr(x, y, z); + if (distSqr < minDistSqr) { + closestRegion = region; + minDistSqr = distSqr; + } + } + } + } + + return closestRegion; + } + + public boolean hasAt(double x, double y, double z) { + Set section = getSectionAt(x, y, z); + if (section != null) { + for (Region region : section) { + if (region.contains(x, y, z)) { + return true; + } + } + } + + return false; + } + + public boolean testAt(double x, double y, double z, Predicate predicate) { + Set section = getSectionAt(x, y, z); + if (section != null) { + for (Region region : section) { + if (region.contains(x, y, z) && predicate.test(region)) { + return true; + } + } + } + + return false; + } + + public CompoundTag writeNBT() { + ListTag listTag = new ListTag(); + + for (Region region : loadedRegions.values()) { + if (region instanceof NBTSerializable serializable) { + //noinspection unchecked + NBTSerializer serializer = (NBTSerializer) serializable.getNBTSerializer(); + CompoundTag serialized = serializer.serializeNBT(region); + serialized.putString("Serializer", serializer.id()); + + CompoundTag tag = new CompoundTag(); + tag.put("Region", serialized); + listTag.add(tag); + } + } + + CompoundTag tag = new CompoundTag(); + tag.put("Regions", listTag); + + return tag; + } + + public void readNBT(CompoundTag tag) { + sections.clear(); + chunks.clear(); + loadedRegions.clear(); + + ListTag tagList = tag.getList("Regions", Tag.TAG_COMPOUND); + for (int i = 0; i < tagList.size(); i++) { + CompoundTag entry = tagList.getCompound(i); + CompoundTag serialized = entry.getCompound("Region"); + String serializerId = serialized.getString("Serializer"); + NBTSerializer nbtSerializer = NBTSerializers.get(serializerId); + if (nbtSerializer != null) { + Object o = nbtSerializer.deserializeNBT(serialized); + if (o instanceof Region region) { + add(region); + } + } + } + } +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/RegionStore.java b/src/main/java/com/github/elenterius/biomancy/world/RegionStore.java new file mode 100644 index 000000000..58f8ea964 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/RegionStore.java @@ -0,0 +1,79 @@ +package com.github.elenterius.biomancy.world; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.saveddata.SavedData; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Set; +import java.util.function.Function; + +public final class RegionStore extends SavedData { + + private final RegionSectionMap regionMap = new RegionSectionMap(); //TODO: replace with R-Tree cache implementation & MVStore R-Tree database + + private RegionStore() {} + + public static RegionStore loadOrGet(ServerLevel level) { + return level + .getDataStorage() + .computeIfAbsent(RegionStore::load, RegionStore::new, "biomancy_region"); + } + + public @Nullable Set getRegionsAt(BlockPos pos) { + return regionMap.getSectionAt(pos); + } + + void unloadChunk(ChunkAccess chunk) { + //regionMap.removeChunk(chunk.getPos().toLong()); + } + + void loadChunk(ChunkAccess chunk) { + //ChunkPos chunkPos = chunk.getPos(); + //long min = SectionPos.asLong(chunkPos.x, chunk.getMinSection(), chunkPos.z); + //long max = SectionPos.asLong(chunkPos.x, chunk.getMaxSection() - 1, chunkPos.z); + //sectionMap.updateOrLoad(new SABB(min, max)); + } + + public Region getOrCreate(BlockPos regionOrigin, Function factory) { + long regionKey = regionOrigin.asLong(); + + if (regionMap.loadedRegions.containsKey(regionKey)) { + return regionMap.loadedRegions.get(regionKey); + } + + Region region = factory.apply(regionOrigin); + regionMap.add(region); + + setDirty(); + return region; + } + + public void remove(BlockPos regionOrigin) { + if (regionMap.remove(regionOrigin.asLong())) { + setDirty(); + } + } + + @Override + public CompoundTag save(CompoundTag tag) { + tag.put("RegionMap", regionMap.writeNBT()); + return tag; + } + + private static RegionStore load(CompoundTag tag) { + RegionStore levelData = new RegionStore(); + levelData.regionMap.readNBT(tag.getCompound("RegionMap")); + return levelData; + } + + @Override + public void save(File file) { + super.save(file); + //TODO: database?? + } + +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/SABB.java b/src/main/java/com/github/elenterius/biomancy/world/SABB.java new file mode 100644 index 000000000..a74c29c76 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/SABB.java @@ -0,0 +1,204 @@ +package com.github.elenterius.biomancy.world; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Cursor3D; +import net.minecraft.core.SectionPos; +import net.minecraft.world.phys.AABB; + +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Section Aligned Bounding Box + * + * @param packedPosMin inclusie + * @param packedPosMax inclusive + * @implNote we store max and min SectionPos as packed long to safe 64 bits + */ +public record SABB(long packedPosMin, long packedPosMax) { + + public static SABB from(SectionPos posA, SectionPos posB) { + int minX = Math.min(posA.x(), posB.x()); + int minY = Math.min(posA.y(), posB.y()); + int minZ = Math.min(posA.z(), posB.z()); + int maxX = Math.max(posA.x(), posB.x()); + int maxY = Math.max(posA.y(), posB.y()); + int maxZ = Math.max(posA.z(), posB.z()); + + long min = SectionPos.asLong(minX, minY, minZ); + long max = SectionPos.asLong(maxX, maxY, maxZ); + + return new SABB(min, max); + } + + public static SABB from(BlockPos posA, BlockPos posB) { + int minX = Math.min(posA.getX(), posB.getX()); + int minY = Math.min(posA.getY(), posB.getY()); + int minZ = Math.min(posA.getZ(), posB.getZ()); + int maxX = Math.max(posA.getX(), posB.getX()); + int maxY = Math.max(posA.getY(), posB.getY()); + int maxZ = Math.max(posA.getZ(), posB.getZ()); + + long min = SectionPos.asLong( + SectionPos.blockToSectionCoord(minX), + SectionPos.blockToSectionCoord(minY), + SectionPos.blockToSectionCoord(minZ) + ); + long max = SectionPos.asLong( + SectionPos.blockToSectionCoord(maxX), + SectionPos.blockToSectionCoord(maxY), + SectionPos.blockToSectionCoord(maxZ) + ); + + return new SABB(min, max); + } + + public static SABB from(AABB boundingBox) { + long min = SectionPos.asLong( + SectionPos.blockToSectionCoord(boundingBox.minX), + SectionPos.blockToSectionCoord(boundingBox.minY), + SectionPos.blockToSectionCoord(boundingBox.minZ) + ); + long max = SectionPos.asLong( + SectionPos.blockToSectionCoord(boundingBox.maxX), + SectionPos.blockToSectionCoord(boundingBox.maxY), + SectionPos.blockToSectionCoord(boundingBox.maxZ) + ); + + return new SABB(min, max); + } + + public int getMinX() { + return SectionPos.x(packedPosMin); + } + + public int getMinY() { + return SectionPos.y(packedPosMin); + } + + public int getMinZ() { + return SectionPos.z(packedPosMin); + } + + public int getMaxX() { + return SectionPos.x(packedPosMax); + } + + public int getMaxY() { + return SectionPos.y(packedPosMax); + } + + public int getMaxZ() { + return SectionPos.z(packedPosMax); + } + + public SectionPos getMinPos() { + return SectionPos.of(packedPosMin); + } + + public SectionPos getMaxPos() { + return SectionPos.of(packedPosMax); + } + + public long getSize() { + long sizeX = getSizeX(); + long sizeY = getSizeY(); + long sizeZ = getSizeZ(); + return sizeX * sizeY * sizeZ; + } + + public int getSizeX() { + int minX = SectionPos.x(packedPosMin); + int maxX = SectionPos.x(packedPosMax); + return (maxX - minX + 1); + } + + public int getSizeY() { + int minY = SectionPos.y(packedPosMin); + int maxY = SectionPos.y(packedPosMax); + return (maxY - minY + 1); + } + + public int getSizeZ() { + int minZ = SectionPos.z(packedPosMin); + int maxZ = SectionPos.z(packedPosMax); + return (maxZ - minZ + 1); + } + + public void forEachSection(SectionPosConsumer action) { + int minX = SectionPos.x(packedPosMin); + int minY = SectionPos.y(packedPosMin); + int minZ = SectionPos.z(packedPosMin); + int maxX = SectionPos.x(packedPosMax); + int maxY = SectionPos.y(packedPosMax); + int maxZ = SectionPos.z(packedPosMax); + + for (int y = minY; y <= maxY; y++) { + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + action.accept(x, y, z); + } + } + } + } + + public void forEachChunk(ChunkPosConsumer action) { + int minX = SectionPos.x(packedPosMin); + int minZ = SectionPos.z(packedPosMin); + int maxX = SectionPos.x(packedPosMax); + int maxZ = SectionPos.z(packedPosMax); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + action.accept(x, z); + } + } + } + + public Stream stream() { + int minX = SectionPos.x(packedPosMin); + int minY = SectionPos.y(packedPosMin); + int minZ = SectionPos.z(packedPosMin); + int maxX = SectionPos.x(packedPosMax); + int maxY = SectionPos.y(packedPosMax); + int maxZ = SectionPos.z(packedPosMax); + long size = (long) (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1); + + return StreamSupport.stream(new Spliterators.AbstractSpliterator<>(size, Spliterator.SIZED) { + final Cursor3D cursor = new Cursor3D(minX, minY, minZ, maxX, maxY, maxZ); + + public boolean tryAdvance(Consumer action) { + if (cursor.advance()) { + action.accept(SectionPos.of(cursor.nextX(), cursor.nextY(), cursor.nextZ())); + return true; + } + return false; + } + }, false); + } + + @Override + public String toString() { + int minX = SectionPos.x(packedPosMin); + int minY = SectionPos.y(packedPosMin); + int minZ = SectionPos.z(packedPosMin); + int maxX = SectionPos.x(packedPosMax); + int maxY = SectionPos.y(packedPosMax); + int maxZ = SectionPos.z(packedPosMax); + long size = ((long) maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1); + + return "SectionABB{" + + "minX=" + minX + + "minY=" + minY + + "minZ=" + minZ + + "maxX=" + maxX + + "maxY=" + maxY + + "maxZ=" + maxZ + + ", size=" + size + + '}'; + } + +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/SectionPosConsumer.java b/src/main/java/com/github/elenterius/biomancy/world/SectionPosConsumer.java new file mode 100644 index 000000000..342bc769b --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/SectionPosConsumer.java @@ -0,0 +1,6 @@ +package com.github.elenterius.biomancy.world; + +@FunctionalInterface +public interface SectionPosConsumer { + void accept(int sectionX, int sectionY, int sectionZ); +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/ShapeManager.java b/src/main/java/com/github/elenterius/biomancy/world/ShapeManager.java deleted file mode 100644 index 4a60e5f25..000000000 --- a/src/main/java/com/github/elenterius/biomancy/world/ShapeManager.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.github.elenterius.biomancy.world; - -import com.github.elenterius.biomancy.BiomancyMod; -import com.github.elenterius.biomancy.util.shape.Shape; -import com.github.elenterius.biomancy.world.mound.MoundShape; -import com.google.common.base.Preconditions; -import com.google.common.collect.MapMaker; -import net.minecraft.core.BlockPos; -import net.minecraft.core.SectionPos; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.LevelReader; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; -import net.minecraftforge.common.ticket.ChunkTicketManager; -import net.minecraftforge.common.ticket.SimpleTicket; -import net.minecraftforge.event.level.ChunkEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -@Mod.EventBusSubscriber(modid = BiomancyMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE) -public class ShapeManager { - private static final Map>> customTicketHandler = new WeakHashMap<>(); - - @SubscribeEvent - public static void onChunkUnload(ChunkEvent.Unload event) { - if (!event.getLevel().isClientSide()) { - removeTickets(event.getChunk()); - } - } - - protected static > T addCustomTicket(Level level, T ticket, ChunkPos masterChunk, ChunkPos... additionalChunks) { - Preconditions.checkArgument(!level.isClientSide, "Water region is only determined server-side"); - Map> ticketMap = customTicketHandler.computeIfAbsent(level, id -> new MapMaker().weakValues().makeMap()); - - @SuppressWarnings("unchecked") - ChunkTicketManager[] additionalTickets = new ChunkTicketManager[additionalChunks.length]; - - for (int i = 0; i < additionalChunks.length; i++) { - additionalTickets[i] = ticketMap.computeIfAbsent(additionalChunks[i], ChunkTicketManager::new); - } - ticket.setManager(ticketMap.computeIfAbsent(masterChunk, ChunkTicketManager::new), additionalTickets); - ticket.validate(); - - return ticket; - } - - public static ShapeTicket addShapeTicket(Level level, Shape shape) { - AABB aabb = shape.getAABB(); - int minX = SectionPos.blockToSectionCoord(aabb.minX); - int maxX = SectionPos.blockToSectionCoord(aabb.maxX); - int minZ = SectionPos.blockToSectionCoord(aabb.minZ); - int maxZ = SectionPos.blockToSectionCoord(aabb.maxZ); - - Set chunkPositions = new HashSet<>(); - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - chunkPositions.add(new ChunkPos(x, z)); - } - } - - ChunkPos mainChunkPos = null; - double minDistance = Double.MAX_VALUE; - - for (ChunkPos chunkPos : chunkPositions) { - double distance = getDistanceSq(chunkPos, shape.getCenter()); - if (distance < minDistance) { - mainChunkPos = chunkPos; - minDistance = distance; - } - } - chunkPositions.remove(mainChunkPos); - - return addCustomTicket(level, new ShapeTicket(shape), mainChunkPos, chunkPositions.toArray(new ChunkPos[0])); - } - - private static double getDistanceSq(ChunkPos pos, Vec3 vec3d) { - double dx = pos.getMiddleBlockX() - vec3d.x; - double dz = pos.getMiddleBlockZ() - vec3d.z; - return dx * dx + dz * dz; - } - - @Nullable - public static Shape getShape(LevelReader level, BlockPos blockPos) { - ChunkTicketManager ticketManager = getTicketManager(new ChunkPos(blockPos), level); - if (ticketManager == null) return null; - - Vec3 position = Vec3.atCenterOf(blockPos); - double minDistSqr = Double.MAX_VALUE; - Shape closestShape = null; - - for (SimpleTicket ticket : ticketManager.getTickets()) { - if (ticket.matches(position) && ticket instanceof ShapeTicket shapeTicket) { - Shape shape = shapeTicket.getShape(); - double distSqr = shape.distanceToSqr(position.x, position.y, position.z); - if (distSqr < minDistSqr) { - closestShape = shape; - minDistSqr = distSqr; - } - } - } - - return closestShape; - } - - public static Optional getMoundShape(LevelReader level, BlockPos blockPos) { - if (getShape(level, blockPos) instanceof MoundShape moundShape) { - return Optional.of(moundShape); - } - return Optional.empty(); - } - - static void removeTickets(ChunkAccess chunk) { - ChunkTicketManager ticketManager = getTicketManager(chunk.getPos(), chunk.getWorldForge()); - if (ticketManager != null) { - ticketManager.getTickets().removeIf(next -> next.unload(ticketManager)); //remove if this is the master manager of the ticket - } - } - - @Nullable - private static ChunkTicketManager getTicketManager(ChunkPos pos, LevelReader level) { - Preconditions.checkArgument(!level.isClientSide(), "Water region is only determined server-side"); - Map> ticketMap = customTicketHandler.get(level); - return ticketMap == null ? null : ticketMap.get(pos); - } - -} diff --git a/src/main/java/com/github/elenterius/biomancy/world/ShapeRegion.java b/src/main/java/com/github/elenterius/biomancy/world/ShapeRegion.java new file mode 100644 index 000000000..9ba094435 --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/ShapeRegion.java @@ -0,0 +1,82 @@ +package com.github.elenterius.biomancy.world; + +import com.github.elenterius.biomancy.util.serialization.NBTSerializable; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; +import com.github.elenterius.biomancy.util.serialization.NBTSerializers; +import com.github.elenterius.biomancy.util.shape.Shape; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; + +public class ShapeRegion extends Region implements NBTSerializable { + + private final Shape shape; + private final SABB sabb; + + protected ShapeRegion(BlockPos origin, Shape shape) { + super(origin); + this.shape = shape; + sabb = SABB.from(shape.getAABB()); + } + + @Override + public SABB getSABB() { + return sabb; + } + + @Override + boolean contains(double x, double y, double z) { + return shape.contains(x, y, z); + } + + @Override + boolean contains(int x, int y, int z) { + return shape.contains(x, y, z); + } + + @Override + double distanceToSqr(double x, double y, double z) { + return shape.distanceToSqr(x, y, z); + } + + public Shape getShape() { + return shape; + } + + @Override + public NBTSerializer getNBTSerializer() { + return NBTSerializers.SHAPE_REGION_SERIALIZER; + } + + public record Serializer(String id) implements NBTSerializer { + + @Override + public CompoundTag serializeNBT(ShapeRegion region) { + CompoundTag tag = new CompoundTag(); + tag.putLong("Id", region.origin.asLong()); + + NBTSerializer serializer = region.shape.getNBTSerializer(); + CompoundTag serialized = serializer.serializeNBT(region.shape); + serialized.putString("Serializer", serializer.id()); + + tag.put("Shape", serialized); + return tag; + } + + @Override + public ShapeRegion deserializeNBT(CompoundTag tag) { + long origin = tag.getLong("Id"); + + CompoundTag serialized = tag.getCompound("Shape"); + String serializerId = serialized.getString("Serializer"); + NBTSerializer nbtSerializer = NBTSerializers.get(serializerId); + if (nbtSerializer != null) { + Object o = nbtSerializer.deserializeNBT(serialized); + if (o instanceof Shape shape) { + return new ShapeRegion(BlockPos.of(origin), shape); + } + } + + return new ShapeRegion(BlockPos.of(origin), Shape.EMPTY); + } + } +} diff --git a/src/main/java/com/github/elenterius/biomancy/world/ShapeTicket.java b/src/main/java/com/github/elenterius/biomancy/world/ShapeTicket.java deleted file mode 100644 index a1fe6a144..000000000 --- a/src/main/java/com/github/elenterius/biomancy/world/ShapeTicket.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.elenterius.biomancy.world; - -import com.github.elenterius.biomancy.util.shape.Shape; -import net.minecraft.world.phys.Vec3; -import net.minecraftforge.common.ticket.SimpleTicket; - -public class ShapeTicket extends SimpleTicket { - private final Shape shape; - - public ShapeTicket(Shape shape) { - this.shape = shape; - } - - @Override - public boolean matches(Vec3 toMatch) { - return shape.contains(toMatch.x, toMatch.y, toMatch.z); - } - - public Shape getShape() { - return shape; - } - -} diff --git a/src/main/java/com/github/elenterius/biomancy/world/mound/MoundChamber.java b/src/main/java/com/github/elenterius/biomancy/world/mound/MoundChamber.java index f07a501b9..531116ea8 100644 --- a/src/main/java/com/github/elenterius/biomancy/world/mound/MoundChamber.java +++ b/src/main/java/com/github/elenterius/biomancy/world/mound/MoundChamber.java @@ -1,5 +1,6 @@ package com.github.elenterius.biomancy.world.mound; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; import com.github.elenterius.biomancy.util.shape.Shape; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; @@ -42,4 +43,9 @@ public double distanceToSqr(double x, double y, double z) { public AABB getAABB() { return shape.getAABB(); } + + @Override + public NBTSerializer getNBTSerializer() { + return null; + } } diff --git a/src/main/java/com/github/elenterius/biomancy/world/mound/MoundGenerator.java b/src/main/java/com/github/elenterius/biomancy/world/mound/MoundGenerator.java index 0d6d21b0f..6cdd6b43c 100644 --- a/src/main/java/com/github/elenterius/biomancy/world/mound/MoundGenerator.java +++ b/src/main/java/com/github/elenterius/biomancy/world/mound/MoundGenerator.java @@ -17,24 +17,37 @@ public final class MoundGenerator { private MoundGenerator() {} - public static MoundShape constructShape(Level level, BlockPos blockOrigin, long seed) { - Context ctx = new Context(); - ctx.random = RandomSource.create(seed); + public static MoundShape constructShape(BlockPos blockOrigin, MoundShape.ProcGenValues procGenValues) { + return genShape(blockOrigin, procGenValues); + } - final int maxBuildHeight = level.getMaxBuildHeight(); - final int seaLevel = level.getSeaLevel(); + public static MoundShape constructShape(Level level, BlockPos blockOrigin, long seed) { + Biome biome = level.getBiome(blockOrigin).get(); + MoundShape.ProcGenValues procGenValues = new MoundShape.ProcGenValues( + seed, + level.getMaxBuildHeight(), + level.getSeaLevel(), + TemperatureUtil.getTemperature(biome, blockOrigin), + biome.getDownfall() + ); + + return genShape(blockOrigin, procGenValues); + } + private static MoundShape genShape(BlockPos blockOrigin, MoundShape.ProcGenValues procGenValues) { + Context ctx = new Context(); + ctx.random = RandomSource.create(procGenValues.seed()); Vec3 origin = Vec3.atCenterOf(blockOrigin); //TODO: add these to the cradle? //////////////// float heightMultiplier = 0; float spireCountModifier = 6; float roomSizeModifier = 4; //clamp between 0 and 4? The more spires the smaller the roomSizeModifier? + ///////////////////////////////////////////////// - Biome biome = level.getBiome(blockOrigin).get(); - float biomeTemperature = TemperatureUtil.getTemperature(biome, blockOrigin); - float biomeHumidity = biome.getDownfall(); + float biomeTemperature = procGenValues.biomeTemperature(); + float biomeHumidity = procGenValues.biomeHumidity(); float heatMultiplier = TemperatureUtil.rescale(biomeTemperature) * 0.5f + biomeTemperature / TemperatureUtil.MAX_TEMP * 0.5f; float coldMultiplier = TemperatureUtil.isFreezing(biomeTemperature) ? 0.1f : 1; @@ -52,6 +65,8 @@ public static MoundShape constructShape(Level level, BlockPos blockOrigin, long float subSpireRadius = ctx.maxMoundRadius / 2; float extraSpires = Mth.clamp(spireCountModifier, 0, countCirclesOnCircumference(ctx.maxMoundRadius, subSpireRadius)); + int maxBuildHeight = procGenValues.maxBuildHeight(); + int seaLevel = procGenValues.seaLevel(); float maxMoundHeight = Mth.clamp(maxBuildHeight * ctx.spikiness, 0, (maxBuildHeight - seaLevel)); ctx.dirLean = new Vec3(ctx.random.nextFloat() - ctx.random.nextFloat(), 0, ctx.random.nextFloat() - ctx.random.nextFloat()).normalize(); @@ -70,7 +85,7 @@ public static MoundShape constructShape(Level level, BlockPos blockOrigin, long genSpire(xn, origin.y, zn, maxMoundHeight / (1.5f + ctx.random.nextFloat() * 1.5f), subRadius, ctx); } - return new MoundShape(blockOrigin, ctx.boundingShapes, ctx.chambers); + return new MoundShape(blockOrigin, ctx.boundingShapes, ctx.chambers, procGenValues); } private static void genSpire(double x, double y, double z, float maxHeight, float baseRadius, Context context) { diff --git a/src/main/java/com/github/elenterius/biomancy/world/mound/MoundShape.java b/src/main/java/com/github/elenterius/biomancy/world/mound/MoundShape.java index 6e344236b..b209722c8 100644 --- a/src/main/java/com/github/elenterius/biomancy/world/mound/MoundShape.java +++ b/src/main/java/com/github/elenterius/biomancy/world/mound/MoundShape.java @@ -1,8 +1,11 @@ package com.github.elenterius.biomancy.world.mound; +import com.github.elenterius.biomancy.util.serialization.NBTSerializer; +import com.github.elenterius.biomancy.util.serialization.NBTSerializers; import com.github.elenterius.biomancy.util.shape.Shape; -import com.github.elenterius.biomancy.util.shape.ShapeMap; +import com.github.elenterius.biomancy.util.shape.ShapeHierarchy; import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; @@ -10,14 +13,16 @@ import java.util.List; public class MoundShape implements Shape { + private final ProcGenValues procGenValues; BlockPos origin; - ShapeMap boundingShapes; - ShapeMap chamberShapes; + ShapeHierarchy boundingShapes; + ShapeHierarchy chamberShapes; - MoundShape(BlockPos origin, List boundingShapes, List chamberShapes) { - this.boundingShapes = new ShapeMap<>(boundingShapes); - this.chamberShapes = new ShapeMap<>(chamberShapes); + MoundShape(BlockPos origin, List boundingShapes, List chamberShapes, ProcGenValues procGenValues) { + this.boundingShapes = new ShapeHierarchy<>(boundingShapes); + this.chamberShapes = new ShapeHierarchy<>(chamberShapes); this.origin = origin; + this.procGenValues = procGenValues; } public BlockPos getOrigin() { @@ -54,6 +59,51 @@ public Shape getBoundingShapeAt(int x, int y, int z) { return boundingShapes.getClosestShapeContaining(x, y, z); } + public ProcGenValues getProcGenValues() { + return procGenValues; + } + + @Override + public NBTSerializer getNBTSerializer() { + return NBTSerializers.MOUND_SERIALIZER; + } + + public record ProcGenValues(long seed, int maxBuildHeight, int seaLevel, float biomeTemperature, float biomeHumidity) { + public void writeTo(CompoundTag tag) { + tag.putLong("Seed", seed); + tag.putInt("MaxBuildHeight", maxBuildHeight); + tag.putInt("SeaLevel", seaLevel); + tag.putFloat("BiomeTemperature", biomeTemperature); + tag.putFloat("BiomeHumidity", biomeHumidity); + } + + public static ProcGenValues readFrom(CompoundTag tag) { + long seed = tag.getLong("Seed"); + int maxBuildHeight = tag.getInt("MaxBuildHeight"); + int seaLevel = tag.getInt("SeaLevel"); + float biomeTemperature = tag.getFloat("BiomeTemperature"); + float biomeHumidity = tag.getFloat("BiomeHumidity"); + return new ProcGenValues(seed, maxBuildHeight, seaLevel, biomeTemperature, biomeHumidity); + } + } + + public record Serializer(String id) implements NBTSerializer { + + @Override + public CompoundTag serializeNBT(MoundShape shape) { + CompoundTag tag = new CompoundTag(); + tag.putLong("Origin", shape.origin.asLong()); + shape.procGenValues.writeTo(tag); + return tag; + } + + @Override + public MoundShape deserializeNBT(CompoundTag tag) { + BlockPos origin = BlockPos.of(tag.getLong("Origin")); + ProcGenValues procGenValues = ProcGenValues.readFrom(tag); + return MoundGenerator.constructShape(origin, procGenValues); + } + } } diff --git a/src/main/java/com/github/elenterius/biomancy/world/mound/package-info.java b/src/main/java/com/github/elenterius/biomancy/world/mound/package-info.java new file mode 100644 index 000000000..7ab4ea96b --- /dev/null +++ b/src/main/java/com/github/elenterius/biomancy/world/mound/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package com.github.elenterius.biomancy.world.mound; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file