From e1a0481cdf65e7ac7290135f20806396ce32a7d3 Mon Sep 17 00:00:00 2001 From: CrazyDev22 Date: Fri, 17 May 2024 16:01:21 +0200 Subject: [PATCH] implement Headless for 1.20.4 --- .../iris/core/commands/CommandDeveloper.java | 6 +- .../com/volmit/iris/core/nms/IHeadless.java | 22 ++ .../com/volmit/iris/core/nms/INMSBinding.java | 5 + .../methods/HeadlessPregenMethod.java | 96 +++++++ .../iris/core/tools/IrisPackBenchmarking.java | 51 +++- .../com/volmit/iris/engine/IrisEngine.java | 13 +- .../volmit/iris/engine/framework/Engine.java | 2 + .../iris/core/nms/v1_20_R3/Headless.java | 262 ++++++++++++++++++ .../iris/core/nms/v1_20_R3/NMSBinding.java | 30 ++ .../nms/v1_20_R3/mca/ChunkSerializer.java | 181 ++++++++++++ .../nms/v1_20_R3/mca/MCATerrainChunk.java | 156 +++++++++++ .../nms/v1_20_R3/mca/RegionFileStorage.java | 109 ++++++++ 12 files changed, 914 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/com/volmit/iris/core/nms/IHeadless.java create mode 100644 core/src/main/java/com/volmit/iris/core/pregenerator/methods/HeadlessPregenMethod.java create mode 100644 nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/Headless.java create mode 100644 nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/ChunkSerializer.java create mode 100644 nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/MCATerrainChunk.java create mode 100644 nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/RegionFileStorage.java diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java b/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java index b62ee3f40..cc26c7a04 100644 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java +++ b/core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java @@ -150,10 +150,12 @@ public class CommandDeveloper implements DecreeExecutor { @Decree(description = "Test") public void packBenchmark( @Param(description = "The pack to bench", aliases = {"pack"}) - IrisDimension dimension + IrisDimension dimension, + @Param(description = "Headless", defaultValue = "false") + boolean headless ) { Iris.info("test"); - IrisPackBenchmarking benchmark = new IrisPackBenchmarking(dimension, 1); + IrisPackBenchmarking benchmark = new IrisPackBenchmarking(dimension, 1, headless); } 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 new file mode 100644 index 000000000..7b32a79ca --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/nms/IHeadless.java @@ -0,0 +1,22 @@ +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; + +public interface IHeadless extends Closeable { + + void saveAll(); + + @RegionCoordinates + boolean exists(int x, int z); + + @RegionCoordinates + void generateRegion(MultiBurst burst, int x, int z, 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 48637dc16..6c22368aa 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 @@ -22,6 +22,7 @@ import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.object.IrisDimension; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.documentation.RegionCoordinates; import com.volmit.iris.util.mantle.Mantle; import com.volmit.iris.util.math.Vector3d; import com.volmit.iris.util.nbt.mca.palette.MCABiomeContainer; @@ -119,4 +120,8 @@ public interface INMSBinding { boolean registerDimension(String name, IrisDimension dimension); void injectBukkit(); + + default IHeadless createHeadless(Engine engine) { + throw new IllegalStateException("Headless mode not supported"); + } } 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 new file mode 100644 index 000000000..e1298da23 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/HeadlessPregenMethod.java @@ -0,0 +1,96 @@ +package com.volmit.iris.core.pregenerator.methods; + +import com.volmit.iris.Iris; +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.util.collection.KList; +import com.volmit.iris.util.mantle.Mantle; +import com.volmit.iris.util.parallel.MultiBurst; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.Future; + +public class HeadlessPregenMethod implements PregeneratorMethod { + private final Engine engine; + private final IHeadless headless; + private final MultiBurst burst; + private final KList> futures; + + public HeadlessPregenMethod(Engine engine) { + this.engine = engine; + this.headless = INMS.get().createHeadless(engine); + this.burst = new MultiBurst("Iris Headless", Thread.MAX_PRIORITY); + this.futures = new KList<>(); + } + + @Override + public void init() {} + + @Override + public void close() { + waitForChunksPartial(0); + burst.close(); + headless.saveAll(); + try { + headless.close(); + } catch (IOException e) { + Iris.error("Failed to close headless"); + e.printStackTrace(); + } + } + + @Override + public void save() { + headless.saveAll(); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return false; + } + + @Override + public String getMethod(int x, int z) { + return "Headless"; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) {} + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + futures.removeIf(Future::isDone); + waitForChunksPartial(512); + futures.add(burst.complete(() -> { + listener.onChunkGenerating(x, z); + headless.generateChunk(x, z); + listener.onChunkGenerated(x, z); + })); + } + + @Override + public Mantle getMantle() { + return engine.getMantle().getMantle(); + } + + private void waitForChunksPartial(int maxWaiting) { + futures.removeWhere(Objects::isNull); + while (futures.size() > maxWaiting) { + try { + Future i = futures.remove(0); + + if (i == null) { + continue; + } + + i.get(); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java b/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java index 7993cf6e7..54f7b3c4f 100644 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java +++ b/core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java @@ -2,9 +2,17 @@ package com.volmit.iris.core.tools; import com.volmit.iris.Iris; +import com.volmit.iris.core.IrisSettings; +import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.pregenerator.PregenTask; +import com.volmit.iris.core.pregenerator.methods.HeadlessPregenMethod; +import com.volmit.iris.core.pregenerator.methods.HybridPregenMethod; +import com.volmit.iris.core.service.StudioSVC; +import com.volmit.iris.engine.IrisEngine; import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.framework.EngineTarget; import com.volmit.iris.engine.object.IrisDimension; +import com.volmit.iris.engine.object.IrisWorld; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.exceptions.IrisException; @@ -36,13 +44,16 @@ public class IrisPackBenchmarking { public static boolean benchmarkInProgress = false; private IrisDimension IrisDimension; private int radius; + private final boolean headless; private boolean finished = false; + private Engine engine; PrecisionStopwatch stopwatch; - public IrisPackBenchmarking(IrisDimension dimension, int r) { + public IrisPackBenchmarking(IrisDimension dimension, int r, boolean headless) { instance = this; this.IrisDimension = dimension; this.radius = r; + this.headless = headless; runBenchmark(); } @@ -52,12 +63,12 @@ public class IrisPackBenchmarking { service.submit(() -> { Iris.info("Setting up benchmark environment "); benchmarkInProgress = true; - File file = new File("benchmark"); + File file = new File(Bukkit.getWorldContainer(), "benchmark"); if (file.exists()) { deleteDirectory(file.toPath()); } - createBenchmark(); - while (!IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) { + engine = createBenchmark(); + while (!headless && !IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) { J.sleep(1000); Iris.debug("Iris PackBenchmark: Waiting..."); } @@ -75,7 +86,6 @@ public class IrisPackBenchmarking { public void finishedBenchmark(KList cps) { try { String time = Form.duration(stopwatch.getMillis()); - Engine engine = IrisToolbelt.access(Bukkit.getWorld("benchmark")).getEngine(); Iris.info("-----------------"); Iris.info("Results:"); Iris.info("- Total time: " + time); @@ -88,8 +98,8 @@ public class IrisPackBenchmarking { File profilers = new File("plugins" + File.separator + "Iris" + File.separator + "packbenchmarks"); profilers.mkdir(); - File results = new File("plugins " + File.separator + "Iris", IrisDimension.getName() + LocalDateTime.now(Clock.systemDefaultZone()) + ".txt"); - results.createNewFile(); + File results = new File("plugins" + File.separator + "Iris", IrisDimension.getName() + " " + LocalDateTime.now(Clock.systemDefaultZone()).toString().replace(':', '-') + ".txt"); + results.getParentFile().mkdirs(); KMap metrics = engine.getMetrics().pull(); try (FileWriter writer = new FileWriter(results)) { writer.write("-----------------\n"); @@ -123,15 +133,32 @@ public class IrisPackBenchmarking { e.printStackTrace(); } } - private void createBenchmark(){ + private Engine createBenchmark(){ try { - IrisToolbelt.createWorld() + if (headless) { + IrisWorld world = IrisWorld.builder() + .name("benchmark") + .minHeight(IrisDimension.getMinHeight()) + .maxHeight(IrisDimension.getMaxHeight()) + .seed(1337) + .worldFolder(new File(Bukkit.getWorldContainer(), "benchmark")) + .environment(IrisDimension.getEnvironment()) + .build(); + Iris.service(StudioSVC.class).installIntoWorld( + Iris.getSender(), + IrisDimension.getLoadKey(), + world.worldFolder()); + var data = IrisData.get(new File(world.worldFolder(), "iris/pack")); + var dim = data.getDimensionLoader().load(IrisDimension.getLoadKey()); + return new IrisEngine(new EngineTarget(world, dim, data), false); + } + return IrisToolbelt.access(IrisToolbelt.createWorld() .dimension(IrisDimension.getName()) .name("benchmark") .seed(1337) .studio(false) .benchmark(true) - .create(); + .create()).getEngine(); } catch (IrisException e) { throw new RuntimeException(e); } @@ -146,8 +173,8 @@ public class IrisPackBenchmarking { .center(new Position2(x, z)) .width(5) .height(5) - .build(), Bukkit.getWorld("benchmark") - ); + .build(), headless ? new HeadlessPregenMethod(engine) : new HybridPregenMethod(engine.getWorld().realWorld(), + IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), engine); } private double calculateAverage(KList list) { diff --git a/core/src/main/java/com/volmit/iris/engine/IrisEngine.java b/core/src/main/java/com/volmit/iris/engine/IrisEngine.java index e1ea0a00e..94b4294eb 100644 --- a/core/src/main/java/com/volmit/iris/engine/IrisEngine.java +++ b/core/src/main/java/com/volmit/iris/engine/IrisEngine.java @@ -286,6 +286,13 @@ public class IrisEngine implements Engine { return generated.get(); } + @Override + public void addGenerated() { + if (generated.incrementAndGet() == 661) { + J.a(() -> getData().savePrefetch(this)); + } + } + @Override public double getGeneratedPerSecond() { if (perSecondLatch.flip()) { @@ -468,11 +475,7 @@ public class IrisEngine implements Engine { getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); getMetrics().getTotal().put(p.getMilliseconds()); - generated.incrementAndGet(); - - if (generated.get() == 661) { - J.a(() -> getData().savePrefetch(this)); - } + addGenerated(); } catch (Throwable e) { Iris.reportError(e); fail("Failed to generate " + x + ", " + z, e); diff --git a/core/src/main/java/com/volmit/iris/engine/framework/Engine.java b/core/src/main/java/com/volmit/iris/engine/framework/Engine.java index e1f6560dc..0809d0932 100644 --- a/core/src/main/java/com/volmit/iris/engine/framework/Engine.java +++ b/core/src/main/java/com/volmit/iris/engine/framework/Engine.java @@ -577,6 +577,8 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat int getGenerated(); + void addGenerated(); + default IrisPosition lookForStreamResult(T find, ProceduralStream stream, Function2 matcher, long timeout) { AtomicInteger checked = new AtomicInteger(); AtomicLong time = new AtomicLong(M.ms()); diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/Headless.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/Headless.java new file mode 100644 index 000000000..92520cdb4 --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/Headless.java @@ -0,0 +1,262 @@ +package com.volmit.iris.core.nms.v1_20_R3; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.core.nms.IHeadless; +import com.volmit.iris.core.nms.v1_20_R3.mca.MCATerrainChunk; +import com.volmit.iris.core.nms.v1_20_R3.mca.RegionFileStorage; +import com.volmit.iris.core.pregenerator.PregenListener; +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.KMap; +import com.volmit.iris.util.context.ChunkContext; +import com.volmit.iris.util.context.IrisContext; +import com.volmit.iris.util.documentation.BlockCoordinates; +import com.volmit.iris.util.documentation.RegionCoordinates; +import com.volmit.iris.util.hunk.Hunk; +import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder; +import com.volmit.iris.util.hunk.view.ChunkDataHunkHolder; +import com.volmit.iris.util.mantle.MantleFlag; +import com.volmit.iris.util.math.RNG; +import com.volmit.iris.util.parallel.MultiBurst; +import com.volmit.iris.util.scheduling.Looper; +import com.volmit.iris.util.scheduling.PrecisionStopwatch; +import net.minecraft.core.Holder; +import net.minecraft.nbt.CompoundTag; +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.ChunkStatus; +import net.minecraft.world.level.chunk.ProtoChunk; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.data.BlockData; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +public class Headless implements IHeadless, LevelHeightAccessor { + private final NMSBinding binding; + private final Engine engine; + private final RegionFileStorage storage; + private final Queue chunkQueue = new ArrayDeque<>(); + private final ReentrantLock manualLock = new ReentrantLock(); + private final KMap> customBiomes = new KMap<>(); + private final KMap> minecraftBiomes = new KMap<>(); + private boolean closed = false; + + public Headless(NMSBinding binding, Engine engine) { + this.binding = binding; + this.engine = engine; + this.storage = new RegionFileStorage(new File(engine.getWorld().worldFolder(), "region").toPath(), false); + var queueLooper = new Looper() { + @Override + protected long loop() { + if (manualLock.isLocked()) { + manualLock.lock(); + manualLock.unlock(); + } + saveAll(); + return closed ? -1 : 100; + } + }; + queueLooper.setName("Region Save Looper"); + queueLooper.start(); + } + + @Override + public boolean exists(int x, int z) { + if (closed) return false; + try { + return storage.getRegionFile(new ChunkPos(x << 5, z << 5), true) != null; + } catch (IOException e) { + return false; + } + } + + @Override + public void saveAll() { + manualLock.lock(); + try { + save(); + } finally { + manualLock.unlock(); + } + } + + private void save() { + if (closed) return; + while (!chunkQueue.isEmpty()) { + ChunkAccess chunk = chunkQueue.poll(); + if (chunk == null) break; + try { + storage.write(chunk.getPos(), binding.serializeChunk(chunk, this)); + } catch (Throwable e) { + Iris.error("Failed to save chunk " + chunk.getPos().x + ", " + chunk.getPos().z); + e.printStackTrace(); + } + } + } + + @Override + public void generateRegion(MultiBurst burst, int x, int z, PregenListener listener) { + if (closed) return; + boolean listening = listener != null; + if (listening) listener.onRegionGenerating(x, z); + CountDownLatch latch = new CountDownLatch(1024); + iterateRegion(x, z, pos -> burst.complete(() -> { + if (listening) listener.onChunkGenerating(pos.x, pos.z); + generateChunk(pos.x, pos.z); + if (listening) listener.onChunkGenerated(pos.x, pos.z); + latch.countDown(); + })); + try { + latch.await(); + } catch (InterruptedException ignored) {} + if (listening) listener.onRegionGenerated(x, z); + } + + @RegionCoordinates + 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)); + } + } + } + + @Override + public void generateChunk(int x, int z) { + if (closed) return; + try { + var pos = new ChunkPos(x, z); + try { + CompoundTag tag = storage.read(pos); + if (tag != null && !"empty".equals(tag.getString("Status"))) { + return; + } + } catch (Throwable ignored) {} + + ProtoChunk chunk = binding.createProtoChunk(pos, this); + var tc = new MCATerrainChunk(chunk); + + ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(tc); + BiomeGridHunkHolder biomes = new BiomeGridHunkHolder(tc, tc.getMinHeight(), tc.getMaxHeight()); + ChunkContext ctx = generate(engine, pos.x << 4, pos.z << 4, blocks, biomes); + blocks.apply(); + biomes.apply(); + + inject(engine, tc.getBiomeBaseInjector(), chunk, ctx); //TODO improve + chunk.setStatus(ChunkStatus.FULL); + chunkQueue.add(chunk); + } catch (Throwable e) { + Iris.error("Failed to generate " + x + ", " + z); + e.printStackTrace(); + } + } + + @BlockCoordinates + private ChunkContext generate(Engine engine, int x, int z, Hunk vblocks, Hunk vbiomes) throws WrongEngineBroException { + if (engine.isClosed()) { + throw new WrongEngineBroException(); + } + + engine.getContext().touch(); + engine.getEngineData().getStatistics().generatedChunk(); + ChunkContext ctx = null; + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); + Hunk blocks = vblocks.listen((xx, y, zz, t) -> engine.catchBlockUpdates(x + xx, y + engine.getMinHeight(), z + zz, t)); + + var dimension = engine.getDimension(); + if (dimension.isDebugChunkCrossSections() && ((x >> 4) % dimension.getDebugCrossSectionsMod() == 0 || (z >> 4) % dimension.getDebugCrossSectionsMod() == 0)) { + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + blocks.set(i, 0, j, Material.CRYING_OBSIDIAN.createBlockData()); + } + } + } else { + ctx = new ChunkContext(x, z, engine.getComplex()); + IrisContext.getOr(engine).setChunkContext(ctx); + + for (EngineStage i : engine.getMode().getStages()) { + i.generate(x, z, blocks, vbiomes, true, ctx); + } + } + + engine.getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true); + engine.getMetrics().getTotal().put(p.getMilliseconds()); + engine.addGenerated(); + + } catch (Throwable e) { + Iris.reportError(e); + engine.fail("Failed to generate " + x + ", " + z, e); + } + return ctx; + } + + private void inject(Engine engine, BiomeBaseInjector injector, ChunkAccess chunk, ChunkContext ctx) { + var pos = chunk.getPos(); + for (int y = engine.getMinHeight(); y < engine.getMaxHeight(); y++) { + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int wX = pos.getBlockX(x); + int wZ = pos.getBlockZ(z); + try { + injector.setBiome(x, y, z, getNoiseBiome(engine, ctx, x, z, wX, y, wZ)); + } catch (Throwable e) { + Iris.error("Failed to inject biome for " + wX + ", " + y + ", " + wZ); + e.printStackTrace(); + } + } + } + } + } + + private Holder getNoiseBiome(Engine engine, ChunkContext ctx, int rX, int rZ, int x, int y, int z) { + RNG rng = new RNG(engine.getSeedManager().getBiome()); + int m = (y - engine.getMinHeight()) << 2; + IrisBiome ib = ctx == null ? + engine.getComplex().getTrueBiomeStream().get(x << 2, z << 2) : + ctx.getBiome().get(rX, rZ); + if (ib.isCustom()) { + return customBiomes.computeIfAbsent(ib.getCustomBiome(rng, x << 2, m, z << 2).getId(), + id -> binding.getBiomeHolder(engine.getDimension().getLoadKey(), id)); + } else { + return minecraftBiomes.computeIfAbsent(ib.getSkyBiome(rng, x << 2, m, z << 2).getKey(), + id -> binding.getBiomeHolder(id.getNamespace(), id.getKey())); + } + } + + @Override + public void close() throws IOException { + if (closed) return; + try { + storage.close(); + } finally { + closed = true; + customBiomes.clear(); + minecraftBiomes.clear(); + } + } + + @Override + public int getHeight() { + return engine.getHeight(); + } + + @Override + public int getMinBuildHeight() { + return engine.getMinHeight(); + } +} 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 5f56f1299..9743b4830 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 @@ -21,9 +21,18 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.Lifecycle; +import com.volmit.iris.core.nms.IHeadless; +import com.volmit.iris.core.nms.v1_20_R3.mca.ChunkSerializer; +import com.volmit.iris.core.nms.v1_20_R3.mca.MCATerrainChunk; +import com.volmit.iris.core.nms.v1_20_R3.mca.RegionFileStorage; import com.volmit.iris.engine.object.IrisDimension; +import com.volmit.iris.util.documentation.RegionCoordinates; import com.volmit.iris.util.format.C; +import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder; +import com.volmit.iris.util.hunk.view.ChunkDataHunkHolder; import com.volmit.iris.util.io.IO; +import com.volmit.iris.util.parallel.MultiBurst; +import com.volmit.iris.util.scheduling.PrecisionStopwatch; import it.unimi.dsi.fastutil.objects.Reference2IntMap; import net.bytebuddy.ByteBuddy; import net.bytebuddy.asm.Advice; @@ -35,7 +44,11 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.progress.ChunkProgressListener; import net.minecraft.util.GsonHelper; import net.minecraft.world.RandomSequences; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.storage.LevelStorageSource; @@ -782,6 +795,11 @@ public class NMSBinding implements INMSBinding { } + @Override + public IHeadless createHeadless(Engine engine) { + return new Headless(this, engine); + } + private static class ServerLevelAdvice { @Advice.OnMethodEnter static void enter(@Advice.Argument(0) MinecraftServer server, @Advice.Argument(2) LevelStorageSource.LevelStorageAccess access, @Advice.Argument(4) ResourceKey key, @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem) { @@ -814,4 +832,16 @@ public class NMSBinding implements INMSBinding { } } } + + Holder.Reference getBiomeHolder(String namespace, String id) { + return getCustomBiomeRegistry().getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, id))).orElse(null); + } + + ProtoChunk createProtoChunk(ChunkPos pos, LevelHeightAccessor heightAccessor) { + return new ProtoChunk(pos, UpgradeData.EMPTY, heightAccessor, getCustomBiomeRegistry(), null); + } + + net.minecraft.nbt.CompoundTag serializeChunk(ChunkAccess chunkAccess, LevelHeightAccessor heightAccessor) { + return ChunkSerializer.write(chunkAccess, heightAccessor, getCustomBiomeRegistry()); + } } diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/ChunkSerializer.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/ChunkSerializer.java new file mode 100644 index 000000000..a90699c53 --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/ChunkSerializer.java @@ -0,0 +1,181 @@ +package com.volmit.iris.core.nms.v1_20_R3.mca; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import it.unimi.dsi.fastutil.shorts.ShortList; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map.Entry; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.Tag; +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.CarvingMask; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainerRO; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.ChunkAccess.TicksToSave; +import net.minecraft.world.level.levelgen.BelowZeroRetrogen; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.GenerationStep.Carving; +import net.minecraft.world.level.levelgen.Heightmap.Types; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.slf4j.Logger; + +import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC; + +public class ChunkSerializer { + private static final Logger LOGGER = LogUtils.getLogger(); + private static Method CODEC = null; + + static { + for (Method method : net.minecraft.world.level.chunk.storage.ChunkSerializer.class.getDeclaredMethods()) { + if (method.getReturnType().equals(Codec.class) && method.getParameterCount() == 1) { + CODEC = method; + CODEC.setAccessible(true); + break; + } + } + } + + public static CompoundTag write(ChunkAccess chunk, LevelHeightAccessor heightAccessor, Registry biomeRegistry) { + ChunkPos pos = chunk.getPos(); + CompoundTag data = NbtUtils.addCurrentDataVersion(new CompoundTag()); + data.putInt("xPos", pos.x); + data.putInt("yPos", ((LevelHeightAccessor) chunk).getMinSection()); + data.putInt("zPos", pos.z); + data.putLong("LastUpdate", 0L); + data.putLong("InhabitedTime", chunk.getInhabitedTime()); + data.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(LOGGER::error).ifPresent(base -> data.put("blending_data", base)); + } + + BelowZeroRetrogen belowzeroretrogen = chunk.getBelowZeroRetrogen(); + if (belowzeroretrogen != null) { + DataResult dataResult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, belowzeroretrogen); + dataResult.resultOrPartial(LOGGER::error).ifPresent(base -> data.put("below_zero_retrogen", base)); + } + + UpgradeData upgradeData = chunk.getUpgradeData(); + if (!upgradeData.isEmpty()) { + data.put("UpgradeData", upgradeData.write()); + } + + LevelChunkSection[] chunkSections = chunk.getSections(); + ListTag sections = new ListTag(); + Codec>> codec; + try { + codec = (Codec>>) CODEC.invoke(null, biomeRegistry); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + int minLightSection = heightAccessor.getMinSection() - 1; + int maxLightSection = minLightSection + heightAccessor.getSectionsCount() + 2; + for (int y = minLightSection; y < maxLightSection; y++) { + int i = ((LevelHeightAccessor) chunk).getSectionIndexFromSectionY(y); + if (i >= 0 && i < chunkSections.length) { + CompoundTag section = new CompoundTag(); + LevelChunkSection chunkSection = chunkSections[i]; + DataResult dataResult = BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, chunkSection.getStates()); + section.put("block_states", dataResult.getOrThrow(false, LOGGER::error)); + dataResult = codec.encodeStart(NbtOps.INSTANCE, chunkSection.getBiomes()); + section.put("biomes", dataResult.getOrThrow(false, LOGGER::error)); + + if (!section.isEmpty()) { + section.putByte("Y", (byte)y); + sections.add(section); + } + } + } + + data.put("sections", sections); + if (chunk.isLightCorrect()) { + data.putBoolean("isLightOn", true); + } + + ListTag blockEntities = new ListTag(); + for (BlockPos blockPos : chunk.getBlockEntitiesPos()) { + CompoundTag blockEntityNbt = chunk.getBlockEntityNbtForSaving(blockPos); + if (blockEntityNbt != null) { + blockEntities.add(blockEntityNbt); + } + } + data.put("block_entities", blockEntities); + + if (chunk instanceof ProtoChunk protoChunk) { + ListTag entities = new ListTag(); + entities.addAll(protoChunk.getEntities()); + data.put("entities", entities); + + CompoundTag carvingMasks = new CompoundTag(); + for (Carving carving : Carving.values()) { + CarvingMask carvingMask = protoChunk.getCarvingMask(carving); + if (carvingMask != null) { + carvingMasks.putLongArray(carving.toString(), carvingMask.toArray()); + } + } + data.put("CarvingMasks", carvingMasks); + } + + saveTicks(data, chunk.getTicksForSerialization()); + data.put("PostProcessing", packOffsets(chunk.getPostProcessing())); + CompoundTag heightmaps = new CompoundTag(); + + for (Entry entry : chunk.getHeightmaps()) { + if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) { + heightmaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData())); + } + } + + data.put("Heightmaps", heightmaps); + + CompoundTag structures = new CompoundTag(); + structures.put("starts", new CompoundTag()); + structures.put("References", new CompoundTag()); + data.put("structures", structures); + if (!chunk.persistentDataContainer.isEmpty()) { + data.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound()); + } + + return data; + } + + private static void saveTicks(CompoundTag compoundTag, TicksToSave ticksToSave) { + compoundTag.put("block_ticks", ticksToSave.blocks().save(0, block -> BuiltInRegistries.BLOCK.getKey(block).toString())); + compoundTag.put("fluid_ticks", ticksToSave.fluids().save(0, fluid -> BuiltInRegistries.FLUID.getKey(fluid).toString())); + } + + public static ListTag packOffsets(ShortList[] offsets) { + ListTag tags = new ListTag(); + for (ShortList shorts : offsets) { + ListTag listTag = new ListTag(); + if (shorts != null) { + for (Short s : shorts) { + listTag.add(ShortTag.valueOf(s)); + } + } + tags.add(listTag); + } + + return tags; + } +} diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/MCATerrainChunk.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/MCATerrainChunk.java new file mode 100644 index 000000000..589c01e93 --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/MCATerrainChunk.java @@ -0,0 +1,156 @@ +package com.volmit.iris.core.nms.v1_20_R3.mca; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.BiomeBaseInjector; +import com.volmit.iris.engine.data.chunk.TerrainChunk; +import com.volmit.iris.util.data.IrisBlockData; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.world.level.LevelHeightAccessor; +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.data.CraftBlockData; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.material.MaterialData; +import org.jetbrains.annotations.NotNull; + +public record MCATerrainChunk(ChunkAccess chunk) implements TerrainChunk { + + @Override + public BiomeBaseInjector getBiomeBaseInjector() { + return (x, y, z, biomeBase) -> chunk.setBiome(x, y, z, (Holder) biomeBase); + } + + @Override + public Biome getBiome(int x, int z) { + return Biome.THE_VOID; + } + + @Override + public Biome getBiome(int x, int y, int z) { + return Biome.THE_VOID; + } + + @Override + public void setBiome(int x, int z, Biome bio) { + setBiome(x, 0, z, bio); + } + + @Override + public void setBiome(int x, int y, int z, Biome bio) { + chunk.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio)); + } + + private LevelHeightAccessor height() { + return chunk; + } + + @Override + public int getMinHeight() { + return height().getMinBuildHeight(); + } + + @Override + public int getMaxHeight() { + return height().getMaxBuildHeight(); + } + + @Override + public void setBlock(int x, int y, int z, BlockData blockData) { + if (y > getMaxHeight() || y < getMinHeight()) { + return; + } + + if (blockData == null) { + Iris.error("NULL BD"); + } + if (blockData instanceof IrisBlockData data) + blockData = data.getBase(); + if (!(blockData instanceof CraftBlockData craftBlockData)) + throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead"); + chunk.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false); + } + + private BlockState getBlockState(int x, int y, int z) { + if (y > getMaxHeight()) { + y = getMaxHeight(); + } + + if (y < getMinHeight()) { + y = getMinHeight(); + } + + return chunk.getBlockState(new BlockPos(x & 15, y, z & 15)); + } + + @NotNull + @Override + public org.bukkit.block.data.BlockData getBlockData(int x, int y, int z) { + return CraftBlockData.fromData(getBlockState(x, y, z)); + } + + @Override + public ChunkGenerator.ChunkData getRaw() { + return null; + } + + @Override + public void setRaw(ChunkGenerator.ChunkData data) { + + } + + @Override + @Deprecated + public void inject(ChunkGenerator.BiomeGrid biome) { + + } + + @Override + public void setBlock(int x, int y, int z, @NotNull Material material) { + + } + + @Override + @Deprecated + public void setBlock(int x, int y, int z, @NotNull MaterialData material) { + + } + + @Override + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, @NotNull Material material) { + + } + + @Override + @Deprecated + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, @NotNull MaterialData material) { + + } + + @Override + public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, @NotNull BlockData blockData) { + + } + + + @NotNull + @Override + public Material getType(int x, int y, int z) { + return getBlockData(x, y, z).getMaterial(); + } + + @NotNull + @Override + public MaterialData getTypeAndData(int x, int y, int z) { + return getBlockData(x, y, z).createBlockState().getData(); + } + + @Override + public byte getData(int x, int y, int z) { + return getTypeAndData(x, y, z).getData(); + } +} diff --git a/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/RegionFileStorage.java b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/RegionFileStorage.java new file mode 100644 index 000000000..d7a9d1cee --- /dev/null +++ b/nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/mca/RegionFileStorage.java @@ -0,0 +1,109 @@ +package com.volmit.iris.core.nms.v1_20_R3.mca; + +import com.google.common.base.Preconditions; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.annotation.Nullable; +import net.minecraft.FileUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.StreamTagVisitor; +import net.minecraft.util.ExceptionCollector; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; + +public final class RegionFileStorage implements AutoCloseable { + public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); + private final Path folder; + private final boolean sync; + + public RegionFileStorage(Path folder, boolean sync) { + this.folder = folder; + this.sync = sync; + } + + public RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { + long id = ChunkPos.asLong(chunkPos.getRegionX(), chunkPos.getRegionZ()); + RegionFile regionFile = this.regionCache.getAndMoveToFirst(id); + if (regionFile != null) { + return regionFile; + } else { + if (this.regionCache.size() >= 256) { + 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(path, this.folder, this.sync); + this.regionCache.putAndMoveToFirst(id, regionFile); + return regionFile; + } + } + } + + @Nullable + public CompoundTag read(ChunkPos chunkPos) throws IOException { + RegionFile regionFile = this.getRegionFile(chunkPos, true); + if (regionFile != null) { + try (DataInputStream datainputstream = regionFile.getChunkDataInputStream(chunkPos)) { + if (datainputstream != null) { + return NbtIo.read(datainputstream); + } + } + + } + return null; + } + + public void scanChunk(ChunkPos chunkPos, StreamTagVisitor visitor) throws IOException { + RegionFile regionFile = this.getRegionFile(chunkPos, true); + if (regionFile != null) { + try (DataInputStream din = regionFile.getChunkDataInputStream(chunkPos)) { + if (din != null) { + NbtIo.parse(din, visitor, NbtAccounter.unlimitedHeap()); + } + } + } + } + + public void write(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException { + RegionFile regionFile = this.getRegionFile(chunkPos, false); + Preconditions.checkArgument(regionFile != null, "Failed to find region file for chunk %s", chunkPos); + if (compound == null) { + regionFile.clear(chunkPos); + } else { + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunkPos)) { + NbtIo.write(compound, dos); + } + } + } + + @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(); + } + + public void flush() throws IOException { + for (RegionFile regionfile : this.regionCache.values()) { + regionfile.flush(); + } + } +} \ No newline at end of file