Compare commits

...

26 Commits

Author SHA1 Message Date
Julian Krings eff598d005 update headless dev command 2025-04-02 16:42:39 +02:00
Julian Krings d86ec7b1cd Merge remote-tracking branch 'origin/dev' into feat/headless
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
#	core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java
#	core/src/main/java/com/volmit/iris/core/tools/IrisPackBenchmarking.java
#	nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
#	nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java
#	nms/v1_21_R2/src/main/java/com/volmit/iris/core/nms/v1_21_R2/NMSBinding.java
#	nms/v1_21_R3/src/main/java/com/volmit/iris/core/nms/v1_21_R3/NMSBinding.java
2025-04-02 16:33:51 +02:00
Julian Krings 407e51378c fix applying x offset to z coords in Spiraler 2025-03-27 15:02:47 +01:00
Julian Krings c468eb1ab1 make pregen use block radius as input 2025-03-27 15:02:47 +01:00
Aidan Aeternum bdb7cc61e5 v+ 2025-03-26 15:41:35 -04:00
Aidan Aeternum e8f9e841c4 Merge pull request #1181 from VolmitSoftware/dev
3.6.5
2025-03-26 15:41:06 -04:00
Julian Krings 1b1b9d97b7 update overworld pack to 31020 2025-03-25 19:15:47 +01:00
Julian Krings 24355064ff add safeguard for missing dimension types to prevent world corruption 2025-03-25 19:14:20 +01:00
Julian Krings 06a45056d9 use flat level source instead of trying to get the levelstems 2025-03-22 12:38:25 +01:00
Julian Krings 0d9a45dfd9 disable headless pregen on world creation for benchmark worlds 2025-03-18 16:12:09 +01:00
Julian Krings 8a55bbfd20 Merge branch 'dev' into feat/headless
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
2025-03-07 16:17:23 +01:00
Julian Krings 5934c43b70 Merge branch 'dev' into feat/headless 2025-02-18 17:10:23 +01:00
Julian Krings 11567b13d3 potentially fix chunk position bug 2025-02-18 17:08:08 +01:00
Julian Krings 2087ba88b1 fix adding chunks to region while being saved 2025-02-09 12:33:20 +01:00
Julian Krings e9d1b9f18e prevent Biome.CUSTOM from being resolved on <1.21.3 2025-02-08 21:45:51 +01:00
Julian Krings 6e84d38680 set chunk status to full AFTER generation 2025-02-08 20:59:20 +01:00
Julian Krings 22622f6e8a set chunk status to full on creation 2025-02-08 20:22:18 +01:00
Julian Krings 735203aa95 exclude asm from shadowJar 2025-02-08 12:20:27 +01:00
Julian Krings 013bc365a9 implement headless on all supported versions 2025-02-08 12:07:13 +01:00
Julian Krings c2dfbac641 refactor headless to decrease duplicate code in nms bindings 2025-02-07 21:51:23 +01:00
Julian Krings 7d472c0b13 Merge branch 'dev' into feat/headless
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
2025-02-06 23:29:22 +01:00
Julian Krings 5b4ab0a3c1 add headless pregen dev command 2025-02-04 10:55:39 +01:00
Julian Krings e8dd81b014 add headless to world creation 2025-01-29 16:36:04 +01:00
Julian Krings d32cc281e3 fix compile after merge 2025-01-29 14:32:04 +01:00
Julian Krings 2ff6b59271 Merge branch 'dev' into feat/headless 2025-01-29 14:30:17 +01:00
Julian Krings 3ff87566f5 implement headless for 1.21.4 2025-01-26 14:28:59 +01:00
62 changed files with 5996 additions and 849 deletions
+6 -1
View File
@@ -33,7 +33,7 @@ plugins {
} }
version '3.6.4-1.20.1-1.21.4' version '3.6.5-1.20.1-1.21.4'
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED // ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
// ======================== WINDOWS ============================= // ======================== WINDOWS =============================
@@ -92,6 +92,11 @@ shadowJar {
relocate 'net.kyori', 'com.volmit.iris.util.kyori' relocate 'net.kyori', 'com.volmit.iris.util.kyori'
relocate 'org.bstats', 'com.volmit.util.metrics' relocate 'org.bstats', 'com.volmit.util.metrics'
archiveFileName.set("Iris-${project.version}.jar") archiveFileName.set("Iris-${project.version}.jar")
dependencies {
exclude(dependency("org.ow2.asm:asm:"))
exclude(dependency("org.jetbrains:"))
}
} }
dependencies { dependencies {
+4 -2
View File
@@ -459,15 +459,17 @@ public class Iris extends VolmitPlugin implements Listener {
initialize("com.volmit.iris.core.service").forEach((i) -> services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i)); initialize("com.volmit.iris.core.service").forEach((i) -> services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i));
INMS.get(); INMS.get();
IO.delete(new File("iris")); IO.delete(new File("iris"));
compat = IrisCompat.configured(getDataFile("compat.json"));
ServerConfigurator.configure();
new IrisContextInjector();
IrisSafeguard.IrisSafeguardSystem(); IrisSafeguard.IrisSafeguardSystem();
getSender().setTag(getTag()); getSender().setTag(getTag());
compat = IrisCompat.configured(getDataFile("compat.json")); IrisSafeguard.earlySplash();
linkMultiverseCore = new MultiverseCoreLink(); linkMultiverseCore = new MultiverseCoreLink();
linkMythicMobs = new MythicMobsLink(); linkMythicMobs = new MythicMobsLink();
configWatcher = new FileWatcher(getDataFile("settings.json")); configWatcher = new FileWatcher(getDataFile("settings.json"));
services.values().forEach(IrisService::onEnable); services.values().forEach(IrisService::onEnable);
services.values().forEach(this::registerListener); services.values().forEach(this::registerListener);
registerListener(new IrisContextInjector());
J.s(() -> { J.s(() -> {
J.a(() -> PaperLib.suggestPaper(this)); J.a(() -> PaperLib.suggestPaper(this));
J.a(() -> IO.delete(getTemp())); J.a(() -> IO.delete(getTemp()));
@@ -31,6 +31,7 @@ import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.collection.KSet; import com.volmit.iris.util.collection.KSet;
import com.volmit.iris.util.format.C; import com.volmit.iris.util.format.C;
import com.volmit.iris.util.misc.ServerProperties;
import com.volmit.iris.util.plugin.VolmitSender; import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import lombok.Data; import lombok.Data;
@@ -97,6 +98,7 @@ public class ServerConfigurator {
} }
KList<File> worlds = new KList<>(); KList<File> worlds = new KList<>();
Bukkit.getServer().getWorlds().forEach(w -> worlds.add(new File(w.getWorldFolder(), "datapacks"))); Bukkit.getServer().getWorlds().forEach(w -> worlds.add(new File(w.getWorldFolder(), "datapacks")));
if (worlds.isEmpty()) worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
return worlds; return worlds;
} }
@@ -22,11 +22,14 @@ import com.volmit.iris.Iris;
import com.volmit.iris.core.ServerConfigurator; import com.volmit.iris.core.ServerConfigurator;
import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.nms.datapack.DataVersion; import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.pregenerator.PregenTask;
import com.volmit.iris.core.pregenerator.methods.HeadlessPregenMethod;
import com.volmit.iris.core.service.IrisEngineSVC; import com.volmit.iris.core.service.IrisEngineSVC;
import com.volmit.iris.core.tools.IrisPackBenchmarking; import com.volmit.iris.core.tools.IrisPackBenchmarking;
import com.volmit.iris.core.tools.IrisToolbelt; import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisDimension; import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
import com.volmit.iris.util.decree.DecreeExecutor; import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin; import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree; import com.volmit.iris.util.decree.annotations.Decree;
@@ -36,6 +39,7 @@ import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.io.CountingDataInputStream; import com.volmit.iris.util.io.CountingDataInputStream;
import com.volmit.iris.util.io.IO; import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.mantle.TectonicPlate; import com.volmit.iris.util.mantle.TectonicPlate;
import com.volmit.iris.util.math.Position2;
import com.volmit.iris.util.nbt.mca.MCAFile; import com.volmit.iris.util.nbt.mca.MCAFile;
import com.volmit.iris.util.nbt.mca.MCAUtil; import com.volmit.iris.util.nbt.mca.MCAUtil;
import com.volmit.iris.util.parallel.MultiBurst; import com.volmit.iris.util.parallel.MultiBurst;
@@ -48,6 +52,7 @@ import org.apache.commons.lang.RandomStringUtils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.util.Vector;
import java.io.*; import java.io.*;
import java.net.InetAddress; import java.net.InetAddress;
@@ -140,12 +145,16 @@ public class CommandDeveloper implements DecreeExecutor {
public void packBenchmark( public void packBenchmark(
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld") @Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
IrisDimension dimension, IrisDimension dimension,
@Param(description = "Radius in regions", defaultValue = "5") @Param(description = "Diameter in regions", defaultValue = "2048")
int radius, int diameter,
@Param(description = "Headless", defaultValue = "true")
boolean headless,
@Param(description = "Open GUI while benchmarking", defaultValue = "false") @Param(description = "Open GUI while benchmarking", defaultValue = "false")
boolean gui boolean gui
) { ) {
new IrisPackBenchmarking(dimension, radius, gui); int rb = diameter << 9;
Iris.info("Benchmarking pack " + dimension.getName() + " with diameter: " + rb + "(" + diameter + ")");
new IrisPackBenchmarking(dimension, diameter, headless, gui);
} }
@Decree(description = "Upgrade to another Minecraft version") @Decree(description = "Upgrade to another Minecraft version")
@@ -213,6 +222,42 @@ public class CommandDeveloper implements DecreeExecutor {
sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects"); sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects");
} }
@Decree(description = "Pregenerate a world")
public void headless(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center
) {
try {
var engine = Optional.ofNullable(IrisToolbelt.access(world))
.map(PlatformChunkGenerator::getEngine)
.orElse(null);
if (engine == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
radius = Math.max(radius, 1024);
IrisToolbelt.pregenerate(PregenTask
.builder()
.center(new Position2(center.getBlockX(), center.getBlockZ()))
.gui(true)
.radiusX(radius)
.radiusZ(radius)
.build(), new HeadlessPregenMethod(engine), engine);
String msg = C.GREEN + "Headless Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Epic fail. See console.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Decree(description = "Test", aliases = {"ip"}) @Decree(description = "Test", aliases = {"ip"})
public void network() { public void network() {
try { try {
File diff suppressed because it is too large Load Diff
@@ -19,9 +19,7 @@
package com.volmit.iris.core.commands; package com.volmit.iris.core.commands;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.gui.PregeneratorJob; import com.volmit.iris.core.gui.PregeneratorJob;
import com.volmit.iris.core.pregenerator.LazyPregenerator;
import com.volmit.iris.core.pregenerator.PregenTask; import com.volmit.iris.core.pregenerator.PregenTask;
import com.volmit.iris.core.tools.IrisToolbelt; import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.util.decree.DecreeExecutor; import com.volmit.iris.util.decree.DecreeExecutor;
@@ -29,12 +27,9 @@ import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param; import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C; import com.volmit.iris.util.format.C;
import com.volmit.iris.util.math.Position2; import com.volmit.iris.util.math.Position2;
import org.bukkit.Bukkit;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import java.io.File;
@Decree(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!") @Decree(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
public class CommandPregen implements DecreeExecutor { public class CommandPregen implements DecreeExecutor {
@Decree(description = "Pregenerate a world") @Decree(description = "Pregenerate a world")
@@ -52,13 +47,12 @@ public class CommandPregen implements DecreeExecutor {
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
} }
radius = Math.max(radius, 1024); radius = Math.max(radius, 1024);
int w = (radius >> 9 + 1) * 2;
IrisToolbelt.pregenerate(PregenTask IrisToolbelt.pregenerate(PregenTask
.builder() .builder()
.center(new Position2(center.getBlockX() >> 9, center.getBlockZ() >> 9)) .center(new Position2(center.getBlockX(), center.getBlockZ()))
.gui(true) .gui(true)
.width(w) .radiusX(radius)
.height(w) .radiusZ(radius)
.build(), world); .build(), world);
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ(); String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg); sender().sendMessage(msg);
@@ -24,7 +24,6 @@ import com.volmit.iris.core.pregenerator.IrisPregenerator;
import com.volmit.iris.core.pregenerator.PregenListener; import com.volmit.iris.core.pregenerator.PregenListener;
import com.volmit.iris.core.pregenerator.PregenTask; import com.volmit.iris.core.pregenerator.PregenTask;
import com.volmit.iris.core.pregenerator.PregeneratorMethod; import com.volmit.iris.core.pregenerator.PregeneratorMethod;
import com.volmit.iris.core.tools.IrisPackBenchmarking;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.format.Form; import com.volmit.iris.util.format.Form;
@@ -45,8 +44,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.volmit.iris.core.tools.IrisPackBenchmarking.benchmarkInProgress;
public class PregeneratorJob implements PregenListener { public class PregeneratorJob implements PregenListener {
private static final Color COLOR_EXISTS = parseColor("#4d7d5b"); private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
private static final Color COLOR_BLACK = parseColor("#4d7d5b"); private static final Color COLOR_BLACK = parseColor("#4d7d5b");
@@ -81,12 +78,12 @@ public class PregeneratorJob implements PregenListener {
this.task = task; this.task = task;
this.pregenerator = new IrisPregenerator(task, method, this); this.pregenerator = new IrisPregenerator(task, method, this);
max = new Position2(0, 0); max = new Position2(0, 0);
min = new Position2(0, 0); min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
task.iterateRegions((xx, zz) -> { task.iterateAllChunks((xx, zz) -> {
min.setX(Math.min(xx << 5, min.getX())); min.setX(Math.min(xx, min.getX()));
min.setZ(Math.min(zz << 5, min.getZ())); min.setZ(Math.min(zz, min.getZ()));
max.setX(Math.max((xx << 5) + 31, max.getX())); max.setX(Math.max(xx, max.getX()));
max.setZ(Math.max((zz << 5) + 31, max.getZ())); max.setZ(Math.max(zz, max.getZ()));
}); });
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) { if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
@@ -162,7 +159,7 @@ public class PregeneratorJob implements PregenListener {
} }
public void drawRegion(int x, int z, Color color) { public void drawRegion(int x, int z, Color color) {
J.a(() -> PregenTask.iterateRegion(x, z, (xx, zz) -> { J.a(() -> task.iterateChunks(x, z, (xx, zz) -> {
draw(xx, zz, color); draw(xx, zz, color);
J.sleep(3); J.sleep(3);
})); }));
@@ -37,7 +37,7 @@ public class INMS {
"1.21.4", "v1_21_R3" "1.21.4", "v1_21_R3"
); );
private static final List<Version> PACKS = List.of( private static final List<Version> PACKS = List.of(
new Version(21, 4, "31010"), new Version(21, 4, "31020"),
new Version(21, 2, "31000"), new Version(21, 2, "31000"),
new Version(20, 1, "3910") new Version(20, 1, "3910")
); );
@@ -22,6 +22,7 @@ import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.container.Pair; import com.volmit.iris.core.nms.container.Pair;
import com.volmit.iris.core.nms.datapack.DataVersion; import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.collection.KMap;
@@ -32,15 +33,12 @@ import com.volmit.iris.util.nbt.mca.palette.MCAPaletteAccess;
import com.volmit.iris.util.nbt.tag.CompoundTag; import com.volmit.iris.util.nbt.tag.CompoundTag;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.entity.Dolphin;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.generator.ChunkGenerator; import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.structure.Structure;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import java.awt.*;
import java.awt.Color; import java.awt.Color;
public interface INMSBinding { public interface INMSBinding {
@@ -93,6 +91,9 @@ public interface INMSBinding {
MCABiomeContainer newBiomeContainer(int min, int max); MCABiomeContainer newBiomeContainer(int min, int max);
default World createWorld(WorldCreator c) { default World createWorld(WorldCreator c) {
if (missingDimensionTypes(true, true, true))
throw new IllegalStateException("Missing dimenstion types to create world");
try (var ignored = injectLevelStems()) { try (var ignored = injectLevelStems()) {
return c.createWorld(); return c.createWorld();
} }
@@ -128,13 +129,13 @@ public interface INMSBinding {
return 441; return 441;
} }
IRegionStorage createRegionStorage(Engine engine);
KList<String> getStructureKeys(); KList<String> getStructureKeys();
default AutoClosing injectLevelStems() { AutoClosing injectLevelStems();
return new AutoClosing(() -> {});
}
default Pair<Integer, AutoClosing> injectUncached(boolean overworld, boolean nether, boolean end) { Pair<Integer, AutoClosing> injectUncached(boolean overworld, boolean nether, boolean end);
return new Pair<>(0, injectLevelStems());
} boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end);
} }
@@ -0,0 +1,17 @@
package com.volmit.iris.core.nms.headless;
import com.volmit.iris.util.documentation.ChunkCoordinates;
import lombok.NonNull;
import java.io.IOException;
public interface IRegion extends AutoCloseable {
@ChunkCoordinates
boolean exists(int x, int z);
void write(@NonNull SerializableChunk chunk) throws IOException;
@Override
void close();
}
@@ -0,0 +1,27 @@
package com.volmit.iris.core.nms.headless;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.documentation.ChunkCoordinates;
import com.volmit.iris.util.documentation.RegionCoordinates;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
public interface IRegionStorage {
@ChunkCoordinates
boolean exists(int x, int z);
@Nullable
@RegionCoordinates
IRegion getRegion(int x, int z, boolean existingOnly) throws IOException;
@NonNull
@ChunkCoordinates
SerializableChunk createChunk(int x, int z);
void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx);
void close();
}
@@ -0,0 +1,12 @@
package com.volmit.iris.core.nms.headless;
import com.volmit.iris.engine.data.chunk.TerrainChunk;
import com.volmit.iris.util.math.Position2;
public interface SerializableChunk extends TerrainChunk {
Position2 getPos();
Object serialize();
void mark();
}
@@ -20,7 +20,10 @@ package com.volmit.iris.core.nms.v1X;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.INMSBinding; import com.volmit.iris.core.nms.INMSBinding;
import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.container.Pair;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.collection.KMap;
@@ -109,6 +112,11 @@ public class NMSBinding1X implements INMSBinding {
return Color.GREEN; return Color.GREEN;
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return null;
}
@Override @Override
public KList<String> getStructureKeys() { public KList<String> getStructureKeys() {
var list = StreamSupport.stream(Registry.STRUCTURE.spliterator(), false) var list = StreamSupport.stream(Registry.STRUCTURE.spliterator(), false)
@@ -118,6 +126,21 @@ public class NMSBinding1X implements INMSBinding {
return new KList<>(list); return new KList<>(list);
} }
@Override
public AutoClosing injectLevelStems() {
return new AutoClosing(() -> {});
}
@Override
public Pair<Integer, AutoClosing> injectUncached(boolean overworld, boolean nether, boolean end) {
return new Pair<>(0, new AutoClosing(() -> {}));
}
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
return false;
}
@Override @Override
public CompoundTag serializeEntity(Entity location) { public CompoundTag serializeEntity(Entity location) {
return null; return null;
@@ -165,8 +165,11 @@ public class ChunkUpdater {
if (rX < dimensions.min.getX() || rX > dimensions.max.getX() || rZ < dimensions.min.getZ() || rZ > dimensions.max.getZ()) { if (rX < dimensions.min.getX() || rX > dimensions.max.getX() || rZ < dimensions.min.getZ() || rZ > dimensions.max.getZ()) {
return; return;
} }
if (!new File(world.getWorldFolder(), "region" + File.separator + rX + "." + rZ + ".mca").exists()) {
return;
}
PregenTask.iterateRegion(rX, rZ, (x, z) -> { task.iterateChunks(rX, rZ, (x, z) -> {
while (paused.get() && !cancelled.get()) { while (paused.get() && !cancelled.get()) {
J.sleep(50); J.sleep(50);
} }
@@ -348,8 +351,8 @@ public class ChunkUpdater {
int width = maxZ - minZ + 1; int width = maxZ - minZ + 1;
return new Dimensions(new Position2(minX, minZ), new Position2(maxX, maxZ), height * width, PregenTask.builder() return new Dimensions(new Position2(minX, minZ), new Position2(maxX, maxZ), height * width, PregenTask.builder()
.width((int) Math.ceil(width / 2d)) .radiusZ((int) Math.ceil(width / 2d * 512))
.height((int) Math.ceil(height / 2d)) .radiusX((int) Math.ceil(height / 2d * 512))
.center(new Position2(oX, oZ)) .center(new Position2(oX, oZ))
.build()); .build());
} }
@@ -0,0 +1,80 @@
package com.volmit.iris.core.pregenerator;
public class EmptyListener implements PregenListener {
public static PregenListener INSTANCE = new EmptyListener();
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method) {
}
@Override
public void onChunkGenerating(int x, int z) {
}
@Override
public void onChunkGenerated(int x, int z) {
}
@Override
public void onRegionGenerated(int x, int z) {
}
@Override
public void onRegionGenerating(int x, int z) {
}
@Override
public void onChunkCleaned(int x, int z) {
}
@Override
public void onRegionSkipped(int x, int z) {
}
@Override
public void onNetworkStarted(int x, int z) {
}
@Override
public void onNetworkFailed(int x, int z) {
}
@Override
public void onNetworkReclaim(int revert) {
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
}
@Override
public void onNetworkDownloaded(int x, int z) {
}
@Override
public void onClose() {
}
@Override
public void onSaving() {
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
}
}
@@ -19,7 +19,6 @@
package com.volmit.iris.core.pregenerator; package com.volmit.iris.core.pregenerator;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.pack.IrisPack;
import com.volmit.iris.core.tools.IrisPackBenchmarking; import com.volmit.iris.core.tools.IrisPackBenchmarking;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KSet; import com.volmit.iris.util.collection.KSet;
@@ -83,7 +82,7 @@ public class IrisPregenerator {
generatedLast = new AtomicInteger(0); generatedLast = new AtomicInteger(0);
generatedLastMinute = new AtomicInteger(0); generatedLastMinute = new AtomicInteger(0);
totalChunks = new AtomicInteger(0); totalChunks = new AtomicInteger(0);
task.iterateRegions((_a, _b) -> totalChunks.addAndGet(1024)); task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
startTime = new AtomicLong(M.ms()); startTime = new AtomicLong(M.ms());
ticker = new Looper() { ticker = new Looper() {
@Override @Override
@@ -194,7 +193,7 @@ public class IrisPregenerator {
} else if (!regions) { } else if (!regions) {
hit = true; hit = true;
listener.onRegionGenerating(x, z); listener.onRegionGenerating(x, z);
PregenTask.iterateRegion(x, z, (xx, zz) -> { task.iterateChunks(x, z, (xx, zz) -> {
while (paused.get() && !shutdown.get()) { while (paused.get() && !shutdown.get()) {
J.sleep(50); J.sleep(50);
} }
@@ -32,17 +32,26 @@ import java.util.Comparator;
@Data @Data
public class PregenTask { public class PregenTask {
private static final Position2 ZERO = new Position2(0, 0); private static final Position2 ZERO = new Position2(0, 0);
private static final KList<Position2> ORDER_CENTER = computeChunkOrder();
private static final KMap<Position2, KList<Position2>> ORDERS = new KMap<>(); private static final KMap<Position2, KList<Position2>> ORDERS = new KMap<>();
@Builder.Default @Builder.Default
private boolean gui = false; private final boolean gui = false;
@Builder.Default @Builder.Default
private Position2 center = new Position2(0, 0); private final Position2 center = new Position2(0, 0);
@Builder.Default @Builder.Default
private int width = 1; private final int radiusX = 1;
@Builder.Default @Builder.Default
private int height = 1; private final int radiusZ = 1;
private final Bounds bounds = new Bounds();
protected PregenTask(boolean gui, Position2 center, int radiusX, int radiusZ) {
this.gui = gui;
this.center = new ProxiedPos(center);
this.radiusX = radiusX;
this.radiusZ = radiusZ;
bounds.update();
}
public static void iterateRegion(int xr, int zr, Spiraled s, Position2 pull) { public static void iterateRegion(int xr, int zr, Spiraled s, Position2 pull) {
for (Position2 i : ORDERS.computeIfAbsent(pull, PregenTask::computeOrder)) { for (Position2 i : ORDERS.computeIfAbsent(pull, PregenTask::computeOrder)) {
@@ -70,29 +79,72 @@ public class PregenTask {
return p; return p;
} }
private static KList<Position2> computeChunkOrder() { public void iterateRegions(Spiraled s) {
Position2 center = new Position2(15, 15); var bound = bounds.region();
KList<Position2> p = new KList<>(); new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> {
new Spiraler(33, 33, (x, z) -> { if (bound.check(x, z)) s.on(x, z);
int xx = x + 15; })).setOffset(center.getX() >> 9, center.getZ() >> 9).drain();
int zz = z + 15;
if (xx < 0 || xx > 31 || zz < 0 || zz > 31) {
return;
}
p.add(new Position2(xx, zz));
}).drain();
p.sort(Comparator.comparing((i) -> i.distance(center)));
return p;
} }
public void iterateRegions(Spiraled s) { public void iterateChunks(int rX, int rZ, Spiraled s) {
new Spiraler(getWidth() * 2, getHeight() * 2, s) var bound = bounds.chunk();
.setOffset(center.getX(), center.getZ()).drain(); iterateRegion(rX, rZ, ((x, z) -> {
if (bound.check(x, z)) s.on(x, z);
}));
} }
public void iterateAllChunks(Spiraled s) { public void iterateAllChunks(Spiraled s) {
new Spiraler(getWidth() * 2, getHeight() * 2, (x, z) -> iterateRegion(x, z, s)) iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s)));
.setOffset(center.getX(), center.getZ()).drain(); }
private class Bounds {
private Bound chunk = null;
private Bound region = null;
public void update() {
int maxX = center.getX() + radiusX;
int maxZ = center.getZ() + radiusZ;
int minX = center.getX() - radiusX;
int minZ = center.getZ() - radiusZ;
chunk = new Bound(minX >> 4, minZ >> 4, Math.ceilDiv(maxX, 16), Math.ceilDiv(maxZ, 16));
region = new Bound(minX >> 9, minZ >> 9, Math.ceilDiv(maxX, 512), Math.ceilDiv(maxZ, 512));
}
public Bound chunk() {
if (chunk == null) update();
return chunk;
}
public Bound region() {
if (region == null) update();
return region;
}
}
private record Bound(int minX, int maxX, int minZ, int maxZ, int sizeX, int sizeZ) {
private Bound(int minX, int minZ, int maxX, int maxZ) {
this(minX, maxX, minZ, maxZ, maxZ - minZ + 1, maxZ - minZ + 1);
}
boolean check(int x, int z) {
return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
}
}
private static class ProxiedPos extends Position2 {
public ProxiedPos(Position2 p) {
super(p.getX(), p.getZ());
}
@Override
public void setX(int x) {
throw new IllegalStateException("This Position2 may not be modified");
}
@Override
public void setZ(int z) {
throw new IllegalStateException("This Position2 may not be modified");
}
} }
} }
@@ -0,0 +1,112 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.pregenerator.methods;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.pregenerator.PregenListener;
import com.volmit.iris.core.pregenerator.PregeneratorMethod;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisHeadless;
import com.volmit.iris.util.mantle.Mantle;
import com.volmit.iris.util.parallel.MultiBurst;
import java.io.IOException;
import java.util.concurrent.Semaphore;
public class HeadlessPregenMethod implements PregeneratorMethod {
private final Engine engine;
private final IrisHeadless headless;
private final Semaphore semaphore;
private final int max;
private final MultiBurst burst;
public HeadlessPregenMethod(Engine engine) {
this(engine, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()));
}
public HeadlessPregenMethod(Engine engine, int threads) {
this.max = Math.max(threads, 4);
this.engine = engine;
this.headless = new IrisHeadless(engine);
burst = new MultiBurst("HeadlessPregen", 8);
this.semaphore = new Semaphore(max);
}
@Override
public void init() {
}
@Override
public void close() {
try {
semaphore.acquire(max);
} catch (InterruptedException ignored) {
}
try {
headless.close();
} catch (IOException e) {
Iris.error("Failed to close headless");
e.printStackTrace();
}
burst.close();
}
@Override
public void save() {
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public String getMethod(int x, int z) {
return "Headless";
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
try {
semaphore.acquire();
} catch (InterruptedException ignored) {
return;
}
burst.complete(() -> {
try {
listener.onChunkGenerating(x, z);
headless.generateChunk(x, z);
listener.onChunkGenerated(x, z);
} finally {
semaphore.release();
}
});
}
@Override
public Mantle getMantle() {
return engine.getMantle().getMantle();
}
}
@@ -1,6 +1,7 @@
package com.volmit.iris.core.safeguard; package com.volmit.iris.core.safeguard;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
public class IrisSafeguard { public class IrisSafeguard {
public static boolean unstablemode = false; public static boolean unstablemode = false;
@@ -11,5 +12,13 @@ public class IrisSafeguard {
Iris.info("Enabled Iris SafeGuard"); Iris.info("Enabled Iris SafeGuard");
ServerBootSFG.BootCheck(); ServerBootSFG.BootCheck();
} }
public static void earlySplash() {
if (ServerBootSFG.safeguardPassed || IrisSettings.get().getGeneral().DoomsdayAnnihilationSelfDestructMode)
return;
Iris.instance.splash();
UtilsSFG.splash();
}
} }
@@ -3,6 +3,7 @@ package com.volmit.iris.core.safeguard;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.INMS; import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.nms.v1X.NMSBinding1X; import com.volmit.iris.core.nms.v1X.NMSBinding1X;
import com.volmit.iris.engine.object.IrisContextInjector;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
@@ -29,6 +30,7 @@ public class ServerBootSFG {
public static boolean isJRE = false; public static boolean isJRE = false;
public static boolean hasPrivileges = true; public static boolean hasPrivileges = true;
public static boolean unsuportedversion = false; public static boolean unsuportedversion = false;
public static boolean missingDimensionTypes = false;
protected static boolean safeguardPassed; protected static boolean safeguardPassed;
public static boolean passedserversoftware = true; public static boolean passedserversoftware = true;
protected static int count; protected static int count;
@@ -110,6 +112,12 @@ public class ServerBootSFG {
severityMedium++; severityMedium++;
} }
if (IrisContextInjector.isMissingDimensionTypes()) {
missingDimensionTypes = true;
joiner.add("Missing Dimension Types");
severityHigh++;
}
allIncompatibilities = joiner.toString(); allIncompatibilities = joiner.toString();
safeguardPassed = (severityHigh == 0 && severityMedium == 0 && severityLow == 0); safeguardPassed = (severityHigh == 0 && severityMedium == 0 && severityLow == 0);
@@ -39,6 +39,11 @@ public class UtilsSFG {
Iris.safeguard(C.RED + "Server Version"); Iris.safeguard(C.RED + "Server Version");
Iris.safeguard(C.RED + "- Iris only supports 1.20.1 > 1.21.4"); Iris.safeguard(C.RED + "- Iris only supports 1.20.1 > 1.21.4");
} }
if (ServerBootSFG.missingDimensionTypes) {
Iris.safeguard(C.RED + "Dimension Types");
Iris.safeguard(C.RED + "- Required Iris dimension types were not loaded.");
Iris.safeguard(C.RED + "- If this still happens after a restart please contact support.");
}
if (!ServerBootSFG.passedserversoftware) { if (!ServerBootSFG.passedserversoftware) {
Iris.safeguard(C.YELLOW + "Unsupported Server Software"); Iris.safeguard(C.YELLOW + "Unsupported Server Software");
Iris.safeguard(C.YELLOW + "- Please consider using Paper or Purpur instead."); Iris.safeguard(C.YELLOW + "- Please consider using Paper or Purpur instead.");
@@ -84,6 +84,11 @@ public class IrisCreator {
* Benchmark mode * Benchmark mode
*/ */
private boolean benchmark = false; private boolean benchmark = false;
/**
* Radius of chunks to pregenerate in the headless mode
* if set to -1, headless mode is disabled
*/
private int headlessRadius = 10;
public static boolean removeFromBukkitYml(String name) throws IOException { public static boolean removeFromBukkitYml(String name) throws IOException {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
@@ -127,7 +132,6 @@ public class IrisCreator {
Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name())); Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
} }
PlatformChunkGenerator access;
AtomicReference<World> world = new AtomicReference<>(); AtomicReference<World> world = new AtomicReference<>();
AtomicDouble pp = new AtomicDouble(0); AtomicDouble pp = new AtomicDouble(0);
O<Boolean> done = new O<>(); O<Boolean> done = new O<>();
@@ -140,30 +144,56 @@ public class IrisCreator {
.create(); .create();
ServerConfigurator.installDataPacks(false); ServerConfigurator.installDataPacks(false);
access = (PlatformChunkGenerator) wc.generator(); PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
PlatformChunkGenerator finalAccess1 = access; if (access == null) {
throw new IrisException("Access is null. Something bad happened.");
}
J.a(() -> if (headlessRadius > 0 && !benchmark) {
{ AtomicBoolean failed = new AtomicBoolean(false);
Supplier<Integer> g = () -> { J.a(() -> {
if (finalAccess1 == null || finalAccess1.getEngine() == null) { int generated = access.getGenerated();
return 0; double total = Math.pow(headlessRadius * 2 + 1, 2);
while (generated < total) {
if (failed.get()) return;
double v = (double) generated / total;
if (sender.isPlayer()) {
sender.sendProgress(v, "Generating headless chunks");
J.sleep(16);
} else {
sender.sendMessage(C.WHITE + "Generating headless chunks " + Form.pc(v) + ((C.GRAY + " (" + ((int) total - generated) + " Left)")));
J.sleep(1000);
}
generated = access.getGenerated();
} }
return finalAccess1.getEngine().getGenerated(); });
};
if(!benchmark) {
if (finalAccess1 == null) return;
int req = finalAccess1.getSpawnChunks().join();
while (g.get() < req) { try {
double v = (double) g.get() / (double) req; access.prepareSpawnChunks(seed, headlessRadius);
} catch (Throwable e) {
Iris.error("Failed to prepare spawn chunks for " + name);
e.printStackTrace();
failed.set(true);
}
}
J.a(() -> {
if(!benchmark) {
int req = access.getSpawnChunks().join();
int generated = access.getGenerated();
while (generated < req) {
double v = (double) generated / (double) req;
if (sender.isPlayer()) { if (sender.isPlayer()) {
sender.sendProgress(v, "Generating"); sender.sendProgress(v, "Generating");
J.sleep(16); J.sleep(16);
} else { } else {
sender.sendMessage(C.WHITE + "Generating " + Form.pc(v) + ((C.GRAY + " (" + (req - g.get()) + " Left)"))); sender.sendMessage(C.WHITE + "Generating " + Form.pc(v) + ((C.GRAY + " (" + (req - generated) + " Left)")));
J.sleep(1000); J.sleep(1000);
} }
generated = access.getGenerated();
} }
} }
}); });
@@ -2,13 +2,22 @@ package com.volmit.iris.core.tools;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.pregenerator.PregenTask; import com.volmit.iris.core.pregenerator.PregenTask;
import com.volmit.iris.core.pregenerator.methods.HeadlessPregenMethod;
import com.volmit.iris.core.pregenerator.methods.HybridPregenMethod;
import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.engine.IrisEngine;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.framework.EngineTarget;
import com.volmit.iris.engine.object.IrisDimension; import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.engine.object.IrisWorld;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.exceptions.IrisException; import com.volmit.iris.util.exceptions.IrisException;
import com.volmit.iris.util.format.Form; import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.PrecisionStopwatch; import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import lombok.Getter; import lombok.Getter;
@@ -17,11 +26,6 @@ import org.bukkit.Bukkit;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Clock; import java.time.Clock;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
@@ -33,13 +37,16 @@ public class IrisPackBenchmarking {
public static boolean benchmarkInProgress = false; public static boolean benchmarkInProgress = false;
private final PrecisionStopwatch stopwatch = new PrecisionStopwatch(); private final PrecisionStopwatch stopwatch = new PrecisionStopwatch();
private final IrisDimension dimension; private final IrisDimension dimension;
private final int radius; private final int diameter;
private final boolean gui; private final boolean gui;
private final boolean headless;
private transient Engine engine;
public IrisPackBenchmarking(IrisDimension dimension, int radius, boolean gui) { public IrisPackBenchmarking(IrisDimension dimension, int diameter, boolean headless, boolean gui) {
instance = this; instance = this;
this.dimension = dimension; this.dimension = dimension;
this.radius = radius; this.diameter = diameter;
this.headless = headless;
this.gui = gui; this.gui = gui;
runBenchmark(); runBenchmark();
} }
@@ -50,12 +57,9 @@ public class IrisPackBenchmarking {
.start(() -> { .start(() -> {
Iris.info("Setting up benchmark environment "); Iris.info("Setting up benchmark environment ");
benchmarkInProgress = true; benchmarkInProgress = true;
File file = new File("benchmark"); IO.delete(new File(Bukkit.getWorldContainer(), "benchmark"));
if (file.exists()) {
deleteDirectory(file.toPath());
}
createBenchmark(); createBenchmark();
while (!IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) { while (!headless && !IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) {
J.sleep(1000); J.sleep(1000);
Iris.debug("Iris PackBenchmark: Waiting..."); Iris.debug("Iris PackBenchmark: Waiting...");
} }
@@ -73,7 +77,6 @@ public class IrisPackBenchmarking {
public void finishedBenchmark(KList<Integer> cps) { public void finishedBenchmark(KList<Integer> cps) {
try { try {
String time = Form.duration(stopwatch.getMillis()); String time = Form.duration(stopwatch.getMillis());
Engine engine = IrisToolbelt.access(Bukkit.getWorld("benchmark")).getEngine();
Iris.info("-----------------"); Iris.info("-----------------");
Iris.info("Results:"); Iris.info("Results:");
Iris.info("- Total time: " + time); Iris.info("- Total time: " + time);
@@ -114,12 +117,16 @@ public class IrisPackBenchmarking {
e.printStackTrace(); e.printStackTrace();
} }
J.s(() -> { if (headless) {
var world = Bukkit.getWorld("benchmark"); engine.close();
if (world == null) return; } else {
IrisToolbelt.evacuate(world); J.s(() -> {
Bukkit.unloadWorld(world, true); var world = Bukkit.getWorld("benchmark");
}); if (world == null) return;
IrisToolbelt.evacuate(world);
Bukkit.unloadWorld(world, true);
});
}
stopwatch.end(); stopwatch.end();
} catch (Exception e) { } catch (Exception e) {
@@ -130,13 +137,34 @@ public class IrisPackBenchmarking {
private void createBenchmark() { private void createBenchmark() {
try { try {
IrisToolbelt.createWorld() if (headless) {
Iris.info("Using headless benchmark!");
IrisWorld world = IrisWorld.builder()
.name("benchmark")
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.seed(1337)
.worldFolder(new File(Bukkit.getWorldContainer(), "benchmark"))
.environment(dimension.getEnvironment())
.build();
Iris.service(StudioSVC.class).installIntoWorld(
Iris.getSender(),
dimension.getLoadKey(),
world.worldFolder());
var data = IrisData.get(new File(world.worldFolder(), "iris/pack"));
var dim = data.getDimensionLoader().load(dimension.getLoadKey());
engine = new IrisEngine(new EngineTarget(world, dim, data), false);
return;
}
engine = IrisToolbelt.access(IrisToolbelt.createWorld()
.dimension(dimension.getLoadKey()) .dimension(dimension.getLoadKey())
.name("benchmark") .name("benchmark")
.seed(1337) .seed(1337)
.studio(false) .studio(false)
.benchmark(true) .benchmark(true)
.create(); .create())
.getEngine();
} catch (IrisException e) { } catch (IrisException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -146,9 +174,15 @@ public class IrisPackBenchmarking {
IrisToolbelt.pregenerate(PregenTask IrisToolbelt.pregenerate(PregenTask
.builder() .builder()
.gui(gui) .gui(gui)
.width(radius) .radiusX(diameter)
.height(radius) .radiusZ(diameter)
.build(), Bukkit.getWorld("benchmark") .build(), headless ?
new HeadlessPregenMethod(engine) :
new HybridPregenMethod(
engine.getWorld().realWorld(),
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())
),
engine
); );
} }
@@ -178,26 +212,4 @@ public class IrisPackBenchmarking {
private int findHighest(KList<Integer> list) { private int findHighest(KList<Integer> list) {
return Collections.max(list); return Collections.max(list);
} }
private boolean deleteDirectory(Path dir) {
try {
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
} }
@@ -304,6 +304,11 @@ public class IrisEngine implements Engine {
return generated.get(); return generated.get();
} }
@Override
public void addGenerated(int x, int z) {
generated.incrementAndGet();
}
@Override @Override
public double getGeneratedPerSecond() { public double getGeneratedPerSecond() {
if (perSecondLatch.flip()) { if (perSecondLatch.flip()) {
@@ -610,6 +610,9 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
int getGenerated(); int getGenerated();
@ChunkCoordinates
void addGenerated(int x, int z);
CompletableFuture<Long> getHash32(); CompletableFuture<Long> getHash32();
default <T> IrisPosition lookForStreamResult(T find, ProceduralStream<T> stream, Function2<T, T, Boolean> matcher, long timeout) { default <T> IrisPosition lookForStreamResult(T find, ProceduralStream<T> stream, Function2<T, T, Boolean> matcher, long timeout) {
@@ -3,6 +3,7 @@ package com.volmit.iris.engine.object;
import com.volmit.iris.core.nms.INMS; import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.nms.container.AutoClosing; import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.util.misc.ServerProperties; import com.volmit.iris.util.misc.ServerProperties;
import lombok.Getter;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@@ -15,6 +16,8 @@ import java.util.List;
import static com.volmit.iris.Iris.instance; import static com.volmit.iris.Iris.instance;
public class IrisContextInjector implements Listener { public class IrisContextInjector implements Listener {
@Getter
private static boolean missingDimensionTypes = false;
private AutoClosing autoClosing = null; private AutoClosing autoClosing = null;
private final int totalWorlds; private final int totalWorlds;
private int worldCounter = 0; private int worldCounter = 0;
@@ -35,6 +38,12 @@ public class IrisContextInjector implements Listener {
if (Bukkit.getAllowNether()) i++; if (Bukkit.getAllowNether()) i++;
if (Bukkit.getAllowEnd()) i++; if (Bukkit.getAllowEnd()) i++;
if (INMS.get().missingDimensionTypes(overworld, nether, end)) {
missingDimensionTypes = true;
totalWorlds = 0;
return;
}
if (overworld || nether || end) { if (overworld || nether || end) {
var pair = INMS.get().injectUncached(overworld, nether, end); var pair = INMS.get().injectUncached(overworld, nether, end);
i += pair.getA() - 3; i += pair.getA() - 3;
@@ -42,6 +51,7 @@ public class IrisContextInjector implements Listener {
} }
totalWorlds = i; totalWorlds = i;
instance.registerListener(this);
} }
@EventHandler @EventHandler
@@ -0,0 +1,325 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.engine.object;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.core.pregenerator.PregenListener;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.framework.EngineStage;
import com.volmit.iris.engine.framework.WrongEngineBroException;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.context.IrisContext;
import com.volmit.iris.util.documentation.BlockCoordinates;
import com.volmit.iris.util.documentation.RegionCoordinates;
import com.volmit.iris.util.hunk.Hunk;
import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder;
import com.volmit.iris.util.hunk.view.SyncChunkDataHunkHolder;
import com.volmit.iris.util.mantle.MantleFlag;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.math.Position2;
import com.volmit.iris.util.parallel.MultiBurst;
import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.Looper;
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.function.Consumer;
public class IrisHeadless {
private final long KEEP_ALIVE = TimeUnit.SECONDS.toMillis(10L);
private final Engine engine;
private final IRegionStorage storage;
private final ExecutorService executor = Executors.newCachedThreadPool();
private final KMap<Long, Region> regions = new KMap<>();
private final AtomicInteger loadedChunks = new AtomicInteger();
private transient CompletingThread regionThread;
private transient boolean closed = false;
public IrisHeadless(Engine engine) {
this.engine = engine;
this.storage = INMS.get().createRegionStorage(engine);
if (storage == null) throw new IllegalStateException("Failed to create region storage!");
engine.getWorld().headless(this);
startRegionCleaner();
}
private void startRegionCleaner() {
var cleaner = new Looper() {
@Override
protected long loop() {
if (closed) return -1;
long time = M.ms() - KEEP_ALIVE;
regions.values()
.stream()
.filter(r -> r.lastEntry < time)
.forEach(Region::submit);
return closed ? -1 : 1000;
}
};
cleaner.setName("Iris Region Cleaner - " + engine.getWorld().name());
cleaner.setPriority(Thread.MIN_PRIORITY);
cleaner.start();
}
public int getLoadedChunks() {
return loadedChunks.get();
}
/**
* Checks if the mca plate is fully generated or not.
*
* @param x coord of the chunk
* @param z coord of the chunk
* @return true if the chunk exists in .mca
*/
public boolean exists(int x, int z) {
if (closed) return false;
if (engine.getWorld().hasRealWorld() && engine.getWorld().realWorld().isChunkLoaded(x, z))
return true;
return storage.exists(x, z);
}
public synchronized CompletableFuture<Void> 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.getX(), pos.getZ());
generateChunk(pos.getX(), pos.getZ());
if (listening) listener.onChunkGenerated(pos.getX(), pos.getZ());
} 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
private static void iterateRegion(int x, int z, Consumer<Position2> chunkPos) {
int cX = x << 5;
int cZ = z << 5;
for (int xx = 0; xx < 32; xx++) {
for (int zz = 0; zz < 32; zz++) {
chunkPos.accept(new Position2(cX + xx, cZ + zz));
}
}
}
public void generateChunk(int x, int z) {
if (closed || exists(x, z)) return;
try {
var chunk = storage.createChunk(x, z);
loadedChunks.incrementAndGet();
SyncChunkDataHunkHolder blocks = new SyncChunkDataHunkHolder(chunk);
BiomeGridHunkHolder biomes = new BiomeGridHunkHolder(chunk, chunk.getMinHeight(), chunk.getMaxHeight());
ChunkContext ctx = generate(engine, x << 4, z << 4, blocks, biomes);
blocks.apply();
biomes.apply();
storage.fillBiomes(chunk, ctx);
chunk.mark();
long key = Cache.key(x >> 5, z >> 5);
regions.computeIfAbsent(key, Region::new)
.add(chunk);
} catch (Throwable e) {
loadedChunks.decrementAndGet();
Iris.error("Failed to generate " + x + ", " + z);
e.printStackTrace();
}
}
@BlockCoordinates
private ChunkContext generate(Engine engine, int x, int z, Hunk<BlockData> vblocks, Hunk<org.bukkit.block.Biome> vbiomes) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
engine.getContext().touch();
engine.getEngineData().getStatistics().generatedChunk();
ChunkContext ctx = null;
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
Hunk<BlockData> blocks = vblocks.listen((xx, y, zz, t) -> engine.catchBlockUpdates(x + xx, y + engine.getMinHeight(), z + zz, t));
var dimension = engine.getDimension();
if (dimension.isDebugChunkCrossSections() && ((x >> 4) % dimension.getDebugCrossSectionsMod() == 0 || (z >> 4) % dimension.getDebugCrossSectionsMod() == 0)) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
blocks.set(i, 0, j, Material.CRYING_OBSIDIAN.createBlockData());
}
}
} else {
ctx = new ChunkContext(x, z, engine.getComplex());
IrisContext.getOr(engine).setChunkContext(ctx);
for (EngineStage i : engine.getMode().getStages()) {
i.generate(x, z, blocks, vbiomes, false, ctx);
}
}
engine.getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.REAL, true);
engine.getMetrics().getTotal().put(p.getMilliseconds());
engine.addGenerated(x,z);
} catch (Throwable e) {
Iris.reportError(e);
engine.fail("Failed to generate " + x + ", " + z, e);
}
return ctx;
}
public void close() throws IOException {
if (closed) return;
try {
if (regionThread != null) {
regionThread.future.join();
regionThread = null;
}
regions.v().forEach(Region::submit);
Iris.info("Waiting for " + loadedChunks.get() + " chunks to unload...");
while (loadedChunks.get() > 0)
J.sleep(1);
Iris.info("All chunks unloaded");
executor.shutdown();
storage.close();
engine.getWorld().headless(null);
} finally {
closed = true;
}
}
private class Region implements Runnable {
private final int x, z;
private final long key;
private final AtomicReferenceArray<SerializableChunk> chunks = new AtomicReferenceArray<>(1024);
private final AtomicReference<Future<?>> full = new AtomicReference<>();
private transient int size;
private transient long lastEntry = M.ms();
public Region(long key) {
this.x = Cache.keyX(key);
this.z = Cache.keyZ(key);
this.key = key;
}
@Override
public void run() {
try (IRegion region = storage.getRegion(x, z, false)) {
assert region != null;
for (int i = 0; i < 1024; i++) {
SerializableChunk chunk = chunks.get(i);
if (chunk == null)
continue;
try {
region.write(chunk);
} catch (Throwable e) {
Iris.error("Failed to save chunk " + chunk.getPos());
e.printStackTrace();
}
loadedChunks.decrementAndGet();
}
} catch (Throwable e) {
Iris.error("Failed to load region file " + x + ", " + z);
e.printStackTrace();
loadedChunks.addAndGet(-size);
}
}
public synchronized void add(SerializableChunk chunk) {
lastEntry = M.ms();
if (chunks.getAndSet(index(chunk.getPos()), chunk) != null)
throw new IllegalStateException("Chunk " + chunk.getPos() + " already exists");
if (++size < 1024)
return;
submit();
}
public void submit() {
regions.remove(key);
full.getAndUpdate(future -> {
if (future != null) return future;
return executor.submit(this);
});
}
private int index(Position2 chunk) {
int x = chunk.getX() & 31;
int z = chunk.getZ() & 31;
return z * 32 + x;
}
}
private static class CompletingThread extends Thread {
private final CompletableFuture<Void> 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);
}
}
}
}
@@ -48,6 +48,7 @@ public class IrisWorld {
private long seed; private long seed;
private World.Environment environment; private World.Environment environment;
private World realWorld; private World realWorld;
private IrisHeadless headless;
private int minHeight; private int minHeight;
private int maxHeight; private int maxHeight;
@@ -21,6 +21,8 @@ package com.volmit.iris.engine.platform;
import com.volmit.iris.Iris; import com.volmit.iris.Iris;
import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.nms.INMS; import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.pregenerator.EmptyListener;
import com.volmit.iris.core.pregenerator.methods.HeadlessPregenMethod;
import com.volmit.iris.core.service.StudioSVC; import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.engine.IrisEngine; import com.volmit.iris.engine.IrisEngine;
import com.volmit.iris.engine.data.chunk.TerrainChunk; import com.volmit.iris.engine.data.chunk.TerrainChunk;
@@ -32,10 +34,13 @@ import com.volmit.iris.engine.object.StudioMode;
import com.volmit.iris.engine.platform.studio.StudioGenerator; import com.volmit.iris.engine.platform.studio.StudioGenerator;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.data.IrisBiomeStorage; import com.volmit.iris.util.data.IrisBiomeStorage;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.hunk.Hunk; import com.volmit.iris.util.hunk.Hunk;
import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder; import com.volmit.iris.util.hunk.view.BiomeGridHunkHolder;
import com.volmit.iris.util.hunk.view.ChunkDataHunkHolder; import com.volmit.iris.util.hunk.view.ChunkDataHunkHolder;
import com.volmit.iris.util.io.ReactiveFolder; import com.volmit.iris.util.io.ReactiveFolder;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.ChronoLatch; import com.volmit.iris.util.scheduling.ChronoLatch;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.Looper; import com.volmit.iris.util.scheduling.Looper;
@@ -252,6 +257,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} }
private Engine getEngine(WorldInfo world) { private Engine getEngine(WorldInfo world) {
return getEngine(world.getSeed());
}
private Engine getEngine(long seed) {
if (setup.get()) { if (setup.get()) {
return getEngine(); return getEngine();
} }
@@ -264,7 +273,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} }
getWorld().setRawWorldSeed(world.getSeed()); getWorld().setRawWorldSeed(seed);
setupEngine(); setupEngine();
setup.set(true); setup.set(true);
this.hotloader = studio ? new Looper() { this.hotloader = studio ? new Looper() {
@@ -335,6 +344,21 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
getEngine(world); getEngine(world);
} }
@Override
public void prepareSpawnChunks(long seed, int radius) {
if (radius < 0 || new File(world.worldFolder(), "level.dat").exists())
return;
var engine = getEngine(seed);
var headless = new HeadlessPregenMethod(engine, 4);
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
headless.generateChunk(x, z, EmptyListener.INSTANCE);
}
}
headless.close();
}
@Override @Override
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) { public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
try { try {
@@ -49,4 +49,12 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
void touch(World world); void touch(World world);
CompletableFuture<Integer> getSpawnChunks(); CompletableFuture<Integer> getSpawnChunks();
void prepareSpawnChunks(long seed, int radius);
default int getGenerated() {
Engine engine = getEngine();
if (engine == null) return 0;
return engine.getGenerated();
}
} }
@@ -0,0 +1,48 @@
package com.volmit.iris.util.hunk.view;
import com.volmit.iris.Iris;
import com.volmit.iris.util.hunk.storage.ArrayHunk;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
public class SyncChunkDataHunkHolder extends ArrayHunk<BlockData> {
private static final BlockData AIR = Material.AIR.createBlockData();
private final ChunkGenerator.ChunkData chunk;
private final Thread mainThread = Thread.currentThread();
public SyncChunkDataHunkHolder(ChunkGenerator.ChunkData chunk) {
super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16);
this.chunk = chunk;
}
@Override
public void setRaw(int x, int y, int z, BlockData data) {
if (Thread.currentThread() != mainThread)
Iris.warn("SyncChunkDataHunkHolder is not on the main thread");
super.setRaw(x, y, z, data);
}
@Override
public BlockData getRaw(int x, int y, int z) {
if (Thread.currentThread() != mainThread)
Iris.warn("SyncChunkDataHunkHolder is not on the main thread");
BlockData b = super.getRaw(x, y, z);
return b != null ? b : AIR;
}
public void apply() {
for (int i = getHeight()-1; i >= 0; i--) {
for (int j = 0; j < getWidth(); j++) {
for (int k = 0; k < getDepth(); k++) {
BlockData b = super.getRaw(j, i, k);
if (b != null) {
chunk.setBlock(j, i + chunk.getMinHeight(), k, b);
}
}
}
}
}
}
@@ -21,6 +21,8 @@ package com.volmit.iris.util.math;
import com.volmit.iris.engine.object.IrisPosition; import com.volmit.iris.engine.object.IrisPosition;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import java.util.function.BiFunction;
public class Position2 { public class Position2 {
private int x; private int x;
private int z; private int z;
@@ -94,4 +96,8 @@ public class Position2 {
public IrisPosition toIris() { public IrisPosition toIris() {
return new IrisPosition(x, 23, z); return new IrisPosition(x, 23, z);
} }
public <T> T convert(BiFunction<Integer, Integer, T> constructor) {
return constructor.apply(x, z);
}
} }
@@ -63,7 +63,7 @@ public class Spiraler {
public void next() { public void next() {
if ((-sizeX / 2 <= x) && (x <= sizeX / 2) && (-sizeZ / 2 <= z) && (z <= sizeZ / 2)) { if ((-sizeX / 2 <= x) && (x <= sizeX / 2) && (-sizeZ / 2 <= z) && (z <= sizeZ / 2)) {
spiraled.on(x + ox, z + ox); spiraled.on(x + ox, z + oz);
} }
if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) { if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) {
@@ -10,7 +10,7 @@ public class ServerProperties {
public static final File SERVER_PROPERTIES; public static final File SERVER_PROPERTIES;
public static final File BUKKIT_YML; public static final File BUKKIT_YML;
public static final String LEVEL_NAME = DATA.getProperty("level-name", "world"); public static final String LEVEL_NAME;
static { static {
String[] args = ProcessHandle.current() String[] args = ProcessHandle.current()
@@ -20,11 +20,13 @@ public class ServerProperties {
String propertiesPath = "server.properties"; String propertiesPath = "server.properties";
String bukkitYml = "bukkit.yml"; String bukkitYml = "bukkit.yml";
String levelName = null;
for (int i = 0; i < args.length - 1; i++) { for (int i = 0; i < args.length - 1; i++) {
switch (args[i]) { switch (args[i]) {
case "-c", "--config" -> propertiesPath = args[i + 1]; case "-c", "--config" -> propertiesPath = args[i + 1];
case "-b", "--bukkit-settings" -> bukkitYml = args[i + 1]; case "-b", "--bukkit-settings" -> bukkitYml = args[i + 1];
case "-w", "--level-name", "--world" -> levelName = args[i + 1];
} }
} }
@@ -35,5 +37,8 @@ public class ServerProperties {
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (levelName != null) LEVEL_NAME = levelName;
else LEVEL_NAME = DATA.getProperty("level-name", "world");
} }
} }
@@ -7,6 +7,8 @@ import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.INMSBinding; import com.volmit.iris.core.nms.INMSBinding;
import com.volmit.iris.core.nms.container.AutoClosing; import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_20_R1.headless.RegionStorage;
import com.volmit.iris.engine.data.cache.AtomicCache; import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
@@ -38,7 +40,9 @@ import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
@@ -47,6 +51,9 @@ import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -639,6 +646,11 @@ public class NMSBinding implements INMSBinding {
} }
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -673,6 +685,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().registryOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -703,23 +724,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.registryOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.registryOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("iris", key.location().getPath()))), dimensions.getHolder(ResourceKey.create(Registries.DIMENSION_TYPE, loc)).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getOrThrow(key).generator() source
), Lifecycle.stable()); ), Lifecycle.stable());
} }
@@ -732,4 +762,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return new ResourceLocation("iris", key.location().getPath());
}
} }
@@ -0,0 +1,215 @@
package com.volmit.iris.core.nms.v1_20_R1.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_20_R1.block.CraftBlock;
import org.bukkit.craftbukkit.v1_20_R1.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
import static com.volmit.iris.core.nms.v1_20_R1.headless.RegionStorage.registryAccess;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
private final Registry<net.minecraft.world.level.biome.Biome> biomes;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinBuildHeight();
this.maxHeight = access.getMaxBuildHeight();
this.biomes = registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.BIOME);
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBlock.biomeBaseToBiome(biomes, access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBlock.biomeToBiomeBase(biomes, bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,72 @@
package com.volmit.iris.core.nms.v1_20_R1.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.storage.RegionFile;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,312 @@
package com.volmit.iris.core.nms.v1_20_R1.headless;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_20_R1.CraftServer;
import org.bukkit.craftbukkit.v1_20_R1.block.CraftBlock;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minBuildHeight;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minBuildHeight = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minBuildHeight;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
Registry<Biome> registry = registryAccess().registryOrThrow(Registries.BIOME);
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.filter(biome -> biome != org.bukkit.block.Biome.CUSTOM)
.collect(Collectors.toMap(Function.identity(), b -> CraftBlock.biomeToBiomeBase(registry, b))));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, path)));
}
static CompoundTag serialize(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag());
tag.putInt("xPos", chunkPos.x);
tag.putInt("yPos", chunk.getMinSection());
tag.putInt("zPos", chunkPos.z);
tag.putLong("LastUpdate", 0);
tag.putLong("InhabitedTime", chunk.getInhabitedTime());
tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
if (blendingdata != null) {
DataResult<Tag> dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt));
}
BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen();
if (retrogen != null) {
DataResult<Tag> dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt));
}
UpgradeData upgradeData = chunk.getUpgradeData();
if (!upgradeData.isEmpty()) {
tag.put("UpgradeData", upgradeData.write());
}
LevelChunkSection[] sections = chunk.getSections();
ListTag sectionsTag = new ListTag();
Registry<Biome> biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME);
Codec<PalettedContainerRO<Holder<Biome>>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
boolean flag = chunk.isLightCorrect();
int minLightSection = chunk.getMinSection() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int j = chunk.getSectionIndexFromSectionY(y);
if (j < 0 || j >= sections.length)
continue;
CompoundTag sectionTag = new CompoundTag();
LevelChunkSection section = sections[j];
sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, LogUtils.getLogger()::error));
sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, LogUtils.getLogger()::error));
if (!sectionTag.isEmpty()) {
sectionTag.putByte("Y", (byte) y);
sectionsTag.add(sectionTag);
}
}
tag.put("sections", sectionsTag);
if (flag) {
tag.putBoolean("isLightOn", true);
}
ListTag blockEntities = new ListTag();
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos);
if (entityNbt != null) {
blockEntities.add(entityNbt);
}
}
tag.put("block_entities", blockEntities);
if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK) {
ProtoChunk protochunk = (ProtoChunk)chunk;
ListTag entities = new ListTag();
entities.addAll(protochunk.getEntities());
tag.put("entities", entities);
CompoundTag carvingMasks = new CompoundTag();
for(GenerationStep.Carving carving : GenerationStep.Carving.values()) {
CarvingMask mask = protochunk.getCarvingMask(carving);
if (mask != null) {
carvingMasks.putLongArray(carving.toString(), mask.toArray());
}
}
tag.put("CarvingMasks", carvingMasks);
}
saveTicks(tag, chunk.getTicksForSerialization());
tag.put("PostProcessing", packOffsets(chunk.getPostProcessing()));
CompoundTag heightMaps = new CompoundTag();
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) {
heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData()));
}
}
tag.put("Heightmaps", heightMaps);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
tag.put("structures", structureData);
if (!chunk.persistentDataContainer.isEmpty()) {
tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
return tag;
}
private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) {
tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString()));
tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString()));
}
}
@@ -17,6 +17,8 @@ import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Lifecycle; import com.mojang.serialization.Lifecycle;
import com.volmit.iris.core.nms.container.AutoClosing; import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_20_R2.headless.RegionStorage;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.minecraft.core.*; import net.minecraft.core.*;
@@ -28,9 +30,14 @@ import net.minecraft.server.WorldLoader;
import net.minecraft.server.commands.data.BlockDataAccessor; import net.minecraft.server.commands.data.BlockDataAccessor;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -640,6 +647,11 @@ public class NMSBinding implements INMSBinding {
return registry.getHolderOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey()))); return registry.getHolderOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey())));
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -674,6 +686,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().registryOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -704,23 +725,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.registryOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.registryOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("iris", key.location().getPath()))), dimensions.getHolder(ResourceKey.create(Registries.DIMENSION_TYPE, loc)).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getOrThrow(key).generator() source
), Lifecycle.stable()); ), Lifecycle.stable());
} }
@@ -733,4 +763,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return new ResourceLocation("iris", key.location().getPath());
}
} }
@@ -0,0 +1,210 @@
package com.volmit.iris.core.nms.v1_20_R2.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_20_R2.block.CraftBiome;
import org.bukkit.craftbukkit.v1_20_R2.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_20_R2.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinBuildHeight();
this.maxHeight = access.getMaxBuildHeight();
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,72 @@
package com.volmit.iris.core.nms.v1_20_R2.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.storage.RegionFile;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,311 @@
package com.volmit.iris.core.nms.v1_20_R2.headless;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_20_R2.CraftServer;
import org.bukkit.craftbukkit.v1_20_R2.block.CraftBiome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minBuildHeight;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minBuildHeight = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minBuildHeight;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.filter(biome -> biome != org.bukkit.block.Biome.CUSTOM)
.collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder)));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
private static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, path)));
}
static CompoundTag serialize(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag());
tag.putInt("xPos", chunkPos.x);
tag.putInt("yPos", chunk.getMinSection());
tag.putInt("zPos", chunkPos.z);
tag.putLong("LastUpdate", 0);
tag.putLong("InhabitedTime", chunk.getInhabitedTime());
tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
if (blendingdata != null) {
DataResult<Tag> dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt));
}
BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen();
if (retrogen != null) {
DataResult<Tag> dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt));
}
UpgradeData upgradeData = chunk.getUpgradeData();
if (!upgradeData.isEmpty()) {
tag.put("UpgradeData", upgradeData.write());
}
LevelChunkSection[] sections = chunk.getSections();
ListTag sectionsTag = new ListTag();
Registry<Biome> biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME);
Codec<PalettedContainerRO<Holder<Biome>>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
boolean flag = chunk.isLightCorrect();
int minLightSection = chunk.getMinSection() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int j = chunk.getSectionIndexFromSectionY(y);
if (j < 0 || j >= sections.length)
continue;
CompoundTag sectionTag = new CompoundTag();
LevelChunkSection section = sections[j];
sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, LogUtils.getLogger()::error));
sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, LogUtils.getLogger()::error));
if (!sectionTag.isEmpty()) {
sectionTag.putByte("Y", (byte) y);
sectionsTag.add(sectionTag);
}
}
tag.put("sections", sectionsTag);
if (flag) {
tag.putBoolean("isLightOn", true);
}
ListTag blockEntities = new ListTag();
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos);
if (entityNbt != null) {
blockEntities.add(entityNbt);
}
}
tag.put("block_entities", blockEntities);
if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK) {
ProtoChunk protochunk = (ProtoChunk)chunk;
ListTag entities = new ListTag();
entities.addAll(protochunk.getEntities());
tag.put("entities", entities);
CompoundTag carvingMasks = new CompoundTag();
for(GenerationStep.Carving carving : GenerationStep.Carving.values()) {
CarvingMask mask = protochunk.getCarvingMask(carving);
if (mask != null) {
carvingMasks.putLongArray(carving.toString(), mask.toArray());
}
}
tag.put("CarvingMasks", carvingMasks);
}
saveTicks(tag, chunk.getTicksForSerialization());
tag.put("PostProcessing", packOffsets(chunk.getPostProcessing()));
CompoundTag heightMaps = new CompoundTag();
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) {
heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData()));
}
}
tag.put("Heightmaps", heightMaps);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
tag.put("structures", structureData);
if (!chunk.persistentDataContainer.isEmpty()) {
tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
return tag;
}
private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) {
tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString()));
tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString()));
}
}
@@ -17,6 +17,8 @@ import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Lifecycle; import com.mojang.serialization.Lifecycle;
import com.volmit.iris.core.nms.container.AutoClosing; import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_20_R3.headless.RegionStorage;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.minecraft.core.*; import net.minecraft.core.*;
@@ -28,9 +30,14 @@ import net.minecraft.server.WorldLoader;
import net.minecraft.server.commands.data.BlockDataAccessor; import net.minecraft.server.commands.data.BlockDataAccessor;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -641,6 +648,11 @@ public class NMSBinding implements INMSBinding {
return registry.getHolderOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey()))); return registry.getHolderOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey())));
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -675,6 +687,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().registryOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -705,23 +726,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.registryOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.registryOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("iris", key.location().getPath()))), dimensions.getHolder(ResourceKey.create(Registries.DIMENSION_TYPE, loc)).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getOrThrow(key).generator() source
), Lifecycle.stable()); ), Lifecycle.stable());
} }
@@ -734,4 +764,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return new ResourceLocation("iris", key.location().getPath());
}
} }
@@ -0,0 +1,211 @@
package com.volmit.iris.core.nms.v1_20_R3.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_20_R3.block.CraftBiome;
import org.bukkit.craftbukkit.v1_20_R3.block.CraftBlockType;
import org.bukkit.craftbukkit.v1_20_R3.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_20_R3.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinBuildHeight();
this.maxHeight = access.getMaxBuildHeight();
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,72 @@
package com.volmit.iris.core.nms.v1_20_R3.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.storage.RegionFile;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,311 @@
package com.volmit.iris.core.nms.v1_20_R3.headless;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_20_R3.CraftServer;
import org.bukkit.craftbukkit.v1_20_R3.block.CraftBiome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minBuildHeight;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minBuildHeight = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minBuildHeight;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.filter(biome -> biome != org.bukkit.block.Biome.CUSTOM)
.collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder)));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
private static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, new ResourceLocation(namespace, path)));
}
static CompoundTag serialize(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag());
tag.putInt("xPos", chunkPos.x);
tag.putInt("yPos", chunk.getMinSection());
tag.putInt("zPos", chunkPos.z);
tag.putLong("LastUpdate", 0);
tag.putLong("InhabitedTime", chunk.getInhabitedTime());
tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
if (blendingdata != null) {
DataResult<Tag> dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt));
}
BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen();
if (retrogen != null) {
DataResult<Tag> dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt));
}
UpgradeData upgradeData = chunk.getUpgradeData();
if (!upgradeData.isEmpty()) {
tag.put("UpgradeData", upgradeData.write());
}
LevelChunkSection[] sections = chunk.getSections();
ListTag sectionsTag = new ListTag();
Registry<Biome> biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME);
Codec<PalettedContainerRO<Holder<Biome>>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
boolean flag = chunk.isLightCorrect();
int minLightSection = chunk.getMinSection() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int j = chunk.getSectionIndexFromSectionY(y);
if (j < 0 || j >= sections.length)
continue;
CompoundTag sectionTag = new CompoundTag();
LevelChunkSection section = sections[j];
sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, LogUtils.getLogger()::error));
sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, LogUtils.getLogger()::error));
if (!sectionTag.isEmpty()) {
sectionTag.putByte("Y", (byte) y);
sectionsTag.add(sectionTag);
}
}
tag.put("sections", sectionsTag);
if (flag) {
tag.putBoolean("isLightOn", true);
}
ListTag blockEntities = new ListTag();
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos);
if (entityNbt != null) {
blockEntities.add(entityNbt);
}
}
tag.put("block_entities", blockEntities);
if (chunk.getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK) {
ProtoChunk protochunk = (ProtoChunk)chunk;
ListTag entities = new ListTag();
entities.addAll(protochunk.getEntities());
tag.put("entities", entities);
CompoundTag carvingMasks = new CompoundTag();
for(GenerationStep.Carving carving : GenerationStep.Carving.values()) {
CarvingMask mask = protochunk.getCarvingMask(carving);
if (mask != null) {
carvingMasks.putLongArray(carving.toString(), mask.toArray());
}
}
tag.put("CarvingMasks", carvingMasks);
}
saveTicks(tag, chunk.getTicksForSerialization());
tag.put("PostProcessing", packOffsets(chunk.getPostProcessing()));
CompoundTag heightMaps = new CompoundTag();
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) {
heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData()));
}
}
tag.put("Heightmaps", heightMaps);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
tag.put("structures", structureData);
if (!chunk.persistentDataContainer.isEmpty()) {
tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
return tag;
}
private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) {
tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString()));
tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString()));
}
}
@@ -14,6 +14,8 @@ import com.mojang.serialization.Lifecycle;
import com.volmit.iris.core.nms.container.AutoClosing; import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.datapack.DataVersion; import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_20_R4.headless.RegionStorage;
import com.volmit.iris.util.nbt.tag.CompoundTag; import com.volmit.iris.util.nbt.tag.CompoundTag;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@@ -38,11 +40,16 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -666,6 +673,11 @@ public class NMSBinding implements INMSBinding {
return keys; return keys;
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -700,6 +712,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().registryOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -730,23 +751,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.registryOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.registryOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.getHolder(new ResourceLocation("iris", key.location().getPath())).orElseThrow(), dimensions.getHolder(ResourceKey.create(Registries.DIMENSION_TYPE, loc)).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getOrThrow(key).generator() source
), RegistrationInfo.BUILT_IN); ), RegistrationInfo.BUILT_IN);
} }
@@ -759,4 +789,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return new ResourceLocation("iris", key.location().getPath());
}
} }
@@ -0,0 +1,211 @@
package com.volmit.iris.core.nms.v1_20_R4.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_20_R4.block.CraftBiome;
import org.bukkit.craftbukkit.v1_20_R4.block.CraftBlockType;
import org.bukkit.craftbukkit.v1_20_R4.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_20_R4.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinBuildHeight();
this.maxHeight = access.getMaxBuildHeight();
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,75 @@
package com.volmit.iris.core.nms.v1_20_R4.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless");
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(info, path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,311 @@
package com.volmit.iris.core.nms.v1_20_R4.headless;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.chunk.status.ChunkType;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_20_R4.CraftServer;
import org.bukkit.craftbukkit.v1_20_R4.block.CraftBiome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minBuildHeight;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minBuildHeight = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minBuildHeight;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.filter(biome -> biome != org.bukkit.block.Biome.CUSTOM)
.collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder)));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
private static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().registryOrThrow(Registries.BIOME).getHolder(new ResourceLocation(namespace, path));
}
static CompoundTag serialize(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag());
tag.putInt("xPos", chunkPos.x);
tag.putInt("yPos", chunk.getMinSection());
tag.putInt("zPos", chunkPos.z);
tag.putLong("LastUpdate", 0);
tag.putLong("InhabitedTime", chunk.getInhabitedTime());
tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
if (blendingdata != null) {
DataResult<Tag> dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt));
}
BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen();
if (retrogen != null) {
DataResult<Tag> dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt));
}
UpgradeData upgradeData = chunk.getUpgradeData();
if (!upgradeData.isEmpty()) {
tag.put("UpgradeData", upgradeData.write());
}
LevelChunkSection[] sections = chunk.getSections();
ListTag sectionsTag = new ListTag();
Registry<Biome> biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME);
Codec<PalettedContainerRO<Holder<Biome>>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
boolean flag = chunk.isLightCorrect();
int minLightSection = chunk.getMinSection() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int j = chunk.getSectionIndexFromSectionY(y);
if (j < 0 || j >= sections.length)
continue;
CompoundTag sectionTag = new CompoundTag();
LevelChunkSection section = sections[j];
sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow());
sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow());
if (!sectionTag.isEmpty()) {
sectionTag.putByte("Y", (byte) y);
sectionsTag.add(sectionTag);
}
}
tag.put("sections", sectionsTag);
if (flag) {
tag.putBoolean("isLightOn", true);
}
ListTag blockEntities = new ListTag();
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos, registryAccess());
if (entityNbt != null) {
blockEntities.add(entityNbt);
}
}
tag.put("block_entities", blockEntities);
if (chunk.getStatus().getChunkType() == ChunkType.PROTOCHUNK) {
ProtoChunk protochunk = (ProtoChunk)chunk;
ListTag entities = new ListTag();
entities.addAll(protochunk.getEntities());
tag.put("entities", entities);
CompoundTag carvingMasks = new CompoundTag();
for(GenerationStep.Carving carving : GenerationStep.Carving.values()) {
CarvingMask mask = protochunk.getCarvingMask(carving);
if (mask != null) {
carvingMasks.putLongArray(carving.toString(), mask.toArray());
}
}
tag.put("CarvingMasks", carvingMasks);
}
saveTicks(tag, chunk.getTicksForSerialization());
tag.put("PostProcessing", packOffsets(chunk.getPostProcessing()));
CompoundTag heightMaps = new CompoundTag();
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getStatus().heightmapsAfter().contains(entry.getKey())) {
heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData()));
}
}
tag.put("Heightmaps", heightMaps);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
tag.put("structures", structureData);
if (!chunk.persistentDataContainer.isEmpty()) {
tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
return tag;
}
private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) {
tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString()));
tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString()));
}
}
@@ -18,6 +18,8 @@ import com.mojang.serialization.Lifecycle;
import com.volmit.iris.core.nms.container.AutoClosing; import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.datapack.DataVersion; import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_21_R1.headless.RegionStorage;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.minecraft.core.*; import net.minecraft.core.*;
@@ -32,11 +34,16 @@ import net.minecraft.server.level.ChunkMap;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.WorldGenContext; import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -670,6 +677,11 @@ public class NMSBinding implements INMSBinding {
return keys; return keys;
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -704,6 +716,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().registryOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -734,23 +755,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.registryOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.registryOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.registry(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.registryOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.getHolder(ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath())).orElseThrow(), dimensions.getHolder(ResourceKey.create(Registries.DIMENSION_TYPE, loc)).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getOrThrow(key).generator() source
), RegistrationInfo.BUILT_IN); ), RegistrationInfo.BUILT_IN);
} }
@@ -763,4 +793,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath());
}
} }
@@ -0,0 +1,211 @@
package com.volmit.iris.core.nms.v1_21_R1.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_21_R1.block.CraftBiome;
import org.bukkit.craftbukkit.v1_21_R1.block.CraftBlockType;
import org.bukkit.craftbukkit.v1_21_R1.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_21_R1.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinBuildHeight();
this.maxHeight = access.getMaxBuildHeight();
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setPersistedStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,75 @@
package com.volmit.iris.core.nms.v1_21_R1.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless");
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(info, path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,307 @@
package com.volmit.iris.core.nms.v1_21_R1.headless;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.chunk.status.ChunkType;
import net.minecraft.world.level.levelgen.BelowZeroRetrogen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_21_R1.CraftServer;
import org.bukkit.craftbukkit.v1_21_R1.block.CraftBiome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.BLOCK_STATE_CODEC;
import static net.minecraft.world.level.chunk.storage.ChunkSerializer.packOffsets;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minBuildHeight;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minBuildHeight = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minBuildHeight;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.filter(biome -> biome != org.bukkit.block.Biome.CUSTOM)
.collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder)));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().registryOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
private static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().registryOrThrow(Registries.BIOME).getHolder(ResourceLocation.fromNamespaceAndPath(namespace, path));
}
static CompoundTag serialize(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
CompoundTag tag = NbtUtils.addCurrentDataVersion(new CompoundTag());
tag.putInt("xPos", chunkPos.x);
tag.putInt("yPos", chunk.getMinSection());
tag.putInt("zPos", chunkPos.z);
tag.putLong("LastUpdate", 0);
tag.putLong("InhabitedTime", chunk.getInhabitedTime());
tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString());
BlendingData blendingdata = chunk.getBlendingData();
if (blendingdata != null) {
DataResult<Tag> dataresult = BlendingData.CODEC.encodeStart(NbtOps.INSTANCE, blendingdata);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("blending_data", nbt));
}
BelowZeroRetrogen retrogen = chunk.getBelowZeroRetrogen();
if (retrogen != null) {
DataResult<Tag> dataresult = BelowZeroRetrogen.CODEC.encodeStart(NbtOps.INSTANCE, retrogen);
dataresult.resultOrPartial(LogUtils.getLogger()::error).ifPresent((nbt) -> tag.put("below_zero_retrogen", nbt));
}
UpgradeData upgradeData = chunk.getUpgradeData();
if (!upgradeData.isEmpty()) {
tag.put("UpgradeData", upgradeData.write());
}
LevelChunkSection[] sections = chunk.getSections();
ListTag sectionsTag = new ListTag();
Registry<Biome> biomeRegistry = registryAccess().registryOrThrow(Registries.BIOME);
Codec<PalettedContainerRO<Holder<Biome>>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
boolean flag = chunk.isLightCorrect();
int minLightSection = chunk.getMinSection() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int j = chunk.getSectionIndexFromSectionY(y);
if (j < 0 || j >= sections.length)
continue;
CompoundTag sectionTag = new CompoundTag();
LevelChunkSection section = sections[j];
sectionTag.put("block_states", BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow());
sectionTag.put("biomes", codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow());
if (!sectionTag.isEmpty()) {
sectionTag.putByte("Y", (byte) y);
sectionsTag.add(sectionTag);
}
}
tag.put("sections", sectionsTag);
if (flag) {
tag.putBoolean("isLightOn", true);
}
ListTag blockEntities = new ListTag();
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag entityNbt = chunk.getBlockEntityNbtForSaving(blockPos, registryAccess());
if (entityNbt != null) {
blockEntities.add(entityNbt);
}
}
tag.put("block_entities", blockEntities);
if (chunk.getPersistedStatus().getChunkType() == ChunkType.PROTOCHUNK) {
ProtoChunk protochunk = (ProtoChunk)chunk;
ListTag entities = new ListTag();
entities.addAll(protochunk.getEntities());
tag.put("entities", entities);
CompoundTag carvingMasks = new CompoundTag();
for(GenerationStep.Carving carving : GenerationStep.Carving.values()) {
CarvingMask mask = protochunk.getCarvingMask(carving);
if (mask != null) {
carvingMasks.putLongArray(carving.toString(), mask.toArray());
}
}
tag.put("CarvingMasks", carvingMasks);
}
saveTicks(tag, chunk.getTicksForSerialization());
tag.put("PostProcessing", packOffsets(chunk.getPostProcessing()));
CompoundTag heightMaps = new CompoundTag();
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getPersistedStatus().heightmapsAfter().contains(entry.getKey())) {
heightMaps.put(entry.getKey().getSerializationKey(), new LongArrayTag(entry.getValue().getRawData()));
}
}
tag.put("Heightmaps", heightMaps);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
tag.put("structures", structureData);
if (!chunk.persistentDataContainer.isEmpty()) {
tag.put("ChunkBukkitValues", chunk.persistentDataContainer.toTagCompound());
}
return tag;
}
private static void saveTicks(CompoundTag tag, ChunkAccess.TicksToSave ticks) {
tag.put("block_ticks", ticks.blocks().save(0, (block) -> BuiltInRegistries.BLOCK.getKey(block).toString()));
tag.put("fluid_ticks", ticks.fluids().save(0, (fluid) -> BuiltInRegistries.FLUID.getKey(fluid).toString()));
}
}
@@ -14,6 +14,8 @@ import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.container.Pair; import com.volmit.iris.core.nms.container.Pair;
import com.volmit.iris.core.nms.datapack.DataVersion; import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_21_R2.headless.RegionStorage;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.minecraft.core.*; import net.minecraft.core.*;
@@ -27,11 +29,16 @@ import net.minecraft.server.commands.data.BlockDataAccessor;
import net.minecraft.tags.TagKey; import net.minecraft.tags.TagKey;
import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.WorldGenContext; import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -660,6 +667,11 @@ public class NMSBinding implements INMSBinding {
return keys; return keys;
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -694,6 +706,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().lookupOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -724,23 +745,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.lookupOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.lookupOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.lookupOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.lookup(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.lookup(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.lookupOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.get(ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath())).orElseThrow(), dimensions.get(loc).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getValueOrThrow(key).generator() source
), RegistrationInfo.BUILT_IN); ), RegistrationInfo.BUILT_IN);
} }
@@ -753,4 +783,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath());
}
} }
@@ -0,0 +1,211 @@
package com.volmit.iris.core.nms.v1_21_R2.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_21_R2.block.CraftBiome;
import org.bukkit.craftbukkit.v1_21_R2.block.CraftBlockType;
import org.bukkit.craftbukkit.v1_21_R2.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_21_R2.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinY();
this.maxHeight = access.getMaxY();
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setPersistedStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,75 @@
package com.volmit.iris.core.nms.v1_21_R2.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless");
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(info, path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,244 @@
package com.volmit.iris.core.nms.v1_21_R2.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import it.unimi.dsi.fastutil.shorts.ShortList;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.Optionull;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_21_R2.CraftServer;
import org.bukkit.craftbukkit.v1_21_R2.block.CraftBiome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minY;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minY = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minY;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder)));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().lookupOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
private static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().lookupOrThrow(Registries.BIOME).get(ResourceLocation.fromNamespaceAndPath(namespace, path));
}
static CompoundTag serialize(ChunkAccess chunk) {
RegistryAccess access = registryAccess();
List<SerializableChunkData.SectionData> list = new ArrayList<>();
LevelChunkSection[] sections = chunk.getSections();
int minLightSection = chunk.getMinSectionY() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int index = chunk.getSectionIndexFromSectionY(y);
if (index < 0 || index >= sections.length) continue;
LevelChunkSection section = sections[index].copy();
list.add(new SerializableChunkData.SectionData(y, section, null, null));
}
List<CompoundTag> blockEntities = new ArrayList<>(chunk.getBlockEntitiesPos().size());
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag nbt = chunk.getBlockEntityNbtForSaving(blockPos, access);
if (nbt != null) {
blockEntities.add(nbt);
}
}
Map<Heightmap.Types, long[]> heightMap = new EnumMap<>(Heightmap.Types.class);
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getPersistedStatus().heightmapsAfter().contains(entry.getKey())) {
heightMap.put(entry.getKey(), entry.getValue().getRawData().clone());
}
}
ChunkAccess.PackedTicks packedTicks = chunk.getTicksForSerialization(0);
ShortList[] postProcessing = Arrays.stream(chunk.getPostProcessing()).map((shortlist) -> shortlist != null ? new ShortArrayList(shortlist) : null).toArray(ShortList[]::new);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
CompoundTag persistentDataContainer = null;
if (!chunk.persistentDataContainer.isEmpty()) {
persistentDataContainer = chunk.persistentDataContainer.toTagCompound();
}
return new SerializableChunkData(access.lookupOrThrow(Registries.BIOME), chunk.getPos(),
chunk.getMinSectionY(), 0, chunk.getInhabitedTime(), chunk.getPersistedStatus(),
Optionull.map(chunk.getBlendingData(), BlendingData::pack), chunk.getBelowZeroRetrogen(),
chunk.getUpgradeData().copy(), null, heightMap, packedTicks, postProcessing,
chunk.isLightCorrect(), list, new ArrayList<>(), blockEntities, structureData, persistentDataContainer)
.write();
}
}
@@ -8,6 +8,8 @@ import com.volmit.iris.core.nms.container.AutoClosing;
import com.volmit.iris.core.nms.container.BiomeColor; import com.volmit.iris.core.nms.container.BiomeColor;
import com.volmit.iris.core.nms.container.Pair; import com.volmit.iris.core.nms.container.Pair;
import com.volmit.iris.core.nms.datapack.DataVersion; import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.v1_21_R3.headless.RegionStorage;
import com.volmit.iris.engine.data.cache.AtomicCache; import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.framework.Engine; import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.util.collection.KList; import com.volmit.iris.util.collection.KList;
@@ -39,7 +41,9 @@ import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
@@ -49,6 +53,9 @@ import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.status.WorldGenContext; import net.minecraft.world.level.chunk.status.WorldGenContext;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
@@ -659,6 +666,11 @@ public class NMSBinding implements INMSBinding {
return keys; return keys;
} }
@Override
public IRegionStorage createRegionStorage(Engine engine) {
return new RegionStorage(engine);
}
@Override @Override
public AutoClosing injectLevelStems() { public AutoClosing injectLevelStems() {
return inject(this::supplier); return inject(this::supplier);
@@ -693,6 +705,15 @@ public class NMSBinding implements INMSBinding {
})); }));
} }
@Override
public boolean missingDimensionTypes(boolean overworld, boolean nether, boolean end) {
var registry = registry().lookupOrThrow(Registries.DIMENSION_TYPE);
if (overworld) overworld = !registry.containsKey(createIrisKey(LevelStem.OVERWORLD));
if (nether) nether = !registry.containsKey(createIrisKey(LevelStem.NETHER));
if (end) end = !registry.containsKey(createIrisKey(LevelStem.END));
return overworld || nether || end;
}
private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) { private WorldLoader.DataLoadContext supplier(WorldLoader.DataLoadContext old) {
return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext( return dataLoadContext.aquire(() -> new WorldLoader.DataLoadContext(
old.resources(), old.resources(),
@@ -723,23 +744,32 @@ public class NMSBinding implements INMSBinding {
private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) { private RegistryAccess.Frozen createRegistryAccess(RegistryAccess.Frozen datapack, boolean copy, boolean overworld, boolean nether, boolean end) {
var access = registry(); var access = registry();
var dimensions = access.lookupOrThrow(Registries.DIMENSION_TYPE); var dimensions = access.lookupOrThrow(Registries.DIMENSION_TYPE);
var levelStems = access.lookupOrThrow(Registries.LEVEL_STEM);
var settings = new FlatLevelGeneratorSettings(
Optional.empty(),
access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
settings.updateLayers();
var source = new FlatLevelSource(settings);
var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental()); var fake = new MappedRegistry<>(Registries.LEVEL_STEM, Lifecycle.experimental());
if (overworld) register(fake, levelStems, dimensions, LevelStem.OVERWORLD); if (overworld) register(fake, dimensions, source, LevelStem.OVERWORLD);
if (nether) register(fake, levelStems, dimensions, LevelStem.NETHER); if (nether) register(fake, dimensions, source, LevelStem.NETHER);
if (end) register(fake, levelStems, dimensions, LevelStem.END); if (end) register(fake, dimensions, source, LevelStem.END);
copy(fake, datapack.lookup(Registries.LEVEL_STEM).orElse(null)); copy(fake, datapack.lookup(Registries.LEVEL_STEM).orElse(null));
if (copy) copy(fake, levelStems); if (copy) copy(fake, access.lookupOrThrow(Registries.LEVEL_STEM));
return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze(); return new RegistryAccess.Frozen.ImmutableRegistryAccess(List.of(fake.freeze())).freeze();
} }
private void register(MappedRegistry<LevelStem> target, Registry<LevelStem> levelStems, Registry<DimensionType> dimensions, ResourceKey<LevelStem> key) { private void register(MappedRegistry<LevelStem> target, Registry<DimensionType> dimensions, FlatLevelSource source, ResourceKey<LevelStem> key) {
var loc = createIrisKey(key);
target.register(key, new LevelStem( target.register(key, new LevelStem(
dimensions.get(ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath())).orElseThrow(), dimensions.get(loc).orElseThrow(() -> new IllegalStateException("Missing dimension type " + loc + " in " + dimensions.keySet())),
levelStems.getValueOrThrow(key).generator() source
), RegistrationInfo.BUILT_IN); ), RegistrationInfo.BUILT_IN);
} }
@@ -752,4 +782,8 @@ public class NMSBinding implements INMSBinding {
target.register(key, value, info); target.register(key, value, info);
}); });
} }
private ResourceLocation createIrisKey(ResourceKey<LevelStem> key) {
return ResourceLocation.fromNamespaceAndPath("iris", key.location().getPath());
}
} }
@@ -0,0 +1,211 @@
package com.volmit.iris.core.nms.v1_21_R3.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.BiomeBaseInjector;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.math.Position2;
import lombok.Data;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_21_R3.block.CraftBiome;
import org.bukkit.craftbukkit.v1_21_R3.block.CraftBlockType;
import org.bukkit.craftbukkit.v1_21_R3.block.data.CraftBlockData;
import org.bukkit.craftbukkit.v1_21_R3.util.CraftMagicNumbers;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.material.MaterialData;
import org.jetbrains.annotations.NotNull;
@Data
public final class DirectTerrainChunk implements SerializableChunk {
private final ProtoChunk access;
private final int minHeight, maxHeight;
public DirectTerrainChunk(ProtoChunk access) {
this.access = access;
this.minHeight = access.getMinY();
this.maxHeight = access.getMaxY();
}
@Override
public BiomeBaseInjector getBiomeBaseInjector() {
return null;
}
@NotNull
@Override
public Biome getBiome(int x, int z) {
return getBiome(x, 0, z);
}
@NotNull
@Override
public Biome getBiome(int x, int y, int z) {
if (y < minHeight || y > maxHeight) return Biome.PLAINS;
return CraftBiome.minecraftHolderToBukkit(access.getNoiseBiome(x >> 2, y >> 2, z >> 2));
}
@Override
public void setBiome(int x, int z, Biome bio) {
for (int y = minHeight; y < maxHeight; y += 4) {
setBiome(x, y, z, bio);
}
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
if (y < minHeight || y > maxHeight) return;
access.setBiome(x & 15, y, z & 15, CraftBiome.bukkitToMinecraftHolder(bio));
}
public void setBlock(int x, int y, int z, Material material) {
this.setBlock(x, y, z, material.createBlockData());
}
public void setBlock(int x, int y, int z, MaterialData material) {
this.setBlock(x, y, z, CraftMagicNumbers.getBlock(material));
}
@Override
public void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData == null) {
Iris.error("NULL BD");
}
if (blockData instanceof IrisCustomData data)
blockData = data.getBase();
if (!(blockData instanceof CraftBlockData craftBlockData))
throw new IllegalArgumentException("Expected CraftBlockData, got " + blockData.getClass().getSimpleName() + " instead");
access.setBlockState(new BlockPos(x & 15, y, z & 15), craftBlockData.getState(), false);
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, Material material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, material.createBlockData());
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, MaterialData material) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, CraftMagicNumbers.getBlock(material));
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
this.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, ((CraftBlockData) blockData).getState());
}
public Material getType(int x, int y, int z) {
return CraftBlockType.minecraftToBukkit(this.getTypeId(x, y, z).getBlock());
}
public MaterialData getTypeAndData(int x, int y, int z) {
return CraftMagicNumbers.getMaterial(this.getTypeId(x, y, z));
}
public BlockData getBlockData(int x, int y, int z) {
return CraftBlockData.fromData(this.getTypeId(x, y, z));
}
@Override
public ChunkGenerator.ChunkData getRaw() {
return null;
}
@Override
public void setRaw(ChunkGenerator.ChunkData data) {
}
@Override
public void inject(ChunkGenerator.BiomeGrid biome) {
}
public void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockState type) {
if (xMin > 15 || yMin >= this.maxHeight || zMin > 15)
return;
if (xMin < 0) {
xMin = 0;
}
if (yMin < this.minHeight) {
yMin = this.minHeight;
}
if (zMin < 0) {
zMin = 0;
}
if (xMax > 16) {
xMax = 16;
}
if (yMax > this.maxHeight) {
yMax = this.maxHeight;
}
if (zMax > 16) {
zMax = 16;
}
if (xMin >= xMax || yMin >= yMax || zMin >= zMax)
return;
for (int y = yMin; y < yMax; ++y) {
for (int x = xMin; x < xMax; ++x) {
for (int z = zMin; z < zMax; ++z) {
this.setBlock(x, y, z, type);
}
}
}
}
public BlockState getTypeId(int x, int y, int z) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return Blocks.AIR.defaultBlockState();
return access.getBlockState(new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z));
}
public byte getData(int x, int y, int z) {
return CraftMagicNumbers.toLegacyData(this.getTypeId(x, y, z));
}
private void setBlock(int x, int y, int z, BlockState type) {
if (x != (x & 15) || y < this.minHeight || y >= this.maxHeight || z != (z & 15))
return;
BlockPos blockPosition = new BlockPos(access.getPos().getMinBlockX() + x, y, access.getPos().getMinBlockZ() + z);
BlockState oldBlockData = access.setBlockState(blockPosition, type, false);
if (type.hasBlockEntity()) {
BlockEntity tileEntity = ((EntityBlock) type.getBlock()).newBlockEntity(blockPosition, type);
if (tileEntity == null) {
access.removeBlockEntity(blockPosition);
} else {
access.setBlockEntity(tileEntity);
}
} else if (oldBlockData != null && oldBlockData.hasBlockEntity()) {
access.removeBlockEntity(blockPosition);
}
}
@Override
public Position2 getPos() {
return new Position2(access.getPos().x, access.getPos().z);
}
@Override
public Object serialize() {
return RegionStorage.serialize(access);
}
@Override
public void mark() {
access.setPersistedStatus(ChunkStatus.FULL);
}
}
@@ -0,0 +1,75 @@
package com.volmit.iris.core.nms.v1_21_R3.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.util.math.M;
import lombok.NonNull;
import lombok.Synchronized;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.file.Path;
class Region implements IRegion, Comparable<Region> {
private static final RegionStorageInfo info = new RegionStorageInfo("headless", Level.OVERWORLD, "headless");
private final RegionFile regionFile;
transient long references;
transient long lastUsed;
Region(Path path, Path folder) throws IOException {
this.regionFile = new RegionFile(info, path, folder, false);
}
@Override
@Synchronized
public boolean exists(int x, int z) {
try (DataInputStream din = regionFile.getChunkDataInputStream(new ChunkPos(x, z))) {
if (din == null) return false;
return !"empty".equals(NbtIo.read(din).getString("Status"));
} catch (IOException e) {
return false;
}
}
@Override
@Synchronized
public void write(@NonNull SerializableChunk chunk) throws IOException {
try (DataOutputStream dos = regionFile.getChunkDataOutputStream(chunk.getPos().convert(ChunkPos::new))) {
NbtIo.write((CompoundTag) chunk.serialize(), dos);
}
}
@Override
public void close() {
--references;
lastUsed = M.ms();
}
public boolean unused() {
return references <= 0;
}
public boolean remove() {
if (!unused()) return false;
try {
regionFile.close();
} catch (IOException e) {
Iris.error("Failed to close region file");
e.printStackTrace();
}
return true;
}
@Override
public int compareTo(Region o) {
return Long.compare(lastUsed, o.lastUsed);
}
}
@@ -0,0 +1,244 @@
package com.volmit.iris.core.nms.v1_21_R3.headless;
import com.volmit.iris.Iris;
import com.volmit.iris.core.nms.headless.IRegion;
import com.volmit.iris.core.nms.headless.IRegionStorage;
import com.volmit.iris.core.nms.headless.SerializableChunk;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.data.cache.Cache;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.context.ChunkContext;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.J;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import it.unimi.dsi.fastutil.shorts.ShortList;
import lombok.Getter;
import lombok.NonNull;
import net.minecraft.FileUtil;
import net.minecraft.Optionull;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.blending.BlendingData;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_21_R3.CraftServer;
import org.bukkit.craftbukkit.v1_21_R3.block.CraftBiome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
public class RegionStorage implements IRegionStorage, LevelHeightAccessor {
private static final AtomicCache<RegistryAccess> CACHE = new AtomicCache<>();
private final KMap<Long, Region> regions = new KMap<>();
private final Path folder;
private final Engine engine;
private final KMap<String, Holder<Biome>> customBiomes = new KMap<>();
private final KMap<org.bukkit.block.Biome, Holder<Biome>> minecraftBiomes;
private final RNG biomeRng;
private final @Getter int minY;
private final @Getter int height;
private transient boolean closed = false;
public RegionStorage(Engine engine) {
this.engine = engine;
this.folder = new File(engine.getWorld().worldFolder(), "region").toPath();
this.biomeRng = new RNG(engine.getSeedManager().getBiome());
this.minY = engine.getDimension().getMinHeight();
this.height = engine.getDimension().getMaxHeight() - minY;
AtomicInteger failed = new AtomicInteger();
var dimKey = engine.getDimension().getLoadKey();
for (var biome : engine.getAllBiomes()) {
if (!biome.isCustom()) continue;
for (var custom : biome.getCustomDerivitives()) {
biomeHolder(dimKey, custom.getId()).ifPresentOrElse(holder -> customBiomes.put(custom.getId(), holder), () -> {
Iris.error("Failed to load custom biome " + dimKey + " " + custom.getId());
failed.incrementAndGet();
});
}
}
if (failed.get() > 0) {
throw new IllegalStateException("Failed to load " + failed.get() + " custom biomes");
}
minecraftBiomes = new KMap<>(org.bukkit.Registry.BIOME.stream()
.collect(Collectors.toMap(Function.identity(), CraftBiome::bukkitToMinecraftHolder)));
minecraftBiomes.values().removeAll(customBiomes.values());
}
@Override
public boolean exists(int x, int z) {
try (IRegion region = getRegion(x >> 5, z >> 5, true)) {
return region != null && region.exists(x, z);
} catch (Exception e) {
return false;
}
}
@Override
public IRegion getRegion(int x, int z, boolean existingOnly) throws IOException {
AtomicReference<IOException> exception = new AtomicReference<>();
Region region = regions.computeIfAbsent(Cache.key(x, z), k -> {
trim();
try {
FileUtil.createDirectoriesSafe(this.folder);
Path path = folder.resolve("r." + x + "." + z + ".mca");
if (existingOnly && !Files.exists(path)) {
return null;
} else {
return new Region(path, this.folder);
}
} catch (IOException e) {
exception.set(e);
return null;
}
});
if (region == null) {
if (exception.get() != null)
throw exception.get();
return null;
}
region.references++;
return region;
}
@NotNull
@Override
public SerializableChunk createChunk(int x, int z) {
return new DirectTerrainChunk(new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY, this, registryAccess().lookupOrThrow(Registries.BIOME), null));
}
@Override
public void fillBiomes(@NonNull SerializableChunk chunk, @Nullable ChunkContext ctx) {
if (!(chunk instanceof DirectTerrainChunk tc))
return;
tc.getAccess().fillBiomesFromNoise((qX, qY, qZ, sampler) -> getNoiseBiome(engine, ctx, qX << 2, qY << 2, qZ << 2), null);
}
@Override
public synchronized void close() {
if (closed) return;
while (!regions.isEmpty()) {
regions.values().removeIf(Region::remove);
J.sleep(1);
}
closed = true;
customBiomes.clear();
minecraftBiomes.clear();
}
private void trim() {
int size = regions.size();
if (size < 256) return;
int remove = size - 255;
var list = regions.values()
.stream()
.filter(Region::unused)
.sorted()
.collect(Collectors.toList())
.reversed();
int skip = list.size() - remove;
if (skip > 0) list.subList(0, skip).clear();
if (list.isEmpty()) return;
regions.values().removeIf(r -> list.contains(r) && r.remove());
}
private Holder<Biome> getNoiseBiome(Engine engine, ChunkContext ctx, int x, int y, int z) {
int m = y - engine.getMinHeight();
IrisBiome ib = ctx == null ? engine.getSurfaceBiome(x, z) : ctx.getBiome().get(x & 15, z & 15);
if (ib.isCustom()) {
return customBiomes.get(ib.getCustomBiome(biomeRng, x, m, z).getId());
} else {
return minecraftBiomes.get(ib.getSkyBiome(biomeRng, x, m, z));
}
}
private static RegistryAccess registryAccess() {
return CACHE.aquire(() -> ((CraftServer) Bukkit.getServer()).getServer().registryAccess());
}
private static Optional<Holder.Reference<Biome>> biomeHolder(String namespace, String path) {
return registryAccess().lookupOrThrow(Registries.BIOME).get(ResourceLocation.fromNamespaceAndPath(namespace, path));
}
static CompoundTag serialize(ChunkAccess chunk) {
RegistryAccess access = registryAccess();
List<SerializableChunkData.SectionData> list = new ArrayList<>();
LevelChunkSection[] sections = chunk.getSections();
int minLightSection = chunk.getMinSectionY() - 1;
int maxLightSection = minLightSection + chunk.getSectionsCount() + 2;
for(int y = minLightSection; y < maxLightSection; ++y) {
int index = chunk.getSectionIndexFromSectionY(y);
if (index < 0 || index >= sections.length) continue;
LevelChunkSection section = sections[index].copy();
list.add(new SerializableChunkData.SectionData(y, section, null, null));
}
List<CompoundTag> blockEntities = new ArrayList<>(chunk.getBlockEntitiesPos().size());
for(BlockPos blockPos : chunk.getBlockEntitiesPos()) {
CompoundTag nbt = chunk.getBlockEntityNbtForSaving(blockPos, access);
if (nbt != null) {
blockEntities.add(nbt);
}
}
Map<Heightmap.Types, long[]> heightMap = new EnumMap<>(Heightmap.Types.class);
for(Map.Entry<Heightmap.Types, Heightmap> entry : chunk.getHeightmaps()) {
if (chunk.getPersistedStatus().heightmapsAfter().contains(entry.getKey())) {
heightMap.put(entry.getKey(), entry.getValue().getRawData().clone());
}
}
ChunkAccess.PackedTicks packedTicks = chunk.getTicksForSerialization(0);
ShortList[] postProcessing = Arrays.stream(chunk.getPostProcessing()).map((shortlist) -> shortlist != null ? new ShortArrayList(shortlist) : null).toArray(ShortList[]::new);
CompoundTag structureData = new CompoundTag();
structureData.put("starts", new CompoundTag());
structureData.put("References", new CompoundTag());
CompoundTag persistentDataContainer = null;
if (!chunk.persistentDataContainer.isEmpty()) {
persistentDataContainer = chunk.persistentDataContainer.toTagCompound();
}
return new SerializableChunkData(access.lookupOrThrow(Registries.BIOME), chunk.getPos(),
chunk.getMinSectionY(), 0, chunk.getInhabitedTime(), chunk.getPersistedStatus(),
Optionull.map(chunk.getBlendingData(), BlendingData::pack), chunk.getBelowZeroRetrogen(),
chunk.getUpgradeData().copy(), null, heightMap, packedTicks, postProcessing,
chunk.isLightCorrect(), list, new ArrayList<>(), blockEntities, structureData, persistentDataContainer)
.write();
}
}