diff --git a/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java b/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java index 866caedbd..dd5b383b9 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java @@ -122,9 +122,7 @@ public interface INMSBinding { return 441; } - default IRegionStorage createRegionStorage(Engine engine) { - return null; - } + IRegionStorage createRegionStorage(Engine engine); KList getStructureKeys(); } diff --git a/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java index fa722e688..742b01b58 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java +++ b/core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java @@ -21,6 +21,7 @@ package com.volmit.iris.core.nms.v1X; import com.volmit.iris.Iris; import com.volmit.iris.core.nms.INMSBinding; import com.volmit.iris.core.nms.container.BiomeColor; +import com.volmit.iris.core.nms.headless.IRegionStorage; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; @@ -109,6 +110,11 @@ public class NMSBinding1X implements INMSBinding { return Color.GREEN; } + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return null; + } + @Override public KList getStructureKeys() { var list = StreamSupport.stream(Registry.STRUCTURE.spliterator(), false) diff --git a/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java index a9ad26383..bd36e4bba 100644 --- a/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java +++ b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java @@ -5,6 +5,8 @@ import com.mojang.datafixers.util.Pair; import com.volmit.iris.Iris; import com.volmit.iris.core.nms.INMSBinding; import com.volmit.iris.core.nms.container.BiomeColor; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_20_R1.headless.RegionStorage; import com.volmit.iris.engine.data.cache.AtomicCache; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.util.collection.KList; @@ -628,4 +630,9 @@ public class NMSBinding implements INMSBinding { } } } + + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); + } } diff --git a/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/DirectTerrainChunk.java b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/DirectTerrainChunk.java new file mode 100644 index 000000000..b0b9dbe58 --- /dev/null +++ b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/DirectTerrainChunk.java @@ -0,0 +1,209 @@ +package com.volmit.iris.core.nms.v1_20_R1.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.util.data.IrisCustomData; +import com.volmit.iris.util.math.Position2; +import lombok.Data; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_20_R1.block.CraftBlock; +import org.bukkit.craftbukkit.v1_20_R1.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +import static com.volmit.iris.core.nms.v1_20_R1.headless.RegionStorage.registryAccess; + +@Data +public final class DirectTerrainChunk implements SerializableChunk { + private final ChunkAccess access; + private final int minHeight, maxHeight; + private final Registry biomes; + + public DirectTerrainChunk(ChunkAccess access) { + this.access = access; + this.minHeight = access.getMinBuildHeight(); + this.maxHeight = access.getMaxBuildHeight(); + this.biomes = registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.BIOME); + } + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return null; + } + + @NotNull + @Override + public Biome getBiome(int x, int z) { + return getBiome(x, 0, z); + } + + @NotNull + @Override + public Biome getBiome(int x, int y, int z) { + if (y < minHeight || y > maxHeight) return Biome.PLAINS; + return CraftBlock.biomeBaseToBiome(biomes, access.getNoiseBiome(x >> 2, y >> 2, z >> 2)); + } + + @Override + public void setBiome(int x, int z, Biome bio) { + for (int y = minHeight; y < maxHeight; y += 4) { + setBiome(x, y, z, bio); + } + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + if (y < minHeight || y > maxHeight) return; + access.setBiome(x & 15, y, z & 15, CraftBlock.biomeToBiomeBase(biomes, bio)); + } + + public void setBlock(int x, int y, int z, Material material) { + this.setBlock(x, y, z, material.createBlockData()); + } + + public void setBlock(int x, int y, int z, MaterialData material) { + this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material)); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisCustomData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData()); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material)); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState()); + } + + public Material getType(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z).getBlock()); + } + + public MaterialData getTypeAndData(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z)); + } + + public BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(this.getTypeId(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) { + if (xMin > 15 || yMin >= this.maxHeight || zMin > 15) + return; + + if (xMin < 0) { + xMin = 0; + } + + if (yMin < this.minHeight) { + yMin = this.minHeight; + } + + if (zMin < 0) { + zMin = 0; + } + + if (xMax > 16) { + xMax = 16; + } + + if (yMax > this.maxHeight) { + yMax = this.maxHeight; + } + + if (zMax > 16) { + zMax = 16; + } + + if (xMin >= xMax || yMin >= yMax || zMin >= zMax) + return; + + for (int y = yMin; y < yMax; ++y) { + for (int x = xMin; x < xMax; ++x) { + for (int z = zMin; z < zMax; ++z) { + this.setBlock(x, y, z, type); + } + } + } + + } + + public BlockState getTypeId(int x, int y, int z) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return Blocks.AIR.defaultBlockState(); + return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z)); + } + + public byte getData(int x, int y, int z) { + return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z)); + } + + private void setBlock(int x, int y, int z, BlockState type) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return; + BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z); + BlockState oldBlockData = access.setBlockState(blockPosition, type, false); + if (type.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type); + if (tileEntity == null) { + access.removeBlockEntity(blockPosition); + } else { + access.setBlockEntity(tileEntity); + } + } else if (oldBlockData != null && oldBlockData.hasBlockEntity()) { + access.removeBlockEntity(blockPosition); + } + + } + + @Override + public Position2 getPos() { + return new Position2(access.getPos().x, access.getPos().z); + } + + @Override + public Object serialize() { + return RegionStorage.serialize(access); + } +} \ No newline at end of file diff --git a/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/Region.java b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/Region.java new file mode 100644 index 000000000..c237a2c08 --- /dev/null +++ b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/Region.java @@ -0,0 +1,60 @@ +package com.volmit.iris.core.nms.v1_20_R1.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import lombok.NonNull; +import lombok.Synchronized; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +class Region implements IRegion { + private final RegionFile regionFile; + transient long references; + + Region(Path path, Path folder) throws IOException { + this.regionFile = new RegionFile(path, folder, true); + } + + @Override + @Synchronized + public boolean exists(int x, int z) { + try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) { + if (din == null) return false; + return !"empty".equals(NbtIo.read(din).getString("Status")); + } catch (IOException e) { + return false; + } + } + + @Override + @Synchronized + public void write(@NonNull SerializableChunk chunk) throws IOException { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) { + NbtIo.write((CompoundTag) chunk.serialize(), dos); + } + } + + @Override + public void close() { + --references; + } + + public boolean remove() { + if (references > 0) return false; + try { + regionFile.close(); + } catch (IOException e) { + Iris.error("Failed to close region file"); + e.printStackTrace(); + } + return true; + } +} diff --git a/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/RegionStorage.java b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/RegionStorage.java new file mode 100644 index 000000000..aa61490b3 --- /dev/null +++ b/nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/headless/RegionStorage.java @@ -0,0 +1,292 @@ +package com.volmit.iris.core.nms.v1_20_R1.headless; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.math.RNG; +import lombok.Getter; +import lombok.NonNull; +import net.minecraft.FileUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_20_R1.CraftServer; +import org.bukkit.craftbukkit.v1_20_R1.block.CraftBlock; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC; +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets; + +public class RegionStorage implements IRegionStorage, LevelHeightAccessor { + private static final AtomicCache CACHE = new AtomicCache<>(); + private final KMap regions = new KMap<>(); + private final Path folder; + + private final Engine engine; + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes; + private final RNG biomeRng; + private final @Getter int minBuildHeight; + private final @Getter int height; + + private transient boolean closed = false; + + public RegionStorage(Engine engine) { + this.engine = engine; + this.folder = new File(engine.getWorld().worldFolder(), "region").toPath(); + this.biomeRng = new RNG(engine.getSeedManager().getBiome()); + + this.minBuildHeight = engine.getDimension().getMinHeight(); + this.height = engine.getDimension().getMaxHeight() - minBuildHeight; + + AtomicInteger failed = new AtomicInteger(); + var dimKey = engine.getDimension().getLoadKey(); + for (var biome : engine.getAllBiomes()) { + if (!biome.isCustom()) continue; + for (var custom : biome.getCustomDerivitives()) { + biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> { + Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId()); + failed.incrementAndGet(); + }); + } + } + if (failed.get() > 0) { + throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes"); + } + + Registry registry = registryAccess().registryOrThrow(Registries.BIOME); + minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream() + .collect(Collectors.toMap(Function.identity(), b -> CraftBlock.biomeToBiomeBase(registry, b)))); + minecraftBiomes.values().removeAll(customBiomes.values()); + } + + @Override + public boolean exists(int x, int z) { + try (IRegion region = getRegion(x, z, true)) { + return region != null && region.exists(x, z); + } catch (Exception e) { + return false; + } + } + + @Override + public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException { + AtomicReference exception = new AtomicReference<>(); + Region region = regions.computeIfAbsent(Cache.key(x, z), k -> { + if (regions.size() >= 256) { + regions.values().removeIf(Region::remove); + } + + try { + FileUtil.createDirectoriesSafe(this.folder); + Path path = folder.resolve("r." + x + "." + z + ".mca"); + if (existingOnly && !Files.exists(path)) { + return null; + } else { + return new Region(path, this.folder); + } + } catch (IOException e) { + exception.set(e); + return null; + } + }); + + if (region == null) { + if (exception.get() != null) + throw exception.get(); + return null; + } + region.references++; + return region; + } + + @NotNull + @Override + public SerializableChunk createChunk(int x, int z) { + return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null)); + } + + @Override + public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) { + if (!(chunk instanceof DirectTerrainChunk tc)) + return; + tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); + } + + @Override + public synchronized void close() { + if (closed) return; + + while (!regions.isEmpty()) { + regions.values().removeIf(Region::remove); + } + + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) { + int m = y - engine.getMinHeight(); + IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId()); + } else { + return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z)); + } + } + + static RegistryAccess registryAccess() { + return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess()); + } + + private static Optional> biomeHolder(String namespace, String path) { + return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, path))); + } + + static CompoundTag serialize(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag()); + tag.putInt("xPos", chunkPos.x); + tag.putInt("yPos", chunk.getMinSection()); + tag.putInt("zPos", chunkPos.z); + tag.putLong("LastUpdate", 0); + tag.putLong("InhabitedTime", chunk.getInhabitedTime()); + tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); + if (blendingdata != null) { + DataResult dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt)); + } + + BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen(); + if (retrogen != null) { + DataResult dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt)); + } + + UpgradeData upgradeData = chunk.getUpgradeData(); + if (!upgradeData.isEmpty()) { + tag.put("UpgradeData", upgradeData.write()); + } + + LevelChunkSection[] sections = chunk.getSections(); + ListTag sectionsTag = new ListTag(); + Registry biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME); + Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); + boolean flag = chunk.isLightCorrect(); + + int minLightSection = chunk.getMinSection() - 1; + int maxLightSection = minLightSection + chunk.getSectionsCount() + 2; + for(int y = minLightSection; y < maxLightSection; ++y) { + int j = chunk.getSectionIndexFromSectionY(y); + if (j < 0 || j >= sections.length) + continue; + CompoundTag sectionTag = new CompoundTag(); + LevelChunkSection section = sections[j]; + sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, LogUtils.getLogger()::error)); + sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, LogUtils.getLogger()::error)); + + if (!sectionTag.isEmpty()) { + sectionTag.putByte("Y", (byte) y); + sectionsTag.add(sectionTag); + } + } + + tag.put("sections", sectionsTag); + if (flag) { + tag.putBoolean("isLightOn", true); + } + + ListTag blockEntities = new ListTag(); + for(BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos); + if (entityNbt != null) { + blockEntities.add(entityNbt); + } + } + + tag.put("block_entities", blockEntities); + if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK) { + ProtoChunk protochunk = (ProtoChunk)chunk; + ListTag entities = new ListTag(); + entities.addAll(protochunk.getEntities()); + tag.put("entities", entities); + CompoundTag carvingMasks = new CompoundTag(); + + for(GenerationStep.Carving carving : GenerationStep.Carving.values()) { + CarvingMask mask = protochunk.getCarvingMask(carving); + if (mask != null) { + carvingMasks.putLongArray(carving.toString(), mask.toArray()); + } + } + + tag.put("CarvingMasks", carvingMasks); + } + + saveTicks(tag, chunk.getTicksForSerialization()); + tag.put("PostProcessing", packOffsets(chunk.getPostProcessing())); + CompoundTag heightMaps = new CompoundTag(); + + for(Map.Entry entry : chunk.getHeightmaps()) { + if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) { + heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData())); + } + } + + tag.put("Heightmaps", heightMaps); + + CompoundTag structureData = new CompoundTag(); + structureData.put("starts", new CompoundTag()); + structureData.put("References", new CompoundTag()); + tag.put("structures", structureData); + if (!chunk.persistentDataContainer.isEmpty()) { + tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound()); + } + + return tag; + } + + private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) { + tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString())); + tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString())); + } +} diff --git a/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java index 804f35074..08dcb5f77 100644 --- a/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java +++ b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java @@ -16,6 +16,8 @@ import java.util.concurrent.atomic.AtomicInteger; import com.mojang.datafixers.util.Pair; import com.volmit.iris.core.nms.container.BiomeColor; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_20_R2.headless.RegionStorage; import com.volmit.iris.util.scheduling.J; import net.minecraft.nbt.*; import net.minecraft.nbt.Tag; @@ -630,4 +632,9 @@ public class NMSBinding implements INMSBinding { public static Holder biomeToBiomeBase(Registry registry, Biome biome) { return registry.getHolderOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey()))); } + + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); + } } diff --git a/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/DirectTerrainChunk.java b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/DirectTerrainChunk.java new file mode 100644 index 000000000..26324b6dc --- /dev/null +++ b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/DirectTerrainChunk.java @@ -0,0 +1,204 @@ +package com.volmit.iris.core.nms.v1_20_R2.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.util.data.IrisCustomData; +import com.volmit.iris.util.math.Position2; +import lombok.Data; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_20_R2.block.CraftBiome; +import org.bukkit.craftbukkit.v1_20_R2.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_20_R2.util.CraftMagicNumbers; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +@Data +public final class DirectTerrainChunk implements SerializableChunk { + private final ChunkAccess access; + private final int minHeight, maxHeight; + + public DirectTerrainChunk(ChunkAccess access) { + this.access = access; + this.minHeight = access.getMinBuildHeight(); + this.maxHeight = access.getMaxBuildHeight(); + } + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return null; + } + + @NotNull + @Override + public Biome getBiome(int x, int z) { + return getBiome(x, 0, z); + } + + @NotNull + @Override + public Biome getBiome(int x, int y, int z) { + if (y < minHeight || y > maxHeight) return Biome.PLAINS; + return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2)); + } + + @Override + public void setBiome(int x, int z, Biome bio) { + for (int y = minHeight; y < maxHeight; y += 4) { + setBiome(x, y, z, bio); + } + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + if (y < minHeight || y > maxHeight) return; + access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio)); + } + + public void setBlock(int x, int y, int z, Material material) { + this.setBlock(x, y, z, material.createBlockData()); + } + + public void setBlock(int x, int y, int z, MaterialData material) { + this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material)); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisCustomData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData()); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material)); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState()); + } + + public Material getType(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z).getBlock()); + } + + public MaterialData getTypeAndData(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z)); + } + + public BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(this.getTypeId(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) { + if (xMin > 15 || yMin >= this.maxHeight || zMin > 15) + return; + + if (xMin < 0) { + xMin = 0; + } + + if (yMin < this.minHeight) { + yMin = this.minHeight; + } + + if (zMin < 0) { + zMin = 0; + } + + if (xMax > 16) { + xMax = 16; + } + + if (yMax > this.maxHeight) { + yMax = this.maxHeight; + } + + if (zMax > 16) { + zMax = 16; + } + + if (xMin >= xMax || yMin >= yMax || zMin >= zMax) + return; + + for (int y = yMin; y < yMax; ++y) { + for (int x = xMin; x < xMax; ++x) { + for (int z = zMin; z < zMax; ++z) { + this.setBlock(x, y, z, type); + } + } + } + + } + + public BlockState getTypeId(int x, int y, int z) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return Blocks.AIR.defaultBlockState(); + return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z)); + } + + public byte getData(int x, int y, int z) { + return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z)); + } + + private void setBlock(int x, int y, int z, BlockState type) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return; + BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z); + BlockState oldBlockData = access.setBlockState(blockPosition, type, false); + if (type.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type); + if (tileEntity == null) { + access.removeBlockEntity(blockPosition); + } else { + access.setBlockEntity(tileEntity); + } + } else if (oldBlockData != null && oldBlockData.hasBlockEntity()) { + access.removeBlockEntity(blockPosition); + } + + } + + @Override + public Position2 getPos() { + return new Position2(access.getPos().x, access.getPos().z); + } + + @Override + public Object serialize() { + return RegionStorage.serialize(access); + } +} \ No newline at end of file diff --git a/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/Region.java b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/Region.java new file mode 100644 index 000000000..086fdd9e6 --- /dev/null +++ b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/Region.java @@ -0,0 +1,60 @@ +package com.volmit.iris.core.nms.v1_20_R2.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import lombok.NonNull; +import lombok.Synchronized; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +class Region implements IRegion { + private final RegionFile regionFile; + transient long references; + + Region(Path path, Path folder) throws IOException { + this.regionFile = new RegionFile(path, folder, true); + } + + @Override + @Synchronized + public boolean exists(int x, int z) { + try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) { + if (din == null) return false; + return !"empty".equals(NbtIo.read(din).getString("Status")); + } catch (IOException e) { + return false; + } + } + + @Override + @Synchronized + public void write(@NonNull SerializableChunk chunk) throws IOException { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) { + NbtIo.write((CompoundTag) chunk.serialize(), dos); + } + } + + @Override + public void close() { + --references; + } + + public boolean remove() { + if (references > 0) return false; + try { + regionFile.close(); + } catch (IOException e) { + Iris.error("Failed to close region file"); + e.printStackTrace(); + } + return true; + } +} diff --git a/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/RegionStorage.java b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/RegionStorage.java new file mode 100644 index 000000000..094b41328 --- /dev/null +++ b/nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/headless/RegionStorage.java @@ -0,0 +1,291 @@ +package com.volmit.iris.core.nms.v1_20_R2.headless; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.math.RNG; +import lombok.Getter; +import lombok.NonNull; +import net.minecraft.FileUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_20_R2.CraftServer; +import org.bukkit.craftbukkit.v1_20_R2.block.CraftBiome; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC; +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets; + +public class RegionStorage implements IRegionStorage, LevelHeightAccessor { + private static final AtomicCache CACHE = new AtomicCache<>(); + private final KMap regions = new KMap<>(); + private final Path folder; + + private final Engine engine; + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes; + private final RNG biomeRng; + private final @Getter int minBuildHeight; + private final @Getter int height; + + private transient boolean closed = false; + + public RegionStorage(Engine engine) { + this.engine = engine; + this.folder = new File(engine.getWorld().worldFolder(), "region").toPath(); + this.biomeRng = new RNG(engine.getSeedManager().getBiome()); + + this.minBuildHeight = engine.getDimension().getMinHeight(); + this.height = engine.getDimension().getMaxHeight() - minBuildHeight; + + AtomicInteger failed = new AtomicInteger(); + var dimKey = engine.getDimension().getLoadKey(); + for (var biome : engine.getAllBiomes()) { + if (!biome.isCustom()) continue; + for (var custom : biome.getCustomDerivitives()) { + biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> { + Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId()); + failed.incrementAndGet(); + }); + } + } + if (failed.get() > 0) { + throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes"); + } + + minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream() + .collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder))); + minecraftBiomes.values().removeAll(customBiomes.values()); + } + + @Override + public boolean exists(int x, int z) { + try (IRegion region = getRegion(x, z, true)) { + return region != null && region.exists(x, z); + } catch (Exception e) { + return false; + } + } + + @Override + public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException { + AtomicReference exception = new AtomicReference<>(); + Region region = regions.computeIfAbsent(Cache.key(x, z), k -> { + if (regions.size() >= 256) { + regions.values().removeIf(Region::remove); + } + + try { + FileUtil.createDirectoriesSafe(this.folder); + Path path = folder.resolve("r." + x + "." + z + ".mca"); + if (existingOnly && !Files.exists(path)) { + return null; + } else { + return new Region(path, this.folder); + } + } catch (IOException e) { + exception.set(e); + return null; + } + }); + + if (region == null) { + if (exception.get() != null) + throw exception.get(); + return null; + } + region.references++; + return region; + } + + @NotNull + @Override + public SerializableChunk createChunk(int x, int z) { + return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null)); + } + + @Override + public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) { + if (!(chunk instanceof DirectTerrainChunk tc)) + return; + tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); + } + + @Override + public synchronized void close() { + if (closed) return; + + while (!regions.isEmpty()) { + regions.values().removeIf(Region::remove); + } + + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) { + int m = y - engine.getMinHeight(); + IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId()); + } else { + return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z)); + } + } + + private static RegistryAccess registryAccess() { + return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess()); + } + + private static Optional> biomeHolder(String namespace, String path) { + return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, path))); + } + + static CompoundTag serialize(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag()); + tag.putInt("xPos", chunkPos.x); + tag.putInt("yPos", chunk.getMinSection()); + tag.putInt("zPos", chunkPos.z); + tag.putLong("LastUpdate", 0); + tag.putLong("InhabitedTime", chunk.getInhabitedTime()); + tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); + if (blendingdata != null) { + DataResult dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt)); + } + + BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen(); + if (retrogen != null) { + DataResult dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt)); + } + + UpgradeData upgradeData = chunk.getUpgradeData(); + if (!upgradeData.isEmpty()) { + tag.put("UpgradeData", upgradeData.write()); + } + + LevelChunkSection[] sections = chunk.getSections(); + ListTag sectionsTag = new ListTag(); + Registry biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME); + Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); + boolean flag = chunk.isLightCorrect(); + + int minLightSection = chunk.getMinSection() - 1; + int maxLightSection = minLightSection + chunk.getSectionsCount() + 2; + for(int y = minLightSection; y < maxLightSection; ++y) { + int j = chunk.getSectionIndexFromSectionY(y); + if (j < 0 || j >= sections.length) + continue; + CompoundTag sectionTag = new CompoundTag(); + LevelChunkSection section = sections[j]; + sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, LogUtils.getLogger()::error)); + sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, LogUtils.getLogger()::error)); + + if (!sectionTag.isEmpty()) { + sectionTag.putByte("Y", (byte) y); + sectionsTag.add(sectionTag); + } + } + + tag.put("sections", sectionsTag); + if (flag) { + tag.putBoolean("isLightOn", true); + } + + ListTag blockEntities = new ListTag(); + for(BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos); + if (entityNbt != null) { + blockEntities.add(entityNbt); + } + } + + tag.put("block_entities", blockEntities); + if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK) { + ProtoChunk protochunk = (ProtoChunk)chunk; + ListTag entities = new ListTag(); + entities.addAll(protochunk.getEntities()); + tag.put("entities", entities); + CompoundTag carvingMasks = new CompoundTag(); + + for(GenerationStep.Carving carving : GenerationStep.Carving.values()) { + CarvingMask mask = protochunk.getCarvingMask(carving); + if (mask != null) { + carvingMasks.putLongArray(carving.toString(), mask.toArray()); + } + } + + tag.put("CarvingMasks", carvingMasks); + } + + saveTicks(tag, chunk.getTicksForSerialization()); + tag.put("PostProcessing", packOffsets(chunk.getPostProcessing())); + CompoundTag heightMaps = new CompoundTag(); + + for(Map.Entry entry : chunk.getHeightmaps()) { + if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) { + heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData())); + } + } + + tag.put("Heightmaps", heightMaps); + + CompoundTag structureData = new CompoundTag(); + structureData.put("starts", new CompoundTag()); + structureData.put("References", new CompoundTag()); + tag.put("structures", structureData); + if (!chunk.persistentDataContainer.isEmpty()) { + tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound()); + } + + return tag; + } + + private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) { + tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString())); + tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString())); + } +} diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java index 59a124514..5d10e5466 100644 --- a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java @@ -16,6 +16,8 @@ import java.util.concurrent.atomic.AtomicInteger; import com.mojang.datafixers.util.Pair; import com.volmit.iris.core.nms.container.BiomeColor; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_20_R3.headless.RegionStorage; import com.volmit.iris.util.scheduling.J; import net.minecraft.nbt.*; import net.minecraft.nbt.Tag; @@ -631,4 +633,9 @@ public class NMSBinding implements INMSBinding { public static Holder biomeToBiomeBase(Registry registry, Biome biome) { return registry.getHolderOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey()))); } + + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); + } } diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/DirectTerrainChunk.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/DirectTerrainChunk.java new file mode 100644 index 000000000..76a548e71 --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/DirectTerrainChunk.java @@ -0,0 +1,205 @@ +package com.volmit.iris.core.nms.v1_20_R3.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.util.data.IrisCustomData; +import com.volmit.iris.util.math.Position2; +import lombok.Data; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_20_R3.block.CraftBiome; +import org.bukkit.craftbukkit.v1_20_R3.block.CraftBlockType; +import org.bukkit.craftbukkit.v1_20_R3.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_20_R3.util.CraftMagicNumbers; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +@Data +public final class DirectTerrainChunk implements SerializableChunk { + private final ChunkAccess access; + private final int minHeight, maxHeight; + + public DirectTerrainChunk(ChunkAccess access) { + this.access = access; + this.minHeight = access.getMinBuildHeight(); + this.maxHeight = access.getMaxBuildHeight(); + } + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return null; + } + + @NotNull + @Override + public Biome getBiome(int x, int z) { + return getBiome(x, 0, z); + } + + @NotNull + @Override + public Biome getBiome(int x, int y, int z) { + if (y < minHeight || y > maxHeight) return Biome.PLAINS; + return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2)); + } + + @Override + public void setBiome(int x, int z, Biome bio) { + for (int y = minHeight; y < maxHeight; y += 4) { + setBiome(x, y, z, bio); + } + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + if (y < minHeight || y > maxHeight) return; + access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio)); + } + + public void setBlock(int x, int y, int z, Material material) { + this.setBlock(x, y, z, material.createBlockData()); + } + + public void setBlock(int x, int y, int z, MaterialData material) { + this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material)); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisCustomData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData()); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material)); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState()); + } + + public Material getType(int x, int y, int z) { + return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock()); + } + + public MaterialData getTypeAndData(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z)); + } + + public BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(this.getTypeId(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) { + if (xMin > 15 || yMin >= this.maxHeight || zMin > 15) + return; + + if (xMin < 0) { + xMin = 0; + } + + if (yMin < this.minHeight) { + yMin = this.minHeight; + } + + if (zMin < 0) { + zMin = 0; + } + + if (xMax > 16) { + xMax = 16; + } + + if (yMax > this.maxHeight) { + yMax = this.maxHeight; + } + + if (zMax > 16) { + zMax = 16; + } + + if (xMin >= xMax || yMin >= yMax || zMin >= zMax) + return; + + for (int y = yMin; y < yMax; ++y) { + for (int x = xMin; x < xMax; ++x) { + for (int z = zMin; z < zMax; ++z) { + this.setBlock(x, y, z, type); + } + } + } + + } + + public BlockState getTypeId(int x, int y, int z) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return Blocks.AIR.defaultBlockState(); + return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z)); + } + + public byte getData(int x, int y, int z) { + return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z)); + } + + private void setBlock(int x, int y, int z, BlockState type) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return; + BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z); + BlockState oldBlockData = access.setBlockState(blockPosition, type, false); + if (type.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type); + if (tileEntity == null) { + access.removeBlockEntity(blockPosition); + } else { + access.setBlockEntity(tileEntity); + } + } else if (oldBlockData != null && oldBlockData.hasBlockEntity()) { + access.removeBlockEntity(blockPosition); + } + + } + + @Override + public Position2 getPos() { + return new Position2(access.getPos().x, access.getPos().z); + } + + @Override + public Object serialize() { + return RegionStorage.serialize(access); + } +} \ No newline at end of file diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/Region.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/Region.java new file mode 100644 index 000000000..5fcc557fc --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/Region.java @@ -0,0 +1,60 @@ +package com.volmit.iris.core.nms.v1_20_R3.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import lombok.NonNull; +import lombok.Synchronized; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +class Region implements IRegion { + private final RegionFile regionFile; + transient long references; + + Region(Path path, Path folder) throws IOException { + this.regionFile = new RegionFile(path, folder, true); + } + + @Override + @Synchronized + public boolean exists(int x, int z) { + try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) { + if (din == null) return false; + return !"empty".equals(NbtIo.read(din).getString("Status")); + } catch (IOException e) { + return false; + } + } + + @Override + @Synchronized + public void write(@NonNull SerializableChunk chunk) throws IOException { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) { + NbtIo.write((CompoundTag) chunk.serialize(), dos); + } + } + + @Override + public void close() { + --references; + } + + public boolean remove() { + if (references > 0) return false; + try { + regionFile.close(); + } catch (IOException e) { + Iris.error("Failed to close region file"); + e.printStackTrace(); + } + return true; + } +} diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/RegionStorage.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/RegionStorage.java new file mode 100644 index 000000000..c009b1cc7 --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/headless/RegionStorage.java @@ -0,0 +1,291 @@ +package com.volmit.iris.core.nms.v1_20_R3.headless; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.math.RNG; +import lombok.Getter; +import lombok.NonNull; +import net.minecraft.FileUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_20_R3.CraftServer; +import org.bukkit.craftbukkit.v1_20_R3.block.CraftBiome; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC; +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets; + +public class RegionStorage implements IRegionStorage, LevelHeightAccessor { + private static final AtomicCache CACHE = new AtomicCache<>(); + private final KMap regions = new KMap<>(); + private final Path folder; + + private final Engine engine; + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes; + private final RNG biomeRng; + private final @Getter int minBuildHeight; + private final @Getter int height; + + private transient boolean closed = false; + + public RegionStorage(Engine engine) { + this.engine = engine; + this.folder = new File(engine.getWorld().worldFolder(), "region").toPath(); + this.biomeRng = new RNG(engine.getSeedManager().getBiome()); + + this.minBuildHeight = engine.getDimension().getMinHeight(); + this.height = engine.getDimension().getMaxHeight() - minBuildHeight; + + AtomicInteger failed = new AtomicInteger(); + var dimKey = engine.getDimension().getLoadKey(); + for (var biome : engine.getAllBiomes()) { + if (!biome.isCustom()) continue; + for (var custom : biome.getCustomDerivitives()) { + biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> { + Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId()); + failed.incrementAndGet(); + }); + } + } + if (failed.get() > 0) { + throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes"); + } + + minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream() + .collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder))); + minecraftBiomes.values().removeAll(customBiomes.values()); + } + + @Override + public boolean exists(int x, int z) { + try (IRegion region = getRegion(x, z, true)) { + return region != null && region.exists(x, z); + } catch (Exception e) { + return false; + } + } + + @Override + public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException { + AtomicReference exception = new AtomicReference<>(); + Region region = regions.computeIfAbsent(Cache.key(x, z), k -> { + if (regions.size() >= 256) { + regions.values().removeIf(Region::remove); + } + + try { + FileUtil.createDirectoriesSafe(this.folder); + Path path = folder.resolve("r." + x + "." + z + ".mca"); + if (existingOnly && !Files.exists(path)) { + return null; + } else { + return new Region(path, this.folder); + } + } catch (IOException e) { + exception.set(e); + return null; + } + }); + + if (region == null) { + if (exception.get() != null) + throw exception.get(); + return null; + } + region.references++; + return region; + } + + @NotNull + @Override + public SerializableChunk createChunk(int x, int z) { + return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null)); + } + + @Override + public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) { + if (!(chunk instanceof DirectTerrainChunk tc)) + return; + tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); + } + + @Override + public synchronized void close() { + if (closed) return; + + while (!regions.isEmpty()) { + regions.values().removeIf(Region::remove); + } + + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) { + int m = y - engine.getMinHeight(); + IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId()); + } else { + return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z)); + } + } + + private static RegistryAccess registryAccess() { + return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess()); + } + + private static Optional> biomeHolder(String namespace, String path) { + return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, path))); + } + + static CompoundTag serialize(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag()); + tag.putInt("xPos", chunkPos.x); + tag.putInt("yPos", chunk.getMinSection()); + tag.putInt("zPos", chunkPos.z); + tag.putLong("LastUpdate", 0); + tag.putLong("InhabitedTime", chunk.getInhabitedTime()); + tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); + if (blendingdata != null) { + DataResult dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt)); + } + + BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen(); + if (retrogen != null) { + DataResult dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt)); + } + + UpgradeData upgradeData = chunk.getUpgradeData(); + if (!upgradeData.isEmpty()) { + tag.put("UpgradeData", upgradeData.write()); + } + + LevelChunkSection[] sections = chunk.getSections(); + ListTag sectionsTag = new ListTag(); + Registry biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME); + Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); + boolean flag = chunk.isLightCorrect(); + + int minLightSection = chunk.getMinSection() - 1; + int maxLightSection = minLightSection + chunk.getSectionsCount() + 2; + for(int y = minLightSection; y < maxLightSection; ++y) { + int j = chunk.getSectionIndexFromSectionY(y); + if (j < 0 || j >= sections.length) + continue; + CompoundTag sectionTag = new CompoundTag(); + LevelChunkSection section = sections[j]; + sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, LogUtils.getLogger()::error)); + sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, LogUtils.getLogger()::error)); + + if (!sectionTag.isEmpty()) { + sectionTag.putByte("Y", (byte) y); + sectionsTag.add(sectionTag); + } + } + + tag.put("sections", sectionsTag); + if (flag) { + tag.putBoolean("isLightOn", true); + } + + ListTag blockEntities = new ListTag(); + for(BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos); + if (entityNbt != null) { + blockEntities.add(entityNbt); + } + } + + tag.put("block_entities", blockEntities); + if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK) { + ProtoChunk protochunk = (ProtoChunk)chunk; + ListTag entities = new ListTag(); + entities.addAll(protochunk.getEntities()); + tag.put("entities", entities); + CompoundTag carvingMasks = new CompoundTag(); + + for(GenerationStep.Carving carving : GenerationStep.Carving.values()) { + CarvingMask mask = protochunk.getCarvingMask(carving); + if (mask != null) { + carvingMasks.putLongArray(carving.toString(), mask.toArray()); + } + } + + tag.put("CarvingMasks", carvingMasks); + } + + saveTicks(tag, chunk.getTicksForSerialization()); + tag.put("PostProcessing", packOffsets(chunk.getPostProcessing())); + CompoundTag heightMaps = new CompoundTag(); + + for(Map.Entry entry : chunk.getHeightmaps()) { + if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) { + heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData())); + } + } + + tag.put("Heightmaps", heightMaps); + + CompoundTag structureData = new CompoundTag(); + structureData.put("starts", new CompoundTag()); + structureData.put("References", new CompoundTag()); + tag.put("structures", structureData); + if (!chunk.persistentDataContainer.isEmpty()) { + tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound()); + } + + return tag; + } + + private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) { + tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString())); + tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString())); + } +} diff --git a/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java index ea7518d6a..c8d9bc390 100644 --- a/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java +++ b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java @@ -10,6 +10,8 @@ import java.util.concurrent.atomic.AtomicInteger; import com.mojang.datafixers.util.Pair; import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.datapack.DataVersion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_20_R4.headless.RegionStorage; import com.volmit.iris.util.nbt.tag.CompoundTag; import com.volmit.iris.util.scheduling.J; import net.minecraft.core.*; @@ -650,4 +652,9 @@ public class NMSBinding implements INMSBinding { return keys; } + + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); + } } diff --git a/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/DirectTerrainChunk.java b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/DirectTerrainChunk.java new file mode 100644 index 000000000..ca1f0f52b --- /dev/null +++ b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/DirectTerrainChunk.java @@ -0,0 +1,205 @@ +package com.volmit.iris.core.nms.v1_20_R4.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.util.data.IrisCustomData; +import com.volmit.iris.util.math.Position2; +import lombok.Data; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_20_R4.block.CraftBiome; +import org.bukkit.craftbukkit.v1_20_R4.block.CraftBlockType; +import org.bukkit.craftbukkit.v1_20_R4.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_20_R4.util.CraftMagicNumbers; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +@Data +public final class DirectTerrainChunk implements SerializableChunk { + private final ChunkAccess access; + private final int minHeight, maxHeight; + + public DirectTerrainChunk(ChunkAccess access) { + this.access = access; + this.minHeight = access.getMinBuildHeight(); + this.maxHeight = access.getMaxBuildHeight(); + } + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return null; + } + + @NotNull + @Override + public Biome getBiome(int x, int z) { + return getBiome(x, 0, z); + } + + @NotNull + @Override + public Biome getBiome(int x, int y, int z) { + if (y < minHeight || y > maxHeight) return Biome.PLAINS; + return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2)); + } + + @Override + public void setBiome(int x, int z, Biome bio) { + for (int y = minHeight; y < maxHeight; y += 4) { + setBiome(x, y, z, bio); + } + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + if (y < minHeight || y > maxHeight) return; + access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio)); + } + + public void setBlock(int x, int y, int z, Material material) { + this.setBlock(x, y, z, material.createBlockData()); + } + + public void setBlock(int x, int y, int z, MaterialData material) { + this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material)); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisCustomData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData()); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material)); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState()); + } + + public Material getType(int x, int y, int z) { + return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock()); + } + + public MaterialData getTypeAndData(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z)); + } + + public BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(this.getTypeId(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) { + if (xMin > 15 || yMin >= this.maxHeight || zMin > 15) + return; + + if (xMin < 0) { + xMin = 0; + } + + if (yMin < this.minHeight) { + yMin = this.minHeight; + } + + if (zMin < 0) { + zMin = 0; + } + + if (xMax > 16) { + xMax = 16; + } + + if (yMax > this.maxHeight) { + yMax = this.maxHeight; + } + + if (zMax > 16) { + zMax = 16; + } + + if (xMin >= xMax || yMin >= yMax || zMin >= zMax) + return; + + for (int y = yMin; y < yMax; ++y) { + for (int x = xMin; x < xMax; ++x) { + for (int z = zMin; z < zMax; ++z) { + this.setBlock(x, y, z, type); + } + } + } + + } + + public BlockState getTypeId(int x, int y, int z) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return Blocks.AIR.defaultBlockState(); + return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z)); + } + + public byte getData(int x, int y, int z) { + return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z)); + } + + private void setBlock(int x, int y, int z, BlockState type) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return; + BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z); + BlockState oldBlockData = access.setBlockState(blockPosition, type, false); + if (type.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type); + if (tileEntity == null) { + access.removeBlockEntity(blockPosition); + } else { + access.setBlockEntity(tileEntity); + } + } else if (oldBlockData != null && oldBlockData.hasBlockEntity()) { + access.removeBlockEntity(blockPosition); + } + + } + + @Override + public Position2 getPos() { + return new Position2(access.getPos().x, access.getPos().z); + } + + @Override + public Object serialize() { + return RegionStorage.serialize(access); + } +} \ No newline at end of file diff --git a/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/Region.java b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/Region.java new file mode 100644 index 000000000..389cf7a99 --- /dev/null +++ b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/Region.java @@ -0,0 +1,63 @@ +package com.volmit.iris.core.nms.v1_20_R4.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import lombok.NonNull; +import lombok.Synchronized; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +class Region implements IRegion { + private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless"); + private final RegionFile regionFile; + transient long references; + + Region(Path path, Path folder) throws IOException { + this.regionFile = new RegionFile(info, path, folder, true); + } + + @Override + @Synchronized + public boolean exists(int x, int z) { + try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) { + if (din == null) return false; + return !"empty".equals(NbtIo.read(din).getString("Status")); + } catch (IOException e) { + return false; + } + } + + @Override + @Synchronized + public void write(@NonNull SerializableChunk chunk) throws IOException { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) { + NbtIo.write((CompoundTag) chunk.serialize(), dos); + } + } + + @Override + public void close() { + --references; + } + + public boolean remove() { + if (references > 0) return false; + try { + regionFile.close(); + } catch (IOException e) { + Iris.error("Failed to close region file"); + e.printStackTrace(); + } + return true; + } +} diff --git a/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/RegionStorage.java b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/RegionStorage.java new file mode 100644 index 000000000..0091d081c --- /dev/null +++ b/nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/headless/RegionStorage.java @@ -0,0 +1,291 @@ +package com.volmit.iris.core.nms.v1_20_R4.headless; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.math.RNG; +import lombok.Getter; +import lombok.NonNull; +import net.minecraft.FileUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.chunk.status.ChunkType; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_20_R4.CraftServer; +import org.bukkit.craftbukkit.v1_20_R4.block.CraftBiome; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC; +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets; + +public class RegionStorage implements IRegionStorage, LevelHeightAccessor { + private static final AtomicCache CACHE = new AtomicCache<>(); + private final KMap regions = new KMap<>(); + private final Path folder; + + private final Engine engine; + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes; + private final RNG biomeRng; + private final @Getter int minBuildHeight; + private final @Getter int height; + + private transient boolean closed = false; + + public RegionStorage(Engine engine) { + this.engine = engine; + this.folder = new File(engine.getWorld().worldFolder(), "region").toPath(); + this.biomeRng = new RNG(engine.getSeedManager().getBiome()); + + this.minBuildHeight = engine.getDimension().getMinHeight(); + this.height = engine.getDimension().getMaxHeight() - minBuildHeight; + + AtomicInteger failed = new AtomicInteger(); + var dimKey = engine.getDimension().getLoadKey(); + for (var biome : engine.getAllBiomes()) { + if (!biome.isCustom()) continue; + for (var custom : biome.getCustomDerivitives()) { + biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> { + Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId()); + failed.incrementAndGet(); + }); + } + } + if (failed.get() > 0) { + throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes"); + } + + minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream() + .collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder))); + minecraftBiomes.values().removeAll(customBiomes.values()); + } + + @Override + public boolean exists(int x, int z) { + try (IRegion region = getRegion(x, z, true)) { + return region != null && region.exists(x, z); + } catch (Exception e) { + return false; + } + } + + @Override + public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException { + AtomicReference exception = new AtomicReference<>(); + Region region = regions.computeIfAbsent(Cache.key(x, z), k -> { + if (regions.size() >= 256) { + regions.values().removeIf(Region::remove); + } + + try { + FileUtil.createDirectoriesSafe(this.folder); + Path path = folder.resolve("r." + x + "." + z + ".mca"); + if (existingOnly && !Files.exists(path)) { + return null; + } else { + return new Region(path, this.folder); + } + } catch (IOException e) { + exception.set(e); + return null; + } + }); + + if (region == null) { + if (exception.get() != null) + throw exception.get(); + return null; + } + region.references++; + return region; + } + + @NotNull + @Override + public SerializableChunk createChunk(int x, int z) { + return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null)); + } + + @Override + public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) { + if (!(chunk instanceof DirectTerrainChunk tc)) + return; + tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); + } + + @Override + public synchronized void close() { + if (closed) return; + + while (!regions.isEmpty()) { + regions.values().removeIf(Region::remove); + } + + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) { + int m = y - engine.getMinHeight(); + IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId()); + } else { + return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z)); + } + } + + private static RegistryAccess registryAccess() { + return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess()); + } + + private static Optional> biomeHolder(String namespace, String path) { + return registryAccess().registryOrThrow(Registries.BIOME).getHolder(new ResourceLocation(namespace, path)); + } + + static CompoundTag serialize(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag()); + tag.putInt("xPos", chunkPos.x); + tag.putInt("yPos", chunk.getMinSection()); + tag.putInt("zPos", chunkPos.z); + tag.putLong("LastUpdate", 0); + tag.putLong("InhabitedTime", chunk.getInhabitedTime()); + tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); + if (blendingdata != null) { + DataResult dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt)); + } + + BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen(); + if (retrogen != null) { + DataResult dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt)); + } + + UpgradeData upgradeData = chunk.getUpgradeData(); + if (!upgradeData.isEmpty()) { + tag.put("UpgradeData", upgradeData.write()); + } + + LevelChunkSection[] sections = chunk.getSections(); + ListTag sectionsTag = new ListTag(); + Registry biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME); + Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); + boolean flag = chunk.isLightCorrect(); + + int minLightSection = chunk.getMinSection() - 1; + int maxLightSection = minLightSection + chunk.getSectionsCount() + 2; + for(int y = minLightSection; y < maxLightSection; ++y) { + int j = chunk.getSectionIndexFromSectionY(y); + if (j < 0 || j >= sections.length) + continue; + CompoundTag sectionTag = new CompoundTag(); + LevelChunkSection section = sections[j]; + sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow()); + sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow()); + + if (!sectionTag.isEmpty()) { + sectionTag.putByte("Y", (byte) y); + sectionsTag.add(sectionTag); + } + } + + tag.put("sections", sectionsTag); + if (flag) { + tag.putBoolean("isLightOn", true); + } + + ListTag blockEntities = new ListTag(); + for(BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos, registryAccess()); + if (entityNbt != null) { + blockEntities.add(entityNbt); + } + } + + tag.put("block_entities", blockEntities); + if (chunk.getStatus().getChunkType() == ChunkType.PROTOCHUNK) { + ProtoChunk protochunk = (ProtoChunk)chunk; + ListTag entities = new ListTag(); + entities.addAll(protochunk.getEntities()); + tag.put("entities", entities); + CompoundTag carvingMasks = new CompoundTag(); + + for(GenerationStep.Carving carving : GenerationStep.Carving.values()) { + CarvingMask mask = protochunk.getCarvingMask(carving); + if (mask != null) { + carvingMasks.putLongArray(carving.toString(), mask.toArray()); + } + } + + tag.put("CarvingMasks", carvingMasks); + } + + saveTicks(tag, chunk.getTicksForSerialization()); + tag.put("PostProcessing", packOffsets(chunk.getPostProcessing())); + CompoundTag heightMaps = new CompoundTag(); + + for(Map.Entry entry : chunk.getHeightmaps()) { + if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) { + heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData())); + } + } + + tag.put("Heightmaps", heightMaps); + + CompoundTag structureData = new CompoundTag(); + structureData.put("starts", new CompoundTag()); + structureData.put("References", new CompoundTag()); + tag.put("structures", structureData); + if (!chunk.persistentDataContainer.isEmpty()) { + tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound()); + } + + return tag; + } + + private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) { + tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString())); + tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString())); + } +} diff --git a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java index be0a12f5b..85e349550 100644 --- a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java +++ b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java @@ -14,6 +14,8 @@ import java.util.concurrent.atomic.AtomicInteger; import com.mojang.datafixers.util.Pair; import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.datapack.DataVersion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_21_R1.headless.RegionStorage; import com.volmit.iris.util.scheduling.J; import net.minecraft.core.component.DataComponents; import net.minecraft.nbt.*; @@ -657,4 +659,9 @@ public class NMSBinding implements INMSBinding { return keys; } + + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); + } } diff --git a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/DirectTerrainChunk.java b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/DirectTerrainChunk.java new file mode 100644 index 000000000..ad0a01f2e --- /dev/null +++ b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/DirectTerrainChunk.java @@ -0,0 +1,205 @@ +package com.volmit.iris.core.nms.v1_21_R1.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.util.data.IrisCustomData; +import com.volmit.iris.util.math.Position2; +import lombok.Data; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_21_R1.block.CraftBiome; +import org.bukkit.craftbukkit.v1_21_R1.block.CraftBlockType; +import org.bukkit.craftbukkit.v1_21_R1.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_21_R1.util.CraftMagicNumbers; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +@Data +public final class DirectTerrainChunk implements SerializableChunk { + private final ChunkAccess access; + private final int minHeight, maxHeight; + + public DirectTerrainChunk(ChunkAccess access) { + this.access = access; + this.minHeight = access.getMinBuildHeight(); + this.maxHeight = access.getMaxBuildHeight(); + } + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return null; + } + + @NotNull + @Override + public Biome getBiome(int x, int z) { + return getBiome(x, 0, z); + } + + @NotNull + @Override + public Biome getBiome(int x, int y, int z) { + if (y < minHeight || y > maxHeight) return Biome.PLAINS; + return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2)); + } + + @Override + public void setBiome(int x, int z, Biome bio) { + for (int y = minHeight; y < maxHeight; y += 4) { + setBiome(x, y, z, bio); + } + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + if (y < minHeight || y > maxHeight) return; + access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio)); + } + + public void setBlock(int x, int y, int z, Material material) { + this.setBlock(x, y, z, material.createBlockData()); + } + + public void setBlock(int x, int y, int z, MaterialData material) { + this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material)); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisCustomData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData()); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material)); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState()); + } + + public Material getType(int x, int y, int z) { + return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock()); + } + + public MaterialData getTypeAndData(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z)); + } + + public BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(this.getTypeId(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) { + if (xMin > 15 || yMin >= this.maxHeight || zMin > 15) + return; + + if (xMin < 0) { + xMin = 0; + } + + if (yMin < this.minHeight) { + yMin = this.minHeight; + } + + if (zMin < 0) { + zMin = 0; + } + + if (xMax > 16) { + xMax = 16; + } + + if (yMax > this.maxHeight) { + yMax = this.maxHeight; + } + + if (zMax > 16) { + zMax = 16; + } + + if (xMin >= xMax || yMin >= yMax || zMin >= zMax) + return; + + for (int y = yMin; y < yMax; ++y) { + for (int x = xMin; x < xMax; ++x) { + for (int z = zMin; z < zMax; ++z) { + this.setBlock(x, y, z, type); + } + } + } + + } + + public BlockState getTypeId(int x, int y, int z) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return Blocks.AIR.defaultBlockState(); + return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z)); + } + + public byte getData(int x, int y, int z) { + return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z)); + } + + private void setBlock(int x, int y, int z, BlockState type) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return; + BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z); + BlockState oldBlockData = access.setBlockState(blockPosition, type, false); + if (type.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type); + if (tileEntity == null) { + access.removeBlockEntity(blockPosition); + } else { + access.setBlockEntity(tileEntity); + } + } else if (oldBlockData != null && oldBlockData.hasBlockEntity()) { + access.removeBlockEntity(blockPosition); + } + + } + + @Override + public Position2 getPos() { + return new Position2(access.getPos().x, access.getPos().z); + } + + @Override + public Object serialize() { + return RegionStorage.serialize(access); + } +} \ No newline at end of file diff --git a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/Region.java b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/Region.java new file mode 100644 index 000000000..0ffef6e35 --- /dev/null +++ b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/Region.java @@ -0,0 +1,63 @@ +package com.volmit.iris.core.nms.v1_21_R1.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import lombok.NonNull; +import lombok.Synchronized; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +class Region implements IRegion { + private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless"); + private final RegionFile regionFile; + transient long references; + + Region(Path path, Path folder) throws IOException { + this.regionFile = new RegionFile(info, path, folder, true); + } + + @Override + @Synchronized + public boolean exists(int x, int z) { + try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) { + if (din == null) return false; + return !"empty".equals(NbtIo.read(din).getString("Status")); + } catch (IOException e) { + return false; + } + } + + @Override + @Synchronized + public void write(@NonNull SerializableChunk chunk) throws IOException { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) { + NbtIo.write((CompoundTag) chunk.serialize(), dos); + } + } + + @Override + public void close() { + --references; + } + + public boolean remove() { + if (references > 0) return false; + try { + regionFile.close(); + } catch (IOException e) { + Iris.error("Failed to close region file"); + e.printStackTrace(); + } + return true; + } +} diff --git a/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/RegionStorage.java b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/RegionStorage.java new file mode 100644 index 000000000..838848671 --- /dev/null +++ b/nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/headless/RegionStorage.java @@ -0,0 +1,287 @@ +package com.volmit.iris.core.nms.v1_21_R1.headless; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.math.RNG; +import lombok.Getter; +import lombok.NonNull; +import net.minecraft.FileUtil; +import net.minecraft.core.*; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.chunk.status.ChunkType; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_21_R1.CraftServer; +import org.bukkit.craftbukkit.v1_21_R1.block.CraftBiome; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC; +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets; + +public class RegionStorage implements IRegionStorage, LevelHeightAccessor { + private static final AtomicCache CACHE = new AtomicCache<>(); + private final KMap regions = new KMap<>(); + private final Path folder; + + private final Engine engine; + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes; + private final RNG biomeRng; + private final @Getter int minBuildHeight; + private final @Getter int height; + + private transient boolean closed = false; + + public RegionStorage(Engine engine) { + this.engine = engine; + this.folder = new File(engine.getWorld().worldFolder(), "region").toPath(); + this.biomeRng = new RNG(engine.getSeedManager().getBiome()); + + this.minBuildHeight = engine.getDimension().getMinHeight(); + this.height = engine.getDimension().getMaxHeight() - minBuildHeight; + + AtomicInteger failed = new AtomicInteger(); + var dimKey = engine.getDimension().getLoadKey(); + for (var biome : engine.getAllBiomes()) { + if (!biome.isCustom()) continue; + for (var custom : biome.getCustomDerivitives()) { + biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> { + Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId()); + failed.incrementAndGet(); + }); + } + } + if (failed.get() > 0) { + throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes"); + } + + minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream() + .collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder))); + minecraftBiomes.values().removeAll(customBiomes.values()); + } + + @Override + public boolean exists(int x, int z) { + try (IRegion region = getRegion(x, z, true)) { + return region != null && region.exists(x, z); + } catch (Exception e) { + return false; + } + } + + @Override + public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException { + AtomicReference exception = new AtomicReference<>(); + Region region = regions.computeIfAbsent(Cache.key(x, z), k -> { + if (regions.size() >= 256) { + regions.values().removeIf(Region::remove); + } + + try { + FileUtil.createDirectoriesSafe(this.folder); + Path path = folder.resolve("r." + x + "." + z + ".mca"); + if (existingOnly && !Files.exists(path)) { + return null; + } else { + return new Region(path, this.folder); + } + } catch (IOException e) { + exception.set(e); + return null; + } + }); + + if (region == null) { + if (exception.get() != null) + throw exception.get(); + return null; + } + region.references++; + return region; + } + + @NotNull + @Override + public SerializableChunk createChunk(int x, int z) { + return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null)); + } + + @Override + public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) { + if (!(chunk instanceof DirectTerrainChunk tc)) + return; + tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); + } + + @Override + public synchronized void close() { + if (closed) return; + + while (!regions.isEmpty()) { + regions.values().removeIf(Region::remove); + } + + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) { + int m = y - engine.getMinHeight(); + IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId()); + } else { + return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z)); + } + } + + private static RegistryAccess registryAccess() { + return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess()); + } + + private static Optional> biomeHolder(String namespace, String path) { + return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceLocation.fromNamespaceAndPath(namespace, path)); + } + + static CompoundTag serialize(ChunkAccess chunk) { + ChunkPos chunkPos = chunk.getPos(); + CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag()); + tag.putInt("xPos", chunkPos.x); + tag.putInt("yPos", chunk.getMinSection()); + tag.putInt("zPos", chunkPos.z); + tag.putLong("LastUpdate", 0); + tag.putLong("InhabitedTime", chunk.getInhabitedTime()); + tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); + if (blendingdata != null) { + DataResult dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt)); + } + + BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen(); + if (retrogen != null) { + DataResult dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen); + dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt)); + } + + UpgradeData upgradeData = chunk.getUpgradeData(); + if (!upgradeData.isEmpty()) { + tag.put("UpgradeData", upgradeData.write()); + } + + LevelChunkSection[] sections = chunk.getSections(); + ListTag sectionsTag = new ListTag(); + Registry biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME); + Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS)); + boolean flag = chunk.isLightCorrect(); + + int minLightSection = chunk.getMinSection() - 1; + int maxLightSection = minLightSection + chunk.getSectionsCount() + 2; + for(int y = minLightSection; y < maxLightSection; ++y) { + int j = chunk.getSectionIndexFromSectionY(y); + if (j < 0 || j >= sections.length) + continue; + CompoundTag sectionTag = new CompoundTag(); + LevelChunkSection section = sections[j]; + sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow()); + sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow()); + + if (!sectionTag.isEmpty()) { + sectionTag.putByte("Y", (byte) y); + sectionsTag.add(sectionTag); + } + } + + tag.put("sections", sectionsTag); + if (flag) { + tag.putBoolean("isLightOn", true); + } + + ListTag blockEntities = new ListTag(); + for(BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos, registryAccess()); + if (entityNbt != null) { + blockEntities.add(entityNbt); + } + } + + tag.put("block_entities", blockEntities); + if (chunk.getPersistedStatus().getChunkType() == ChunkType.PROTOCHUNK) { + ProtoChunk protochunk = (ProtoChunk)chunk; + ListTag entities = new ListTag(); + entities.addAll(protochunk.getEntities()); + tag.put("entities", entities); + CompoundTag carvingMasks = new CompoundTag(); + + for(GenerationStep.Carving carving : GenerationStep.Carving.values()) { + CarvingMask mask = protochunk.getCarvingMask(carving); + if (mask != null) { + carvingMasks.putLongArray(carving.toString(), mask.toArray()); + } + } + + tag.put("CarvingMasks", carvingMasks); + } + + saveTicks(tag, chunk.getTicksForSerialization()); + tag.put("PostProcessing", packOffsets(chunk.getPostProcessing())); + CompoundTag heightMaps = new CompoundTag(); + + for(Map.Entry entry : chunk.getHeightmaps()) { + if (chunk.getPersistedStatus().heightmapsAfter().contains(entry.getKey())) { + heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData())); + } + } + + tag.put("Heightmaps", heightMaps); + + CompoundTag structureData = new CompoundTag(); + structureData.put("starts", new CompoundTag()); + structureData.put("References", new CompoundTag()); + tag.put("structures", structureData); + if (!chunk.persistentDataContainer.isEmpty()) { + tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound()); + } + + return tag; + } + + private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) { + tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString())); + tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString())); + } +} diff --git a/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/NMSBinding.java b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/NMSBinding.java index 0522d35f9..d7bd95e7a 100644 --- a/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/NMSBinding.java +++ b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/NMSBinding.java @@ -9,6 +9,8 @@ import java.util.concurrent.atomic.AtomicInteger; import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.datapack.DataVersion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_21_R2.headless.RegionStorage; import com.volmit.iris.util.scheduling.J; import net.minecraft.core.*; import net.minecraft.core.Registry; @@ -644,4 +646,9 @@ public class NMSBinding implements INMSBinding { return keys; } + + @Override + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); + } } diff --git a/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/DirectTerrainChunk.java b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/DirectTerrainChunk.java new file mode 100644 index 000000000..70733007f --- /dev/null +++ b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/DirectTerrainChunk.java @@ -0,0 +1,205 @@ +package com.volmit.iris.core.nms.v1_21_R2.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.util.data.IrisCustomData; +import com.volmit.iris.util.math.Position2; +import lombok.Data; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.bukkit.Material; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.v1_21_R2.block.CraftBiome; +import org.bukkit.craftbukkit.v1_21_R2.block.CraftBlockType; +import org.bukkit.craftbukkit.v1_21_R2.block.data.CraftBlockData; +import org.bukkit.craftbukkit.v1_21_R2.util.CraftMagicNumbers; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +@Data +public final class DirectTerrainChunk implements SerializableChunk { + private final ChunkAccess access; + private final int minHeight, maxHeight; + + public DirectTerrainChunk(ChunkAccess access) { + this.access = access; + this.minHeight = access.getMinY(); + this.maxHeight = access.getMaxY(); + } + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return null; + } + + @NotNull + @Override + public Biome getBiome(int x, int z) { + return getBiome(x, 0, z); + } + + @NotNull + @Override + public Biome getBiome(int x, int y, int z) { + if (y < minHeight || y > maxHeight) return Biome.PLAINS; + return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2)); + } + + @Override + public void setBiome(int x, int z, Biome bio) { + for (int y = minHeight; y < maxHeight; y += 4) { + setBiome(x, y, z, bio); + } + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + if (y < minHeight || y > maxHeight) return; + access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio)); + } + + public void setBlock(int x, int y, int z, Material material) { + this.setBlock(x, y, z, material.createBlockData()); + } + + public void setBlock(int x, int y, int z, MaterialData material) { + this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material)); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisCustomData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData()); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material)); + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) { + this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState()); + } + + public Material getType(int x, int y, int z) { + return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock()); + } + + public MaterialData getTypeAndData(int x, int y, int z) { + return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z)); + } + + public BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(this.getTypeId(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) { + if (xMin > 15 || yMin >= this.maxHeight || zMin > 15) + return; + + if (xMin < 0) { + xMin = 0; + } + + if (yMin < this.minHeight) { + yMin = this.minHeight; + } + + if (zMin < 0) { + zMin = 0; + } + + if (xMax > 16) { + xMax = 16; + } + + if (yMax > this.maxHeight) { + yMax = this.maxHeight; + } + + if (zMax > 16) { + zMax = 16; + } + + if (xMin >= xMax || yMin >= yMax || zMin >= zMax) + return; + + for (int y = yMin; y < yMax; ++y) { + for (int x = xMin; x < xMax; ++x) { + for (int z = zMin; z < zMax; ++z) { + this.setBlock(x, y, z, type); + } + } + } + + } + + public BlockState getTypeId(int x, int y, int z) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return Blocks.AIR.defaultBlockState(); + return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z)); + } + + public byte getData(int x, int y, int z) { + return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z)); + } + + private void setBlock(int x, int y, int z, BlockState type) { + if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15)) + return; + BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z); + BlockState oldBlockData = access.setBlockState(blockPosition, type, false); + if (type.hasBlockEntity()) { + BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type); + if (tileEntity == null) { + access.removeBlockEntity(blockPosition); + } else { + access.setBlockEntity(tileEntity); + } + } else if (oldBlockData != null && oldBlockData.hasBlockEntity()) { + access.removeBlockEntity(blockPosition); + } + + } + + @Override + public Position2 getPos() { + return new Position2(access.getPos().x, access.getPos().z); + } + + @Override + public Object serialize() { + return RegionStorage.serialize(access); + } +} \ No newline at end of file diff --git a/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/Region.java b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/Region.java new file mode 100644 index 000000000..d9810252d --- /dev/null +++ b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/Region.java @@ -0,0 +1,63 @@ +package com.volmit.iris.core.nms.v1_21_R2.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import lombok.NonNull; +import lombok.Synchronized; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; + +class Region implements IRegion { + private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless"); + private final RegionFile regionFile; + transient long references; + + Region(Path path, Path folder) throws IOException { + this.regionFile = new RegionFile(info, path, folder, true); + } + + @Override + @Synchronized + public boolean exists(int x, int z) { + try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) { + if (din == null) return false; + return !"empty".equals(NbtIo.read(din).getString("Status")); + } catch (IOException e) { + return false; + } + } + + @Override + @Synchronized + public void write(@NonNull SerializableChunk chunk) throws IOException { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) { + NbtIo.write((CompoundTag) chunk.serialize(), dos); + } + } + + @Override + public void close() { + --references; + } + + public boolean remove() { + if (references > 0) return false; + try { + regionFile.close(); + } catch (IOException e) { + Iris.error("Failed to close region file"); + e.printStackTrace(); + } + return true; + } +} diff --git a/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/RegionStorage.java b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/RegionStorage.java new file mode 100644 index 000000000..91c594ad7 --- /dev/null +++ b/nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/headless/RegionStorage.java @@ -0,0 +1,225 @@ +package com.volmit.iris.core.nms.v1_21_R2.headless; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.headless.IRegion; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.headless.SerializableChunk; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.math.RNG; +import it.unimi.dsi.fastutil.shorts.ShortArrayList; +import it.unimi.dsi.fastutil.shorts.ShortList; +import lombok.Getter; +import lombok.NonNull; +import net.minecraft.FileUtil; +import net.minecraft.Optionull; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_21_R2.CraftServer; +import org.bukkit.craftbukkit.v1_21_R2.block.CraftBiome; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class RegionStorage implements IRegionStorage, LevelHeightAccessor { + private static final AtomicCache CACHE = new AtomicCache<>(); + private final KMap regions = new KMap<>(); + private final Path folder; + + private final Engine engine; + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes; + private final RNG biomeRng; + private final @Getter int minY; + private final @Getter int height; + + private transient boolean closed = false; + + public RegionStorage(Engine engine) { + this.engine = engine; + this.folder = new File(engine.getWorld().worldFolder(), "region").toPath(); + this.biomeRng = new RNG(engine.getSeedManager().getBiome()); + + this.minY = engine.getDimension().getMinHeight(); + this.height = engine.getDimension().getMaxHeight() - minY; + + AtomicInteger failed = new AtomicInteger(); + var dimKey = engine.getDimension().getLoadKey(); + for (var biome : engine.getAllBiomes()) { + if (!biome.isCustom()) continue; + for (var custom : biome.getCustomDerivitives()) { + biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> { + Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId()); + failed.incrementAndGet(); + }); + } + } + if (failed.get() > 0) { + throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes"); + } + + minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream() + .collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder))); + minecraftBiomes.values().removeAll(customBiomes.values()); + } + + @Override + public boolean exists(int x, int z) { + try (IRegion region = getRegion(x, z, true)) { + return region != null && region.exists(x, z); + } catch (Exception e) { + return false; + } + } + + @Override + public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException { + AtomicReference exception = new AtomicReference<>(); + Region region = regions.computeIfAbsent(Cache.key(x, z), k -> { + if (regions.size() >= 256) { + regions.values().removeIf(Region::remove); + } + + try { + FileUtil.createDirectoriesSafe(this.folder); + Path path = folder.resolve("r." + x + "." + z + ".mca"); + if (existingOnly && !Files.exists(path)) { + return null; + } else { + return new Region(path, this.folder); + } + } catch (IOException e) { + exception.set(e); + return null; + } + }); + + if (region == null) { + if (exception.get() != null) + throw exception.get(); + return null; + } + region.references++; + return region; + } + + @NotNull + @Override + public SerializableChunk createChunk(int x, int z) { + return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().lookupOrThrow(Registries.BIOME), null)); + } + + @Override + public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) { + if (!(chunk instanceof DirectTerrainChunk tc)) + return; + tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); + } + + @Override + public synchronized void close() { + if (closed) return; + + while (!regions.isEmpty()) { + regions.values().removeIf(Region::remove); + } + + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) { + int m = y - engine.getMinHeight(); + IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId()); + } else { + return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z)); + } + } + + private static RegistryAccess registryAccess() { + return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess()); + } + + private static Optional> biomeHolder(String namespace, String path) { + return registryAccess().lookupOrThrow(Registries.BIOME).get(ResourceLocation.fromNamespaceAndPath(namespace, path)); + } + + static CompoundTag serialize(ChunkAccess chunk) { + RegistryAccess access = registryAccess(); + List list = new ArrayList<>(); + LevelChunkSection[] sections = chunk.getSections(); + + int minLightSection = chunk.getMinSectionY() - 1; + int maxLightSection = minLightSection + chunk.getSectionsCount() + 2; + for(int y = minLightSection; y < maxLightSection; ++y) { + int index = chunk.getSectionIndexFromSectionY(y); + if (index < 0 || index >= sections.length) continue; + LevelChunkSection section = sections[index].copy(); + list.add(new SerializableChunkData.SectionData(y, section, null, null)); + } + + List blockEntities = new ArrayList<>(chunk.getBlockEntitiesPos().size()); + + for(BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag nbt = chunk.getBlockEntityNbtForSaving(blockPos, access); + if (nbt != null) { + blockEntities.add(nbt); + } + } + Map heightMap = new EnumMap<>(Heightmap.Types.class); + for(Map.Entry entry : chunk.getHeightmaps()) { + if (chunk.getPersistedStatus().heightmapsAfter().contains(entry.getKey())) { + heightMap.put(entry.getKey(), entry.getValue().getRawData().clone()); + } + } + + ChunkAccess.PackedTicks packedTicks = chunk.getTicksForSerialization(0); + ShortList[] postProcessing = Arrays.stream(chunk.getPostProcessing()).map((shortlist) -> shortlist != null ? new ShortArrayList(shortlist) : null).toArray(ShortList[]::new); + CompoundTag structureData = new CompoundTag(); + structureData.put("starts", new CompoundTag()); + structureData.put("References", new CompoundTag()); + + CompoundTag persistentDataContainer = null; + if (!chunk.persistentDataContainer.isEmpty()) { + persistentDataContainer = chunk.persistentDataContainer.toTagCompound(); + } + + return new SerializableChunkData(access.lookupOrThrow(Registries.BIOME), chunk.getPos(), + chunk.getMinSectionY(), 0, chunk.getInhabitedTime(), chunk.getPersistedStatus(), + Optionull.map(chunk.getBlendingData(), BlendingData::pack), chunk.getBelowZeroRetrogen(), + chunk.getUpgradeData().copy(), null, heightMap, packedTicks, postProcessing, + chunk.isLightCorrect(), list, new ArrayList<>(), blockEntities, structureData, persistentDataContainer) + .write(); + } +}