diff --git a/build.gradle b/build.gradle index c49d5bb4d..1e24647ea 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,7 @@ def JVM_VERSION = Map.of( "v1_21_R1", 21, "v1_20_R4", 21, ) +def entryPoint = 'com.volmit.iris.server.EntryPoint' NMS_BINDINGS.each { nms -> project(":nms:${nms.key}") { apply plugin: 'java' @@ -86,7 +87,6 @@ shadowJar { dependsOn(":nms:${it.key}:remap") from("${project(":nms:${it.key}").layout.buildDirectory.asFile.get()}/libs/${it.key}-mapped.jar") } - NMS_BINDINGS.each {dependsOn(":nms:${it.key}:build")} //dependsOn(':com.volmit.gui:build') //minimize() @@ -95,6 +95,10 @@ shadowJar { relocate 'io.papermc.lib', 'com.volmit.iris.util.paper' relocate 'net.kyori', 'com.volmit.iris.util.kyori' archiveFileName.set("Iris-${project.version}.jar") + + manifest { + attributes 'Main-Class': entryPoint + } } dependencies { @@ -132,9 +136,9 @@ allprojects { // Shaded implementation 'com.dfsek:Paralithic:0.4.0' implementation 'io.papermc:paperlib:1.0.5' - implementation "net.kyori:adventure-text-minimessage:4.13.1" + implementation "net.kyori:adventure-text-minimessage:4.17.0" implementation 'net.kyori:adventure-platform-bukkit:4.3.4' - implementation 'net.kyori:adventure-api:4.13.1' + implementation 'net.kyori:adventure-api:4.17.0' //implementation 'org.bytedeco:javacpp:1.5.10' //implementation 'org.bytedeco:cuda-platform:12.3-8.9-1.5.10' //implementation "org.deeplearning4j:deeplearning4j-core:1.0.0-M2.1" @@ -156,6 +160,7 @@ allprojects { compileOnly 'net.bytebuddy:byte-buddy-agent:1.12.8' compileOnly 'org.bytedeco:javacpp:1.5.10' compileOnly 'org.bytedeco:cuda-platform:12.3-8.9-1.5.10' + compileOnly 'io.netty:netty-all:4.1.112.Final' } /** diff --git a/core/src/main/java/com/volmit/iris/Iris.java b/core/src/main/java/com/volmit/iris/Iris.java index 01669d411..65344377d 100644 --- a/core/src/main/java/com/volmit/iris/Iris.java +++ b/core/src/main/java/com/volmit/iris/Iris.java @@ -41,6 +41,8 @@ import com.volmit.iris.engine.object.IrisDimension; import com.volmit.iris.engine.object.IrisWorld; import com.volmit.iris.engine.platform.BukkitChunkGenerator; import com.volmit.iris.engine.platform.DummyChunkGenerator; +import com.volmit.iris.server.master.IrisMasterServer; +import com.volmit.iris.server.node.IrisServer; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.exceptions.IrisException; @@ -91,10 +93,7 @@ import org.jetbrains.annotations.Nullable; import java.io.*; import java.lang.annotation.Annotation; import java.net.URL; -import java.util.Collection; -import java.util.Date; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -112,6 +111,7 @@ public class Iris extends VolmitPlugin implements Listener { public static MythicMobsLink linkMythicMobs; public static IrisCompat compat; public static FileWatcher configWatcher; + private static IrisServer server; private static VolmitSender sender; static { @@ -475,6 +475,29 @@ public class Iris extends VolmitPlugin implements Listener { services.values().forEach(this::registerListener); ServerConfigurator.setupDataPack(); installMainDimension(); + try { + info("Starting server..."); + try { + int port = Integer.parseInt(System.getProperty("com.volmit.iris.server.port")); + String[] remote = Optional.ofNullable(System.getProperty("com.volmit.iris.server.remote")) + .map(String::trim) + .map(s -> s.isBlank() ? null : s.split(",")) + .orElse(new String[0]); + server = remote.length > 0 ? new IrisMasterServer(port, remote) : new IrisServer(port); + } catch (NullPointerException | NumberFormatException ignored) { + var serverSettings = IrisSettings.get().getServer(); + if (serverSettings.isActive()) { + server = serverSettings.isRemote() ? + new IrisMasterServer(serverSettings.getPort(), serverSettings.remote) : + new IrisServer(serverSettings.getPort()); + } + } + } catch (InterruptedException ignored) { + } catch (Throwable e) { + error("Failed to start server: " + e.getClass().getSimpleName()); + e.printStackTrace(); + } + if (!IrisSafeguard.instance.acceptUnstable && IrisSafeguard.instance.unstablemode) { Iris.info(C.RED + "World loading has been disabled until the incompatibility is resolved."); Iris.info(C.DARK_RED + "Alternatively, go to plugins/iris/settings.json and set ignoreBootMode to true."); diff --git a/core/src/main/java/com/volmit/iris/core/IrisSettings.java b/core/src/main/java/com/volmit/iris/core/IrisSettings.java index c17c8f0a2..46f088854 100644 --- a/core/src/main/java/com/volmit/iris/core/IrisSettings.java +++ b/core/src/main/java/com/volmit/iris/core/IrisSettings.java @@ -44,6 +44,7 @@ public class IrisSettings { private IrisSettingsPerformance performance = new IrisSettingsPerformance(); private IrisWorldDump worldDump = new IrisWorldDump(); private IrisWorldSettings irisWorldSettings = new IrisWorldSettings(); + private IrisServerSettings server = new IrisServerSettings(); public static int getThreadCount(int c) { return switch (c) { @@ -180,6 +181,7 @@ public class IrisSettings { public static class IrisSettingsGUI { public boolean useServerLaunchedGuis = true; public boolean maximumPregenGuiFPS = false; + public boolean colorMode = true; } @Data @@ -202,6 +204,17 @@ public class IrisSettings { public int mcaCacheSize = 3; } + @Data + public static class IrisServerSettings { + public boolean active = false; + public int port = 1337; + public String[] remote = new String[0]; + + public boolean isRemote() { + return remote.length != 0; + } + } + // todo: Goal:Have these as the default world settings and when put in bukkit.yml it will again overwrite that world from these. @Data public static class IrisWorldSettings { 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 20296b8b3..59e50ae70 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 @@ -19,7 +19,6 @@ package com.volmit.iris.core.commands; import com.volmit.iris.Iris; -import com.volmit.iris.core.IrisSettings; import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.tools.IrisPackBenchmarking; import com.volmit.iris.core.tools.IrisToolbelt; @@ -35,8 +34,6 @@ import com.volmit.iris.util.format.C; import com.volmit.iris.util.format.Form; import com.volmit.iris.util.io.IO; import com.volmit.iris.util.mantle.TectonicPlate; -import com.volmit.iris.util.nbt.mca.MCAFile; -import com.volmit.iris.util.nbt.mca.MCAUtil; import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.plugin.VolmitSender; import net.jpountz.lz4.LZ4BlockInputStream; @@ -102,8 +99,10 @@ public class CommandDeveloper implements DecreeExecutor { @Decree(description = "Test") public void packBenchmark( - @Param(description = "The pack to bench", aliases = {"pack"}) + @Param(description = "The pack to bench", defaultValue = "overworld", aliases = {"pack"}) IrisDimension dimension, + @Param(description = "The address to use", defaultValue = "-") + String address, @Param(description = "Headless", defaultValue = "true") boolean headless, @Param(description = "GUI", defaultValue = "false") @@ -113,7 +112,7 @@ public class CommandDeveloper implements DecreeExecutor { ) { int rb = diameter << 9; Iris.info("Benchmarking pack " + dimension.getName() + " with diameter: " + rb + "(" + diameter + ")"); - IrisPackBenchmarking benchmark = new IrisPackBenchmarking(dimension, diameter, headless, gui); + IrisPackBenchmarking benchmark = new IrisPackBenchmarking(dimension, address.replace("-", "").trim(), diameter, headless, gui); benchmark.runBenchmark(); } diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java b/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java index 98343d02d..8587f594a 100644 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java +++ b/core/src/main/java/com/volmit/iris/core/commands/CommandObject.java @@ -24,6 +24,7 @@ import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.service.ObjectSVC; import com.volmit.iris.core.service.StudioSVC; import com.volmit.iris.core.service.WandSVC; +import com.volmit.iris.core.tools.IrisConverter; import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.object.*; import com.volmit.iris.util.data.Cuboid; @@ -211,6 +212,16 @@ public class CommandObject implements DecreeExecutor { } } + @Decree(description = "Convert .schem files in the 'convert' folder to .iob files.") + public void convert () { + try { + IrisConverter.convertSchematics(sender()); + } catch (Exception e) { + e.printStackTrace(); + } + + } + @Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d") public void dust() { player().getInventory().addItem(WandSVC.createDust()); @@ -382,7 +393,7 @@ public class CommandObject implements DecreeExecutor { return; } try { - o.write(file); + o.write(file, sender()); } catch (IOException e) { sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage()); Iris.reportError(e); diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java index be61b0462..53051bccb 100644 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java @@ -50,6 +50,7 @@ import com.volmit.iris.util.math.Spiraler; import com.volmit.iris.util.parallel.BurstExecutor; import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.plugin.VolmitSender; +import com.volmit.iris.util.scheduling.ChronoLatch; import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.O; import com.volmit.iris.util.scheduling.jobs.QueueJob; @@ -57,6 +58,7 @@ import io.papermc.lib.PaperLib; import org.bukkit.*; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; +import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; @@ -71,6 +73,7 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Date; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.function.Supplier; @@ -328,7 +331,7 @@ public class CommandStudio implements DecreeExecutor { @Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER) - public void distances(@Param(description = "The radius") int radius) { + public void distances(@Param(description = "The radius in chunks") int radius) { var engine = engine(); if (engine == null) { sender().sendMessage(C.RED + "Only works in an Iris world!"); @@ -342,15 +345,24 @@ public class CommandStudio implements DecreeExecutor { sender.sendMessage(C.GRAY + "Generating data..."); var loc = player().getLocation(); + int totalTasks = d * d; + AtomicInteger completedTasks = new AtomicInteger(0); + int c = J.ar(() -> { + sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding structures"); + }, 0); + new Spiraler(d, d, (x, z) -> executor.queue(() -> { var struct = engine.getStructureAt(x, z); if (struct != null) { data.computeIfAbsent(struct.getLoadKey(), (k) -> new KList<>()).add(new Position2(x, z)); } + completedTasks.incrementAndGet(); })).setOffset(loc.getBlockX(), loc.getBlockZ()).drain(); executor.complete(); multiBurst.close(); + J.car(c); + for (var key : data.keySet()) { var list = data.get(key); KList distances = new KList<>(list.size() - 1); @@ -383,6 +395,7 @@ public class CommandStudio implements DecreeExecutor { } } + @Decree(description = "Render a world map (External GUI)", aliases = "render") public void map() { if (noGUI()) return; diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java index 1e3035cc6..017f7e42d 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java +++ b/core/src/main/java/com/volmit/iris/core/gui/NoiseExplorerGUI.java @@ -19,6 +19,7 @@ package com.volmit.iris.core.gui; import com.volmit.iris.Iris; +import com.volmit.iris.core.IrisSettings; import com.volmit.iris.core.events.IrisEngineHotloadEvent; import com.volmit.iris.engine.object.NoiseStyle; import com.volmit.iris.util.collection.KList; @@ -61,7 +62,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List @SuppressWarnings("CanBeFinal") RollingSequence r = new RollingSequence(20); @SuppressWarnings("CanBeFinal") - boolean colorMode = true; + boolean colorMode = IrisSettings.get().getGui().colorMode; double scale = 1; CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong())); @SuppressWarnings("CanBeFinal") @@ -274,7 +275,10 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List n = n > 1 ? 1 : n < 0 ? 0 : n; try { - Color color = colorMode ? Color.getHSBColor((float) (n), 1f - (float) (n * n * n * n * n * n), 1f - (float) n) : Color.getHSBColor(0f, 0f, (float) n); + //Color color = colorMode ? Color.getHSBColor((float) (n), 1f - (float) (n * n * n * n * n * n), 1f - (float) n) : Color.getHSBColor(0f, 0f, (float) n); + //Color color = colorMode ? Color.getHSBColor((float) (n), (float) (n * n * n * n * n * n), (float) n) : Color.getHSBColor(0f, 0f, (float) n); + Color color = colorMode ? Color.getHSBColor((float) n, (float) (n * n * n * n * n * n), (float) n) : Color.getHSBColor(0f, 0f, (float) n); + int rgb = color.getRGB(); img.setRGB(xx, z, rgb); } catch (Throwable ignored) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java b/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java index b7e961e52..ef2a8359e 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java @@ -92,7 +92,12 @@ public class PregeneratorJob implements PregenListener { open(); } - J.a(this.pregenerator::start, 20); + var t = new Thread(() -> { + J.sleep(1000); + this.pregenerator.start(); + }, "Iris Pregenerator"); + t.setPriority(Thread.MIN_PRIORITY); + t.start(); } diff --git a/core/src/main/java/com/volmit/iris/core/link/MMOItemsDataProvider.java b/core/src/main/java/com/volmit/iris/core/link/MMOItemsDataProvider.java index 2e4f8c52a..8cbcab37e 100644 --- a/core/src/main/java/com/volmit/iris/core/link/MMOItemsDataProvider.java +++ b/core/src/main/java/com/volmit/iris/core/link/MMOItemsDataProvider.java @@ -23,6 +23,7 @@ import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.scheduling.J; import net.Indyuce.mmoitems.MMOItems; +import net.Indyuce.mmoitems.api.ItemTier; import net.Indyuce.mmoitems.api.Type; import net.Indyuce.mmoitems.api.block.CustomBlock; import org.bukkit.Bukkit; @@ -66,8 +67,13 @@ public class MMOItemsDataProvider extends ExternalDataProvider { Runnable run = () -> { try { var type = api().getTypes().get(parts[1]); - int level = customNbt.containsKey("level") ? (int) customNbt.get("level") : -1; - var tier = api().getTiers().get(String.valueOf(customNbt.get("tier"))); + int level = -1; + ItemTier tier = null; + + if (customNbt != null) { + level = (int) customNbt.getOrDefault("level", -1); + tier = api().getTiers().get(String.valueOf(customNbt.get("tier"))); + } ItemStack itemStack; if (type == null) { 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 index 90d56c0de..1c27b5a36 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/IHeadless.java +++ b/core/src/main/java/com/volmit/iris/core/nms/IHeadless.java @@ -19,22 +19,30 @@ package com.volmit.iris.core.nms; import com.volmit.iris.core.pregenerator.PregenListener; +import com.volmit.iris.server.node.IrisSession; +import com.volmit.iris.server.packet.work.ChunkPacket; 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 { + void setSession(IrisSession session); + int getLoadedChunks(); @ChunkCoordinates boolean exists(int x, int z); @RegionCoordinates - void generateRegion(MultiBurst burst, int x, int z, PregenListener listener); + CompletableFuture generateRegion(MultiBurst burst, int x, int z, int maxConcurrent, PregenListener listener); @ChunkCoordinates void generateChunk(int x, int z); + + @ChunkCoordinates + void addChunk(ChunkPacket packet); } diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java index 08307b28c..eea5ed08b 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenTask.java @@ -23,6 +23,8 @@ import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.math.Position2; import com.volmit.iris.util.math.Spiraled; import com.volmit.iris.util.math.Spiraler; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -30,6 +32,7 @@ import java.util.Comparator; @Builder @Data +@AllArgsConstructor(access = AccessLevel.PROTECTED) public class PregenTask { private static final Position2 ZERO = new Position2(0, 0); private static final KList ORDER_CENTER = computeChunkOrder(); diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java index b6cb55102..b5bbb4305 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -22,7 +22,6 @@ import com.volmit.iris.Iris; import com.volmit.iris.core.pregenerator.PregenListener; import com.volmit.iris.core.pregenerator.PregeneratorMethod; import com.volmit.iris.core.tools.IrisToolbelt; -import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.mantle.Mantle; @@ -35,14 +34,11 @@ import org.bukkit.World; import java.util.ArrayList; import java.util.Map; -import java.util.Objects; import java.util.concurrent.Future; public class AsyncPregenMethod implements PregeneratorMethod { private final World world; - private final Engine engine; private final MultiBurst burst; - private final KList> future; private final Map lastUse; @@ -51,9 +47,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!"); } this.world = world; - this.engine = IrisToolbelt.access(world).getEngine(); - burst = new MultiBurst("AsyncPregen", 8 ); - future = new KList<>(1024); + burst = new MultiBurst("Iris Async Pregen", Thread.MIN_PRIORITY); + future = new KList<>(256); this.lastUse = new KMap<>(); } @@ -81,22 +76,18 @@ public class AsyncPregenMethod implements PregeneratorMethod { private void completeChunk(int x, int z, PregenListener listener) { try { - future.add(PaperLib.getChunkAtAsync(world, x, z, true).thenApply((i) -> { - if (i == null) return 0; + PaperLib.getChunkAtAsync(world, x, z, true).thenAccept((i) -> { lastUse.put(i, M.ms()); listener.onChunkGenerated(x, z); listener.onChunkCleaned(x, z); - return 0; - })); - + }).get(); + } catch (InterruptedException ignored) { } catch (Throwable e) { e.printStackTrace(); } } private void waitForChunksPartial(int maxWaiting) { - future.removeWhere(Objects::isNull); - while (future.size() > maxWaiting) { try { Future i = future.remove(0); @@ -125,8 +116,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { e.printStackTrace(); } } - - future.removeWhere(Objects::isNull); } @Override @@ -143,6 +132,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { public void close() { waitForChunks(); unloadAndSaveAllChunks(); + burst.close(); } @Override 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 d4ef57f3f..7e42de852 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 @@ -65,6 +65,7 @@ public class HeadlessPregenMethod implements PregeneratorMethod { Iris.error("Failed to close headless"); e.printStackTrace(); } + burst.close(); } @Override diff --git a/core/src/main/java/com/volmit/iris/core/service/WandSVC.java b/core/src/main/java/com/volmit/iris/core/service/WandSVC.java index b1ba81544..68eca93ee 100644 --- a/core/src/main/java/com/volmit/iris/core/service/WandSVC.java +++ b/core/src/main/java/com/volmit/iris/core/service/WandSVC.java @@ -35,8 +35,8 @@ import com.volmit.iris.util.plugin.IrisService; import com.volmit.iris.util.plugin.VolmitSender; import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.SR; +import com.volmit.iris.util.scheduling.jobs.Job; import org.bukkit.*; -import org.bukkit.block.Block; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -57,8 +57,8 @@ import java.util.concurrent.CountDownLatch; public class WandSVC implements IrisService { private static final Particle CRIT_MAGIC = E.getOrDefault(Particle.class, "CRIT_MAGIC", "CRIT"); - private static final Particle REDSTONE = E.getOrDefault(Particle.class, "REDSTONE", "DUST"); - private static final int BLOCKS_PER_TICK = Integer.parseInt(System.getProperty("iris.blocks_per_tick", "1000")); + private static final Particle REDSTONE = E.getOrDefault(Particle.class, "REDSTONE", "DUST"); + private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30")); private static ItemStack dust; private static ItemStack wand; @@ -83,28 +83,80 @@ public class WandSVC implements IrisService { Cuboid c = new Cuboid(f[0], f[1]); IrisObject s = new IrisObject(c.getSizeX(), c.getSizeY(), c.getSizeZ()); - var it = c.iterator(); + var it = c.chunkedIterator(); + + int total = c.getSizeX() * c.getSizeY() * c.getSizeZ(); var latch = new CountDownLatch(1); - new SR() { + new Job() { + private int i; + private Chunk chunk; + @Override - public void run() { - for (int i = 0; i < BLOCKS_PER_TICK; i++) { - if (!it.hasNext()) { - cancel(); - latch.countDown(); - return; - } - - var b = it.next(); - if (b.getType().equals(Material.AIR)) - continue; - - BlockVector bv = b.getLocation().subtract(c.getLowerNE().toVector()).toVector().toBlockVector(); - s.setUnsigned(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ(), b); - } + public String getName() { + return "Scanning Selection"; } - }; - latch.await(); + + @Override + public void execute() { + new SR() { + @Override + public void run() { + var time = M.ms() + MS_PER_TICK; + while (time > M.ms()) { + if (!it.hasNext()) { + if (chunk != null) { + chunk.removePluginChunkTicket(Iris.instance); + chunk = null; + } + + cancel(); + latch.countDown(); + return; + } + + try { + var b = it.next(); + var bChunk = b.getChunk(); + if (chunk == null) { + chunk = bChunk; + chunk.addPluginChunkTicket(Iris.instance); + } else if (chunk != bChunk) { + chunk.removePluginChunkTicket(Iris.instance); + chunk = bChunk; + } + + if (b.getType().equals(Material.AIR)) + continue; + + BlockVector bv = b.getLocation().subtract(c.getLowerNE().toVector()).toVector().toBlockVector(); + s.setUnsigned(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ(), b); + } finally { + i++; + } + } + } + }; + try { + latch.await(); + } catch (InterruptedException ignored) {} + } + + @Override + public void completeWork() {} + + @Override + public int getTotalWork() { + return total; + } + + @Override + public int getWorkCompleted() { + return i; + } + }.execute(new VolmitSender(p), true, () -> {}); + try { + latch.await(); + } catch (InterruptedException ignored) {} return s; } catch (Throwable e) { diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java b/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java index 1bba33d17..de896c764 100644 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java +++ b/core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java @@ -19,7 +19,7 @@ package com.volmit.iris.core.tools; import com.volmit.iris.Iris; -import com.volmit.iris.engine.object.IrisObject; +import com.volmit.iris.engine.object.*; import com.volmit.iris.util.format.C; import com.volmit.iris.util.format.Form; import com.volmit.iris.util.nbt.io.NBTUtil; @@ -28,15 +28,14 @@ import com.volmit.iris.util.nbt.tag.*; import com.volmit.iris.util.plugin.VolmitSender; import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.PrecisionStopwatch; +import org.apache.commons.io.FileUtils; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; -import org.bukkit.util.Vector; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -48,11 +47,15 @@ public class IrisConverter { FilenameFilter filter = (dir, name) -> name.endsWith(".schem"); File[] fileList = folder.listFiles(filter); + if (fileList == null) { + sender.sendMessage("No schematic files to convert found in " + folder.getAbsolutePath()); + return; + } ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.submit(() -> { - for (File schem : fileList) { - try { - PrecisionStopwatch p = new PrecisionStopwatch(); + for (File schem : fileList) { + try { + PrecisionStopwatch p = PrecisionStopwatch.start(); boolean largeObject = false; NamedTag tag = null; try { @@ -63,346 +66,84 @@ public class IrisConverter { } CompoundTag compound = (CompoundTag) tag.getTag(); - if (compound.containsKey("Palette") && compound.containsKey("Width") && compound.containsKey("Height") && compound.containsKey("Length")) { - int objW = ((ShortTag) compound.get("Width")).getValue(); - int objH = ((ShortTag) compound.get("Height")).getValue(); - int objD = ((ShortTag) compound.get("Length")).getValue(); - int mv = objW * objH * objD; - AtomicInteger v = new AtomicInteger(0); - AtomicInteger fv = new AtomicInteger(0); - if (mv > 500_000) { - largeObject = true; - Iris.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); - Iris.info(C.GRAY + "- It may take a while"); - if (sender.isPlayer()) { - J.a(() -> { -// while (v.get() != mv) { -// double pr = ((double) v.get() / (double ) mv); -// sender.sendProgress(pr, "Converting"); -// J.sleep(16); -// } - }); - } - } - - CompoundTag paletteTag = (CompoundTag) compound.get("Palette"); - Map blockmap = new HashMap<>(paletteTag.size(), 0.9f); - for (Map.Entry> entry : paletteTag.getValue().entrySet()) { - String blockName = entry.getKey(); - BlockData bd = Bukkit.createBlockData(blockName); - Tag blockTag = entry.getValue(); - int blockId = ((IntTag) blockTag).getValue(); - blockmap.put(blockId, bd); - } - - ByteArrayTag byteArray = (ByteArrayTag) compound.get("BlockData"); - byte[] originalBlockArray = byteArray.getValue(); - int b = 0; - int a = 0; - Map y = new HashMap<>(); - Map x = new HashMap<>(); - Map z = new HashMap<>(); - - // Height adjustments - for (int h = 0; h < objH; h++) { - if (b == 0) { - y.put(h, (byte) 0); - } - if (b > 0) { - y.put(h, (byte) 1); - } - a = 0; - b = 0; - for (int d = 0; d < objD; d++) { - for (int w = 0; w < objW; w++) { - BlockData db = blockmap.get((int) originalBlockArray[fv.get()]); - if (db.getAsString().contains("minecraft:air")) { - a++; - } else { - b++; - } - fv.getAndAdd(1); - } - } - } - fv.set(0); - - // Width adjustments - for (int w = 0; w < objW; w++) { - if (b == 0) { - x.put(w, (byte) 0); - } - if (b > 0) { - x.put(w, (byte) 1); - } - a = 0; - b = 0; - for (int h = 0; h < objH; h++) { - for (int d = 0; d < objD; d++) { - BlockData db = blockmap.get((int) originalBlockArray[fv.get()]); - if (db.getAsString().contains("minecraft:air")) { - a++; - } else { - b++; - } - fv.getAndAdd(1); - } - } - } - fv.set(0); - - // Depth adjustments - for (int d = 0; d < objD; d++) { - if (b == 0) { - z.put(d, (byte) 0); - } - if (b > 0) { - z.put(d, (byte) 1); - } - a = 0; - b = 0; - for (int h = 0; h < objH; h++) { - for (int w = 0; w < objW; w++) { - BlockData db = blockmap.get((int) originalBlockArray[fv.get()]); - if (db.getAsString().contains("minecraft:air")) { - a++; - } else { - b++; - } - fv.getAndAdd(1); - } - } - } - fv.set(0); - int CorrectObjH = getCorrectY(y, objH); - int CorrectObjW = getCorrectX(x, objW); - int CorrectObjD = getCorrectZ(z, objD); - - //IrisObject object = new IrisObject(CorrectObjW, CorrectObjH, CorrectObjH); - IrisObject object = new IrisObject(objW, objH, objD); - Vector originalVector = new Vector(objW, objH, objD); - - - int[] yc = null; - int[] xc = null; - int[] zc = null; - - - int fo = 0; - int so = 0; - int o = 0; - int c = 0; - for (Integer i : y.keySet()) { - if (y.get(i) == 0) { - o++; - } - if (y.get(i) == 1) { - c++; - if (c == 1) { - fo = o; - } - o = 0; - } - } - so = o; - yc = new int[]{fo, so}; - - fo = 0; - so = 0; - o = 0; - c = 0; - for (Integer i : x.keySet()) { - if (x.get(i) == 0) { - o++; - } - if (x.get(i) == 1) { - c++; - if (c == 1) { - fo = o; - } - o = 0; - } - } - so = o; - xc = new int[]{fo, so}; - - fo = 0; - so = 0; - o = 0; - c = 0; - for (Integer i : z.keySet()) { - if (z.get(i) == 0) { - o++; - } - if (z.get(i) == 1) { - c++; - if (c == 1) { - fo = o; - } - o = 0; - } - } - so = o; - zc = new int[]{fo, so}; - - int h1, h2, w1, w2, v1 = 0, volume = objW * objH * objD; - Map blockLocationMap = new LinkedHashMap<>(); - boolean hasAir = false; - int pos = 0; - for (int i : originalBlockArray) { - blockLocationMap.put(pos, i); - pos++; - } - - - for (int h = 0; h < objH; h++) { - for (int d = 0; d < objD; d++) { - for (int w = 0; w < objW; w++) { - BlockData bd = blockmap.get((int) originalBlockArray[v.get()]); - if (!bd.getMaterial().isAir()) { - object.setUnsigned(w, h, d, bd); - } - v.getAndAdd(1); - } - } - } - - - try { - object.write(new File(folder, schem.getName().replace(".schem", ".iob"))); - } catch (IOException e) { - Iris.info(C.RED + "Failed to save: " + schem.getName()); - throw new RuntimeException(e); - } + if (compound.containsKey("Palette") && compound.containsKey("Width") && compound.containsKey("Height") && compound.containsKey("Length")) { + int objW = ((ShortTag) compound.get("Width")).getValue(); + int objH = ((ShortTag) compound.get("Height")).getValue(); + int objD = ((ShortTag) compound.get("Length")).getValue(); + int i = -1; + int mv = objW * objH * objD; + AtomicInteger v = new AtomicInteger(0); + if (mv > 500_000) { + largeObject = true; + Iris.info(C.GRAY + "Converting.. "+ schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); + Iris.info(C.GRAY + "- It may take a while"); if (sender.isPlayer()) { - if (largeObject) { - sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); - } else { - sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); + i = J.ar(() -> { + sender.sendProgress((double) v.get() / mv, "Converting"); + }, 0); + } + } + + CompoundTag paletteTag = (CompoundTag) compound.get("Palette"); + Map blockmap = new HashMap<>(paletteTag.size(), 0.9f); + for (Map.Entry> entry : paletteTag.getValue().entrySet()) { + String blockName = entry.getKey(); + BlockData bd = Bukkit.createBlockData(blockName); + Tag blockTag = entry.getValue(); + int blockId = ((IntTag) blockTag).getValue(); + blockmap.put(blockId, bd); + } + + ByteArrayTag byteArray = (ByteArrayTag) compound.get("BlockData"); + byte[] originalBlockArray = byteArray.getValue(); + + IrisObject object = new IrisObject(objW, objH, objD); + for (int h = 0; h < objH; h++) { + for (int d = 0; d < objD; d++) { + for (int w = 0; w < objW; w++) { + BlockData bd = blockmap.get((int) originalBlockArray[v.get()]); + if (!bd.getMaterial().isAir()) { + object.setUnsigned(w, h, d, bd); + } + v.getAndAdd(1); } } - if (largeObject) { - Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); - } else { - Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); - } - // schem.delete(); } - } catch (Exception e) { - Iris.info(C.RED + "Failed to convert: " + schem.getName()); + if (i != -1) J.car(i); + try { + object.shrinkwrap(); + object.write(new File(folder, schem.getName().replace(".schem", ".iob"))); + } catch (IOException e) { + Iris.info(C.RED + "Failed to save: " + schem.getName()); + throw new RuntimeException(e); + } if (sender.isPlayer()) { - sender.sendMessage(C.RED + "Failed to convert: " + schem.getName()); + if (largeObject) { + sender.sendMessage(C.IRIS + "Converted "+ schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); + } else { + sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); + } } - e.printStackTrace(); - Iris.reportError(e); + if (largeObject) { + Iris.info(C.GRAY + "Converted "+ schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis())); + } else { + Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob")); + } + FileUtils.delete(schem); } + } catch (Exception e) { + Iris.info(C.RED + "Failed to convert: " + schem.getName()); + if (sender.isPlayer()) { + sender.sendMessage(C.RED + "Failed to convert: " + schem.getName()); + } + e.printStackTrace(); + Iris.reportError(e); } + } + sender.sendMessage(C.GRAY + "converted: " + fileList.length); }); } - public static boolean isNewPointFurther(int[] originalPoint, int[] oldPoint, int[] newPoint) { - int oX = oldPoint[1]; - int oY = oldPoint[2]; - int oZ = oldPoint[3]; - - int nX = newPoint[1]; - int nY = newPoint[2]; - int nZ = newPoint[3]; - - int orX = originalPoint[1]; - int orY = originalPoint[2]; - int orZ = originalPoint[3]; - - double oldDistance = Math.sqrt(Math.pow(oX - orX, 2) + Math.pow(oY - orY, 2) + Math.pow(oZ - orZ, 2)); - double newDistance = Math.sqrt(Math.pow(nX - orX, 2) + Math.pow(nY - orY, 2) + Math.pow(nZ - orZ, 2)); - - if (newDistance > oldDistance) { - return true; - } - return false; - } - - public static int[] getCoordinates(int pos, int obX, int obY, int obZ) { - int z = 0; - int[] coords = new int[4]; - for (int h = 0; h < obY; h++) { - for (int d = 0; d < obZ; d++) { - for (int w = 0; w < obX; w++) { - if (z == pos) { - coords[1] = w; - coords[2] = h; - coords[3] = d; - return coords; - } - z++; - } - } - } - return null; - } - - public static int getCorrectY(Map y, int H) { - int fo = 0; - int so = 0; - int o = 0; - int c = 0; - for (Integer i : y.keySet()) { - if (y.get(i) == 0) { - o++; - } - if (y.get(i) == 1) { - c++; - if (c == 1) { - fo = o; - } - o = 0; - } - } - so = o; - return H = H - (fo + so); - } - - public static int getCorrectX(Map x, int W) { - int fo = 0; - int so = 0; - int o = 0; - int c = 0; - for (Integer i : x.keySet()) { - if (x.get(i) == 0) { - o++; - } - if (x.get(i) == 1) { - c++; - if (c == 1) { - fo = o; - } - o = 0; - } - } - so = o; - return W = W - (fo + so); - } - - public static int getCorrectZ(Map z, int D) { - int fo = 0; - int so = 0; - int o = 0; - int c = 0; - for (Integer i : z.keySet()) { - if (z.get(i) == 0) { - o++; - } - if (z.get(i) == 1) { - c++; - if (c == 1) { - fo = o; - } - o = 0; - } - } - so = o; - return D = D - (fo + so); - } } 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 c8495a338..3cc9a2944 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 @@ -31,6 +31,8 @@ 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.server.pregen.CloudMethod; +import com.volmit.iris.server.pregen.CloudTask; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.exceptions.IrisException; @@ -65,10 +67,12 @@ public class IrisPackBenchmarking { private int radius; private boolean finished = false; private Engine engine; + private String address; - public IrisPackBenchmarking(IrisDimension dimension, int r, boolean headless, boolean gui) { + public IrisPackBenchmarking(IrisDimension dimension, String address, int r, boolean headless, boolean gui) { instance = this; this.IrisDimension = dimension; + this.address = address; this.radius = r; this.headless = headless; this.gui = gui; @@ -90,7 +94,13 @@ public class IrisPackBenchmarking { } Iris.info("Starting Benchmark!"); stopwatch.begin(); - startBenchmark(); + try { + if (address != null && !address.isBlank()) + startCloudBenchmark(); + else startBenchmark(); + } catch (Throwable e) { + e.printStackTrace(); + } }, "PackBenchmarking").start(); } @@ -197,6 +207,19 @@ public class IrisPackBenchmarking { IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), engine); } + private void startCloudBenchmark() throws InterruptedException { + int x = 0; + int z = 0; + IrisToolbelt.pregenerate(CloudTask + .couldBuilder() + .gui(gui) + .center(new Position2(x, z)) + .width(radius) + .height(radius) + .distance(engine.getMantle().getRadius() * 2) + .build(), new CloudMethod(address, engine), engine); + } + private double calculateAverage(KList list) { double sum = 0; for (int num : list) { diff --git a/core/src/main/java/com/volmit/iris/core/wand/WandSelection.java b/core/src/main/java/com/volmit/iris/core/wand/WandSelection.java index e5754afad..2cd37c20b 100644 --- a/core/src/main/java/com/volmit/iris/core/wand/WandSelection.java +++ b/core/src/main/java/com/volmit/iris/core/wand/WandSelection.java @@ -33,6 +33,7 @@ public class WandSelection { private static final Particle REDSTONE = E.getOrDefault(Particle.class, "REDSTONE", "DUST"); private final Cuboid c; private final Player p; + private static final double STEP = 0.10; public WandSelection(Cuboid c, Player p) { this.c = c; @@ -40,77 +41,58 @@ public class WandSelection { } public void draw() { - double accuracy; - double dist; + Location playerLoc = p.getLocation(); + double maxDistanceSquared = 256 * 256; + int particleCount = 0; - for (double i = c.getLowerX() - 1; i < c.getUpperX() + 1; i += 0.25) { - for (double j = c.getLowerY() - 1; j < c.getUpperY() + 1; j += 0.25) { - for (double k = c.getLowerZ() - 1; k < c.getUpperZ() + 1; k += 0.25) { - boolean ii = i == c.getLowerX() || i == c.getUpperX(); - boolean jj = j == c.getLowerY() || j == c.getUpperY(); - boolean kk = k == c.getLowerZ() || k == c.getUpperZ(); + // cube! + Location[][] edges = { + {c.getLowerNE(), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ())}, + {c.getLowerNE(), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ())}, + {c.getLowerNE(), new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ())}, + {new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ())}, + {new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)}, + {new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)} + }; - if ((ii && jj) || (ii && kk) || (kk && jj)) { - Vector push = new Vector(0, 0, 0); + for (Location[] edge : edges) { + Vector direction = edge[1].toVector().subtract(edge[0].toVector()); + double length = direction.length(); + direction.normalize(); - if (i == c.getLowerX()) { - push.add(new Vector(-0.55, 0, 0)); - } + for (double d = 0; d <= length; d += STEP) { + Location particleLoc = edge[0].clone().add(direction.clone().multiply(d)); - if (j == c.getLowerY()) { - push.add(new Vector(0, -0.55, 0)); - } - - if (k == c.getLowerZ()) { - push.add(new Vector(0, 0, -0.55)); - } - - if (i == c.getUpperX()) { - push.add(new Vector(0.55, 0, 0)); - } - - if (j == c.getUpperY()) { - push.add(new Vector(0, 0.55, 0)); - } - - if (k == c.getUpperZ()) { - push.add(new Vector(0, 0, 0.55)); - } - - Location a = new Location(c.getWorld(), i, j, k).add(0.5, 0.5, 0.5).add(push); - accuracy = M.lerpInverse(0, 64 * 64, p.getLocation().distanceSquared(a)); - dist = M.lerp(0.125, 3.5, accuracy); - - if (M.r(M.min(dist * 5, 0.9D) * 0.995)) { - continue; - } - - if (ii && jj) { - a.add(0, 0, RNG.r.d(-0.3, 0.3)); - } - - if (kk && jj) { - a.add(RNG.r.d(-0.3, 0.3), 0, 0); - } - - if (ii && kk) { - a.add(0, RNG.r.d(-0.3, 0.3), 0); - } - - if (p.getLocation().distanceSquared(a) < 256 * 256) { - Color color = Color.getHSBColor((float) (0.5f + (Math.sin((i + j + k + (p.getTicksLived() / 2f)) / (20f)) / 2)), 1, 1); - int r = color.getRed(); - int g = color.getGreen(); - int b = color.getBlue(); - - p.spawnParticle(REDSTONE, a.getX(), a.getY(), a.getZ(), - 1, 0, 0, 0, 0, - new Particle.DustOptions(org.bukkit.Color.fromRGB(r, g, b), - (float) dist * 3f)); - } - } + if (playerLoc.distanceSquared(particleLoc) > maxDistanceSquared) { + continue; } + + spawnParticle(particleLoc, playerLoc); + particleCount++; } } } + + private void spawnParticle(Location particleLoc, Location playerLoc) { + double accuracy = M.lerpInverse(0, 64 * 64, playerLoc.distanceSquared(particleLoc)); + double dist = M.lerp(0.125, 3.5, accuracy); + + if (M.r(Math.min(dist * 5, 0.9D) * 0.995)) { + return; + } + + float hue = (float) (0.5f + (Math.sin((particleLoc.getX() + particleLoc.getY() + particleLoc.getZ() + (p.getTicksLived() / 2f)) / 20f) / 2)); + Color color = Color.getHSBColor(hue, 1, 1); + + p.spawnParticle(REDSTONE, particleLoc, + 0, 0, 0, 0, 1, + new Particle.DustOptions(org.bukkit.Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue()), + (float) dist * 3f)); + } } 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 9287fb171..4b429971f 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 @@ -53,6 +53,7 @@ import com.volmit.iris.util.math.Position2; import com.volmit.iris.util.math.RNG; import com.volmit.iris.util.matter.MatterCavern; import com.volmit.iris.util.matter.MatterUpdate; +import com.volmit.iris.util.matter.TileWrapper; import com.volmit.iris.util.matter.slices.container.JigsawPieceContainer; import com.volmit.iris.util.parallel.BurstExecutor; import com.volmit.iris.util.parallel.MultiBurst; @@ -283,9 +284,9 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat var mc = getMantle().getMantle().getChunk(c.getX(), c.getZ()); mc.raiseFlag(MantleFlag.TILE, () -> J.s(() -> { - mc.iterate(TileData.class, (x, y, z, tile) -> { + mc.iterate(TileWrapper.class, (x, y, z, tile) -> { int betterY = y + getWorld().minHeight(); - if (!TileData.setTileState(c.getBlock(x, betterY, z), tile)) + if (!TileData.setTileState(c.getBlock(x, betterY, z), tile.getData())) Iris.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", x, betterY, z, c.getBlock(x, betterY, z).getBlockData().getMaterial().getKey(), tile.getMaterial().name()); }); })); diff --git a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java b/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java index 69aee2375..1807631df 100644 --- a/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java +++ b/core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java @@ -57,7 +57,7 @@ public class MantleJigsawComponent extends IrisMantleComponent { @ChunkCoordinates private void generateJigsaw(MantleWriter writer, int x, int z, IrisBiome biome, IrisRegion region) { - long seed = cng.fit(Integer.MIN_VALUE, Integer.MIN_VALUE, x, z); + long seed = cng.fit(Integer.MIN_VALUE, Integer.MAX_VALUE, x, z); if (getDimension().getStronghold() != null) { List poss = getDimension().getStrongholds(seed()); @@ -130,7 +130,7 @@ public class MantleJigsawComponent extends IrisMantleComponent { public IrisJigsawStructure guess(int x, int z) { // todo The guess doesnt bring into account that the placer may return -1 // todo doesnt bring skipped placements into account - long seed = cng.fit(Integer.MIN_VALUE, Integer.MIN_VALUE, x, z); + long seed = cng.fit(Integer.MIN_VALUE, Integer.MAX_VALUE, x, z); IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8); IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8); diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java index 790c34727..b1d04606b 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java +++ b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomeCustom.java @@ -104,7 +104,7 @@ public class IrisBiomeCustom { JSONObject po = new JSONObject(); po.put("type", ambientParticle.getParticle().name().toLowerCase()); particle.put("options", po); - particle.put("probability", ambientParticle.getRarity()); + particle.put("probability", 1f/ambientParticle.getRarity()); effects.put("particle", particle); } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java index dddd36228..9ce29c71b 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java +++ b/core/src/main/java/com/volmit/iris/engine/object/IrisObject.java @@ -43,6 +43,7 @@ import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.plugin.VolmitSender; import com.volmit.iris.util.scheduling.IrisLock; import com.volmit.iris.util.scheduling.PrecisionStopwatch; +import com.volmit.iris.util.scheduling.jobs.Job; import com.volmit.iris.util.stream.ProceduralStream; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -63,7 +64,9 @@ import org.bukkit.util.Vector; import java.io.*; import java.util.*; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; @Accessors(chain = true) @@ -384,6 +387,88 @@ public class IrisObject extends IrisRegistrant { } } + public void write(OutputStream o, VolmitSender sender) throws IOException { + AtomicReference ref = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + new Job() { + private int total = getBlocks().size() * 3 + getStates().size(); + private int c = 0; + + @Override + public String getName() { + return "Saving Object"; + } + + @Override + public void execute() { + try { + DataOutputStream dos = new DataOutputStream(o); + dos.writeInt(w); + dos.writeInt(h); + dos.writeInt(d); + dos.writeUTF("Iris V2 IOB;"); + + KList palette = new KList<>(); + + for (BlockData i : getBlocks().values()) { + palette.addIfMissing(i.getAsString()); + ++c; + } + total -= getBlocks().size() - palette.size(); + + dos.writeShort(palette.size()); + + for (String i : palette) { + dos.writeUTF(i); + ++c; + } + + dos.writeInt(getBlocks().size()); + + for (BlockVector i : getBlocks().keySet()) { + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + dos.writeShort(palette.indexOf(getBlocks().get(i).getAsString())); + ++c; + } + + dos.writeInt(getStates().size()); + for (BlockVector i : getStates().keySet()) { + dos.writeShort(i.getBlockX()); + dos.writeShort(i.getBlockY()); + dos.writeShort(i.getBlockZ()); + getStates().get(i).toBinary(dos); + ++c; + } + } catch (IOException e) { + ref.set(e); + } finally { + latch.countDown(); + } + } + + @Override + public void completeWork() {} + + @Override + public int getTotalWork() { + return total; + } + + @Override + public int getWorkCompleted() { + return c; + } + }.execute(sender, true, () -> {}); + + try { + latch.await(); + } catch (InterruptedException ignored) {} + if (ref.get() != null) + throw ref.get(); + } + public void read(File file) throws IOException { var fin = new BufferedInputStream(new FileInputStream(file)); try { @@ -408,6 +493,16 @@ public class IrisObject extends IrisRegistrant { out.close(); } + public void write(File file, VolmitSender sender) throws IOException { + if (file == null) { + return; + } + + FileOutputStream out = new FileOutputStream(file); + write(out, sender); + out.close(); + } + public void shrinkwrap() { BlockVector min = new BlockVector(); BlockVector max = new BlockVector(); @@ -478,7 +573,7 @@ public class IrisObject extends IrisRegistrant { getBlocks().put(v, data); TileData state = TileData.getTileState(block); if (state != null) { - Iris.info("Saved State " + v); + Iris.debug("Saved State " + v); getStates().put(v, state); } } diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java index 881d0e953..ac4b81b12 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java +++ b/core/src/main/java/com/volmit/iris/engine/object/IrisObjectRotation.java @@ -22,6 +22,7 @@ import com.volmit.iris.Iris; import com.volmit.iris.engine.object.annotations.Desc; import com.volmit.iris.engine.object.annotations.Snippet; import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -30,9 +31,12 @@ import org.bukkit.Axis; import org.bukkit.Material; import org.bukkit.block.BlockFace; import org.bukkit.block.data.*; +import org.bukkit.block.data.type.Wall; +import org.bukkit.block.structure.StructureRotation; import org.bukkit.util.BlockVector; import java.util.List; +import java.util.Map; @Snippet("object-rotator") @Accessors(chain = true) @@ -41,6 +45,8 @@ import java.util.List; @Desc("Configures rotation for iris") @Data public class IrisObjectRotation { + private static final List WALL_FACES = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST); + @Desc("If this rotator is enabled or not") private boolean enabled = true; @@ -283,6 +289,22 @@ public class IrisObjectRotation { for (BlockFace i : faces) { g.setFace(i, true); } + } else if (d instanceof Wall wall) { + KMap faces = new KMap<>(); + + for (BlockFace i : WALL_FACES) { + Wall.Height h = wall.getHeight(i); + BlockVector bv = new BlockVector(i.getModX(), i.getModY(), i.getModZ()); + bv = rotate(bv.clone(), spinx, spiny, spinz); + BlockFace r = getFace(bv); + if (WALL_FACES.contains(r)) { + faces.put(r, h); + } + } + + for (BlockFace i : WALL_FACES) { + wall.setHeight(i, faces.getOrDefault(i, Wall.Height.NONE)); + } } else if (d.getMaterial().equals(Material.NETHER_PORTAL) && d instanceof Orientable g) { //TODO: Fucks up logs BlockFace f = faceForAxis(g.getAxis()); diff --git a/core/src/main/java/com/volmit/iris/server/EntryPoint.java b/core/src/main/java/com/volmit/iris/server/EntryPoint.java new file mode 100644 index 000000000..13ab1c90f --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/EntryPoint.java @@ -0,0 +1,116 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2024 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.server; + +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +@Log(topic = "Iris-Server") +public class EntryPoint { + + public static void main(String[] args) throws Throwable { + if (args.length < 4) { + log.info("Usage: java -jar Iris.jar [nodes]"); + System.exit(-1); + return; + } + + String[] nodes = new String[args.length - 4]; + System.arraycopy(args, 4, nodes, 0, nodes.length); + try { + runServer(args[0], Integer.parseInt(args[1]), Integer.parseInt(args[2]), Integer.parseInt(args[3]), nodes); + } catch (Throwable e) { + log.log(Level.SEVERE, "Failed to start server", e); + System.exit(-1); + } + } + + private static void runServer(String version, int minMemory, int maxMemory, int serverPort, String[] nodes) throws IOException { + File serverJar = new File("cache", "spigot-"+version+".jar"); + if (!serverJar.getParentFile().exists() && !serverJar.getParentFile().mkdirs()) { + log.severe("Failed to create cache directory"); + System.exit(-1); + return; + } + + + if (!serverJar.exists()) { + try (var in = new URL("https://download.getbukkit.org/spigot/spigot-"+ version+".jar").openStream()) { + Files.copy(in, serverJar.toPath()); + } + log.info("Downloaded spigot-"+version+".jar to "+serverJar.getAbsolutePath()); + } + + File pluginFile = new File("plugins/Iris.jar"); + if (pluginFile.exists()) pluginFile.delete(); + if (!pluginFile.getParentFile().exists() && !pluginFile.getParentFile().mkdirs()) { + log.severe("Failed to create plugins directory"); + System.exit(-1); + return; + } + + boolean windows = System.getProperty("os.name").toLowerCase().contains("win"); + String path = System.getProperty("java.home") + File.separator + "bin" + File.separator + (windows ? "java.exe" : "java"); + + try { + File irisFile = new File(EntryPoint.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + if (!irisFile.isFile()) { + log.severe("Failed to locate the Iris plugin jar"); + System.exit(-1); + return; + } + Files.createSymbolicLink(pluginFile.toPath(), irisFile.toPath()); + } catch (URISyntaxException ignored) {} + + List cmd = new ArrayList<>(List.of( + path, + "-Xms" + minMemory + "M", + "-Xmx" + maxMemory + "M", + "-XX:+AlwaysPreTouch", + "-XX:+HeapDumpOnOutOfMemoryError", + "-Ddisable.watchdog=true", + "-Dcom.mojang.eula.agree=true", + "-Dcom.volmit.iris.server.port="+serverPort + )); + if (nodes.length > 0) + cmd.add("-Dcom.volmit.iris.server.remote=" + String.join(",", nodes)); + cmd.addAll(List.of("-jar", serverJar.getAbsolutePath(), "nogui")); + + var process = new ProcessBuilder(cmd) + .inheritIO() + .start(); + Runtime.getRuntime().addShutdownHook(new Thread(process::destroy)); + + + while (true) { + try { + process.waitFor(); + break; + } catch (InterruptedException ignored) {} + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/IrisConnection.java b/core/src/main/java/com/volmit/iris/server/IrisConnection.java new file mode 100644 index 000000000..e87ef1de0 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/IrisConnection.java @@ -0,0 +1,199 @@ +package com.volmit.iris.server; + +import com.volmit.iris.server.execption.RejectedException; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.packet.handle.Decoder; +import com.volmit.iris.server.packet.handle.Encoder; +import com.volmit.iris.server.packet.handle.Prepender; +import com.volmit.iris.server.packet.handle.Splitter; +import com.volmit.iris.server.util.ErrorPacket; +import com.volmit.iris.server.util.ConnectionHolder; +import com.volmit.iris.server.util.PacketListener; +import com.volmit.iris.server.util.PacketSendListener; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.TimeoutException; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +import javax.annotation.Nullable; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; +import java.util.logging.Level; + +@RequiredArgsConstructor +@Log(topic = "IrisConnection") +public class IrisConnection extends SimpleChannelInboundHandler { + private static EventLoopGroup WORKER; + + private Channel channel; + private SocketAddress address; + private final PacketListener listener; + private final Queue queue = new ConcurrentLinkedQueue<>(); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Packet packet) throws Exception { + if (!channel.isOpen() || listener == null || !listener.isAccepting()) return; + + try { + listener.onPacket(packet); + } catch (RejectedException e) { + send(new ErrorPacket("Rejected: " + e.getMessage())); + } + } + + public void send(Packet packet) { + this.send(packet, null); + } + + public void send(Packet packet, @Nullable PacketSendListener listener) { + if (!isConnected()) { + queue.add(new PacketHolder(packet, listener)); + return; + } + + flushQueue(); + sendPacket(packet, listener); + } + + public boolean isConnected() { + return channel != null && channel.isOpen(); + } + + public void disconnect() { + try { + if (channel != null && channel.isOpen()) { + log.info("Closed on " + address); + channel.close(); + } + if (listener != null) + listener.onDisconnect(); + } catch (Throwable e) { + log.log(Level.SEVERE, "Failed to close on " + address, e); + } + } + + public void execute(Runnable runnable) { + if (channel == null || !channel.isOpen()) return; + channel.eventLoop().execute(runnable); + } + + private void flushQueue() { + if (this.channel != null && this.channel.isOpen()) { + synchronized(this.queue) { + PacketHolder packetHolder; + while((packetHolder = this.queue.poll()) != null) { + sendPacket(packetHolder.packet, packetHolder.listener); + } + } + } + } + + private void sendPacket(Packet packet, @Nullable PacketSendListener listener) { + if (!channel.eventLoop().inEventLoop()) { + channel.eventLoop().execute(() -> sendPacket(packet, listener)); + return; + } + + ChannelFuture channelFuture = channel.writeAndFlush(packet); + + if (listener != null) { + channelFuture.addListener(future -> { + if (future.isSuccess()) { + listener.onSuccess(); + } else { + Packet fallback = listener.onFailure(); + if (fallback == null) return; + channel.writeAndFlush(fallback) + .addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } + }); + } + channelFuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + + channel = ctx.channel(); + address = channel.remoteAddress(); + log.info("Opened on " + channel.remoteAddress()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + disconnect(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (!channel.isOpen()) return; + ErrorPacket error; + if (cause instanceof TimeoutException) { + error = new ErrorPacket("Timed out"); + } else { + error = new ErrorPacket("Internal Exception: " + cause.getMessage()); + log.log(Level.SEVERE, "Failed to send packet", cause); + } + + sendPacket(error, PacketSendListener.thenRun(this::disconnect)); + channel.config().setAutoRead(false); + } + + @Override + public String toString() { + return "IrisConnection{address=%s}".formatted(address); + } + + public static void configureSerialization(Channel channel, ConnectionHolder holder) { + channel.pipeline() + .addLast("timeout", new ReadTimeoutHandler(30)) + .addLast("splitter", new Splitter()) + .addLast("decoder", new Decoder()) + .addLast("prepender", new Prepender()) + .addLast("encoder", new Encoder()) + .addLast("packet_handler", holder.getConnection()); + } + + public static T connect(InetSocketAddress address, T holder) throws InterruptedException { + new Bootstrap() + .group(getWorker()) + .handler(new ChannelInitializer<>() { + @Override + protected void initChannel(Channel channel) { + channel.config().setOption(ChannelOption.TCP_NODELAY, true); + IrisConnection.configureSerialization(channel, holder); + } + }) + .channel(NioSocketChannel.class) + .connect(address) + .sync(); + return holder; + } + + private static class PacketHolder { + private final Packet packet; + @Nullable + private final PacketSendListener listener; + + public PacketHolder(Packet packet, @Nullable PacketSendListener listener) { + this.packet = packet; + this.listener = listener; + } + } + + private static EventLoopGroup getWorker() { + if (WORKER == null) { + WORKER = new NioEventLoopGroup(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> WORKER.shutdownGracefully())); + } + return WORKER; + } +} diff --git a/core/src/main/java/com/volmit/iris/server/execption/RejectedException.java b/core/src/main/java/com/volmit/iris/server/execption/RejectedException.java new file mode 100644 index 000000000..e4f1579bf --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/execption/RejectedException.java @@ -0,0 +1,8 @@ +package com.volmit.iris.server.execption; + +public class RejectedException extends Exception { + + public RejectedException(String message) { + super(message); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/master/IrisMasterClient.java b/core/src/main/java/com/volmit/iris/server/master/IrisMasterClient.java new file mode 100644 index 000000000..af4ed90aa --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/master/IrisMasterClient.java @@ -0,0 +1,69 @@ +package com.volmit.iris.server.master; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.packet.Packets; +import com.volmit.iris.server.packet.init.InfoPacket; +import com.volmit.iris.server.packet.init.PingPacket; +import com.volmit.iris.server.util.ConnectionHolder; +import com.volmit.iris.server.util.PacketListener; +import com.volmit.iris.server.util.PacketSendListener; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class IrisMasterClient implements ConnectionHolder, PacketListener { + private final @Getter IrisConnection connection = new IrisConnection(this); + private final IrisMasterSession session; + private final CompletableFuture pingResponse = new CompletableFuture<>(); + private final CompletableFuture nodeCount = new CompletableFuture<>(); + + IrisMasterClient(String version, IrisMasterSession session){ + this.session = session; + Packets.PING.newPacket() + .setVersion(version) + .send(connection); + try { + var packet = pingResponse.get(); + if (!packet.getVersion().contains(version)) + throw new IllegalStateException("Remote server version does not match"); + } catch (ExecutionException | InterruptedException e) { + throw new IllegalStateException("Failed to get ping packet", e); + } + } + + protected void send(Packet packet) { + connection.send(packet); + } + + protected void send(Packet packet, @Nullable PacketSendListener listener) { + connection.send(packet, listener); + } + + @Override + public void onPacket(Packet raw) { + if (!pingResponse.isDone() && raw instanceof PingPacket packet) { + pingResponse.complete(packet); + return; + } + if (!nodeCount.isDone() && raw instanceof InfoPacket packet && packet.getNodeCount() > 0) { + nodeCount.complete(packet.getNodeCount()); + return; + } + session.onClientPacket(this, raw); + } + + public int getNodeCount() { + try { + return nodeCount.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public void disconnect() { + connection.disconnect(); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/master/IrisMasterServer.java b/core/src/main/java/com/volmit/iris/server/master/IrisMasterServer.java new file mode 100644 index 000000000..e1346005f --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/master/IrisMasterServer.java @@ -0,0 +1,125 @@ +package com.volmit.iris.server.master; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.node.IrisServer; +import com.volmit.iris.server.util.PregenHolder; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.collection.KSet; +import lombok.extern.java.Log; + +import java.net.InetSocketAddress; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Log(topic = "Iris-MasterServer") +public class IrisMasterServer extends IrisServer { + private static IrisMasterServer instance; + private final KMap>> sessions = new KMap<>(); + private final KMap> nodes = new KMap<>(); + + public IrisMasterServer(int port, String[] remote) throws InterruptedException { + super("Iris-MasterServer", port, IrisMasterSession::new); + if (instance != null && !instance.isRunning()) + close("Server already running"); + instance = this; + + for (var address : remote) { + try { + var split = address.split(":"); + if (split.length != 2 || !split[1].matches("\\d+")) { + log.warning("Invalid remote server address: " + address); + continue; + } + + addNode(new InetSocketAddress(split[0], Integer.parseInt(split[1]))); + } catch (Throwable e) { + log.log(Level.WARNING, "Failed to parse address: " + address, e); + } + } + } + + private void addNode(InetSocketAddress address) throws InterruptedException { + var ping = new PingConnection(address); + try { + for (String version : ping.getVersion().get()) + addNode(address, version); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + public void addNode(InetSocketAddress address, String version) { + nodes.computeIfAbsent(version, v -> new KSet<>()).add(address); + } + + public void removeNode(InetSocketAddress address) { + for (var set : nodes.values()) { + set.remove(address); + } + } + + public static void close(UUID session) { + var map = get().sessions.remove(session); + if (map == null) return; + map.keySet().forEach(IrisMasterClient::disconnect); + map.clear(); + } + + public static KList getVersions() { + return get().nodes.k(); + } + + public static KMap> getNodes(String version, IrisMasterSession session) { + var master = get(); + var uuid = session.getUuid(); + close(uuid); + + master.getLogger().info("Requesting nodes for session " + uuid); + var map = new KMap>(); + for (var address : master.nodes.getOrDefault(version, new KSet<>())) { + try { + map.put(IrisConnection.connect(address, new IrisMasterClient(version, session)), new KMap<>()); + } catch (Throwable e) { + master.getLogger().log(Level.WARNING, "Failed to connect to server " + address, e); + master.removeNode(address); + } + } + + master.sessions.put(uuid, map); + return map; + } + + @Override + public void close() throws Exception { + log.info("Closing!"); + super.close(); + sessions.values() + .stream() + .map(KMap::keySet) + .flatMap(Set::stream) + .forEach(IrisMasterClient::disconnect); + sessions.clear(); + } + + @Override + protected Logger getLogger() { + return log; + } + + private void close(String message) throws IllegalStateException { + try { + close(); + } catch (Exception ignored) {} + throw new IllegalStateException(message); + } + + public static IrisMasterServer get() { + if (instance == null) + throw new IllegalStateException("IrisMasterServer not running"); + return instance; + } +} diff --git a/core/src/main/java/com/volmit/iris/server/master/IrisMasterSession.java b/core/src/main/java/com/volmit/iris/server/master/IrisMasterSession.java new file mode 100644 index 000000000..985555c6b --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/master/IrisMasterSession.java @@ -0,0 +1,130 @@ +package com.volmit.iris.server.master; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.execption.RejectedException; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.packet.Packets; +import com.volmit.iris.server.packet.init.EnginePacket; +import com.volmit.iris.server.packet.init.FilePacket; +import com.volmit.iris.server.packet.init.InfoPacket; +import com.volmit.iris.server.packet.init.PingPacket; +import com.volmit.iris.server.packet.work.ChunkPacket; +import com.volmit.iris.server.packet.work.DonePacket; +import com.volmit.iris.server.packet.work.MantleChunkPacket; +import com.volmit.iris.server.packet.work.PregenPacket; +import com.volmit.iris.server.util.*; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; +import lombok.Getter; +import lombok.extern.java.Log; + +import java.util.Comparator; +import java.util.UUID; +import java.util.logging.Level; + +@Log(topic = "IrisMasterSession") +public class IrisMasterSession implements ConnectionHolder, PacketListener { + private final @Getter IrisConnection connection = new IrisConnection(this); + private final @Getter UUID uuid = UUID.randomUUID(); + private final KMap map = new KMap<>(); + private final CPSLooper cpsLooper = new CPSLooper("IrisMasterSession-" + uuid, connection); + private final KMap waiting = new KMap<>(); + private KMap> clients; + private int radius = -1; + + @Override + public void onPacket(Packet raw) throws Exception { + if (clients == null) { + if (raw instanceof PingPacket packet) { + var versions = packet.getVersion(); + PacketSendListener listener = versions.size() != 1 ? PacketSendListener.thenRun(connection::disconnect) : null; + if (listener == null) { + clients = IrisMasterServer.getNodes(versions.get(0), this); + if (clients.isEmpty()) { + connection.disconnect(); + return; + } + + var nodeCount = clients.keySet() + .stream() + .mapToInt(IrisMasterClient::getNodeCount) + .sum(); + cpsLooper.setNodeCount(nodeCount); + } + packet.setVersion(IrisMasterServer.getVersions()) + .send(connection, listener); + } else throw new RejectedException("Not a ping packet"); + } + + if (raw instanceof FilePacket packet) { + if (radius != -1) + throw new RejectedException("Engine already setup"); + waiting.put(packet.getId(), new CompletingHolder(clients.k())); + clients.keySet().forEach(client -> client.send(packet)); + } else if (raw instanceof EnginePacket packet) { + if (radius != -1) + throw new RejectedException("Engine already setup"); + radius = packet.getRadius(); + waiting.put(packet.getId(), new CompletingHolder(clients.k())); + clients.keySet().forEach(client -> client.send(packet)); + } else if (raw instanceof PregenPacket packet) { + if (radius == -1) + throw new RejectedException("Engine not setup"); + var client = pick(); + map.put(packet.getId(), client); + new PregenHolder(packet, radius, true, null) + .put(clients.get(client)); + + client.send(packet); + } else if (raw instanceof MantleChunkPacket packet) { + var client = map.get(packet.getPregenId()); + client.send(packet); + } + } + + protected void onClientPacket(IrisMasterClient client, Packet raw) { + if (raw instanceof ErrorPacket packet) { + packet.log(log, Level.SEVERE); + } else if (raw instanceof ChunkPacket) { + connection.send(raw); + } else if (raw instanceof MantleChunkPacket packet) { + var map = clients.get(client); + if (map.get(packet.getPregenId()) + .remove(packet.getX(), packet.getZ())) + map.get(packet.getPregenId()); + + connection.send(packet); + } else if (raw instanceof MantleChunkPacket.Request packet) { + connection.send(packet); + } else if (raw instanceof InfoPacket packet) { + int i = packet.getGenerated(); + if (i != -1) cpsLooper.addChunks(i); + } else if (raw instanceof DonePacket packet) { + var holder = waiting.get(packet.getId()); + if (holder.remove(client)) { + connection.send(Packets.DONE.newPacket().setId(packet.getId())); + waiting.remove(packet.getId()); + } + } + } + + @Override + public void onDisconnect() { + IrisMasterServer.close(uuid); + } + + private IrisMasterClient pick() throws RejectedException { + return clients.keySet() + .stream() + .min(Comparator.comparingInt(c -> clients.get(c).size())) + .orElseThrow(() -> new RejectedException("No clients available")); + } + + private record CompletingHolder(KList clients) { + + public synchronized boolean remove(IrisMasterClient client) { + clients.remove(client); + return clients.isEmpty(); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/master/PingConnection.java b/core/src/main/java/com/volmit/iris/server/master/PingConnection.java new file mode 100644 index 000000000..800549a28 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/master/PingConnection.java @@ -0,0 +1,32 @@ +package com.volmit.iris.server.master; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.packet.Packets; +import com.volmit.iris.server.packet.init.PingPacket; +import com.volmit.iris.server.util.ConnectionHolder; +import com.volmit.iris.server.util.PacketListener; +import com.volmit.iris.util.collection.KList; +import lombok.Getter; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; + +@Getter +public class PingConnection implements ConnectionHolder, PacketListener { + private final IrisConnection connection = new IrisConnection(this); + private final CompletableFuture> version = new CompletableFuture<>(); + + public PingConnection(InetSocketAddress address) throws InterruptedException { + IrisConnection.connect(address, this); + Packets.PING.newPacket().send(connection); + } + + @Override + public void onPacket(Packet packet) throws Exception { + if (packet instanceof PingPacket p) { + version.complete(p.getVersion()); + connection.disconnect(); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/node/IrisServer.java b/core/src/main/java/com/volmit/iris/server/node/IrisServer.java new file mode 100644 index 000000000..fd4f647ec --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/node/IrisServer.java @@ -0,0 +1,68 @@ +package com.volmit.iris.server.node; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.util.ConnectionHolder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +import java.util.function.Supplier; +import java.util.logging.Logger; + +@Log(topic = "Iris-Server") +public class IrisServer implements AutoCloseable { + private final NioEventLoopGroup bossGroup, workerGroup; + private final Channel channel; + private @Getter boolean running = true; + + public IrisServer(int port) throws InterruptedException { + this("Iris-Server", port, IrisSession::new); + } + + protected IrisServer(String name, int port, Supplier factory) throws InterruptedException { + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(); + + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(name, LogLevel.DEBUG)) + .childHandler(new Initializer(factory)); + + channel = bootstrap.bind(port).sync().channel(); + + getLogger().info("Started on port " + port); + } + + @Override + public void close() throws Exception { + if (!running) return; + running = false; + channel.close().sync(); + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + + getLogger().info("Stopped"); + } + + protected Logger getLogger() { + return log; + } + + @RequiredArgsConstructor + private static class Initializer extends ChannelInitializer { + private final Supplier factory; + + @Override + protected void initChannel(Channel ch) { + IrisConnection.configureSerialization(ch, factory.get()); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/node/IrisSession.java b/core/src/main/java/com/volmit/iris/server/node/IrisSession.java new file mode 100644 index 000000000..c9242a514 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/node/IrisSession.java @@ -0,0 +1,113 @@ +package com.volmit.iris.server.node; + +import com.volmit.iris.core.nms.IHeadless; +import com.volmit.iris.core.nms.INMS; +import com.volmit.iris.engine.data.cache.Cache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.execption.RejectedException; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.packet.Packets; +import com.volmit.iris.server.packet.init.EnginePacket; +import com.volmit.iris.server.packet.init.FilePacket; +import com.volmit.iris.server.packet.init.PingPacket; +import com.volmit.iris.server.util.CPSLooper; +import com.volmit.iris.server.util.ConnectionHolder; +import com.volmit.iris.server.util.PacketListener; +import com.volmit.iris.server.packet.work.ChunkPacket; +import com.volmit.iris.server.packet.work.MantleChunkPacket; +import com.volmit.iris.server.packet.work.PregenPacket; +import com.volmit.iris.server.util.PregenHolder; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.io.IO; +import com.volmit.iris.util.parallel.MultiBurst; +import lombok.Getter; + +import java.io.File; +import java.util.UUID; + +public class IrisSession implements ConnectionHolder, PacketListener { + private static final MultiBurst burst = new MultiBurst("IrisHeadless", 8); + private final @Getter IrisConnection connection = new IrisConnection(this); + private final File base = new File("cache/" + UUID.randomUUID()); + private final KMap chunks = new KMap<>(); + private final KMap pregens = new KMap<>(); + private final CPSLooper cpsLooper = new CPSLooper("IrisSession-"+base.getName(), connection); + + private Engine engine; + private IHeadless headless; + + @Override + public void onPacket(Packet raw) throws Exception { + cpsLooper.setNodeCount(1); + if (raw instanceof FilePacket packet) { + if (engine != null) throw new RejectedException("Engine already setup"); + + packet.write(base); + Packets.DONE.newPacket() + .setId(packet.getId()) + .send(connection); + } else if (raw instanceof EnginePacket packet) { + if (engine != null) throw new RejectedException("Engine already setup"); + engine = packet.getEngine(base); + headless = INMS.get().createHeadless(engine); + headless.setSession(this); + + Packets.DONE.newPacket() + .setId(packet.getId()) + .send(connection); + } else if (raw instanceof MantleChunkPacket packet) { + if (engine == null) throw new RejectedException("Engine not setup"); + packet.set(engine.getMantle().getMantle()); + + var holder = chunks.get(packet.getPregenId()); + if (holder.remove(packet.getX(), packet.getZ())) { + headless.generateRegion(burst, holder.getX(), holder.getZ(), 20, null) + .thenRun(() -> { + holder.iterate(chunkPos -> { + var resp = Packets.MANTLE_CHUNK.newPacket(); + resp.setPregenId(holder.getId()); + resp.read(chunkPos, engine.getMantle().getMantle()); + connection.send(resp); + }); + chunks.remove(holder.getId()); + }); + } + } else if (raw instanceof PregenPacket packet) { + if (engine == null) throw new RejectedException("Engine not setup"); + var radius = engine.getMantle().getRadius(); + + var holder = new PregenHolder(packet, radius, true, null); + var request = Packets.MANTLE_CHUNK_REQUEST.newPacket() + .setPregenId(packet.getId()); + holder.iterate(request::add); + var pregen = new PregenHolder(packet, 0, true, null); + pregens.put(Cache.key(pregen.getX(), pregen.getZ()), pregen); + + chunks.put(packet.getId(), holder); + connection.send(request); + } else if (raw instanceof PingPacket packet) { + packet.setBukkit().send(connection); + } else throw new RejectedException("Unhandled packet: " + raw.getClass().getSimpleName()); + } + + public void completeChunk(int x, int z, byte[] data) { + cpsLooper.addChunks(1); + long id = Cache.key(x >> 5, z >> 5); + var pregen = pregens.get(id); + if (pregen.remove(x, z)) + pregens.remove(id); + connection.send(new ChunkPacket(pregen.getId(), x, z, data)); + } + + @Override + public void onDisconnect() { + if (engine != null) { + engine.close(); + engine = null; + headless = null; + } + cpsLooper.exit(); + IO.delete(base); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/Packet.java b/core/src/main/java/com/volmit/iris/server/packet/Packet.java new file mode 100644 index 000000000..103420093 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/Packet.java @@ -0,0 +1,24 @@ +package com.volmit.iris.server.packet; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.util.PacketSendListener; +import io.netty.buffer.ByteBuf; + +import java.io.IOException; + +public interface Packet { + void read(ByteBuf byteBuf) throws IOException; + void write(ByteBuf byteBuf) throws IOException; + + default Packets getType() { + return Packets.get(getClass()); + } + + default void send(IrisConnection connection) { + send(connection, null); + } + + default void send(IrisConnection connection, PacketSendListener listener) { + connection.send(this, listener); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/Packets.java b/core/src/main/java/com/volmit/iris/server/packet/Packets.java new file mode 100644 index 000000000..59f7efb29 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/Packets.java @@ -0,0 +1,94 @@ +package com.volmit.iris.server.packet; + +import com.volmit.iris.server.packet.init.EnginePacket; +import com.volmit.iris.server.packet.init.FilePacket; +import com.volmit.iris.server.packet.init.InfoPacket; +import com.volmit.iris.server.packet.init.PingPacket; +import com.volmit.iris.server.packet.work.DonePacket; +import com.volmit.iris.server.util.ErrorPacket; +import com.volmit.iris.server.packet.work.ChunkPacket; +import com.volmit.iris.server.packet.work.MantleChunkPacket; +import com.volmit.iris.server.packet.work.PregenPacket; +import lombok.AccessLevel; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.Supplier; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class Packets { + private static final List> REGISTRY; + private static final Map, Packets> MAP; + + public static final Packets ERROR; + public static final Packets INFO; + public static final Packets PING; + public static final Packets FILE; + public static final Packets ENGINE; + + public static final Packets DONE; + public static final Packets PREGEN; + public static final Packets CHUNK; + public static final Packets MANTLE_CHUNK; + public static final Packets MANTLE_CHUNK_REQUEST; + + private final Class type; + private final Supplier factory; + private int id = -1; + + public int getId() { + if (id == -1) throw new IllegalStateException("Unknown packet type: " + this); + return id; + } + + public T newPacket() { + return factory.get(); + } + + @NotNull + public static Packets get(int id) { + return REGISTRY.get(id); + } + + @NotNull + public static Packet newPacket(int id) { + return get(id).newPacket(); + } + + @NotNull + public static Packets get(Class type) { + var t = MAP.get(type); + if (t == null) throw new IllegalArgumentException("Unknown packet type: " + type); + return (Packets) t; + } + + public static int getId(Class type) { + return get(type).getId(); + } + + static { + ERROR = new Packets<>(ErrorPacket.class, ErrorPacket::new); + INFO = new Packets<>(InfoPacket.class, InfoPacket::new); + PING = new Packets<>(PingPacket.class, PingPacket::new); + FILE = new Packets<>(FilePacket.class, FilePacket::new); + ENGINE = new Packets<>(EnginePacket.class, EnginePacket::new); + + DONE = new Packets<>(DonePacket.class, DonePacket::new); + PREGEN = new Packets<>(PregenPacket.class, PregenPacket::new); + CHUNK = new Packets<>(ChunkPacket.class, ChunkPacket::new); + MANTLE_CHUNK = new Packets<>(MantleChunkPacket.class, MantleChunkPacket::new); + MANTLE_CHUNK_REQUEST = new Packets<>(MantleChunkPacket.Request.class, MantleChunkPacket.Request::new); + + REGISTRY = List.of(ERROR, INFO, PING, FILE, ENGINE, DONE, PREGEN, CHUNK, MANTLE_CHUNK, MANTLE_CHUNK_REQUEST); + + var map = new HashMap, Packets>(); + for (int i = 0; i < REGISTRY.size(); i++) { + var entry = REGISTRY.get(i); + entry.id = i; + map.put(entry.type, entry); + } + MAP = Collections.unmodifiableMap(map); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/handle/Decoder.java b/core/src/main/java/com/volmit/iris/server/packet/handle/Decoder.java new file mode 100644 index 000000000..a3cf54c85 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/handle/Decoder.java @@ -0,0 +1,17 @@ +package com.volmit.iris.server.packet.handle; + +import com.volmit.iris.server.packet.Packets; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +public class Decoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List list) throws Exception { + var packet = Packets.newPacket(byteBuf.readByte()); + packet.read(byteBuf); + list.add(packet); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/handle/Encoder.java b/core/src/main/java/com/volmit/iris/server/packet/handle/Encoder.java new file mode 100644 index 000000000..0ca3b11bf --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/handle/Encoder.java @@ -0,0 +1,14 @@ +package com.volmit.iris.server.packet.handle; + +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class Encoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf byteBuf) throws Exception { + byteBuf.writeByte(packet.getType().getId()); + packet.write(byteBuf); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/handle/Prepender.java b/core/src/main/java/com/volmit/iris/server/packet/handle/Prepender.java new file mode 100644 index 000000000..3c7af8bb1 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/handle/Prepender.java @@ -0,0 +1,14 @@ +package com.volmit.iris.server.packet.handle; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class Prepender extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception { + int i = in.readableBytes(); + out.writeInt(i); + out.writeBytes(in, in.readerIndex(), i); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/handle/Splitter.java b/core/src/main/java/com/volmit/iris/server/packet/handle/Splitter.java new file mode 100644 index 000000000..cdfd75582 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/handle/Splitter.java @@ -0,0 +1,33 @@ +package com.volmit.iris.server.packet.handle; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +public class Splitter extends ByteToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List list) throws Exception { + if (!byteBuf.isReadable(4)) + return; + byteBuf.markReaderIndex(); + + byte[] bytes = new byte[4]; + byteBuf.readBytes(bytes); + var buffer = Unpooled.wrappedBuffer(bytes); + try { + int j = buffer.readInt(); + if (byteBuf.readableBytes() >= j) { + list.add(byteBuf.readBytes(j)); + return; + } + + byteBuf.resetReaderIndex(); + } finally { + buffer.release(); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/init/EnginePacket.java b/core/src/main/java/com/volmit/iris/server/packet/init/EnginePacket.java new file mode 100644 index 000000000..8a6ba80f9 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/init/EnginePacket.java @@ -0,0 +1,60 @@ +package com.volmit.iris.server.packet.init; + +import com.volmit.iris.core.loader.IrisData; +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.IrisWorld; +import com.volmit.iris.server.util.ByteBufUtil; +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class EnginePacket implements Packet { + private UUID id = UUID.randomUUID(); + private String dimension; + private long seed; + private int radius; + + @Override + public void read(ByteBuf byteBuf) throws IOException { + id = new UUID(byteBuf.readLong(), byteBuf.readLong()); + dimension = ByteBufUtil.readString(byteBuf); + seed = byteBuf.readLong(); + radius = byteBuf.readInt(); + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(id.getMostSignificantBits()); + byteBuf.writeLong(id.getLeastSignificantBits()); + ByteBufUtil.writeString(byteBuf, dimension); + byteBuf.writeLong(seed); + byteBuf.writeInt(radius); + } + + public Engine getEngine(File base) { + var data = IrisData.get(new File(base, "iris/pack")); + var type = data.getDimensionLoader().load(dimension); + var world = IrisWorld.builder() + .name(base.getName()) + .seed(seed) + .worldFolder(base) + .minHeight(type.getMinHeight()) + .maxHeight(type.getMaxHeight()) + .environment(type.getEnvironment()) + .build(); + + return new IrisEngine(new EngineTarget(world, type, data), false); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/init/FilePacket.java b/core/src/main/java/com/volmit/iris/server/packet/init/FilePacket.java new file mode 100644 index 000000000..e43f5949a --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/init/FilePacket.java @@ -0,0 +1,56 @@ +package com.volmit.iris.server.packet.init; + +import com.volmit.iris.server.util.ByteBufUtil; +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.*; +import java.util.UUID; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class FilePacket implements Packet { + private UUID id = UUID.randomUUID(); + private String path; + private long offset; + private long length; + private byte[] data; + + @Override + public void read(ByteBuf byteBuf) throws IOException { + id = new UUID(byteBuf.readLong(), byteBuf.readLong()); + path = ByteBufUtil.readString(byteBuf); + offset = byteBuf.readLong(); + length = byteBuf.readLong(); + data = ByteBufUtil.readBytes(byteBuf); + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(id.getMostSignificantBits()); + byteBuf.writeLong(id.getLeastSignificantBits()); + ByteBufUtil.writeString(byteBuf, path); + byteBuf.writeLong(offset); + byteBuf.writeLong(length); + ByteBufUtil.writeBytes(byteBuf, data); + } + + public void write(File base) throws IOException { + File f = new File(base, path); + if (!f.getAbsolutePath().startsWith(base.getAbsolutePath())) + throw new IOException("Invalid path " + path); + if (!f.getParentFile().exists() && !f.getParentFile().mkdirs()) + throw new IOException("Failed to create directory " + f.getParentFile()); + + try (var raf = new RandomAccessFile(f, "rws")) { + if (raf.length() < length) + raf.setLength(length); + raf.seek(offset); + raf.write(data); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/init/InfoPacket.java b/core/src/main/java/com/volmit/iris/server/packet/init/InfoPacket.java new file mode 100644 index 000000000..cd688d9e0 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/init/InfoPacket.java @@ -0,0 +1,33 @@ +package com.volmit.iris.server.packet.init; + +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.IOException; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class InfoPacket implements Packet { + private int nodeCount = -1; + private int cps = -1; + private int generated = -1; + + @Override + public void read(ByteBuf byteBuf) throws IOException { + nodeCount = byteBuf.readInt(); + cps = byteBuf.readInt(); + generated = byteBuf.readInt(); + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeInt(nodeCount); + byteBuf.writeInt(cps); + byteBuf.writeInt(generated); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/init/PingPacket.java b/core/src/main/java/com/volmit/iris/server/packet/init/PingPacket.java new file mode 100644 index 000000000..8921547fe --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/init/PingPacket.java @@ -0,0 +1,58 @@ +package com.volmit.iris.server.packet.init; + +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.util.ByteBufUtil; +import com.volmit.iris.util.collection.KList; +import io.netty.buffer.ByteBuf; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Bukkit; + +import java.io.IOException; +import java.util.UUID; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class PingPacket implements Packet { + private UUID id = UUID.randomUUID(); + private KList version = new KList<>(); + + @Override + public void read(ByteBuf byteBuf) throws IOException { + id = new UUID(byteBuf.readLong(), byteBuf.readLong()); + int size = byteBuf.readInt(); + + version = new KList<>(); + for (int i = 0; i < size; i++) { + version.add(ByteBufUtil.readString(byteBuf)); + } + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(id.getMostSignificantBits()); + byteBuf.writeLong(id.getLeastSignificantBits()); + + byteBuf.writeInt(version.size()); + for (String s : version) { + ByteBufUtil.writeString(byteBuf, s); + } + } + + public PingPacket setBukkit() { + this.version = new KList<>(Bukkit.getBukkitVersion().split("-")[0]); + return this; + } + + public PingPacket setVersion(String version) { + this.version = new KList<>(version); + return this; + } + + public PingPacket setVersion(KList version) { + this.version = version; + return this; + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/work/ChunkPacket.java b/core/src/main/java/com/volmit/iris/server/packet/work/ChunkPacket.java new file mode 100644 index 000000000..fdef5d0c4 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/work/ChunkPacket.java @@ -0,0 +1,37 @@ +package com.volmit.iris.server.packet.work; + +import com.volmit.iris.server.util.ByteBufUtil; +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.IOException; +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ChunkPacket implements Packet { + private UUID pregenId; + private int x, z; + private byte[] data; + + @Override + public void read(ByteBuf byteBuf) throws IOException { + pregenId = new UUID(byteBuf.readLong(), byteBuf.readLong()); + x = byteBuf.readInt(); + z = byteBuf.readInt(); + data = ByteBufUtil.readBytes(byteBuf); + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(pregenId.getMostSignificantBits()); + byteBuf.writeLong(pregenId.getLeastSignificantBits()); + byteBuf.writeInt(x); + byteBuf.writeInt(z); + ByteBufUtil.writeBytes(byteBuf, data); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/work/DonePacket.java b/core/src/main/java/com/volmit/iris/server/packet/work/DonePacket.java new file mode 100644 index 000000000..f07f6fc4a --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/work/DonePacket.java @@ -0,0 +1,28 @@ +package com.volmit.iris.server.packet.work; + +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.IOException; +import java.util.UUID; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class DonePacket implements Packet { + private UUID id = UUID.randomUUID(); + + @Override + public void read(ByteBuf byteBuf) throws IOException { + id = new UUID(byteBuf.readLong(), byteBuf.readLong()); + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(id.getMostSignificantBits()); + byteBuf.writeLong(id.getLeastSignificantBits()); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/work/MantleChunkPacket.java b/core/src/main/java/com/volmit/iris/server/packet/work/MantleChunkPacket.java new file mode 100644 index 000000000..e92b482d6 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/work/MantleChunkPacket.java @@ -0,0 +1,103 @@ +package com.volmit.iris.server.packet.work; + +import com.volmit.iris.server.util.ByteBufUtil; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.documentation.ChunkCoordinates; +import com.volmit.iris.util.mantle.Mantle; +import com.volmit.iris.util.mantle.MantleChunk; +import com.volmit.iris.util.math.Position2; +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import net.jpountz.lz4.LZ4BlockInputStream; +import net.jpountz.lz4.LZ4BlockOutputStream; + +import java.io.*; +import java.util.UUID; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class MantleChunkPacket implements Packet { + private UUID pregenId; + private int x, z; + private MantleChunk chunk; + + @Override + public void read(ByteBuf byteBuf) throws IOException { + pregenId = new UUID(byteBuf.readLong(), byteBuf.readLong()); + x = byteBuf.readInt(); + z = byteBuf.readInt(); + int sectionHeight = byteBuf.readInt(); + try (var din = new DataInputStream(new BufferedInputStream(new LZ4BlockInputStream(new ByteArrayInputStream(ByteBufUtil.readBytes(byteBuf)))))) { + chunk = new MantleChunk(sectionHeight, din); + } catch (ClassNotFoundException e) { + throw new IOException("Failed to read chunk", e); + } + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(pregenId.getMostSignificantBits()); + byteBuf.writeLong(pregenId.getLeastSignificantBits()); + byteBuf.writeInt(x); + byteBuf.writeInt(z); + byteBuf.writeInt(chunk.getSectionHeight()); + var out = new ByteArrayOutputStream(); + try (var dos = new DataOutputStream(new LZ4BlockOutputStream(out))) { + chunk.write(dos); + } + ByteBufUtil.writeBytes(byteBuf, out.toByteArray()); + } + + @ChunkCoordinates + public MantleChunkPacket read(Position2 pos, Mantle mantle) { + this.x = pos.getX(); + this.z = pos.getZ(); + this.chunk = mantle.getChunk(x, z); + return this; + } + + public void set(Mantle mantle) { + mantle.setChunk(x, z, chunk); + } + + @Data + @Accessors(chain = true) + @NoArgsConstructor + public static class Request implements Packet { + private UUID pregenId; + private KList positions = new KList<>(); + + @Override + public void read(ByteBuf byteBuf) throws IOException { + pregenId = new UUID(byteBuf.readLong(), byteBuf.readLong()); + var count = byteBuf.readInt(); + positions = new KList<>(count); + for (int i = 0; i < count; i++) { + positions.add(new Position2(byteBuf.readInt(), byteBuf.readInt())); + } + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(pregenId.getMostSignificantBits()); + byteBuf.writeLong(pregenId.getLeastSignificantBits()); + byteBuf.writeInt(positions.size()); + for (Position2 p : positions) { + byteBuf.writeInt(p.getX()); + byteBuf.writeInt(p.getZ()); + } + } + + @ChunkCoordinates + public void add(Position2 chunkPos) { + if (positions == null) + positions = new KList<>(); + positions.add(chunkPos); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/packet/work/PregenPacket.java b/core/src/main/java/com/volmit/iris/server/packet/work/PregenPacket.java new file mode 100644 index 000000000..d8f7fc241 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/packet/work/PregenPacket.java @@ -0,0 +1,35 @@ +package com.volmit.iris.server.packet.work; + +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.IOException; +import java.util.UUID; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +public class PregenPacket implements Packet { + private UUID id = UUID.randomUUID(); + private int x, z; + + @Override + public void read(ByteBuf byteBuf) throws IOException { + id = new UUID(byteBuf.readLong(), byteBuf.readLong()); + x = byteBuf.readInt(); + z = byteBuf.readInt(); + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + byteBuf.writeLong(id.getMostSignificantBits()); + byteBuf.writeLong(id.getLeastSignificantBits()); + byteBuf.writeInt(x); + byteBuf.writeInt(z); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/pregen/CloudMethod.java b/core/src/main/java/com/volmit/iris/server/pregen/CloudMethod.java new file mode 100644 index 000000000..54134a588 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/pregen/CloudMethod.java @@ -0,0 +1,284 @@ +package com.volmit.iris.server.pregen; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.IrisSettings; +import com.volmit.iris.core.gui.PregeneratorJob; +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.server.IrisConnection; +import com.volmit.iris.server.execption.RejectedException; +import com.volmit.iris.server.packet.Packet; +import com.volmit.iris.server.packet.Packets; +import com.volmit.iris.server.packet.init.InfoPacket; +import com.volmit.iris.server.packet.init.PingPacket; +import com.volmit.iris.server.packet.work.ChunkPacket; +import com.volmit.iris.server.packet.work.DonePacket; +import com.volmit.iris.server.packet.work.MantleChunkPacket; +import com.volmit.iris.server.util.*; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.mantle.Mantle; +import com.volmit.iris.util.parallel.MultiBurst; +import lombok.Getter; +import lombok.extern.java.Log; +import org.bukkit.World; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.logging.Level; + +@Log(topic = "CloudPregen") +public class CloudMethod implements PregeneratorMethod, ConnectionHolder, PacketListener { + private final @Getter IrisConnection connection = new IrisConnection(this); + private final Engine engine; + private final IHeadless headless; + private final KMap holders = new KMap<>(); + private final CompletableFuture future = new CompletableFuture<>(); + private final KMap> locks = new KMap<>(); + + public CloudMethod(String address, Engine engine) throws InterruptedException { + var split = address.split(":"); + if (split.length != 2 || !split[1].matches("\\d+")) + throw new IllegalArgumentException("Invalid remote server address: " + address); + + IrisConnection.connect(new InetSocketAddress(split[0], Integer.parseInt(split[1])), this); + + this.engine = engine; + this.headless = INMS.get().createHeadless(engine); + } + + @Override + public void init() { + var studio = engine.isStudio(); + var base = studio ? + engine.getData().getDataFolder() : + engine.getWorld().worldFolder(); + var name = engine.getWorld().name(); + var exit = new AtomicBoolean(false); + var limited = new LimitedSemaphore(IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())); + + var remoteVersion = new CompletableFuture<>(); + var ping = Packets.PING.newPacket() + .setBukkit(); + locks.put(ping.getId(), remoteVersion); + ping.send(connection); + + try { + var o = remoteVersion.get(); + if (!(o instanceof PingPacket packet)) + throw new IllegalStateException("Invalid response from remote server"); + if (!packet.getVersion().contains(ping.getVersion().get(0))) + throw new IllegalStateException("Remote server version does not match"); + } catch (Throwable e) { + connection.disconnect(); + throw new IllegalStateException("Failed to connect to remote server", e); + } + + + log.info(name + ": Uploading pack..."); + iterate(engine.getData().getDataFolder(), f -> { + if (exit.get()) return; + + try { + limited.acquire(); + } catch (InterruptedException e) { + exit.set(true); + return; + } + + MultiBurst.burst.complete(() -> { + try { + upload(exit, base, f, studio, 8192); + } finally { + limited.release(); + } + }); + }); + + try { + limited.acquireAll(); + } catch (InterruptedException ignored) {} + + log.info(name + ": Done uploading pack"); + log.info(name + ": Initializing Engine..."); + var future = new CompletableFuture<>(); + var packet = Packets.ENGINE.newPacket() + .setDimension(engine.getDimension().getLoadKey()) + .setSeed(engine.getWorld().getRawWorldSeed()) + .setRadius(engine.getMantle().getRadius()); + + locks.put(packet.getId(), future); + packet.send(connection); + + try { + future.get(); + } catch (Throwable ignored) {} + log.info(name + ": Done initializing Engine"); + } + + private void upload(AtomicBoolean exit, File base, File f, boolean studio, int packetSize) { + if (exit.get() || (!studio && !f.getAbsolutePath().startsWith(base.getAbsolutePath()))) + return; + + String path = studio ? "iris/pack/" : ""; + path += f.getAbsolutePath().substring(base.getAbsolutePath().length() + 1); + + try (FileInputStream in = new FileInputStream(f)) { + long offset = 0; + byte[] data; + while ((data = in.readNBytes(packetSize)).length > 0 && !exit.get()) { + var future = new CompletableFuture<>(); + var packet = Packets.FILE.newPacket() + .setPath(path) + .setOffset(offset) + .setLength(f.length()) + .setData(data); + + locks.put(packet.getId(), future); + packet.send(connection); + future.get(); + + offset += data.length; + } + } catch (IOException | ExecutionException | InterruptedException e) { + Iris.error("Failed to upload " + f); + e.printStackTrace(); + exit.set(true); + } + } + + private void iterate(File file, Consumer consumer) { + var queue = new ArrayDeque(); + queue.add(file); + + while (!queue.isEmpty()) { + var f = queue.remove(); + if (f.isFile()) + consumer.accept(f); + if (f.isDirectory()) { + var files = f.listFiles(); + if (files == null) continue; + queue.addAll(Arrays.asList(files)); + } + } + } + + @Override + public void close() { + close0(); + connection.disconnect(); + } + + @Override + public void save() {} + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return true; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + var semaphore = future.join(); + try { + semaphore.acquire(); + } catch (InterruptedException e) { + semaphore.release(); + return; + } + + var p = Packets.PREGEN.newPacket() + .setX(x) + .setZ(z); + new PregenHolder(p, engine.getMantle().getRadius(), true, listener) + .put(holders); + p.send(connection); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) {} + + @Override + public String getMethod(int x, int z) { + return "Cloud"; + } + + @Override + public Mantle getMantle() { + return engine.getMantle().getMantle(); + } + + @Override + public World getWorld() { + return engine.getWorld().realWorld(); + } + + @Override + public void onPacket(Packet raw) throws Exception { + if (raw instanceof ChunkPacket packet) { + headless.addChunk(packet); + holders.get(packet.getPregenId()) + .getListener() + .onChunkGenerated(packet.getX(), packet.getZ()); + } else if (raw instanceof MantleChunkPacket packet) { + if (holders.get(packet.getPregenId()) + .remove(packet.getX(), packet.getZ())) { + future.join().release(); + } + packet.set(getMantle()); + } else if (raw instanceof MantleChunkPacket.Request packet) { + var mantle = getMantle(); + for (var chunk : packet.getPositions()) { + Packets.MANTLE_CHUNK.newPacket() + .setPregenId(packet.getPregenId()) + .read(chunk, mantle) + .send(connection); + } + } else if (raw instanceof InfoPacket packet) { + if (packet.getNodeCount() > 0 && !future.isDone()) + future.complete(new LimitedSemaphore(packet.getNodeCount())); + //if (packet.getCps() >= 0) + // Iris.info("Cloud CPS: " + packet.getCps()); + } else if (raw instanceof DonePacket packet) { + locks.remove(packet.getId()).complete(null); + } else if (raw instanceof PingPacket packet) { + locks.remove(packet.getId()).complete(packet); + } else if (raw instanceof ErrorPacket packet) { + packet.log(log, Level.SEVERE); + } else throw new RejectedException("Unhandled packet: " + raw.getClass().getSimpleName()); + } + + @Override + public void onDisconnect() { + try { + if (!future.isDone()) + future.cancel(false); + } catch (Throwable ignored) {} + PregeneratorJob.shutdownInstance(); + } + + private void close0() { + if (!future.isCancelled()) { + try { + future.join().acquireAll(); + } catch (InterruptedException ignored) {} + } + + try { + headless.close(); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to close headless", e); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/pregen/CloudTask.java b/core/src/main/java/com/volmit/iris/server/pregen/CloudTask.java new file mode 100644 index 000000000..18a3895cc --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/pregen/CloudTask.java @@ -0,0 +1,70 @@ +package com.volmit.iris.server.pregen; + +import com.volmit.iris.core.pregenerator.PregenTask; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.math.Position2; +import com.volmit.iris.util.math.Spiraled; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Comparator; +import java.util.Map; + +@ToString +@Builder(builderMethodName = "couldBuilder") +@EqualsAndHashCode(callSuper = true) +public class CloudTask extends PregenTask { + @Builder.Default + private boolean resetCache = false; + @Builder.Default + private boolean gui = false; + @Builder.Default + private Position2 center = new Position2(0, 0); + @Builder.Default + private int width = 1; + @Builder.Default + private int height = 1; + private int distance; + + private CloudTask(boolean resetCache, boolean gui, Position2 center, int width, int height, int distance) { + super(resetCache, gui, center, width, height); + this.resetCache = resetCache; + this.gui = gui; + this.center = center; + this.width = width; + this.height = height; + + int d = distance & 31; + if (d > 0) d = 32 - d; + this.distance = 32 + d + distance >> 5; + } + + @Override + public void iterateRegions(Spiraled s) { + var c = Comparator.comparingInt(DPos2::distance); + for (int oX = 0; oX < distance; oX++) { + for (int oZ = 0; oZ < distance; oZ++) { + var p = new KList(); + for (int x = -width; x <= width - oX; x+=distance) { + for (int z = -height; z <= height - oZ; z+=distance) { + s.on(x + oX, z + oZ); + //p.add(new DPos2(x + oX, z + oZ)); + } + } + p.sort(c); + p.forEach(i -> i.on(s)); + } + } + } + + private record DPos2(int x, int z, int distance) { + private DPos2(int x, int z) { + this(x, z, x * x + z * z); + } + + public void on(Spiraled s) { + s.on(x, z); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/ByteBufUtil.java b/core/src/main/java/com/volmit/iris/server/util/ByteBufUtil.java new file mode 100644 index 000000000..22c75db0f --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/ByteBufUtil.java @@ -0,0 +1,27 @@ +package com.volmit.iris.server.util; + +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; + +public class ByteBufUtil { + + public static String readString(ByteBuf byteBuf) { + return new String(readBytes(byteBuf), StandardCharsets.UTF_8); + } + + public static void writeString(ByteBuf byteBuf, String s) { + writeBytes(byteBuf, s.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] readBytes(ByteBuf byteBuf) { + byte[] bytes = new byte[byteBuf.readInt()]; + byteBuf.readBytes(bytes); + return bytes; + } + + public static void writeBytes(ByteBuf byteBuf, byte[] bytes) { + byteBuf.writeInt(bytes.length); + byteBuf.writeBytes(bytes); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/CPSLooper.java b/core/src/main/java/com/volmit/iris/server/util/CPSLooper.java new file mode 100644 index 000000000..a5f56ee24 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/CPSLooper.java @@ -0,0 +1,67 @@ +package com.volmit.iris.server.util; + +import com.volmit.iris.server.IrisConnection; +import com.volmit.iris.server.packet.Packets; +import com.volmit.iris.util.math.M; +import com.volmit.iris.util.math.RollingSequence; +import com.volmit.iris.util.scheduling.Looper; +import lombok.Setter; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +public class CPSLooper extends Looper { + private final RollingSequence chunksPerSecond = new RollingSequence(10); + private final AtomicInteger generated = new AtomicInteger(); + private final AtomicInteger generatedLast = new AtomicInteger(); + private final AtomicBoolean running = new AtomicBoolean(true); + private final IrisConnection connection; + private int nodeCount = 0; + + public CPSLooper(String name, IrisConnection connection) { + this.connection = connection; + setName(name); + setPriority(Thread.MAX_PRIORITY); + start(); + } + + public void addChunks(int count) { + generated.addAndGet(count); + } + + public void exit() { + running.set(false); + } + + public synchronized void setNodeCount(int count) { + if (nodeCount != 0 || count < 1) + return; + nodeCount = count; + Packets.INFO.newPacket() + .setNodeCount(nodeCount) + .send(connection); + } + + @Override + protected long loop() { + if (!running.get()) + return -1; + long t = M.ms(); + + int secondGenerated = generated.get() - generatedLast.get(); + generatedLast.set(generated.get()); + chunksPerSecond.put(secondGenerated); + + if (secondGenerated > 0 && nodeCount > 0) { + Packets.INFO.newPacket() + .setNodeCount(nodeCount) + .setCps((int) Math.round(chunksPerSecond.getAverage())) + .setGenerated(secondGenerated) + .send(connection); + } + + return Math.max(5000 - (M.ms() - t), 0); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/ConnectionHolder.java b/core/src/main/java/com/volmit/iris/server/util/ConnectionHolder.java new file mode 100644 index 000000000..acb40d465 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/ConnectionHolder.java @@ -0,0 +1,8 @@ +package com.volmit.iris.server.util; + +import com.volmit.iris.server.IrisConnection; + +public interface ConnectionHolder { + + IrisConnection getConnection(); +} diff --git a/core/src/main/java/com/volmit/iris/server/util/ErrorPacket.java b/core/src/main/java/com/volmit/iris/server/util/ErrorPacket.java new file mode 100644 index 000000000..4236246fd --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/ErrorPacket.java @@ -0,0 +1,54 @@ +package com.volmit.iris.server.util; + +import com.volmit.iris.server.packet.Packet; +import io.netty.buffer.ByteBuf; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Data +@NoArgsConstructor +public class ErrorPacket implements Packet { + private String message; + private String stackTrace; + + public ErrorPacket(String message) { + this.message = message; + } + + public ErrorPacket(String message, Throwable cause) { + this.message = message; + StringWriter writer = new StringWriter(); + cause.printStackTrace(new PrintWriter(writer)); + stackTrace = writer.toString(); + } + + @Override + public void read(ByteBuf byteBuf) throws IOException { + message = ByteBufUtil.readString(byteBuf); + if (byteBuf.readBoolean()) { + stackTrace = ByteBufUtil.readString(byteBuf); + } + } + + @Override + public void write(ByteBuf byteBuf) throws IOException { + ByteBufUtil.writeString(byteBuf, message); + byteBuf.writeBoolean(stackTrace != null); + if (stackTrace != null) { + ByteBufUtil.writeString(byteBuf, stackTrace); + } + } + + public void log(Logger logger, Level level) { + if (stackTrace == null) { + logger.log(level, message); + return; + } + logger.log(level, message + "\n" + stackTrace); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/LimitedSemaphore.java b/core/src/main/java/com/volmit/iris/server/util/LimitedSemaphore.java new file mode 100644 index 000000000..ecca55dc4 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/LimitedSemaphore.java @@ -0,0 +1,41 @@ +package com.volmit.iris.server.util; + +import lombok.Getter; + +import java.util.concurrent.Semaphore; + +@Getter +public class LimitedSemaphore extends Semaphore { + private final int permits; + + public LimitedSemaphore(int permits) { + super(permits); + this.permits = permits; + } + + public void runBlocking(Runnable runnable) throws InterruptedException { + try { + acquire(); + runnable.run(); + } finally { + release(); + } + } + + public void runAllBlocking(Runnable runnable) throws InterruptedException { + try { + acquireAll(); + runnable.run(); + } finally { + releaseAll(); + } + } + + public void acquireAll() throws InterruptedException { + acquire(permits); + } + + public void releaseAll() { + release(permits); + } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/PacketListener.java b/core/src/main/java/com/volmit/iris/server/util/PacketListener.java new file mode 100644 index 000000000..2a0923a06 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/PacketListener.java @@ -0,0 +1,14 @@ +package com.volmit.iris.server.util; + +import com.volmit.iris.server.packet.Packet; + +public interface PacketListener { + + void onPacket(Packet packet) throws Exception; + + default void onDisconnect() {} + + default boolean isAccepting() { + return true; + } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/PacketSendListener.java b/core/src/main/java/com/volmit/iris/server/util/PacketSendListener.java new file mode 100644 index 000000000..000b0790e --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/PacketSendListener.java @@ -0,0 +1,26 @@ +package com.volmit.iris.server.util; + +import com.volmit.iris.server.packet.Packet; + +import javax.annotation.Nullable; + +public interface PacketSendListener { + static PacketSendListener thenRun(Runnable runnable) { + return new PacketSendListener() { + public void onSuccess() { + runnable.run(); + } + + @Nullable + public Packet onFailure() { + runnable.run(); + return null; + } + }; + } + + default void onSuccess() {} + + @Nullable + default Packet onFailure() { return null; } +} diff --git a/core/src/main/java/com/volmit/iris/server/util/PregenHolder.java b/core/src/main/java/com/volmit/iris/server/util/PregenHolder.java new file mode 100644 index 000000000..76b1cabb3 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/server/util/PregenHolder.java @@ -0,0 +1,62 @@ +package com.volmit.iris.server.util; + +import com.volmit.iris.core.pregenerator.PregenListener; +import com.volmit.iris.server.packet.work.PregenPacket; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.math.Position2; +import lombok.AccessLevel; +import lombok.Getter; + +import java.util.UUID; +import java.util.function.Consumer; + +@Getter +public class PregenHolder { + private final UUID id; + private final int x, z, r; + @Getter(AccessLevel.NONE) + private final KList chunks = new KList<>(); + private final PregenListener listener; + + public PregenHolder(PregenPacket packet, int r, boolean fill, PregenListener listener) { + this.id = packet.getId(); + this.x = packet.getX(); + this.z = packet.getZ(); + this.r = r; + this.listener = listener; + + if (fill) + iterate(chunks::add); + } + + public void put(KMap holders) { + holders.put(id, this); + } + + public synchronized boolean remove(int x, int z) { + chunks.remove(new Position2(x, z)); + boolean b = chunks.isEmpty(); + if (b && listener != null) listener.onRegionGenerated(x, z); + return b; + } + + public void iterate(Consumer consumer) { + int cX = x << 5; + int cZ = z << 5; + for (int x = -r; x <= r; x++) { + for (int z = -r; z <= r; z++) { + if (x == 0 && z == 0) { + for (int xx = 0; xx < 32; xx++) { + for (int zz = 0; zz < 32; zz++) { + consumer.accept(new Position2(x + cX + xx, z + cZ + zz)); + } + } + continue; + } + + consumer.accept(new Position2(x + cX + x < 0 ? 0 : 32, z + cZ + z < 0 ? 0 : 32)); + } + } + } +} diff --git a/core/src/main/java/com/volmit/iris/util/data/Cuboid.java b/core/src/main/java/com/volmit/iris/util/data/Cuboid.java index b4e0172a8..8f5d5afd3 100644 --- a/core/src/main/java/com/volmit/iris/util/data/Cuboid.java +++ b/core/src/main/java/com/volmit/iris/util/data/Cuboid.java @@ -20,6 +20,7 @@ package com.volmit.iris.util.data; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.math.Direction; +import com.volmit.iris.util.math.Position2; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.configuration.serialization.ConfigurationSerializable; @@ -651,6 +652,10 @@ public class Cuboid implements Iterable, Cloneable, ConfigurationSerializ return new CuboidIterator(getWorld(), x1, y1, z1, x2, y2, z2); } + public Iterator chunkedIterator() { + return new ChunkedCuboidIterator(getWorld(), x1, y1, z1, x2, y2, z2); + } + /* * (non-Javadoc) * @@ -746,4 +751,82 @@ public class Cuboid implements Iterable, Cloneable, ConfigurationSerializ } } + public static class ChunkedCuboidIterator implements Iterator { + private final World w; + private final int minRX, minY, minRZ, maxRX, maxY, maxRZ; + private final int minCX, minCZ, maxCX, maxCZ; + private int mX, mZ, bX, rX, rZ, y; + + private Position2 chunk; + private int cX, cZ; + + public ChunkedCuboidIterator(World w, int x1, int y1, int z1, int x2, int y2, int z2) { + this.w = w; + minY = Math.min(y1, y2); + maxY = Math.max(y1, y2); + int minX = Math.min(x1, x2); + int minZ = Math.min(z1, z2); + int maxX = Math.max(x1, x2); + int maxZ = Math.max(z1, z2); + minRX = minX & 15; + minRZ = minZ & 15; + maxRX = maxX & 15; + maxRZ = maxZ & 15; + + minCX = minX >> 4; + minCZ = minZ >> 4; + maxCX = maxX >> 4; + maxCZ = maxZ >> 4; + cX = minCX; + cZ = minCZ; + + rX = minX & 15; + rZ = minZ & 15; + y = minY; + } + + @Override + public boolean hasNext() { + return chunk != null || hasNextChunk(); + } + + public boolean hasNextChunk() { + return cX <= maxCX && cZ <= maxCZ; + } + + @Override + public Block next() { + if (chunk == null) { + chunk = new Position2(cX, cZ); + if (++cX > maxCX) { + cX = minCX; + cZ++; + } + + mX = chunk.getX() == maxCX ? maxRX : 15; + mZ = chunk.getZ() == maxCZ ? maxRZ : 15; + rX = bX = chunk.getX() == minCX ? minRX : 0; + rZ = chunk.getZ() == minCZ ? minRZ : 0; + } + + var b = w.getBlockAt((chunk.getX() << 4) + rX, y, (chunk.getZ() << 4) + rZ); + if (++y >= maxY) { + y = minY; + if (++rX > mX) { + if (++rZ > mZ) { + chunk = null; + return b; + } + rX = bX; + } + } + + return b; + } + + @Override + public void remove() { + // nop + } + } } \ No newline at end of file diff --git a/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java b/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java index e4dcbe650..4f607dfb5 100644 --- a/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java +++ b/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java @@ -195,6 +195,11 @@ public class Mantle { return get(x >> 5, z >> 5).getOrCreate(x & 31, z & 31); } + @ChunkCoordinates + public void setChunk(int x, int z, MantleChunk chunk) { + get(x >> 5, z >> 5).set(x & 31, z & 31, chunk); + } + /** * Flag or unflag a chunk * diff --git a/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java b/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java index 38278b762..d00d38f3b 100644 --- a/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java +++ b/core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java @@ -100,6 +100,10 @@ public class MantleChunk { return flags.get(flag.ordinal()) == 1; } + public int getSectionHeight() { + return sections.length(); + } + /** * Check if a section exists (same as get(section) != null) * diff --git a/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java b/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java index a32645115..9212aaae2 100644 --- a/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java +++ b/core/src/main/java/com/volmit/iris/util/mantle/TectonicPlate.java @@ -158,6 +158,20 @@ public class TectonicPlate { return chunk; } + /** + * Set a tectonic plate + * + * @param x the chunk relative x (0-31) + * @param z the chunk relative z (0-31) + * @param chunk the chunk + */ + @ChunkCoordinates + public void set(int x, int z, MantleChunk chunk) { + if (x != chunk.getX() || z != chunk.getZ()) + throw new IllegalArgumentException("X/Z of chunk must match the plate"); + chunks.set(index(x, z), chunk); + } + @ChunkCoordinates private int index(int x, int z) { return Cache.to1D(x, z, 0, 32, 32); diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index bca7e4638..3ebee0f20 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -24,6 +24,7 @@ libraries: - rhino:js:1.7R2 - bsf:bsf:2.4.0 - org.lz4:lz4-java:1.8.0 + - io.netty:netty-all:4.1.112.Final commands: iris: aliases: [ ir, irs ] 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 index d171aa022..f0c5ee5aa 100644 --- 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 @@ -30,6 +30,8 @@ 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.server.node.IrisSession; +import com.volmit.iris.server.packet.work.ChunkPacket; import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.context.ChunkContext; @@ -61,6 +63,7 @@ import net.minecraft.world.level.chunk.storage.RegionFile; import org.bukkit.Material; import org.bukkit.block.data.BlockData; +import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; @@ -82,6 +85,8 @@ public class Headless implements IHeadless, LevelHeightAccessor { private final RNG BIOME_RNG; private final @Getter int minBuildHeight; private final @Getter int height; + private IrisSession session; + private CompletingThread regionThread; private boolean closed = false; public Headless(NMSBinding binding, Engine engine) { @@ -127,6 +132,12 @@ public class Headless implements IHeadless, LevelHeightAccessor { cleaner.start(); } + public void setSession(IrisSession session) { + if (this.session != null) + throw new IllegalStateException("Session already set"); + this.session = session; + } + @Override public int getLoadedChunks() { return loadedChunks.get(); @@ -153,21 +164,42 @@ public class Headless implements IHeadless, LevelHeightAccessor { } @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); + 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()) + throw new IllegalStateException("Region generation already in progress"); + + regionThread = new CompletingThread(() -> { + boolean listening = listener != null; + Semaphore semaphore = new Semaphore(maxConcurrent); + CountDownLatch latch = new CountDownLatch(1024); + + iterateRegion(x, z, pos -> { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + semaphore.release(); + return; + } + + burst.complete(() -> { + try { + if (listening) listener.onChunkGenerating(pos.x, pos.z); + generateChunk(pos.x, pos.z); + if (listening) listener.onChunkGenerated(pos.x, pos.z); + } finally { + semaphore.release(); + latch.countDown(); + } + }); + }); + try { + latch.await(); + } catch (InterruptedException ignored) {} + if (listening) listener.onRegionGenerated(x, z); + }, "Region Generator - " + x + "," + z, Thread.MAX_PRIORITY); + + return regionThread.future; } @RegionCoordinates @@ -199,6 +231,12 @@ public class Headless implements IHeadless, LevelHeightAccessor { inject(engine, chunk, ctx); chunk.setStatus(ChunkStatus.FULL); + if (session != null) { + session.completeChunk(x, z, write(chunk)); + loadedChunks.decrementAndGet(); + return; + } + long key = Cache.key(pos.getRegionX(), pos.getRegionZ()); regions.computeIfAbsent(key, Region::new) .add(chunk); @@ -209,6 +247,16 @@ public class Headless implements IHeadless, LevelHeightAccessor { } } + @Override + public void addChunk(ChunkPacket packet) { + if (closed) return; + if (session != null) throw new IllegalStateException("Headless running as Server"); + var pos = new ChunkPos(packet.getX(), packet.getZ()); + regions.computeIfAbsent(Cache.key(pos.getRegionX(), pos.getRegionZ()), Region::new) + .add(packet); + loadedChunks.incrementAndGet(); + } + @BlockCoordinates private ChunkContext generate(Engine engine, int x, int z, Hunk vblocks, Hunk vbiomes) throws WrongEngineBroException { if (engine.isClosed()) { @@ -284,6 +332,11 @@ public class Headless implements IHeadless, LevelHeightAccessor { public void close() throws IOException { if (closed) return; try { + if (regionThread != null) { + regionThread.future.join(); + regionThread = null; + } + regions.values().forEach(Region::submit); Iris.info("Waiting for " + loadedChunks.get() + " chunks to unload..."); while (loadedChunks.get() > 0 || !regions.isEmpty()) @@ -304,6 +357,7 @@ public class Headless implements IHeadless, LevelHeightAccessor { private final int x, z; private final long key; private final KList chunks = new KList<>(1024); + private final KList remoteChunks = new KList<>(1024); private final AtomicBoolean full = new AtomicBoolean(); private long lastEntry = M.ms(); @@ -334,13 +388,31 @@ public class Headless implements IHeadless, LevelHeightAccessor { } loadedChunks.decrementAndGet(); } + for (var chunk : remoteChunks) { + var pos = new ChunkPos(chunk.getX(), chunk.getZ()); + try (DataOutputStream dos = regionFile.getChunkDataOutputStream(pos)) { + dos.write(chunk.getData()); + } catch (Throwable e) { + Iris.error("Failed to save remote chunk " + pos.x + ", " + pos.z); + e.printStackTrace(); + } + loadedChunks.decrementAndGet(); + } regions.remove(key); } public synchronized void add(ProtoChunk chunk) { chunks.add(chunk); lastEntry = M.ms(); - if (chunks.size() < 1024) + if (chunks.size() + remoteChunks.size() < 1024) + return; + submit(); + } + + public synchronized void add(ChunkPacket packet) { + remoteChunks.add(packet); + lastEntry = M.ms(); + if (chunks.size() + remoteChunks.size() < 1024) return; submit(); } @@ -350,4 +422,31 @@ public class Headless implements IHeadless, LevelHeightAccessor { executor.submit(this); } } + + private byte[] write(ProtoChunk chunk) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(out)) { + NbtIo.write(binding.serializeChunk(chunk, Headless.this), dos); + return out.toByteArray(); + } + } + + private static class CompletingThread extends Thread { + private final CompletableFuture future = new CompletableFuture<>(); + + private CompletingThread(Runnable task, String name, int priority) { + super(task, name); + setPriority(priority); + start(); + } + + @Override + public void run() { + try { + super.run(); + } finally { + future.complete(null); + } + } + } }