From 8a18b6ff8bd737f51b65eddea489b624b72e1e2d Mon Sep 17 00:00:00 2001 From: piexel Date: Sun, 11 Oct 2026 16:48:05 +0200 Subject: [PATCH] Support Minecraft 26.1.2 and 26.2 Adds the v26_1_R1 binding, which covers both versions. 26.x servers are not obfuscated and need Java 25, so the build moves to Gradle 9.5.1 and paperweight-userdev 2.0.0-beta.21, and the binding compiles against the 26.1.2 dev bundle with a Java 25 toolchain while everything else stays on Java 21. On 26.x every world is a dimension inside the main level, so the binding provides DimensionWorldStorage: - world folders resolve to /dimensions// - data packs go to the level's datapacks folder - Iris data in a pre-26.1 world folder is moved into the dimension before Paper imports the world and deletes the old folder - "main=true" swaps the world in as the level's overworld instead of changing level-name Also: - INMS compares 1.x and 26.x versions in release order - data pack formats 101 (26.1.2) and 107 (26.2), declared with min_format/max_format from format 82 on - dimension types get the fields 26.1 added - the Java safeguard accepts Java 25 Model used: Claude Opus 5.5 --- build.gradle.kts | 15 +- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/kotlin/NMSBinding.kt | 31 +- .../iris/core/commands/CommandIris.java | 27 +- .../java/com/volmit/iris/core/nms/INMS.java | 36 +- .../iris/core/nms/datapack/DataVersion.java | 14 +- .../nms/datapack/v2601/DataFixerV2601.java | 28 + .../iris/core/world/LegacyWorldStorage.java | 22 + .../volmit/iris/core/world/WorldStorage.java | 6 + .../iris/engine/object/IrisDimension.java | 4 +- .../volmit/iris/core/safeguard/task/Tasks.kt | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../core/nms/v26_1_R1/CustomBiomeSource.java | 169 ++++ .../nms/v26_1_R1/DimensionWorldStorage.java | 115 +++ .../core/nms/v26_1_R1/IrisChunkGenerator.java | 449 +++++++++ .../iris/core/nms/v26_1_R1/NMSBinding.java | 860 ++++++++++++++++++ settings.gradle.kts | 1 + 17 files changed, 1726 insertions(+), 59 deletions(-) create mode 100644 core/src/main/java/com/volmit/iris/core/nms/datapack/v2601/DataFixerV2601.java create mode 100644 nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/CustomBiomeSource.java create mode 100644 nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/DimensionWorldStorage.java create mode 100644 nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/IrisChunkGenerator.java create mode 100644 nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/NMSBinding.java diff --git a/build.gradle.kts b/build.gradle.kts index 5d4d0496d..506c4eca6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ plugins { } group = "com.volmit" -version = "3.9.2-1.20.1-1.21.11" +version = "3.9.2-1.20.1-26.2" apply() @@ -64,6 +64,7 @@ val color = "truecolor" val errorReporting = "true" == findProperty("errorReporting") val nmsBindings = mapOf( + "v26_1_R1" to "26.1.2.build.74-stable", "v1_21_R7" to "1.21.11-R0.1-SNAPSHOT", "v1_21_R6" to "1.21.10-R0.1-SNAPSHOT", "v1_21_R5" to "1.21.8-R0.1-SNAPSHOT", @@ -76,7 +77,9 @@ val nmsBindings = mapOf( "v1_20_R2" to "1.20.2-R0.1-SNAPSHOT", "v1_20_R1" to "1.20.1-R0.1-SNAPSHOT", ) -val jvmVersion = mapOf() +val jvmVersion = mapOf("v26_1_R1" to 25) +// From 26.1 on the server is not obfuscated, so these bindings compile against Paper's dev bundle as-is. +val userDevBindings = setOf("v26_1_R1") nmsBindings.forEach { (key, value) -> project(":nms:$key") { apply() @@ -84,7 +87,7 @@ nmsBindings.forEach { (key, value) -> nmsBinding { jvm = jvmVersion.getOrDefault(key, 21) version = value - type = NMSBinding.Type.DIRECT + type = if (key in userDevBindings) NMSBinding.Type.USER_DEV else NMSBinding.Type.DIRECT } dependencies { @@ -96,7 +99,7 @@ nmsBindings.forEach { (key, value) -> tasks.register("runServer-$key") { group = "servers" - minecraftVersion(value.split("-")[0]) + minecraftVersion(value.substringBefore("-").substringBefore(".build")) minHeapSize = serverMinHeap maxHeapSize = serverMaxHeap pluginJars(tasks.jar.flatMap { it.archiveFile }) @@ -115,7 +118,7 @@ val included: Configuration by configurations.creating val jarJar: Configuration by configurations.creating dependencies { for (key in nmsBindings.keys) { - included(project(":nms:$key", "reobf")) + included(project(":nms:$key", if (key in userDevBindings) "runtimeElements" else "reobf")) } included(project(":core", "shadow")) jarJar(project(":core:agent")) @@ -190,7 +193,7 @@ allprojects { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) + languageVersion.set(JavaLanguageVersion.of(jvmVersion.getOrDefault(name, 21))) } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 983ee94c1..f164ab888 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -27,6 +27,6 @@ repositories { dependencies { implementation("org.ow2.asm:asm:9.8") implementation("com.github.VolmitSoftware:NMSTools:c88961416f") - implementation("io.papermc.paperweight:paperweight-userdev:2.0.0-beta.18") + implementation("io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") } diff --git a/buildSrc/src/main/kotlin/NMSBinding.kt b/buildSrc/src/main/kotlin/NMSBinding.kt index 937f98dde..7a0b7f8ff 100644 --- a/buildSrc/src/main/kotlin/NMSBinding.kt +++ b/buildSrc/src/main/kotlin/NMSBinding.kt @@ -50,18 +50,7 @@ class NMSBinding : Plugin { it.version.set(config.version) } - configurations.register(REOBF_CONFIG) { conf -> - conf.isCanBeConsumed = true - conf.isCanBeResolved = false - conf.attributes { - it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) - it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) - it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) - it.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) - it.attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, objects.named(Obfuscation.OBFUSCATED)) - } - conf.outgoing.artifact(tasks.named("remap")) - } + registerReobfConfiguration("remap") } val (major, minor) = config.version.parseVersion() @@ -71,6 +60,20 @@ class NMSBinding : Plugin { rootProject.tasks.named("prepareKotlinBuildScriptModel") { it.dependsOn("$path:convert") } } + private fun Project.registerReobfConfiguration(artifactTask: String) { + val conf = configurations.create(REOBF_CONFIG) + conf.isCanBeConsumed = true + conf.isCanBeResolved = false + conf.attributes { + it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) + it.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + it.attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, objects.named(Obfuscation.OBFUSCATED)) + } + conf.outgoing.artifact(tasks.named(artifactTask)) + } + @DisableCachingByDefault abstract class ConversionTask @Inject constructor(type: Type) : DefaultTask() { private val pattern: Regex @@ -163,8 +166,10 @@ class NMSBinding : Plugin { private val NEW_LINE = System.lineSeparator() private val NEW_LINE_BYTES = NEW_LINE.encodeToByteArray() +// "1.21.11-R0.1-SNAPSHOT" -> (21, 11), "26.1.2.build.74-stable" -> (26, 1) private fun String.parseVersion() = substringBefore('-').split(".").let { - it[1].toInt() to it[2].toInt() + if (it[0] == "1") it[1].toInt() to it[2].toInt() + else it[0].toInt() to it[1].toInt() } class Config( diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java b/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java index 75fe327be..bf3ff89c1 100644 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java +++ b/core/src/main/java/com/volmit/iris/core/commands/CommandIris.java @@ -33,11 +33,8 @@ import com.volmit.iris.util.decree.annotations.Decree; import com.volmit.iris.util.decree.annotations.Param; import com.volmit.iris.util.decree.specialhandlers.NullablePlayerHandler; import com.volmit.iris.util.format.C; -import com.volmit.iris.util.io.IO; -import com.volmit.iris.util.misc.ServerProperties; import com.volmit.iris.util.plugin.VolmitSender; import com.volmit.iris.util.scheduling.J; -import lombok.SneakyThrows; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; @@ -46,7 +43,6 @@ import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; import java.io.*; -import java.util.List; import java.util.concurrent.atomic.AtomicReference; import static com.volmit.iris.core.service.EditSVC.deletingWorld; @@ -109,9 +105,10 @@ public class CommandIris implements DecreeExecutor { .studio(false) .create(); if (main) { + WorldStorage storage = WorldStorage.get(); Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> { if (old != null) Runtime.getRuntime().removeShutdownHook(old); - return new Thread(() -> updateMainWorld(name)); + return new Thread(() -> makeMainWorld(storage, name)); })); } } catch (Throwable e) { @@ -126,20 +123,12 @@ public class CommandIris implements DecreeExecutor { if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts."); } - @SneakyThrows - private void updateMainWorld(String newName) { - File worlds = Bukkit.getWorldContainer(); - var data = ServerProperties.DATA; - try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { - data.load(in); - } - for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) { - IO.copyDirectory(new File(worlds, ServerProperties.LEVEL_NAME + "/" + sub).toPath(), new File(worlds, newName + "/" + sub).toPath()); - } - - data.setProperty("level-name", newName); - try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { - data.store(out, null); + private static void makeMainWorld(WorldStorage storage, String worldName) { + try { + storage.makeMainWorld(worldName); + } catch (Throwable e) { + Iris.error("Failed to make " + worldName + " the main world!"); + e.printStackTrace(); } } diff --git a/core/src/main/java/com/volmit/iris/core/nms/INMS.java b/core/src/main/java/com/volmit/iris/core/nms/INMS.java index a5ed8ba11..1fe6ef112 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/INMS.java +++ b/core/src/main/java/com/volmit/iris/core/nms/INMS.java @@ -28,9 +28,10 @@ import java.util.List; public class INMS { private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ? new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, null) : - new Version(21, 11, null); + new Version(26, 2, null); private static final List REVISION = List.of( + new Version(26, 1, "v26_1_R1"), new Version(21, 11, "v1_21_R7"), new Version(21, 9, "v1_21_R6"), new Version(21, 6, "v1_21_R5"), @@ -103,27 +104,34 @@ public class INMS { } private static String getTag(List versions, String def) { - var version = Bukkit.getServer().getBukkitVersion().split("-")[0].split("\\.", 3); - int major = 0; - int minor = 0; - - if (version.length > 2) { - major = Integer.parseInt(version[1]); - minor = Integer.parseInt(version[2]); - } else if (version.length == 2) { - major = Integer.parseInt(version[1]); - } - if (CURRENT.major < major || CURRENT.minor < minor) { + Version server = serverVersion(); + if (server.isNewerThan(CURRENT)) { return versions.getFirst().tag; } for (var p : versions) { - if (p.major > major || p.minor > minor) + if (p.isNewerThan(server)) continue; return p.tag; } return def; } - private record Version(int major, int minor, String tag) {} + // "1.21.11" -> (21, 11) and "26.1.2" -> (26, 1), so both numbering schemes compare in release order. + private static Version serverVersion() { + String[] parts = Bukkit.getServer().getBukkitVersion().split("-")[0].split("\\."); + int first = Integer.parseInt(parts[0]); + int second = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + if (first > 1) { + return new Version(first, second, null); + } + int third = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + return new Version(second, third, null); + } + + private record Version(int major, int minor, String tag) { + boolean isNewerThan(Version other) { + return major != other.major ? major > other.major : minor > other.minor; + } + } } diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java b/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java index 205ad5e31..0aafaf0fe 100644 --- a/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java +++ b/core/src/main/java/com/volmit/iris/core/nms/datapack/DataVersion.java @@ -5,6 +5,7 @@ import com.volmit.iris.core.nms.datapack.v1192.DataFixerV1192; import com.volmit.iris.core.nms.datapack.v1206.DataFixerV1206; import com.volmit.iris.core.nms.datapack.v1213.DataFixerV1213; import com.volmit.iris.core.nms.datapack.v1217.DataFixerV1217; +import com.volmit.iris.core.nms.datapack.v2601.DataFixerV2601; import com.volmit.iris.util.collection.KMap; import lombok.AccessLevel; import lombok.Getter; @@ -18,11 +19,15 @@ public enum DataVersion { V1_19_2("1.19.2", 10, DataFixerV1192::new), V1_20_5("1.20.6", 41, DataFixerV1206::new), V1_21_3("1.21.3", 57, DataFixerV1213::new), - V1_21_11("1.21.11", 75, DataFixerV1217::new); + V1_21_11("1.21.11", 75, DataFixerV1217::new), + V26_1_2("26.1.2", 101, DataFixerV2601::new), + V26_2("26.2", 107, DataFixerV2601::new); private static final KMap cache = new KMap<>(); @Getter(AccessLevel.NONE) private final Supplier constructor; private final String version; + private static final int FIRST_RANGED_PACK_FORMAT = 82; + private final int packFormat; DataVersion(String version, int packFormat, Supplier constructor) { @@ -31,6 +36,13 @@ public enum DataVersion { this.version = version; } + public String packMetaFormat() { + if (packFormat < FIRST_RANGED_PACK_FORMAT) { + return "\"pack_format\": " + packFormat; + } + return "\"min_format\": " + packFormat + ", \"max_format\": " + packFormat; + } + public IDataFixer get() { return cache.computeIfAbsent(this, k -> constructor.get()); } diff --git a/core/src/main/java/com/volmit/iris/core/nms/datapack/v2601/DataFixerV2601.java b/core/src/main/java/com/volmit/iris/core/nms/datapack/v2601/DataFixerV2601.java new file mode 100644 index 000000000..51b028c9d --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/nms/datapack/v2601/DataFixerV2601.java @@ -0,0 +1,28 @@ +package com.volmit.iris.core.nms.datapack.v2601; + +import com.volmit.iris.core.nms.datapack.v1217.DataFixerV1217; +import com.volmit.iris.util.json.JSONObject; + +public class DataFixerV2601 extends DataFixerV1217 { + @Override + public void fixDimension(Dimension dimension, JSONObject json) { + super.fixDimension(dimension, json); + + JSONObject attributes = json.getJSONObject("attributes"); + switch (dimension) { + case OVERWORLD -> { + json.put("default_clock", "minecraft:overworld"); + attributes.put("minecraft:visual/ambient_light_color", "#0a0a0a"); + } + case NETHER -> { + json.put("has_fixed_time", true); + attributes.put("minecraft:visual/ambient_light_color", "#302821"); + } + case END -> { + json.put("default_clock", "minecraft:the_end"); + json.put("has_fixed_time", true); + attributes.put("minecraft:visual/ambient_light_color", "#3f473f"); + } + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/world/LegacyWorldStorage.java b/core/src/main/java/com/volmit/iris/core/world/LegacyWorldStorage.java index 8f271b85c..46e205003 100644 --- a/core/src/main/java/com/volmit/iris/core/world/LegacyWorldStorage.java +++ b/core/src/main/java/com/volmit/iris/core/world/LegacyWorldStorage.java @@ -1,14 +1,20 @@ package com.volmit.iris.core.world; import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.io.IO; import com.volmit.iris.util.misc.ServerProperties; import org.bukkit.Bukkit; import org.bukkit.World; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; public class LegacyWorldStorage implements WorldStorage { public static final LegacyWorldStorage INSTANCE = new LegacyWorldStorage(); + private static final List SERVER_WIDE_MAIN_WORLD_FOLDERS = List.of("datapacks", "playerdata", "advancements", "stats"); @Override public File worldFolder(String worldName) { @@ -26,4 +32,20 @@ public class LegacyWorldStorage implements WorldStorage { } return folders; } + + @Override + public void makeMainWorld(String worldName) throws IOException { + var data = ServerProperties.DATA; + try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) { + data.load(in); + } + for (String sub : SERVER_WIDE_MAIN_WORLD_FOLDERS) { + IO.copyDirectory(new File(worldFolder(ServerProperties.LEVEL_NAME), sub).toPath(), new File(worldFolder(worldName), sub).toPath()); + } + + data.setProperty("level-name", worldName); + try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) { + data.store(out, null); + } + } } diff --git a/core/src/main/java/com/volmit/iris/core/world/WorldStorage.java b/core/src/main/java/com/volmit/iris/core/world/WorldStorage.java index 77fe64630..452a4802f 100644 --- a/core/src/main/java/com/volmit/iris/core/world/WorldStorage.java +++ b/core/src/main/java/com/volmit/iris/core/world/WorldStorage.java @@ -4,6 +4,7 @@ import com.volmit.iris.core.nms.INMS; import com.volmit.iris.util.collection.KList; import java.io.File; +import java.io.IOException; public interface WorldStorage { static WorldStorage get() { @@ -17,6 +18,11 @@ public interface WorldStorage { KList datapackFolders(); + /** + * Takes effect from the next start. Called while the server shuts down. + */ + void makeMainWorld(String worldName) throws IOException; + /** * Has to run before the server loads the world, because the server deletes the old folder while migrating it. */ diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java index ef4842056..53f6a23dc 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java @@ -503,10 +503,10 @@ public class IrisDimension extends IrisRegistrant { { "pack": { "description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.", - "pack_format": {} + {} } } - """.replace("{}", INMS.get().getDataVersion().getPackFormat() + ""); + """.replace("{}", INMS.get().getDataVersion().packMetaFormat()); for (File datapacks : folders) { File mcm = new File(datapacks, "iris/pack.mcmeta"); diff --git a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt b/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt index e4c7d225b..8d5b499b9 100644 --- a/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt +++ b/core/src/main/kotlin/com/volmit/iris/core/safeguard/task/Tasks.kt @@ -110,10 +110,10 @@ private val diskSpace by task { private val java by task { val version = Iris.getJavaVersion() val jdk = runCatching { ToolProvider.getSystemJavaCompiler() }.getOrNull() != null - if (version in setOf(21) && jdk) STABLE.withDiagnostics() + if (version in setOf(21, 25) && jdk) STABLE.withDiagnostics() else WARNING.withDiagnostics( WARN.create("Unsupported Java version"), - WARN.create("- Please consider using JDK 21 Instead of ${if(jdk) "JDK" else "JRE"} $version") + WARN.create("- Please consider using JDK 21 (Minecraft 1.21 and older) or JDK 25 (Minecraft 26.1 and newer) Instead of ${if(jdk) "JDK" else "JRE"} $version") ) } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da47..5dd3c0121 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/CustomBiomeSource.java b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/CustomBiomeSource.java new file mode 100644 index 000000000..3ac6f0d47 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/CustomBiomeSource.java @@ -0,0 +1,169 @@ +package com.volmit.iris.core.nms.v26_1_R1; + +import com.mojang.serialization.MapCodec; +import com.volmit.iris.Iris; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisBiome; +import com.volmit.iris.engine.object.IrisBiomeCustom; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.math.RNG; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class CustomBiomeSource extends BiomeSource { + + private final long seed; + private final Engine engine; + private final Registry biomeCustomRegistry; + private final Registry biomeRegistry; + private final AtomicCache registryAccess = new AtomicCache<>(); + private final RNG rng; + private final KMap> customBiomes; + + public CustomBiomeSource(long seed, Engine engine, World world) { + this.engine = engine; + this.seed = seed; + this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null); + this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null); + this.rng = new RNG(engine.getSeedManager().getBiome()); + this.customBiomes = fillCustomBiomes(biomeCustomRegistry, engine); + } + + private static List> getAllBiomes(Registry customRegistry, Registry registry, Engine engine) { + List> b = new ArrayList<>(); + + for (IrisBiome i : engine.getAllBiomes()) { + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + b.add(customRegistry.get(customRegistry.getResourceKey(customRegistry + .getValue(Identifier.fromNamespaceAndPath(engine.getDimension().getLoadKey(), j.getId()))).get()).get()); + } + } else { + b.add(NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative())); + } + } + + return b; + } + + private static Object getFor(Class type, Object source) { + Object o = fieldFor(type, source); + + if (o != null) { + return o; + } + + return invokeFor(type, source); + } + + private static Object fieldFor(Class returns, Object in) { + return fieldForClass(returns, in.getClass(), in); + } + + private static Object invokeFor(Class returns, Object in) { + for (Method i : in.getClass().getMethods()) { + if (i.getReturnType().equals(returns)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); + return i.invoke(in); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + return null; + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class returnType, Class sourceType, Object in) { + for (Field i : sourceType.getDeclaredFields()) { + if (i.getType().equals(returnType)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); + return (T) i.get(in); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + return null; + } + + @Override + protected Stream> collectPossibleBiomes() { + return getAllBiomes( + ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())) + .lookup(Registries.BIOME).orElse(null), + ((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null), + engine).stream(); + } + private KMap> fillCustomBiomes(Registry customRegistry, Engine engine) { + KMap> m = new KMap<>(); + + for (IrisBiome i : engine.getAllBiomes()) { + if (i.isCustom()) { + for (IrisBiomeCustom j : i.getCustomDerivitives()) { + Identifier resourceLocation = Identifier.fromNamespaceAndPath(engine.getDimension().getLoadKey(), j.getId()); + Biome biome = customRegistry.getValue(resourceLocation); + Optional> optionalBiomeKey = customRegistry.getResourceKey(biome); + if (optionalBiomeKey.isEmpty()) { + Iris.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName()); + continue; + } + ResourceKey biomeKey = optionalBiomeKey.get(); + Optional> optionalReferenceHolder = customRegistry.get(biomeKey); + if (optionalReferenceHolder.isEmpty()) { + Iris.error("Cannot find reference to biome " + biomeKey + " for engine " + engine.getName()); + continue; + } + m.put(j.getId(), optionalReferenceHolder.get()); + } + } + } + + return m; + } + + private RegistryAccess registry() { + return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + } + + @Override + protected MapCodec codec() { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + int m = (y - engine.getMinHeight()) << 2; + IrisBiome ib = engine.getComplex().getTrueBiomeStream().get(x << 2, z << 2); + if (ib.isCustom()) { + return customBiomes.get(ib.getCustomBiome(rng, x << 2, m, z << 2).getId()); + } else { + org.bukkit.block.Biome v = ib.getSkyBiome(rng, x << 2, m, z << 2); + return NMSBinding.biomeToBiomeBase(biomeRegistry, v); + } + } +} \ No newline at end of file diff --git a/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/DimensionWorldStorage.java b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/DimensionWorldStorage.java new file mode 100644 index 000000000..2d1cfe4e4 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/DimensionWorldStorage.java @@ -0,0 +1,115 @@ +package com.volmit.iris.core.nms.v26_1_R1; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.world.WorldStorage; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.misc.ServerProperties; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +class DimensionWorldStorage implements WorldStorage { + private static final String DIMENSIONS_FOLDER = "dimensions"; + private static final String DATAPACKS_FOLDER = "datapacks"; + private static final String MINECRAFT_NAMESPACE = "minecraft"; + private static final String OVERWORLD_KEY = "overworld"; + private static final String PREVIOUS_OVERWORLD_PREFIX = "overworld_before_"; + + // Paper only keeps vanilla data when it imports a pre-26.1 world and deletes the old folder. + private static final List IRIS_ENTRIES = List.of("iris", "mantle", "lazygen.json", "turbogen.json", "DeepSearch.json"); + + @Override + public File worldFolder(String worldName) { + World world = Bukkit.getWorld(worldName); + if (world != null) { + return world.getWorldFolder(); + } + + // Paper keys a world created by name the same way, so this is where the server will put it. + NamespacedKey key = new WorldCreator(worldName).key(); + return levelDirectory() + .resolve(DIMENSIONS_FOLDER) + .resolve(key.getNamespace()) + .resolve(key.getKey()) + .toFile(); + } + + @Override + public KList datapackFolders() { + return new KList().qadd(levelDirectory().resolve(DATAPACKS_FOLDER).toFile()); + } + + @Override + public void makeMainWorld(String worldName) throws IOException { + // Players, data packs, the nether and the end belong to the level, so only the overworld is swapped. + Path world = worldFolder(worldName).toPath(); + Path overworld = levelDirectory().resolve(DIMENSIONS_FOLDER).resolve(MINECRAFT_NAMESPACE).resolve(OVERWORLD_KEY); + if (Files.exists(overworld)) { + Path previous = overworld.resolveSibling(PREVIOUS_OVERWORLD_PREFIX + world.getFileName()); + Files.move(overworld, previous); + Iris.info("Moved the previous main world to " + previous); + } + + Files.move(world, overworld); + moveGeneratorToMainWorld(worldName); + Iris.info(worldName + " is now the main world."); + } + + private static void moveGeneratorToMainWorld(String worldName) throws IOException { + YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML); + ConfigurationSection worlds = bukkit.getConfigurationSection("worlds"); + if (worlds == null) { + return; + } + + String generator = worlds.getString(worldName + ".generator"); + worlds.set(worldName, null); + worlds.set(ServerProperties.LEVEL_NAME + ".generator", generator); + bukkit.save(ServerProperties.BUKKIT_YML); + } + + @Override + public void adoptLegacyData(String worldName) { + File legacyFolder = new File(Bukkit.getWorldContainer(), worldName); + File worldFolder = worldFolder(worldName); + if (!legacyFolder.isDirectory() || legacyFolder.getAbsoluteFile().equals(worldFolder.getAbsoluteFile())) { + return; + } + + for (String entry : IRIS_ENTRIES) { + moveEntry(new File(legacyFolder, entry), new File(worldFolder, entry)); + } + } + + private static void moveEntry(File source, File target) { + if (!source.exists()) { + return; + } + if (target.exists()) { + Iris.warn("Not moving " + source + " because " + target + " already exists."); + return; + } + + try { + Files.createDirectories(target.toPath().getParent()); + Files.move(source.toPath(), target.toPath()); + Iris.info("Moved " + source + " to " + target); + } catch (IOException e) { + throw new UncheckedIOException("Failed to move " + source + " to " + target, e); + } + } + + private static Path levelDirectory() { + return Bukkit.getServer().getLevelDirectory().toAbsolutePath().normalize(); + } +} diff --git a/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/IrisChunkGenerator.java b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/IrisChunkGenerator.java new file mode 100644 index 000000000..67ca892a5 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/IrisChunkGenerator.java @@ -0,0 +1,449 @@ +package com.volmit.iris.core.nms.v26_1_R1; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.MapCodec; +import com.volmit.iris.Iris; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.framework.ResultLocator; +import com.volmit.iris.engine.framework.WrongEngineBroException; +import com.volmit.iris.engine.object.IrisJigsawStructure; +import com.volmit.iris.engine.object.IrisJigsawStructurePlacement; +import com.volmit.iris.engine.object.IrisStructurePopulator; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.collection.KSet; +import com.volmit.iris.util.mantle.flag.MantleFlag; +import com.volmit.iris.util.math.Position2; +import com.volmit.iris.util.reflect.WrappedField; +import com.volmit.iris.util.reflect.WrappedReturningMethod; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.core.*; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.tags.StructureTags; +import net.minecraft.tags.TagKey; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.*; +import net.minecraft.world.level.biome.*; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.*; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.generator.CustomChunkGenerator; +import org.bukkit.craftbukkit.generator.structure.CraftStructure; +import org.bukkit.event.world.AsyncStructureSpawnEvent; +import org.spigotmc.SpigotWorldConfig; + +import javax.annotation.Nullable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; + +public class IrisChunkGenerator extends CustomChunkGenerator { + private static final WrappedField BIOME_SOURCE; + private static final WrappedReturningMethod SET_HEIGHT; + private final ChunkGenerator delegate; + private final Engine engine; + private final KMap, KSet> structures = new KMap<>(); + private final IrisStructurePopulator populator; + + public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { + super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null); + this.delegate = delegate; + this.engine = engine; + this.populator = new IrisStructurePopulator(engine); + var dimension = engine.getDimension(); + + KSet placements = new KSet<>(); + addAll(dimension.getJigsawStructures(), placements); + for (var region : dimension.getAllRegions(engine)) { + addAll(region.getJigsawStructures(), placements); + for (var biome : region.getAllBiomes(engine)) + addAll(biome.getJigsawStructures(), placements); + } + var stronghold = dimension.getStronghold(); + if (stronghold != null) + placements.add(engine.getData().getJigsawStructureLoader().load(stronghold)); + placements.removeIf(Objects::isNull); + + var registry = ((CraftWorld) world).getHandle().registryAccess().lookup(Registries.STRUCTURE).orElseThrow(); + for (var s : placements) { + try { + String raw = s.getStructureKey(); + if (raw == null) continue; + boolean tag = raw.startsWith("#"); + if (tag) raw = raw.substring(1); + + var location = Identifier.parse(raw); + if (!tag) { + structures.computeIfAbsent(ResourceKey.create(Registries.STRUCTURE, location), k -> new KSet<>()).add(s.getLoadKey()); + continue; + } + + var key = TagKey.create(Registries.STRUCTURE, location); + var set = registry.get(key).orElse(null); + if (set == null) { + Iris.error("Could not find structure tag: " + raw); + continue; + } + for (var holder : set) { + var resourceKey = holder.unwrapKey().orElse(null); + if (resourceKey == null) continue; + structures.computeIfAbsent(resourceKey, k -> new KSet<>()).add(s.getLoadKey()); + } + } catch (Throwable e) { + Iris.error("Failed to load structure: " + s.getLoadKey()); + e.printStackTrace(); + } + } + } + + private void addAll(KList placements, KSet structures) { + if (placements == null) return; + placements.stream() + .map(IrisJigsawStructurePlacement::getStructure) + .map(engine.getData().getJigsawStructureLoader()::load) + .filter(Objects::nonNull) + .forEach(structures::add); + } + + @Override + public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { + if (holders.size() == 0) return null; + if (holders.unwrapKey().orElse(null) == StructureTags.EYE_OF_ENDER_LOCATED) { + var next = engine.getNearestStronghold(new Position2(pos.getX(), pos.getZ())); + return next == null ? null : new Pair<>(new BlockPos(next.getX(), 0, next.getZ()), holders.get(0)); + } + if (engine.getDimension().isDisableExplorerMaps()) + return null; + + KMap> structures = new KMap<>(); + for (var holder : holders) { + if (holder == null) continue; + var key = holder.unwrapKey().orElse(null); + var set = this.structures.get(key); + if (set == null) continue; + for (var structure : set) { + structures.put(structure, holder); + } + } + if (structures.isEmpty()) + return null; + + var locator = ResultLocator.locateStructure(structures.keySet()) + .then((e, p , s) -> structures.get(s.getLoadKey())); + if (findUnexplored) + locator = locator.then((e, p, s) -> e.getMantle().getMantle().getChunk(p.getX(), p.getZ()).isFlagged(MantleFlag.DISCOVERED) ? null : s); + + try { + var result = locator.find(engine, new Position2(pos.getX() >> 4, pos.getZ() >> 4), radius * 10L, i -> {}, false).get(); + if (result == null) return null; + var blockPos = new BlockPos(result.getBlockX(), 0, result.getBlockZ()); + return Pair.of(blockPos, result.obj()); + } catch (WrongEngineBroException | ExecutionException | InterruptedException e) { + return null; + } + } + + @Override + protected MapCodec codec() { + return MapCodec.unit(null); + } + + @Override + public ChunkGenerator getDelegate() { + if (delegate instanceof CustomChunkGenerator chunkGenerator) + return chunkGenerator.getDelegate(); + return delegate; + } + + @Override + public int getMinY() { + return delegate.getMinY(); + } + + @Override + public int getSeaLevel() { + return delegate.getSeaLevel(); + } + + @Override + public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { + if (!structureManager.shouldGenerateStructures()) + return; + var chunkPos = access.getPos(); + var sectionPos = SectionPos.bottomOf(access); + var registry = registryAccess.lookupOrThrow(Registries.STRUCTURE); + populator.populateStructures(chunkPos.x(), chunkPos.z(), (key, ignoreBiomes) -> { + var loc = Identifier.tryParse(key); + if (loc == null) return false; + var holder = registry.get(loc).orElse(null); + if (holder == null) return false; + var structure = holder.value(); + var biomes = structure.biomes(); + + var start = structure.generate( + holder, + levelKey, + registryAccess, + this, + biomeSource, + structureState.randomState(), + templateManager, + structureState.getLevelSeed(), + chunkPos, + fetchReferences(structureManager, access, sectionPos, structure), + access, + biome -> ignoreBiomes || biomes.contains(biome) + ); + + if (!start.isValid()) + return false; + + BoundingBox box = start.getBoundingBox(); + AsyncStructureSpawnEvent event = new AsyncStructureSpawnEvent( + structureManager.level.getMinecraftWorld().getWorld(), + CraftStructure.minecraftToBukkit(structure), + new org.bukkit.util.BoundingBox( + box.minX(), + box.minY(), + box.minZ(), + box.maxX(), + box.maxY(), + box.maxZ() + ), chunkPos.x(), chunkPos.z()); + Bukkit.getPluginManager().callEvent(event); + if (!event.isCancelled()) { + structureManager.setStartForStructure(sectionPos, structure, start, access); + } + return true; + }); + } + + private static int fetchReferences(StructureManager structureManager, ChunkAccess access, SectionPos sectionPos, Structure structure) { + StructureStart structurestart = structureManager.getStartForStructure(sectionPos, structure, access); + return structurestart != null ? structurestart.getReferences() : 0; + } + + @Override + public ChunkGeneratorStructureState createState(HolderLookup holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) { + return delegate.createState(holderlookup, randomstate, i, conf); + } + + @Override + public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) { + delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess); + } + + @Override + public CompletableFuture createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { + return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess); + } + + @Override + public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) { + delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess); + } + + @Override + public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) { + delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess); + } + + @Override + public CompletableFuture fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) { + return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess); + } + + @Override + public WeightedList getMobsAt(Holder holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { + return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) { + applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true); + } + + @Override + public void addDebugScreenInfo(List list, RandomState randomstate, BlockPos blockposition) { + delegate.addDebugScreenInfo(list, randomstate, blockposition); + } + + @Override + public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { + addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); + delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); + } + + @Override + public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { + if (!structureManager.shouldGenerateStructures()) + return; + + SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); + BlockPos blockPos = sectionPos.origin(); + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + long i = random.setDecorationSeed(level.getSeed(), blockPos.getX(), blockPos.getZ()); + var structures = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); + var list = structures.stream() + .sorted(Comparator.comparingInt(s -> s.step().ordinal())) + .toList(); + + var surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); + var ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); + var motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING); + var motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES); + + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int wX = x + blockPos.getX(); + int wZ = z + blockPos.getZ(); + + int noAir = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1; + int noFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1; + SET_HEIGHT.invoke(ocean, x, z, Math.min(noFluid, ocean.getFirstAvailable(x, z))); + SET_HEIGHT.invoke(surface, x, z, Math.min(noAir, surface.getFirstAvailable(x, z))); + SET_HEIGHT.invoke(motion, x, z, Math.min(noAir, motion.getFirstAvailable(x, z))); + SET_HEIGHT.invoke(motionNoLeaves, x, z, Math.min(noAir, motionNoLeaves.getFirstAvailable(x, z))); + } + } + + for (int j = 0; j < list.size(); j++) { + Structure structure = list.get(j); + random.setFeatureSeed(i, j, structure.step().ordinal()); + Supplier supplier = () -> structures.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString); + + try { + level.setCurrentlyGenerating(supplier); + structureManager.startsForStructure(sectionPos, structure).forEach((start) -> start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos())); + } catch (Exception exception) { + CrashReport crashReport = CrashReport.forThrowable(exception, "Feature placement"); + CrashReportCategory category = crashReport.addCategory("Feature"); + category.setDetail("Description", supplier::get); + throw new ReportedException(crashReport); + } + } + + Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS); + } + + private static BoundingBox getWritableArea(ChunkAccess ichunkaccess) { + ChunkPos chunkPos = ichunkaccess.getPos(); + int minX = chunkPos.getMinBlockX(); + int minZ = chunkPos.getMinBlockZ(); + LevelHeightAccessor heightAccessor = ichunkaccess.getHeightAccessorForGeneration(); + int minY = heightAccessor.getMinY() + 1; + int maxY = heightAccessor.getMaxY(); + return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15); + } + + @Override + public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) { + delegate.spawnOriginalMobs(regionlimitedworldaccess); + } + + @Override + public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) { + return delegate.getSpawnHeight(levelheightaccessor); + } + + @Override + public int getGenDepth() { + return delegate.getGenDepth(); + } + + @Override + public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1; + } + + @Override + public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { + int block = engine.getHeight(i, j, true); + int water = engine.getHeight(i, j, false); + BlockState[] column = new BlockState[levelheightaccessor.getHeight()]; + for (int k = 0; k < column.length; k++) { + if (k <= block) column[k] = Blocks.STONE.defaultBlockState(); + else if (k <= water) column[k] = Blocks.WATER.defaultBlockState(); + else column[k] = Blocks.AIR.defaultBlockState(); + } + return new NoiseColumn(levelheightaccessor.getMinY(), column); + } + + @Override + public Optional getTypeNameForDataFixer() { + return delegate.getTypeNameForDataFixer(); + } + + @Override + public void validate() { + delegate.validate(); + } + + @Override + @SuppressWarnings("deprecation") + public BiomeGenerationSettings getBiomeGenerationSettings(Holder holder) { + return delegate.getBiomeGenerationSettings(holder); + } + + static { + Field biomeSource = null; + for (Field field : ChunkGenerator.class.getDeclaredFields()) { + if (!field.getType().equals(BiomeSource.class)) + continue; + biomeSource = field; + break; + } + if (biomeSource == null) + throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!"); + + Method setHeight = null; + for (Method method : Heightmap.class.getDeclaredMethods()) { + var types = method.getParameterTypes(); + if (types.length != 3 || !Arrays.equals(types, new Class[]{int.class, int.class, int.class}) + || !method.getReturnType().equals(void.class)) + continue; + setHeight = method; + break; + } + if (setHeight == null) + throw new RuntimeException("Could not find setHeight method in Heightmap!"); + + BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName()); + SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes()); + } + + private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) { + try { + BIOME_SOURCE.set(generator, source); + if (generator instanceof CustomChunkGenerator custom) + BIOME_SOURCE.set(custom.getDelegate(), source); + + return generator; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/NMSBinding.java b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/NMSBinding.java new file mode 100644 index 000000000..07c334eb4 --- /dev/null +++ b/nms/v26_1_R1/src/main/java/com/volmit/iris/core/nms/v26_1_R1/NMSBinding.java @@ -0,0 +1,860 @@ +package com.volmit.iris.core.nms.v26_1_R1; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.volmit.iris.Iris; +import com.volmit.iris.core.nms.INMSBinding; +import com.volmit.iris.core.nms.container.BiomeColor; +import com.volmit.iris.core.nms.container.Pair; +import com.volmit.iris.core.nms.container.StructurePlacement; +import com.volmit.iris.core.nms.container.BlockProperty; +import com.volmit.iris.core.nms.datapack.DataVersion; +import com.volmit.iris.core.world.WorldStorage; +import com.volmit.iris.engine.data.cache.AtomicCache; +import com.volmit.iris.engine.framework.Engine; +import com.volmit.iris.engine.object.IrisJigsawStructurePlacement; +import com.volmit.iris.engine.platform.PlatformChunkGenerator; +import com.volmit.iris.util.agent.Agent; +import com.volmit.iris.util.collection.KList; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.format.C; +import com.volmit.iris.util.hunk.Hunk; +import com.volmit.iris.util.json.JSONObject; +import com.volmit.iris.util.mantle.Mantle; +import com.volmit.iris.util.math.Vector3d; +import com.volmit.iris.util.matter.MatterBiomeInject; +import com.volmit.iris.util.nbt.mca.NBTWorld; +import com.volmit.iris.util.nbt.mca.palette.*; +import com.volmit.iris.util.nbt.tag.CompoundTag; +import com.volmit.iris.util.scheduling.J; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.shorts.ShortList; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; +import net.minecraft.core.*; +import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.commands.data.BlockDataAccessor; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.TagKey; +import net.minecraft.world.RandomSequences; +import net.minecraft.world.attribute.EnvironmentAttributes; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.biome.Biomes; +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.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.status.WorldGenContext; +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 net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import org.bukkit.*; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftChunk; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.bukkit.craftbukkit.block.CraftBlockStates; +import org.bukkit.craftbukkit.block.data.CraftBlockData; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.awt.Color; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public class NMSBinding implements INMSBinding { + private final KMap baseBiomeCache = new KMap<>(); + private final BlockData AIR = Material.AIR.createBlockData(); + private final AtomicCache> biomeMapCache = new AtomicCache<>(); + private final AtomicBoolean injected = new AtomicBoolean(); + private final AtomicCache> registryCache = new AtomicCache<>(); + private final AtomicCache> globalCache = new AtomicCache<>(); + private final AtomicCache registryAccess = new AtomicCache<>(); + private final AtomicCache byIdRef = new AtomicCache<>(); + private final WorldStorage worldStorage = new DimensionWorldStorage(); + private Field biomeStorageCache = null; + + private static Object getFor(Class type, Object source) { + Object o = fieldFor(type, source); + + if (o != null) { + return o; + } + + return invokeFor(type, source); + } + + private static Object invokeFor(Class returns, Object in) { + for (Method i : in.getClass().getMethods()) { + if (i.getReturnType().equals(returns)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()"); + return i.invoke(in); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + return null; + } + + private static Object fieldFor(Class returns, Object in) { + return fieldForClass(returns, in.getClass(), in); + } + + @SuppressWarnings("unchecked") + private static T fieldForClass(Class returnType, Class sourceType, Object in) { + for (Field i : sourceType.getDeclaredFields()) { + if (i.getType().equals(returnType)) { + i.setAccessible(true); + try { + Iris.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName()); + return (T) i.get(in); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } + return null; + } + + private static Class getClassType(Class type, int ordinal) { + return type.getDeclaredClasses()[ordinal]; + } + + @Override + public boolean hasTile(Material material) { + return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material)); + } + + @Override + public boolean hasTile(Location l) { + return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null; + } + + @Override + @SuppressWarnings("unchecked") + public KMap serializeTile(Location location) { + BlockEntity e = ((CraftWorld) location.getWorld()).getHandle().getBlockEntity(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + + if (e == null) { + return null; + } + + net.minecraft.nbt.CompoundTag tag = e.saveWithoutMetadata(registry()); + return (KMap) convertFromTag(tag, 0, 64); + } + + @Contract(value = "null, _, _ -> null", pure = true) + private Object convertFromTag(Tag tag, int depth, int maxDepth) { + if (tag == null || depth > maxDepth) return null; + return switch (tag) { + case CollectionTag collection -> { + KList list = new KList<>(); + + for (Object i : collection) { + if (i instanceof Tag t) + list.add(convertFromTag(t, depth + 1, maxDepth)); + else list.add(i); + } + yield list; + } + case net.minecraft.nbt.CompoundTag compound -> { + KMap map = new KMap<>(); + + for (String key : compound.keySet()) { + var child = compound.get(key); + if (child == null) continue; + var value = convertFromTag(child, depth + 1, maxDepth); + if (value == null) continue; + map.put(key, value); + } + yield map; + } + case NumericTag numeric -> numeric.box(); + default -> tag.asString().orElse(null); + }; + } + + @Override + public void deserializeTile(KMap map, Location pos) { + net.minecraft.nbt.CompoundTag tag = (net.minecraft.nbt.CompoundTag) convertToTag(map, 0, 64); + var level = ((CraftWorld) pos.getWorld()).getHandle(); + var blockPos = new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + J.s(() -> merge(level, blockPos, tag)); + } + + private void merge(ServerLevel level, BlockPos blockPos, net.minecraft.nbt.CompoundTag tag) { + var blockEntity = level.getBlockEntity(blockPos); + if (blockEntity == null) { + Iris.warn("[NMS] BlockEntity not found at " + blockPos); + var state = level.getBlockState(blockPos); + if (!state.hasBlockEntity()) + return; + + blockEntity = ((EntityBlock) state.getBlock()) + .newBlockEntity(blockPos, state); + } + var accessor = new BlockDataAccessor(blockEntity, blockPos); + accessor.setData(accessor.getData().merge(tag)); + } + + private Tag convertToTag(Object object, int depth, int maxDepth) { + if (object == null || depth > maxDepth) return EndTag.INSTANCE; + return switch (object) { + case Map map -> { + var tag = new net.minecraft.nbt.CompoundTag(); + for (var i : map.entrySet()) { + tag.put(i.getKey().toString(), convertToTag(i.getValue(), depth + 1, maxDepth)); + } + yield tag; + } + case List list -> { + var tag = new ListTag(); + for (var i : list) { + tag.add(convertToTag(i, depth + 1, maxDepth)); + } + yield tag; + } + case Byte number -> ByteTag.valueOf(number); + case Short number -> ShortTag.valueOf(number); + case Integer number -> IntTag.valueOf(number); + case Long number -> LongTag.valueOf(number); + case Float number -> FloatTag.valueOf(number); + case Double number -> DoubleTag.valueOf(number); + case String string -> StringTag.valueOf(string); + default -> EndTag.INSTANCE; + }; + } + + @Override + public CompoundTag serializeEntity(Entity location) { + return null;// TODO: + } + + @Override + public Entity deserializeEntity(CompoundTag s, Location newPosition) { + return null;// TODO: + } + + @Override + public boolean supportsCustomHeight() { + return true; + } + + private RegistryAccess registry() { + return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())); + } + + private Registry getCustomBiomeRegistry() { + return registry().lookup(Registries.BIOME).orElse(null); + } + + private Registry getBlockRegistry() { + return registry().lookup(Registries.BLOCK).orElse(null); + } + + @Override + public Object getBiomeBaseFromId(int id) { + return getCustomBiomeRegistry().get(id); + } + + @Override + public int getMinHeight(World world) { + return world.getMinHeight(); + } + + @Override + public boolean supportsCustomBiomes() { + return true; + } + + @Override + public int getTrueBiomeBaseId(Object biomeBase) { + return getCustomBiomeRegistry().getId(((Holder) biomeBase).value()); + } + + @Override + public Object getTrueBiomeBase(Location location) { + return ((CraftWorld) location.getWorld()).getHandle().getBiome(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ())); + } + + @Override + public String getTrueBiomeBaseKey(Location location) { + return getKeyForBiomeBase(getTrueBiomeBase(location)); + } + + @Override + public Object getCustomBiomeBaseFor(String mckey) { + return getCustomBiomeRegistry().getValue(net.minecraft.resources.Identifier.parse(mckey)); + } + + @Override + public Object getCustomBiomeBaseHolderFor(String mckey) { + return getCustomBiomeRegistry().get(getTrueBiomeBaseId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(mckey)))).orElse(null); + } + + public int getBiomeBaseIdForKey(String key) { + return getCustomBiomeRegistry().getId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(key)).map(Holder::value).orElse(null)); + } + + @Override + public String getKeyForBiomeBase(Object biomeBase) { + return getCustomBiomeRegistry().getKey((net.minecraft.world.level.biome.Biome) biomeBase).getPath(); // something, not something:something + } + + @Override + public Object getBiomeBase(World world, Biome biome) { + return biomeToBiomeBase(((CraftWorld) world).getHandle() + .registryAccess().lookup(Registries.BIOME).orElse(null), biome); + } + + @Override + public Object getBiomeBase(Object registry, Biome biome) { + Object v = baseBiomeCache.get(biome); + + if (v != null) { + return v; + } + //noinspection unchecked + v = biomeToBiomeBase((Registry) registry, biome); + if (v == null) { + // Ok so there is this new biome name called "CUSTOM" in Paper's new releases. + // But, this does NOT exist within CraftBukkit which makes it return an error. + // So, we will just return the ID that the plains biome returns instead. + //noinspection unchecked + return biomeToBiomeBase((Registry) registry, Biome.PLAINS); + } + baseBiomeCache.put(biome, v); + return v; + } + + @Override + public KList getBiomes() { + return new KList<>(Biome.values()).qadd(Biome.CHERRY_GROVE).qdel(Biome.CUSTOM); + } + + @Override + public boolean isBukkit() { + return true; + } + + @Override + public int getBiomeId(Biome biome) { + for (World i : Bukkit.getWorlds()) { + if (i.getEnvironment().equals(World.Environment.NORMAL)) { + Registry registry = ((CraftWorld) i).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null); + return registry.getId((net.minecraft.world.level.biome.Biome) getBiomeBase(registry, biome)); + } + } + + return biome.ordinal(); + } + + private MCAIdMap getBiomeMapping() { + return biomeMapCache.aquire(() -> new MCAIdMap<>() { + @NotNull + @Override + public Iterator iterator() { + return getCustomBiomeRegistry().iterator(); + } + + @Override + public int getId(net.minecraft.world.level.biome.Biome paramT) { + return getCustomBiomeRegistry().getId(paramT); + } + + @Override + public net.minecraft.world.level.biome.Biome byId(int paramInt) { + return (net.minecraft.world.level.biome.Biome) getBiomeBaseFromId(paramInt); + } + }); + } + + @NotNull + private MCABiomeContainer getBiomeContainerInterface(MCAIdMap biomeMapping, MCAChunkBiomeContainer base) { + return new MCABiomeContainer() { + @Override + public int[] getData() { + return base.writeBiomes(); + } + + @Override + public void setBiome(int x, int y, int z, int id) { + base.setBiome(x, y, z, biomeMapping.byId(id)); + } + + @Override + public int getBiome(int x, int y, int z) { + return biomeMapping.getId(base.getBiome(x, y, z)); + } + }; + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public MCABiomeContainer newBiomeContainer(int min, int max, int[] data) { + MCAChunkBiomeContainer base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max, data); + return getBiomeContainerInterface(getBiomeMapping(), base); + } + + @Override + public int countCustomBiomes() { + AtomicInteger a = new AtomicInteger(0); + + getCustomBiomeRegistry().keySet().forEach((i) -> { + if (i.getNamespace().equals("minecraft")) { + return; + } + + a.incrementAndGet(); + Iris.debug("Custom Biome: " + i); + }); + + return a.get(); + } + + public boolean supportsDataPacks() { + return true; + } + + public void setBiomes(int cx, int cz, World world, Hunk biomes) { + LevelChunk c = ((CraftWorld) world).getHandle().getChunk(cx, cz); + biomes.iterateSync((x, y, z, b) -> c.setNoiseBiome(x, y, z, (Holder) b)); + c.markUnsaved(); + } + + @Override + public void forceBiomeInto(int x, int y, int z, Object somethingVeryDirty, ChunkGenerator.BiomeGrid chunk) { + try { + ChunkAccess s = (ChunkAccess) getFieldForBiomeStorage(chunk).get(chunk); + Holder biome = (Holder) somethingVeryDirty; + s.setNoiseBiome(x, y, z, biome); + } catch (IllegalAccessException e) { + Iris.reportError(e); + e.printStackTrace(); + } + } + + private Field getFieldForBiomeStorage(Object storage) { + Field f = biomeStorageCache; + + if (f != null) { + return f; + } + try { + f = storage.getClass().getDeclaredField("biome"); + f.setAccessible(true); + return f; + } catch (Throwable e) { + Iris.reportError(e); + e.printStackTrace(); + Iris.error(storage.getClass().getCanonicalName()); + } + + biomeStorageCache = f; + return null; + } + + @Override + public MCAPaletteAccess createPalette() { + MCAIdMapper registry = registryCache.aquireNasty(() -> { + Field cf = IdMapper.class.getDeclaredField("tToId"); + Field df = IdMapper.class.getDeclaredField("idToT"); + Field bf = IdMapper.class.getDeclaredField("nextId"); + cf.setAccessible(true); + df.setAccessible(true); + bf.setAccessible(true); + IdMapper blockData = Block.BLOCK_STATE_REGISTRY; + int b = bf.getInt(blockData); + Object2IntMap c = (Object2IntMap) cf.get(blockData); + List d = (List) df.get(blockData); + return new MCAIdMapper(c, d, b); + }); + MCAPalette global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, + i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState(), + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + ((CraftBlockData) AIR).getState()); + return new MCAWrappedPalettedContainer<>(container, + i -> NBTWorld.getCompound(CraftBlockData.createData(i)), + i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState()); + } + + @Override + public void injectBiomesFromMantle(Chunk e, Mantle mantle) { + ChunkAccess chunk = ((CraftChunk) e).getHandle(ChunkStatus.FULL); + AtomicInteger c = new AtomicInteger(); + AtomicInteger r = new AtomicInteger(); + mantle.iterateChunk(e.getX(), e.getZ(), MatterBiomeInject.class, (x, y, z, b) -> { + if (b != null) { + if (b.isCustom()) { + chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry().get(b.getBiomeId()).get()); + c.getAndIncrement(); + } else { + chunk.setNoiseBiome(x, y, z, (Holder) getBiomeBase(e.getWorld(), b.getBiome())); + r.getAndIncrement(); + } + } + }); + } + + public ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException { + if (customNbt != null && !customNbt.isEmpty()) { + net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack); + + try { + net.minecraft.nbt.CompoundTag tag = TagParser.parseCompoundFully((new JSONObject(customNbt)).toString()); + tag.merge(s.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag()); + s.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); + } catch (CommandSyntaxException var5) { + throw new IllegalArgumentException(var5); + } + + return CraftItemStack.asBukkitCopy(s); + } else { + return itemStack; + } + } + + public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException { + var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap; + var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class); + worldGenContextField.setAccessible(true); + var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap); + var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null); + if (dimensionType != null && !dimensionType.identifier().getNamespace().equals("iris")) + Iris.error("Loaded world %s with invalid dimension type! (%s)", world.getName(), dimensionType.identifier().toString()); + + var newContext = new WorldGenContext( + worldGenContext.level(), new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world), + worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener()); + + worldGenContextField.set(chunkMap, newContext); + } + + public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) { + Field[] fields = EntityType.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(EntityType.class)) { + try { + EntityType entityType = (EntityType) field.get(null); + if (entityType.getDescriptionId().equals("entity.minecraft." + entity.name().toLowerCase())) { + Vector v1 = new Vector<>(); + v1.add(entityType.getHeight()); + entityType.getDimensions(); + Vector3d box = new Vector3d( entityType.getWidth(), entityType.getHeight(), entityType.getWidth()); + //System.out.println("Entity Type: " + entityType.getDescriptionId() + ", " + "Height: " + height + ", Width: " + width); + return box; + } + } catch (IllegalAccessException e) { + Iris.error("Unable to get entity dimensions!"); + e.printStackTrace(); + } + } + } + return null; + } + + + @Override + public Entity spawnEntity(Location location, org.bukkit.entity.EntityType type, CreatureSpawnEvent.SpawnReason reason) { + return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason); + } + + @Override + public Color getBiomeColor(Location location, BiomeColor type) { + ServerLevel reader = ((CraftWorld) location.getWorld()).getHandle(); + var pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + var holder = reader.getBiome(pos); + var biome = holder.value(); + if (biome == null) throw new IllegalArgumentException("Invalid biome: " + holder.unwrapKey().orElse(null)); + + var attributes = reader.environmentAttributes(); + int rgba = switch (type) { + case FOG -> attributes.getValue(EnvironmentAttributes.FOG_COLOR, pos); + case WATER -> biome.getWaterColor(); + case WATER_FOG -> attributes.getValue(EnvironmentAttributes.WATER_FOG_COLOR, pos); + case SKY -> attributes.getValue(EnvironmentAttributes.SKY_COLOR, pos); + case FOLIAGE -> biome.getFoliageColor(); + case GRASS -> biome.getGrassColor(location.getBlockX(), location.getBlockZ()); + }; + if (rgba == 0) { + if (BiomeColor.FOLIAGE == type && biome.getSpecialEffects().foliageColorOverride().isEmpty()) + return null; + if (BiomeColor.GRASS == type && biome.getSpecialEffects().grassColorOverride().isEmpty()) + return null; + } + return new Color(rgba, true); + } + + private static Field getField(Class clazz, Class fieldType) throws NoSuchFieldException { + try { + for (Field f : clazz.getDeclaredFields()) { + if (f.getType().equals(fieldType)) + return f; + } + throw new NoSuchFieldException(fieldType.getName()); + } catch (NoSuchFieldException var4) { + Class superClass = clazz.getSuperclass(); + if (superClass == null) { + throw var4; + } else { + return getField(superClass, fieldType); + } + } + } + + public static Holder biomeToBiomeBase(Registry registry, Biome biome) { + return registry.getOrThrow(ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biome.getKey()))); + } + + @Override + public DataVersion getDataVersion() { + return Bukkit.getMinecraftVersion().startsWith("26.1") ? DataVersion.V26_1_2 : DataVersion.V26_2; + } + + @Override + public WorldStorage getWorldStorage() { + return worldStorage; + } + + @Override + public int getSpawnChunkCount(World world) { + return 0; + } + + @Override + public KList getStructureKeys() { + KList keys = new KList<>(); + + var registry = registry().lookup(Registries.STRUCTURE).orElse(null); + if (registry == null) return keys; + registry.keySet().stream().map(Identifier::toString).forEach(keys::add); + registry.getTags() + .map(HolderSet.Named::key) + .map(TagKey::location) + .map(Identifier::toString) + .map(s -> "#" + s) + .forEach(keys::add); + + return keys; + } + + @Override + public boolean missingDimensionTypes(String... keys) { + var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE); + return !Arrays.stream(keys) + .map(key -> Identifier.fromNamespaceAndPath("iris", key)) + .allMatch(type::containsKey); + } + + @Override + public boolean injectBukkit() { + if (injected.getAndSet(true)) + return true; + try { + Iris.info("Injecting Bukkit"); + var buddy = new ByteBuddy(); + buddy.redefine(ServerLevel.class) + .visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor() + .and(ElementMatchers.takesArgument(0, MinecraftServer.class)) + .and(ElementMatchers.takesArgument(5, LevelStem.class)) + .and(ElementMatchers.takesArgument(12, ChunkGenerator.class)))) + .make() + .load(ServerLevel.class.getClassLoader(), Agent.installed()); + for (Class clazz : List.of(ChunkAccess.class, ProtoChunk.class)) { + buddy.redefine(clazz) + .visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class)))) + .make() + .load(clazz.getClassLoader(), Agent.installed()); + } + + return true; + } catch (Throwable e) { + Iris.error(C.RED + "Failed to inject Bukkit"); + e.printStackTrace(); + } + return false; + } + + @Override + public KMap> getBlockProperties() { + KMap> states = new KMap<>(); + + for (var block : registry().lookupOrThrow(Registries.BLOCK)) { + var state = block.defaultBlockState(); + if (state == null) state = block.getStateDefinition().any(); + final var finalState = state; + + states.put(CraftMagicNumbers.getMaterial(block), block.getStateDefinition() + .getProperties() + .stream() + .map(p -> createProperty(p, finalState)) + .toList()); + } + return states; + } + + private > BlockProperty createProperty(Property property, BlockState state) { + return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName); + } + + @Override + public void placeStructures(Chunk chunk) { + var craft = ((CraftChunk) chunk); + var level = craft.getCraftWorld().getHandle(); + var access = ((CraftChunk) chunk).getHandle(ChunkStatus.FULL); + level.getChunkSource().getGenerator().applyBiomeDecoration(level, access, level.structureManager()); + } + + @Override + public KMap collectStructures() { + var structureSets = registry().lookupOrThrow(Registries.STRUCTURE_SET); + var structurePlacements = registry().lookupOrThrow(Registries.STRUCTURE_PLACEMENT); + return structureSets.keySet() + .stream() + .map(structureSets::get) + .filter(Optional::isPresent) + .map(Optional::get) + .map(holder -> { + var set = holder.value(); + var placement = set.placement(); + var key = holder.key().identifier(); + StructurePlacement.StructurePlacementBuilder builder; + if (placement instanceof RandomSpreadStructurePlacement random) { + builder = StructurePlacement.RandomSpread.builder() + .separation(random.separation()) + .spacing(random.spacing()) + .spreadType(switch (random.spreadType()) { + case LINEAR -> IrisJigsawStructurePlacement.SpreadType.LINEAR; + case TRIANGULAR -> IrisJigsawStructurePlacement.SpreadType.TRIANGULAR; + }); + } else if (placement instanceof ConcentricRingsStructurePlacement rings) { + builder = StructurePlacement.ConcentricRings.builder() + .distance(rings.distance()) + .spread(rings.spread()) + .count(rings.count()); + } else { + Iris.warn("Unsupported structure placement for set " + key + " with type " + structurePlacements.getKey(placement.type())); + return null; + } + + return new Pair<>(new com.volmit.iris.core.link.Identifier(key.getNamespace(), key.getPath()), builder + .salt(placement.salt) + .frequency(placement.frequency) + .structures(set.structures() + .stream() + .map(entry -> new StructurePlacement.Structure( + entry.weight(), + entry.structure() + .unwrapKey() + .map(ResourceKey::identifier) + .map(Identifier::toString) + .orElse(null), + entry.structure().tags() + .map(TagKey::location) + .map(Identifier::toString) + .toList() + )) + .filter(StructurePlacement.Structure::isValid) + .toList()) + .build()); + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Pair::getA, Pair::getB, (a, b) -> a, KMap::new)); + } + + public LevelStem levelStem(RegistryAccess access, ChunkGenerator raw) { + if (!(raw instanceof PlatformChunkGenerator gen)) + throw new IllegalStateException("Generator is not platform chunk generator!"); + + var dimensionKey = Identifier.fromNamespaceAndPath("iris", gen.getTarget().getDimension().getDimensionTypeKey()); + var dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey)); + return new LevelStem(dimensionType, chunkGenerator(access)); + } + + private net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator(RegistryAccess access) { + 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(); + return new FlatLevelSource(settings); + } + + private static class ChunkAccessAdvice { + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) { + return index >= access.getPostProcessing().length; + } + } + + private static class ServerLevelAdvice { + @Advice.OnMethodEnter + static void enter( + @Advice.Argument(0) MinecraftServer server, + @Advice.Argument(value = 5, readOnly = false) LevelStem levelStem, + @Advice.Argument(11) World.Environment env, + @Advice.Argument(12) ChunkGenerator gen + ) { + if (gen == null || !gen.getClass().getPackageName().startsWith("com.volmit.iris")) + return; + + try { + Object bindings = Class.forName("com.volmit.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris") + .getClass() + .getClassLoader()) + .getDeclaredMethod("get") + .invoke(null); + levelStem = (LevelStem) bindings.getClass() + .getDeclaredMethod("levelStem", RegistryAccess.class, ChunkGenerator.class) + .invoke(bindings, server.registryAccess(), gen); + } catch (Throwable e) { + throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e); + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 77a1d78eb..df27c7255 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,7 @@ rootProject.name = "Iris" include(":core", ":core:agent") include( + ":nms:v26_1_R1", ":nms:v1_21_R7", ":nms:v1_21_R6", ":nms:v1_21_R5",