From c2dfbac641026c93cb9da44a3e6602bfb6a2fd54 Mon Sep 17 00:00:00 2001 From: Julian Krings Date: Fri, 7 Feb 2025 21:51:23 +0100 Subject: [PATCH] refactor headless to decrease duplicate code in nms bindings --- .../com/volmit/iris/core/nms/IHeadless.java | 22 -- .../com/volmit/iris/core/nms/INMSBinding.java | 3 +- .../iris/core/nms/headless/IRegion.java | 17 ++ .../core/nms/headless/IRegionStorage.java | 27 +++ .../core/nms/headless/SerializableChunk.java | 10 + .../methods/HeadlessPregenMethod.java | 7 +- .../iris/engine/object/IrisHeadless.java | 229 ++++-------------- .../volmit/iris/engine/object/IrisWorld.java | 3 +- .../com/volmit/iris/util/math/Position2.java | 6 + .../iris/core/nms/v1_21_R3/NMSBinding.java | 8 +- .../v1_21_R3/headless/DirectTerrainChunk.java | 15 +- .../core/nms/v1_21_R3/headless/Region.java | 63 +++++ .../v1_21_R3/headless/RegionFileStorage.java | 83 ------- .../nms/v1_21_R3/headless/RegionStorage.java | 225 +++++++++++++++++ 14 files changed, 415 insertions(+), 303 deletions(-) delete mode 100644 core/src/main/java/com/volmit/iris/core/nms/IHeadless.java create mode 100644 core/src/main/java/com/volmit/iris/core/nms/headless/IRegion.java create mode 100644 core/src/main/java/com/volmit/iris/core/nms/headless/IRegionStorage.java create mode 100644 core/src/main/java/com/volmit/iris/core/nms/headless/SerializableChunk.java rename nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Headless.java => core/src/main/java/com/volmit/iris/engine/object/IrisHeadless.java (51%) create mode 100644 nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Region.java delete mode 100644 nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionFileStorage.java create mode 100644 nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionStorage.java diff --git a/core/src/main/java/com/volmit/iris/core/nms/IHeadless.java b/core/src/main/java/com/volmit/iris/core/nms/IHeadless.java deleted file mode 100644 index c5686bb8d..000000000 --- a/core/src/main/java/com/volmit/iris/core/nms/IHeadless.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.volmit.iris.core.nms; - -import com.volmit.iris.core.pregenerator.PregenListener; -import com.volmit.iris.util.documentation.ChunkCoordinates; -import com.volmit.iris.util.documentation.RegionCoordinates; -import com.volmit.iris.util.parallel.MultiBurst; - -import java.io.Closeable; -import java.util.concurrent.CompletableFuture; - -public interface IHeadless extends Closeable { - int getLoadedChunks(); - - @ChunkCoordinates - boolean exists(int x, int z); - - @RegionCoordinates - CompletableFuture generateRegion(MultiBurst burst, int x, int z, int maxConcurrent, PregenListener listener); - - @ChunkCoordinates - void generateChunk(int x, int z); -} 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 bdc96c379..866caedbd 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 @@ -20,6 +20,7 @@ package com.volmit.iris.core.nms; 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.engine.framework.Engine; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; @@ -121,7 +122,7 @@ public interface INMSBinding { return 441; } - default IHeadless createHeadless(Engine engine) { + default IRegionStorage createRegionStorage(Engine engine) { return null; } diff --git a/core/src/main/java/com/volmit/iris/core/nms/headless/IRegion.java b/core/src/main/java/com/volmit/iris/core/nms/headless/IRegion.java new file mode 100644 index 000000000..7c7edc9b4 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/nms/headless/IRegion.java @@ -0,0 +1,17 @@ +package com.volmit.iris.core.nms.headless; + +import com.volmit.iris.util.documentation.ChunkCoordinates; +import lombok.NonNull; + +import java.io.IOException; + +public interface IRegion extends AutoCloseable { + + @ChunkCoordinates + boolean exists(int x, int z); + + void write(@NonNull SerializableChunk chunk) throws IOException; + + @Override + void close(); +} diff --git a/core/src/main/java/com/volmit/iris/core/nms/headless/IRegionStorage.java b/core/src/main/java/com/volmit/iris/core/nms/headless/IRegionStorage.java new file mode 100644 index 000000000..c609c1a3d --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/nms/headless/IRegionStorage.java @@ -0,0 +1,27 @@ +package com.volmit.iris.core.nms.headless; + +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.documentation.ChunkCoordinates; +import com.volmit.iris.util.documentation.RegionCoordinates; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public interface IRegionStorage { + + @ChunkCoordinates + boolean exists(int x, int z); + + @Nullable + @RegionCoordinates + IRegion getRegion(int x, int z, boolean existingOnly) throws IOException; + + @NonNull + @ChunkCoordinates + SerializableChunk createChunk(int x, int z); + + void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx); + + void close(); +} diff --git a/core/src/main/java/com/volmit/iris/core/nms/headless/SerializableChunk.java b/core/src/main/java/com/volmit/iris/core/nms/headless/SerializableChunk.java new file mode 100644 index 000000000..4fafaafc3 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/nms/headless/SerializableChunk.java @@ -0,0 +1,10 @@ +package com.volmit.iris.core.nms.headless; + +import com.volmit.iris.engine.data.chunk.TerrainChunk; +import com.volmit.iris.util.math.Position2; + +public interface SerializableChunk extends TerrainChunk { + Position2 getPos(); + + Object serialize(); +} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HeadlessPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HeadlessPregenMethod.java index 6fb4acaf5..1fd38c8a7 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HeadlessPregenMethod.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HeadlessPregenMethod.java @@ -20,11 +20,10 @@ package com.volmit.iris.core.pregenerator.methods; import com.volmit.iris.Iris; import com.volmit.iris.core.IrisSettings; -import com.volmit.iris.core.nms.IHeadless; -import com.volmit.iris.core.nms.INMS; import com.volmit.iris.core.pregenerator.PregenListener; import com.volmit.iris.core.pregenerator.PregeneratorMethod; import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisHeadless; import com.volmit.iris.util.mantle.Mantle; import com.volmit.iris.util.parallel.MultiBurst; @@ -33,7 +32,7 @@ import java.util.concurrent.Semaphore; public class HeadlessPregenMethod implements PregeneratorMethod { private final Engine engine; - private final IHeadless headless; + private final IrisHeadless headless; private final Semaphore semaphore; private final int max; private final MultiBurst burst; @@ -45,7 +44,7 @@ public class HeadlessPregenMethod implements PregeneratorMethod { public HeadlessPregenMethod(Engine engine, int threads) { this.max = Math.max(threads, 4); this.engine = engine; - this.headless = INMS.get().createHeadless(engine); + this.headless = new IrisHeadless(engine); burst = new MultiBurst("HeadlessPregen", 8); this.semaphore = new Semaphore(max); } diff --git a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Headless.java b/core/src/main/java/com/volmit/iris/engine/object/IrisHeadless.java similarity index 51% rename from nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Headless.java rename to core/src/main/java/com/volmit/iris/engine/object/IrisHeadless.java index 8917970ca..172b9a9bb 100644 --- a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Headless.java +++ b/core/src/main/java/com/volmit/iris/engine/object/IrisHeadless.java @@ -16,17 +16,18 @@ * along with this program. If not, see . */ -package com.volmit.iris.core.nms.v1_21_R3.headless; +package com.volmit.iris.engine.object; import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.IHeadless; +import com.volmit.iris.core.nms.INMS; +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.core.pregenerator.PregenListener; -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.framework.EngineStage; import com.volmit.iris.engine.framework.WrongEngineBroException; -import com.volmit.iris.engine.object.IrisBiome; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.context.ChunkContext; @@ -38,89 +39,35 @@ import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder; import com.volmit.iris.util.hunk.view.SyncChunkDataHunkHolder; import com.volmit.iris.util.mantle.MantleFlag; import com.volmit.iris.util.math.M; -import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.math.Position2; import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.Looper; import com.volmit.iris.util.scheduling.PrecisionStopwatch; -import it.unimi.dsi.fastutil.shorts.ShortArrayList; -import it.unimi.dsi.fastutil.shorts.ShortList; -import lombok.Getter; -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.nbt.NbtIo; -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.*; -import net.minecraft.world.level.chunk.status.ChunkStatus; -import net.minecraft.world.level.chunk.storage.RegionFile; -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.Material; import org.bukkit.block.data.BlockData; -import org.bukkit.craftbukkit.v1_21_R3.CraftServer; -import org.bukkit.craftbukkit.v1_21_R3.block.CraftBiome; -import java.io.DataOutputStream; import java.io.IOException; -import java.util.*; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; -public class Headless implements IHeadless, LevelHeightAccessor { - private static final AtomicCache CACHE = new AtomicCache<>(); +public class IrisHeadless { private final long KEEP_ALIVE = TimeUnit.SECONDS.toMillis(10L); private final Engine engine; - private final RegionFileStorage storage; + private final IRegionStorage storage; private final ExecutorService executor = Executors.newCachedThreadPool(); private final KMap regions = new KMap<>(); private final AtomicInteger loadedChunks = new AtomicInteger(); - 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 CompletingThread regionThread; private transient boolean closed = false; - public Headless(Engine engine) { + public IrisHeadless(Engine engine) { this.engine = engine; - this.storage = new RegionFileStorage(engine.getWorld().worldFolder()); - this.biomeRng = new RNG(engine.getSeedManager().getBiome()); - this.minY = engine.getDimension().getMinHeight(); - this.height = engine.getDimension().getMaxHeight() - minY; + this.storage = INMS.get().createRegionStorage(engine); + if (storage == null) throw new IllegalStateException("Failed to create region storage!"); engine.getWorld().headless(this); - - 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()); startRegionCleaner(); } @@ -142,7 +89,6 @@ public class Headless implements IHeadless, LevelHeightAccessor { cleaner.start(); } - @Override public int getLoadedChunks() { return loadedChunks.get(); } @@ -154,20 +100,13 @@ public class Headless implements IHeadless, LevelHeightAccessor { * @param z coord of the chunk * @return true if the chunk exists in .mca */ - @Override public boolean exists(int x, int z) { if (closed) return false; if (engine.getWorld().hasRealWorld() && engine.getWorld().realWorld().isChunkLoaded(x, z)) return true; - try { - CompoundTag tag = storage.read(new ChunkPos(x, z)); - return tag != null && !"empty".equals(tag.getString("Status")); - } catch (IOException e) { - return false; - } + return storage.exists(x, z); } - @Override public synchronized CompletableFuture generateRegion(MultiBurst burst, int x, int z, int maxConcurrent, PregenListener listener) { if (closed) return CompletableFuture.completedFuture(null); if (regionThread != null && !regionThread.future.isDone()) @@ -188,9 +127,9 @@ public class Headless implements IHeadless, LevelHeightAccessor { burst.complete(() -> { try { - if (listening) listener.onChunkGenerating(pos.x, pos.z); - generateChunk(pos.x, pos.z); - if (listening) listener.onChunkGenerated(pos.x, pos.z); + if (listening) listener.onChunkGenerating(pos.getX(), pos.getZ()); + generateChunk(pos.getX(), pos.getZ()); + if (listening) listener.onChunkGenerated(pos.getX(), pos.getZ()); } finally { semaphore.release(); latch.countDown(); @@ -207,35 +146,31 @@ public class Headless implements IHeadless, LevelHeightAccessor { } @RegionCoordinates - private static void iterateRegion(int x, int z, Consumer chunkPos) { + private static void iterateRegion(int x, int z, Consumer chunkPos) { int cX = x << 5; int cZ = z << 5; for (int xx = 0; xx < 32; xx++) { for (int zz = 0; zz < 32; zz++) { - chunkPos.accept(new ChunkPos(cX + xx, cZ + zz)); + chunkPos.accept(new Position2(cX + xx, cZ + zz)); } } } - @Override public void generateChunk(int x, int z) { if (closed || exists(x, z)) return; try { - var pos = new ChunkPos(x, z); - ProtoChunk chunk = newProtoChunk(pos); - var tc = new DirectTerrainChunk(chunk); + var chunk = storage.createChunk(x, z); loadedChunks.incrementAndGet(); - SyncChunkDataHunkHolder blocks = new SyncChunkDataHunkHolder(tc); - BiomeGridHunkHolder biomes = new BiomeGridHunkHolder(tc, tc.getMinHeight(), tc.getMaxHeight()); - ChunkContext ctx = generate(engine, pos.x << 4, pos.z << 4, blocks, biomes); + SyncChunkDataHunkHolder blocks = new SyncChunkDataHunkHolder(chunk); + BiomeGridHunkHolder biomes = new BiomeGridHunkHolder(chunk, chunk.getMinHeight(), chunk.getMaxHeight()); + ChunkContext ctx = generate(engine, x << 4, z << 4, blocks, biomes); blocks.apply(); biomes.apply(); - chunk.fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null); - chunk.setPersistedStatus(ChunkStatus.FULL); + storage.fillBiomes(chunk, ctx); - long key = Cache.key(pos.getRegionX(), pos.getRegionZ()); + long key = Cache.key(x >> 5, z >> 5); regions.computeIfAbsent(key, Region::new) .add(chunk); } catch (Throwable e) { @@ -284,17 +219,6 @@ public class Headless implements IHeadless, LevelHeightAccessor { return ctx; } - 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)); - } - } - - @Override public void close() throws IOException { if (closed) return; try { @@ -313,16 +237,14 @@ public class Headless implements IHeadless, LevelHeightAccessor { engine.getWorld().headless(null); } finally { closed = true; - customBiomes.clear(); - minecraftBiomes.clear(); } } private class Region implements Runnable { private final int x, z; private final long key; - private final KList chunks = new KList<>(1024); - private final AtomicBoolean full = new AtomicBoolean(); + private final KList chunks = new KList<>(1024); + private final AtomicReference> full = new AtomicReference<>(); private long lastEntry = M.ms(); public Region(long key) { @@ -333,32 +255,27 @@ public class Headless implements IHeadless, LevelHeightAccessor { @Override public void run() { - RegionFile regionFile; - try { - regionFile = storage.getRegionFile(new ChunkPos(x, z), false); - } catch (IOException e) { + try (IRegion region = storage.getRegion(x, z, false)){ + assert region != null; + + for (var chunk : chunks) { + try { + region.write(chunk); + } catch (Throwable e) { + Iris.error("Failed to save chunk " + chunk.getPos()); + e.printStackTrace(); + } + loadedChunks.decrementAndGet(); + } + } catch (Throwable e) { Iris.error("Failed to load region file " + x + ", " + z); Iris.reportError(e); - return; - } - if (regionFile == null) { - Iris.error("Failed to load region file " + x + ", " + z); - return; } - for (var chunk : chunks) { - try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos())) { - NbtIo.write(write(chunk), dos); - } catch (Throwable e) { - Iris.error("Failed to save chunk " + chunk.getPos().x + ", " + chunk.getPos().z); - e.printStackTrace(); - } - loadedChunks.decrementAndGet(); - } regions.remove(key); } - public synchronized void add(ProtoChunk chunk) { + public synchronized void add(SerializableChunk chunk) { chunks.add(chunk); lastEntry = M.ms(); if (chunks.size() < 1024) @@ -367,63 +284,13 @@ public class Headless implements IHeadless, LevelHeightAccessor { } public void submit() { - if (full.getAndSet(true)) return; - executor.submit(this); + full.getAndUpdate(future -> { + if (future != null) return future; + return executor.submit(this); + }); } } - private CompoundTag write(ProtoChunk chunk) { - RegistryAccess access = registryAccess(); - List list = new ArrayList<>(); - LevelChunkSection[] sections = chunk.getSections(); - - int minLightSection = getMinSectionY() - 1; - int maxLightSection = minLightSection + 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(); - } - - private ProtoChunk newProtoChunk(ChunkPos pos) { - return new ProtoChunk(pos, UpgradeData.EMPTY, this, registryAccess().lookupOrThrow(Registries.BIOME), null); - } - private static class CompletingThread extends Thread { private final CompletableFuture future = new CompletableFuture<>(); @@ -442,12 +309,4 @@ public class Headless implements IHeadless, LevelHeightAccessor { } } } - - 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)); - } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java b/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java index aff9803a0..16457ae2e 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java +++ b/core/src/main/java/com/volmit/iris/engine/object/IrisWorld.java @@ -19,7 +19,6 @@ package com.volmit.iris.engine.object; import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.IHeadless; import com.volmit.iris.core.tools.IrisToolbelt; import com.volmit.iris.util.collection.KList; import lombok.*; @@ -49,7 +48,7 @@ public class IrisWorld { private long seed; private World.Environment environment; private World realWorld; - private IHeadless headless; + private IrisHeadless headless; private int minHeight; private int maxHeight; diff --git a/core/src/main/java/com/volmit/iris/util/math/Position2.java b/core/src/main/java/com/volmit/iris/util/math/Position2.java index 2175118ee..5f067e201 100644 --- a/core/src/main/java/com/volmit/iris/util/math/Position2.java +++ b/core/src/main/java/com/volmit/iris/util/math/Position2.java @@ -21,6 +21,8 @@ package com.volmit.iris.util.math; import com.volmit.iris.engine.object.IrisPosition; import org.bukkit.util.Vector; +import java.util.function.BiFunction; + public class Position2 { private int x; private int z; @@ -94,4 +96,8 @@ public class Position2 { public IrisPosition toIris() { return new IrisPosition(x, 23, z); } + + public T convert(BiFunction constructor) { + return constructor.apply(x, z); + } } diff --git a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/NMSBinding.java b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/NMSBinding.java index b65c33037..c787d7a7b 100644 --- a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/NMSBinding.java +++ b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/NMSBinding.java @@ -2,11 +2,11 @@ package com.volmit.iris.core.nms.v1_21_R3; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.volmit.iris.Iris; -import com.volmit.iris.core.nms.IHeadless; import com.volmit.iris.core.nms.INMSBinding; import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.datapack.DataVersion; -import com.volmit.iris.core.nms.v1_21_R3.headless.Headless; +import com.volmit.iris.core.nms.headless.IRegionStorage; +import com.volmit.iris.core.nms.v1_21_R3.headless.RegionStorage; import com.volmit.iris.engine.data.cache.AtomicCache; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.util.collection.KList; @@ -647,7 +647,7 @@ public class NMSBinding implements INMSBinding { } @Override - public IHeadless createHeadless(Engine engine) { - return new Headless(engine); + public IRegionStorage createRegionStorage(Engine engine) { + return new RegionStorage(engine); } } diff --git a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/DirectTerrainChunk.java b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/DirectTerrainChunk.java index bb0ed8b62..496f0cdca 100644 --- a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/DirectTerrainChunk.java +++ b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/DirectTerrainChunk.java @@ -2,8 +2,9 @@ package com.volmit.iris.core.nms.v1_21_R3.headless; import com.volmit.iris.Iris; import com.volmit.iris.core.nms.BiomeBaseInjector; -import com.volmit.iris.engine.data.chunk.TerrainChunk; +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; @@ -23,7 +24,7 @@ import org.bukkit.material.MaterialData; import org.jetbrains.annotations.NotNull; @Data -public final class DirectTerrainChunk implements TerrainChunk { +public final class DirectTerrainChunk implements SerializableChunk { private final ChunkAccess access; private final int minHeight, maxHeight; @@ -191,4 +192,14 @@ public final class DirectTerrainChunk implements TerrainChunk { } } + + @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_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Region.java b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Region.java new file mode 100644 index 000000000..8758233c2 --- /dev/null +++ b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/Region.java @@ -0,0 +1,63 @@ +package com.volmit.iris.core.nms.v1_21_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.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_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionFileStorage.java b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionFileStorage.java deleted file mode 100644 index 7ffe43214..000000000 --- a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionFileStorage.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.volmit.iris.core.nms.v1_21_R3.headless; - -import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -import net.minecraft.FileUtil; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtIo; -import net.minecraft.util.ExceptionCollector; -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 org.jetbrains.annotations.Nullable; - -import java.io.DataInputStream; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public class RegionFileStorage implements AutoCloseable { - private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless"); - private final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); - private final Path folder; - - public RegionFileStorage(File folder) { - this.folder = new File(folder, "region").toPath(); - } - - public RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { - long id = ChunkPos.asLong(chunkPos.getRegionX(), chunkPos.getRegionZ()); - RegionFile regionFile; - synchronized (this.regionCache) { - regionFile = this.regionCache.getAndMoveToFirst(id); - } - if (regionFile != null) { - return regionFile; - } else { - if (this.regionCache.size() >= 256) { - synchronized (this.regionCache) { - this.regionCache.removeLast().close(); - } - } - - FileUtil.createDirectoriesSafe(this.folder); - Path path = folder.resolve("r." + chunkPos.getRegionX() + "." + chunkPos.getRegionZ() + ".mca"); - if (existingOnly && !Files.exists(path)) { - return null; - } else { - regionFile = new RegionFile(info, path, this.folder, true); - synchronized (this.regionCache) { - this.regionCache.putAndMoveToFirst(id, regionFile); - } - return regionFile; - } - } - } - - @Nullable - public CompoundTag read(ChunkPos chunkPos) throws IOException { - RegionFile regionFile = this.getRegionFile(chunkPos, true); - if (regionFile == null) return null; - - try (DataInputStream din = regionFile.getChunkDataInputStream(chunkPos)) { - if (din == null) return null; - return NbtIo.read(din); - } - } - - @Override - public void close() throws IOException { - ExceptionCollector collector = new ExceptionCollector<>(); - - for (RegionFile regionFile : this.regionCache.values()) { - try { - regionFile.close(); - } catch (IOException e) { - collector.add(e); - } - } - - collector.throwIfPresent(); - } -} diff --git a/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionStorage.java b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionStorage.java new file mode 100644 index 000000000..83340c44f --- /dev/null +++ b/nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/headless/RegionStorage.java @@ -0,0 +1,225 @@ +package com.volmit.iris.core.nms.v1_21_R3.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_R3.CraftServer; +import org.bukkit.craftbukkit.v1_21_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.*; +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(); + } +}