Compare commits

..

34 Commits

Author SHA1 Message Date
Julian Krings 6d3edff459 make studio tools work on folia 2025-09-21 20:54:13 +02:00
Julian Krings e06724fcf6 Merge branch 'refs/heads/dev' into feat/folia
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	core/src/main/java/com/volmit/iris/core/tools/IrisCreator.java
#	core/src/main/java/com/volmit/iris/engine/framework/Engine.java
2025-09-21 20:41:11 +02:00
Julian Krings b8219fac1b Merge branch 'dev' into feat/folia
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	core/src/main/java/com/volmit/iris/engine/framework/Engine.java
#	core/src/main/java/com/volmit/iris/engine/object/IrisEntity.java
2025-09-06 18:37:29 +02:00
Julian Krings bddc061f46 make chunk retrieval to teleport async 2025-08-02 23:31:17 +02:00
Julian Krings bf6af9a58d Merge branch 'dev' into feat/folia 2025-08-02 23:26:34 +02:00
Julian Krings aaf2f2f8a6 fix teleport after world creation 2025-08-02 23:15:57 +02:00
Julian Krings dc8cf0ad38 remove last usages of the bukkit scheduler 2025-08-02 23:15:21 +02:00
Julian Krings bd07f5d325 add link for Worlds to bring back world creation on folia 2025-08-02 22:23:05 +02:00
Julian Krings bd722fdacb Merge branch 'dev' into feat/folia
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
#	gradle/libs.versions.toml
2025-08-02 21:25:40 +02:00
Julian Krings d5ec6a18a4 fix loot not being applied 2025-07-26 12:31:24 +02:00
Julian Krings 2f16c0cfb7 Merge branch 'dev' into feat/folia 2025-07-25 22:34:17 +02:00
Julian Krings f7ac827692 don't relocate platform utils for now 2025-07-25 22:34:05 +02:00
Julian Krings bddc62f385 fix object saving 2025-07-20 00:50:11 +02:00
Julian Krings 68a214edb5 move platform utils to the version catalog 2025-07-19 23:02:49 +02:00
Julian Krings 49d2392c80 use platform utils for regen 2025-07-19 22:55:25 +02:00
Julian Krings fcbbd2135b apply folia modification to 1.21.7 nms 2025-07-19 22:53:06 +02:00
Julian Krings c3442ab2ce Merge branch 'dev' into feat/folia
# Conflicts:
#	build.gradle.kts
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
#	core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	core/src/main/resources/plugin.yml
#	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
#	settings.gradle.kts
2025-07-19 22:50:48 +02:00
Julian Krings fd3971018b update platform utils 2025-06-23 16:16:25 +02:00
Julian Krings b440d0257d fix iris what commands 2025-06-19 19:50:08 +02:00
Julian Krings 42a26a1de2 fix spawning empty particles and improve message for invalid tile states 2025-06-19 19:43:53 +02:00
Julian Krings c8eab22427 fix a few sync teleports 2025-06-19 18:28:46 +02:00
Julian Krings fa3e35f702 fix pregen save chunk failing 2025-06-19 18:28:22 +02:00
Julian Krings cf0bc81778 replace scheduler and paperlib with platform utils 2025-06-19 17:46:08 +02:00
Julian Krings bef99f18c3 fix effects 2025-06-18 12:00:55 +02:00
Julian Krings 96a384c09c fix modern tile states not applying 2025-06-18 11:51:09 +02:00
Julian Krings d61b2205c0 handle failing restart command better 2025-06-18 11:25:41 +02:00
Julian Krings ebdfb94392 Merge branch 'dev' into feat/folia 2025-06-17 21:28:29 +02:00
Julian Krings 1c5fe016cb handle failing world creation 2025-06-15 12:36:53 +02:00
Julian Krings 0957b9baf2 Merge branch 'dev' into feat/folia
# Conflicts:
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-15 12:24:18 +02:00
Julian Krings 7570064b1a fix locate command not teleporting 2025-06-14 19:36:32 +02:00
Julian Krings e461c1e199 fix updater and mob spawning instantly failing 2025-06-14 19:35:28 +02:00
Julian Krings 35b879f0df get iris to load on folia, but it will not load worlds as this is currently not possible 2025-06-13 21:44:02 +02:00
Julian Krings ba6fac5422 more helper methods 2025-06-13 21:05:11 +02:00
Julian Krings 2577344ac0 implement platform specific schedulers 2025-06-13 19:48:42 +02:00
1244 changed files with 92462 additions and 53216 deletions
+1 -3
View File
@@ -10,6 +10,4 @@ libs/
collection/
/core/src/main/java/art/arcane/iris/util/uniques/
DataPackExamples/
/core/src/main/java/com/volmit/iris/util/uniques/

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+5 -5
View File
@@ -7,7 +7,7 @@ The master branch is for the latest version of minecraft.
# Building
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.[README.md](README.md)
development.
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
@@ -15,17 +15,17 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
### Command Line Builds
1. Install [Java JDK 25](https://adoptium.net/temurin/releases/?version=25)
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
* Windows
1. Start > Type `env` and press Enter
2. Advanced > Environment Variables
3. Under System Variables, click `New...`
4. Variable Name: `JAVA_HOME`
5. Variable Value: `C:\Program Files\Java\jdk-25` (verify this exists after installing java don't just copy
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
the example text)
* MacOS
1. Run `/usr/libexec/java_home -V` and look for Java 25
1. Run `/usr/libexec/java_home -V` and look for Java 21
2. Run `sudo nano ~/.zshenv`
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
@@ -45,7 +45,7 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
Everyone needs a tool-belt.
```java
package art.arcane.iris.core.tools;
package com.volmit.iris.core.tools;
// Get IrisDataManager from a world
IrisToolbelt.access(anyWorld).getCompound().getData();
-270
View File
@@ -1,270 +0,0 @@
import de.undercouch.gradle.tasks.download.Download
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar
import org.gradle.jvm.toolchain.JavaLanguageVersion
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 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/>.
*/
buildscript {
repositories {
maven {
url = uri('https://jitpack.io')
}
}
dependencies {
classpath('com.github.VolmitSoftware:NMSTools:c88961416f')
}
}
plugins {
id 'java'
id 'java-library'
alias(libs.plugins.download)
}
group = 'art.arcane'
version = '4.0.0-26.1'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
.get()
apply plugin: ApiGenerator
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
// ======================== WINDOWS =============================
registerCustomOutputTask('Cyberpwn', 'C://Users/cyberpwn/Documents/development/server/plugins')
registerCustomOutputTask('Psycho', 'C://Dan/MinecraftDevelopment/Server/plugins')
registerCustomOutputTask('ArcaneArts', 'C://Users/arcane/Documents/development/server/plugins')
registerCustomOutputTask('Coco', 'D://mcsm/plugins')
registerCustomOutputTask('Strange', 'D://Servers/1.17 Test Server/plugins')
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.19.4/plugins')
registerCustomOutputTask('CrazyDev22', 'C://Users/Julian/Desktop/server/plugins')
registerCustomOutputTask('PixelFury', 'C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins')
registerCustomOutputTask('PixelFuryDev', 'C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins')
// ========================== UNIX ==============================
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins')
registerCustomOutputTaskUnix('PsychoLT', '/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/dropins/plugins')
registerCustomOutputTaskUnix('PixelMac', '/Users/test/Desktop/mcserver/plugins')
registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugins')
// ==============================================================
def nmsBindings = [
v1_21_R7: '1.21.11-R0.1-SNAPSHOT',
]
Class nmsTypeClass = Class.forName('NMSBinding$Type')
nmsBindings.each { key, value ->
project(":nms:${key}") {
apply plugin: JavaPlugin
def nmsConfig = new Config()
nmsConfig.jvm = 25
nmsConfig.version = value
nmsConfig.type = Enum.valueOf(nmsTypeClass, 'USER_DEV')
extensions.extraProperties.set('nms', nmsConfig)
plugins.apply(NMSBinding)
dependencies {
compileOnly(project(':core'))
compileOnly(volmLibCoordinate) {
changing = true
transitive = false
}
compileOnly(rootProject.libs.annotations)
compileOnly(rootProject.libs.byteBuddy.core)
}
}
}
def included = configurations.create('included')
def jarJar = configurations.create('jarJar')
dependencies {
nmsBindings.keySet().each { key ->
add('included', project(path: ":nms:${key}", configuration: 'runtimeElements'))
}
add('included', project(path: ':core', configuration: 'shadow'))
add('jarJar', project(':core:agent'))
}
tasks.named('jar', Jar).configure {
inputs.files(included)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(jarJar, provider { included.resolve().collect { zipTree(it) } })
archiveFileName.set("Iris-${project.version}.jar")
}
tasks.register('iris', Copy) {
group = 'iris'
dependsOn('jar')
from(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
into(layout.buildDirectory)
}
tasks.register('irisDev', Copy) {
group = 'iris'
from(project(':core').layout.buildDirectory.files('libs/core-javadoc.jar', 'libs/core-sources.jar'))
rename { String fileName -> fileName.replace('core', "Iris-${project.version}") }
into(layout.buildDirectory)
dependsOn(':core:sourcesJar')
dependsOn(':core:javadocJar')
}
def cli = file('sentry-cli.exe')
tasks.register('downloadCli', Download) {
group = 'io.sentry'
src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty('os.name')}&package=sentry-cli")
dest(cli)
doLast {
cli.setExecutable(true)
}
}
tasks.register('release') {
group = 'io.sentry'
dependsOn('downloadCli')
doLast {
String url = 'http://sentry.volmit.com:8080'
def authToken = project.findProperty('sentry.auth.token') ?: System.getenv('SENTRY_AUTH_TOKEN')
String org = 'sentry'
String projectName = 'iris'
runCommand(cli, '--url', url, '--auth-token', authToken, 'releases', 'new', '-o', org, '-p', projectName, version)
runCommand(cli, '--url', url, '--auth-token', authToken, 'releases', 'set-commits', '-o', org, '-p', projectName, version, '--auto', '--ignore-missing')
//exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version)
cli.delete()
}
}
void runCommand(Object... command) {
Process process = new ProcessBuilder(command.collect { it.toString() }).start()
process.inputStream.readLines().each { println(it) }
process.errorStream.readLines().each { println(it) }
process.waitFor()
}
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor(0, 'seconds')
resolutionStrategy.cacheDynamicVersionsFor(0, 'seconds')
}
allprojects {
apply plugin: 'java'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
maven { url = uri('https://repo.papermc.io/repository/maven-public/') }
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
maven { url = uri('https://jitpack.io') } // EcoItems, score
maven { url = uri('https://repo.nexomc.com/releases/') } // nexo
maven { url = uri('https://maven.devs.beer/') } // itemsadder
maven { url = uri('https://repo.extendedclip.com/releases/') } // placeholderapi
maven { url = uri('https://mvn.lumine.io/repository/maven-public/') } // mythic
maven { url = uri('https://nexus.phoenixdevt.fr/repository/maven-public/') } //MMOItems
maven { url = uri('https://repo.onarandombox.com/content/groups/public/') } //Multiverse Core
maven { url = uri('https://repo.momirealms.net/releases/') } // CraftEngine
}
dependencies {
// Provided or Classpath
compileOnly(rootProject.libs.lombok)
annotationProcessor(rootProject.libs.lombok)
}
/**
* We need parameter meta for the decree command system
*/
tasks.named('compileJava', JavaCompile).configure {
options.compilerArgs.add('-parameters')
options.encoding = 'UTF-8'
options.debugOptions.debugLevel = 'none'
options.release.set(25)
}
tasks.named('javadoc').configure {
options.encoding = 'UTF-8'
options.quiet()
//options.addStringOption("Xdoclint:none") // TODO: Re-enable this
}
tasks.register('sourcesJar', Jar) {
archiveClassifier.set('sources')
from(sourceSets.main.allSource)
}
tasks.register('javadocJar', Jar) {
archiveClassifier.set('javadoc')
from(tasks.named('javadoc').map { it.destinationDir })
}
}
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) {
System.err.println()
System.err.println('=========================================================================================================')
System.err.println('You must run gradle on Java 25 or newer. You are using ' + JavaVersion.current())
System.err.println()
System.err.println('=== For IDEs ===')
System.err.println('1. Configure the project for Java 25 toolchain')
System.err.println('2. Configure the bundled gradle to use Java 25+ in settings')
System.err.println()
System.err.println('=== For Command Line (gradlew) ===')
System.err.println('1. Install JDK 25 from https://adoptium.net/temurin/releases/?version=25')
System.err.println('2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-25')
System.err.println('3. Open a new command prompt window to get the new environment variables if need be.')
System.err.println('=========================================================================================================')
System.err.println()
System.exit(69)
}
void registerCustomOutputTask(String name, String path) {
if (!System.getProperty('os.name').toLowerCase().contains('windows')) {
return
}
tasks.register("build${name}", Copy) {
group = 'development'
outputs.upToDateWhen { false }
dependsOn('iris')
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
into(file(path))
rename { String ignored -> 'Iris.jar' }
}
}
void registerCustomOutputTaskUnix(String name, String path) {
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
return
}
tasks.register("build${name}", Copy) {
group = 'development'
outputs.upToDateWhen { false }
dependsOn('iris')
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
into(file(path))
rename { String ignored -> 'Iris.jar' }
}
}
+301
View File
@@ -0,0 +1,301 @@
import com.volmit.nmstools.NMSToolsExtension
import com.volmit.nmstools.NMSToolsPlugin
import de.undercouch.gradle.tasks.download.Download
import xyz.jpenilla.runpaper.task.RunServer
import xyz.jpenilla.runtask.service.DownloadsAPIService
import kotlin.system.exitProcess
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 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/>.
*/
buildscript {
repositories.maven("https://jitpack.io")
dependencies.classpath("com.github.VolmitSoftware:NMSTools:c5cbc46ce6")
}
plugins {
java
`java-library`
alias(libs.plugins.shadow)
alias(libs.plugins.download)
alias(libs.plugins.runPaper)
}
group = "com.volmit"
version = "3.7.2-1.20.1-1.21.8"
apply<ApiGenerator>()
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
// ======================== WINDOWS =============================
registerCustomOutputTask("Cyberpwn", "C://Users/cyberpwn/Documents/development/server/plugins")
registerCustomOutputTask("Psycho", "C://Dan/MinecraftDevelopment/Server/plugins")
registerCustomOutputTask("ArcaneArts", "C://Users/arcane/Documents/development/server/plugins")
registerCustomOutputTask("Coco", "D://mcsm/plugins")
registerCustomOutputTask("Strange", "D://Servers/1.17 Test Server/plugins")
registerCustomOutputTask("Vatuu", "D://Minecraft/Servers/1.19.4/plugins")
registerCustomOutputTask("CrazyDev22", "C://Users/Julian/Desktop/server/plugins")
registerCustomOutputTask("PixelFury", "C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins")
registerCustomOutputTask("PixelFuryDev", "C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins")
// ========================== UNIX ==============================
registerCustomOutputTaskUnix("CyberpwnLT", "/Users/danielmills/development/server/plugins")
registerCustomOutputTaskUnix("PsychoLT", "/Users/brianfopiano/Developer/RemoteGit/Server/plugins")
registerCustomOutputTaskUnix("PixelMac", "/Users/test/Desktop/mcserver/plugins")
registerCustomOutputTaskUnix("CrazyDev22LT", "/home/julian/Desktop/server/plugins")
// ==============================================================
val serverMinHeap = "2G"
val serverMaxHeap = "8G"
//Valid values are: none, truecolor, indexed256, indexed16, indexed8
val color = "truecolor"
val errorReporting = findProperty("errorReporting") as Boolean? ?: false
val nmsBindings = mapOf(
"v1_21_R5" to "1.21.7-R0.1-SNAPSHOT",
"v1_21_R4" to "1.21.5-R0.1-SNAPSHOT",
"v1_21_R3" to "1.21.4-R0.1-SNAPSHOT",
"v1_21_R2" to "1.21.3-R0.1-SNAPSHOT",
"v1_21_R1" to "1.21.1-R0.1-SNAPSHOT",
"v1_20_R4" to "1.20.6-R0.1-SNAPSHOT",
"v1_20_R3" to "1.20.4-R0.1-SNAPSHOT",
"v1_20_R2" to "1.20.2-R0.1-SNAPSHOT",
"v1_20_R1" to "1.20.1-R0.1-SNAPSHOT",
)
val jvmVersion = mapOf<String, Int>()
nmsBindings.forEach { key, value ->
project(":nms:$key") {
apply<JavaPlugin>()
apply<NMSToolsPlugin>()
repositories {
maven("https://libraries.minecraft.net")
}
extensions.configure(NMSToolsExtension::class) {
jvm = jvmVersion.getOrDefault(key, 21)
version = value
}
dependencies {
compileOnly(project(":core"))
compileOnly(rootProject.libs.annotations)
compileOnly(rootProject.libs.byteBuddy.core)
compileOnly(rootProject.libs.platformUtils) {
isTransitive = false
}
}
}
tasks.register<RunServer>("runServer-$key") {
group = "servers"
minecraftVersion(value.split("-")[0])
minHeapSize = serverMinHeap
maxHeapSize = serverMaxHeap
pluginJars(tasks.jar.flatMap { it.archiveFile })
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(jvmVersion.getOrDefault(key, 21))}
runDirectory.convention(layout.buildDirectory.dir("run/$key"))
systemProperty("disable.watchdog", "true")
systemProperty("net.kyori.ansi.colorLevel", color)
systemProperty("com.mojang.eula.agree", true)
systemProperty("iris.suppressReporting", !errorReporting)
jvmArgs("-javaagent:${project(":core:agent").tasks.jar.flatMap { it.archiveFile }.get().asFile.absolutePath}")
}
tasks.register<RunServer>("runFolia-$key") {
group = "servers"
downloadsApiService = DownloadsAPIService.folia(project)
minecraftVersion(value.split("-")[0])
minHeapSize = serverMinHeap
maxHeapSize = serverMaxHeap
pluginJars(tasks.jar.flatMap { it.archiveFile })
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(jvmVersion.getOrDefault(key, 21))}
runDirectory.convention(layout.buildDirectory.dir("run/$key"))
systemProperty("disable.watchdog", "")
systemProperty("net.kyori.ansi.colorLevel", color)
systemProperty("com.mojang.eula.agree", true)
systemProperty("iris.suppressReporting", !errorReporting)
jvmArgs("-javaagent:${project(":core:agent").tasks.jar.flatMap { it.archiveFile }.get().asFile.absolutePath}")
}
}
tasks {
jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
nmsBindings.forEach { key, _ ->
from(project(":nms:$key").tasks.named("remap").map { zipTree(it.outputs.files.singleFile) })
}
from(project(":core").tasks.shadowJar.flatMap { it.archiveFile }.map { zipTree(it) })
from(project(":core:agent").tasks.jar.flatMap { it.archiveFile })
archiveFileName.set("Iris-${project.version}.jar")
}
register<Copy>("iris") {
group = "iris"
dependsOn("jar")
from(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
into(layout.buildDirectory)
}
register<Copy>("irisDev") {
group = "iris"
from(project(":core").layout.buildDirectory.files("libs/core-javadoc.jar", "libs/core-sources.jar"))
rename { it.replace("core", "Iris-${project.version}") }
into(layout.buildDirectory)
dependsOn(":core:sourcesJar")
dependsOn(":core:javadocJar")
}
val cli = file("sentry-cli.exe")
register<Download>("downloadCli") {
group = "io.sentry"
src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty("os.name")}&package=sentry-cli")
dest(cli)
doLast {
cli.setExecutable(true)
}
}
register("release") {
group = "io.sentry"
dependsOn("downloadCli")
doLast {
val url = "http://sentry.volmit.com:8080"
val authToken = project.findProperty("sentry.auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN")
val org = "sentry"
val projectName = "iris"
exec(cli, "--url", url , "--auth-token", authToken, "releases", "new", "-o", org, "-p", projectName, version)
exec(cli, "--url", url , "--auth-token", authToken, "releases", "set-commits", "-o", org, "-p", projectName, version, "--auto", "--ignore-missing")
//exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version)
cli.delete()
}
}
}
fun exec(vararg command: Any) {
val p = ProcessBuilder(command.map { it.toString() })
.start()
p.inputStream.reader().useLines { it.forEach(::println) }
p.errorStream.reader().useLines { it.forEach(::println) }
p.waitFor()
}
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor(60, "minutes")
resolutionStrategy.cacheDynamicVersionsFor(60, "minutes")
}
allprojects {
apply<JavaPlugin>()
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.codemc.org/repository/maven-public/")
maven("https://jitpack.io") // EcoItems, score
maven("https://repo.nexomc.com/releases/") // nexo
maven("https://maven.devs.beer/") // itemsadder
maven("https://repo.extendedclip.com/releases/") // placeholderapi
maven("https://mvn.lumine.io/repository/maven-public/") // mythic
maven("https://nexus.phoenixdevt.fr/repository/maven-public/") //MMOItems
maven("https://repo.onarandombox.com/content/groups/public/") //Multiverse Core
maven("https://repo.thenextlvl.net/releases") //Worlds
}
dependencies {
// Provided or Classpath
compileOnly(rootProject.libs.lombok)
annotationProcessor(rootProject.libs.lombok)
}
/**
* We need parameter meta for the decree command system
*/
tasks {
compileJava {
options.compilerArgs.add("-parameters")
options.encoding = "UTF-8"
}
javadoc {
options.encoding = "UTF-8"
options.quiet()
//options.addStringOption("Xdoclint:none") // TODO: Re-enable this
}
register<Jar>("sourcesJar") {
archiveClassifier.set("sources")
from(sourceSets.main.map { it.allSource })
}
register<Jar>("javadocJar") {
archiveClassifier.set("javadoc")
from(javadoc.map { it.destinationDir!! })
}
}
}
if (JavaVersion.current().toString() != "21") {
System.err.println()
System.err.println("=========================================================================================================")
System.err.println("You must run gradle on Java 21. You are using " + JavaVersion.current())
System.err.println()
System.err.println("=== For IDEs ===")
System.err.println("1. Configure the project for Java 21")
System.err.println("2. Configure the bundled gradle to use Java 21 in settings")
System.err.println()
System.err.println("=== For Command Line (gradlew) ===")
System.err.println("1. Install JDK 21 from https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html")
System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-21.0.4")
System.err.println("3. Open a new command prompt window to get the new environment variables if need be.")
System.err.println("=========================================================================================================")
System.err.println()
exitProcess(69)
}
fun registerCustomOutputTask(name: String, path: String) {
if (!System.getProperty("os.name").lowercase().contains("windows")) {
return
}
tasks.register<Copy>("build$name") {
group = "development"
outputs.upToDateWhen { false }
dependsOn("iris")
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
into(file(path))
rename { "Iris.jar" }
}
}
fun registerCustomOutputTaskUnix(name: String, path: String) {
if (System.getProperty("os.name").lowercase().contains("windows")) {
return
}
tasks.register<Copy>("build$name") {
group = "development"
outputs.upToDateWhen { false }
dependsOn("iris")
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
into(file(path))
rename { "Iris.jar" }
}
}
-25
View File
@@ -1,25 +0,0 @@
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id 'java'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
gradlePluginPortal()
maven {
url = uri('https://jitpack.io')
}
}
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')
}
+11
View File
@@ -0,0 +1,11 @@
plugins {
kotlin("jvm") version "2.0.20"
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.ow2.asm:asm:9.8")
}
-162
View File
@@ -1,162 +0,0 @@
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.publish.maven.MavenPublication;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.jvm.tasks.Jar;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
public class ApiGenerator implements Plugin<Project> {
@Override
public void apply(Project target) {
target.getPlugins().apply("maven-publish");
TaskProvider<GenerateApiTask> task = target.getTasks().register("irisApi", GenerateApiTask.class);
PublishingExtension publishing = target.getExtensions().findByType(PublishingExtension.class);
if (publishing == null) {
throw new GradleException("Publishing extension not found");
}
publishing.getRepositories().maven(repository -> {
repository.setName("deployDir");
repository.setUrl(targetDirectory(target).toURI());
});
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
publication.setGroupId(target.getName());
publication.setVersion(target.getVersion().toString());
publication.artifact(task);
});
}
public static File targetDirectory(Project project) {
String dir = System.getenv("DEPLOY_DIR");
if (dir == null) {
return project.getLayout().getBuildDirectory().dir("api").get().getAsFile();
}
return new File(dir);
}
}
abstract class GenerateApiTask extends DefaultTask {
private final File inputFile;
private final File outputFile;
public GenerateApiTask() {
setGroup("iris");
dependsOn("jar");
finalizedBy("publishMavenPublicationToDeployDirRepository");
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().getAbsolutePath()));
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
this.inputFile = jarTask.get().getArchiveFile().get().getAsFile();
this.outputFile = ApiGenerator.targetDirectory(getProject()).toPath().resolve(this.inputFile.getName()).toFile();
}
@InputFile
public File getInputFile() {
return inputFile;
}
@OutputFile
public File getOutputFile() {
return outputFile;
}
@TaskAction
public void generate() throws IOException {
File parent = outputFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
try (JarFile jar = new JarFile(inputFile);
JarOutputStream out = new JarOutputStream(new FileOutputStream(outputFile))) {
jar.stream()
.parallel()
.filter(entry -> !entry.isDirectory())
.filter(entry -> entry.getName().endsWith(".class"))
.forEach(entry -> writeStrippedClass(jar, out, entry));
}
}
private static void writeStrippedClass(JarFile jar, JarOutputStream out, JarEntry entry) {
byte[] bytes;
try (InputStream input = jar.getInputStream(entry)) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new MethodClearingVisitor(writer);
ClassReader reader = new ClassReader(input);
reader.accept(visitor, 0);
bytes = writer.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
synchronized (out) {
try {
JarEntry outputEntry = new JarEntry(entry.getName());
out.putNextEntry(outputEntry);
out.write(bytes);
out.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
class MethodClearingVisitor extends ClassVisitor {
public MethodClearingVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions));
}
}
class ExceptionThrowingMethodVisitor extends MethodVisitor {
public ExceptionThrowingMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
if (mv == null) {
return;
}
mv.visitCode();
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException");
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn("Only API");
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"java/lang/IllegalStateException",
"<init>",
"(Ljava/lang/String;)V",
false
);
mv.visitInsn(Opcodes.ATHROW);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
}
-5
View File
@@ -1,5 +0,0 @@
public class Config {
public int jvm = 25;
public NMSBinding.Type type = NMSBinding.Type.DIRECT;
public String version;
}
-282
View File
@@ -1,282 +0,0 @@
import com.volmit.nmstools.NMSToolsExtension;
import com.volmit.nmstools.NMSToolsPlugin;
import io.papermc.paperweight.userdev.PaperweightUser;
import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension;
import io.papermc.paperweight.userdev.PaperweightUserExtension;
import io.papermc.paperweight.userdev.attribute.Obfuscation;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Named;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Bundling;
import org.gradle.api.attributes.Category;
import org.gradle.api.attributes.LibraryElements;
import org.gradle.api.attributes.Usage;
import org.gradle.api.file.FileTree;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskAction;
import org.gradle.jvm.toolchain.JavaLanguageVersion;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.gradle.work.DisableCachingByDefault;
import javax.inject.Inject;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static io.papermc.paperweight.util.constants.ConstantsKt.REOBF_CONFIG;
public class NMSBinding implements Plugin<Project> {
private static final String NEW_LINE = System.lineSeparator();
private static final byte[] NEW_LINE_BYTES = NEW_LINE.getBytes(StandardCharsets.UTF_8);
@Override
public void apply(Project target) {
ExtraPropertiesExtension extra = target.getExtensions().getExtraProperties();
Object configValue = extra.has("nms") ? extra.get("nms") : null;
if (!(configValue instanceof Config)) {
throw new GradleException("No NMS binding configuration found");
}
Config config = (Config) configValue;
int jvm = config.jvm;
Type type = config.type;
if (type == Type.USER_DEV) {
target.getPlugins().apply(PaperweightUser.class);
PaperweightUserDependenciesExtension dependenciesExtension =
target.getDependencies().getExtensions().findByType(PaperweightUserDependenciesExtension.class);
if (dependenciesExtension != null) {
dependenciesExtension.paperDevBundle(config.version);
}
JavaPluginExtension java = target.getExtensions().findByType(JavaPluginExtension.class);
if (java == null) {
throw new GradleException("Java plugin not found");
}
java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.of(jvm));
JavaToolchainService javaToolchains = target.getExtensions().getByType(JavaToolchainService.class);
target.getExtensions().configure(PaperweightUserExtension.class,
extension -> extension.getJavaLauncher().set(javaToolchains.launcherFor(java.getToolchain())));
} else {
extra.set("nmsTools.useBuildTools", type == Type.BUILD_TOOLS);
target.getPlugins().apply(NMSToolsPlugin.class);
target.getExtensions().configure(NMSToolsExtension.class, extension -> {
extension.getJvm().set(jvm);
extension.getVersion().set(config.version);
});
}
String outgoingArtifactTask = type == Type.USER_DEV ? "jar" : "remap";
ObjectFactory objects = target.getObjects();
Configuration reobfConfiguration = target.getConfigurations().findByName(REOBF_CONFIG);
if (reobfConfiguration == null) {
reobfConfiguration = target.getConfigurations().create(REOBF_CONFIG);
}
target.getConfigurations().named(REOBF_CONFIG).configure(configuration -> {
configuration.setCanBeConsumed(true);
configuration.setCanBeResolved(false);
configuration.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, named(objects, Usage.class, Usage.JAVA_RUNTIME));
configuration.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, named(objects, Category.class, Category.LIBRARY));
configuration.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named(objects, LibraryElements.class, LibraryElements.JAR));
configuration.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, named(objects, Bundling.class, Bundling.EXTERNAL));
configuration.getAttributes().attribute(Obfuscation.Companion.getOBFUSCATION_ATTRIBUTE(), named(objects, Obfuscation.class, Obfuscation.OBFUSCATED));
configuration.getOutgoing().artifact(target.getTasks().named(outgoingArtifactTask));
});
int[] version = parseVersion(config.version);
int major = version[0];
int minor = version[1];
if (major <= 20 && minor <= 4) {
return;
}
target.getTasks().register("convert", ConversionTask.class, type);
target.getTasks().named("compileJava").configure(task -> task.dependsOn("convert"));
target.getRootProject().getTasks()
.matching(task -> task.getName().equals("prepareKotlinBuildScriptModel"))
.configureEach(task -> task.dependsOn(target.getPath() + ":convert"));
}
public static void nmsBinding(Project project, Action<Config> action) {
Config config = new Config();
action.execute(config);
project.getExtensions().getExtraProperties().set("nms", config);
project.getPlugins().apply(NMSBinding.class);
}
private static int[] parseVersion(String version) {
String trimmed = version;
int suffix = trimmed.indexOf('-');
if (suffix >= 0) {
trimmed = trimmed.substring(0, suffix);
}
String[] parts = trimmed.split("\\.");
return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])};
}
private static <T extends Named> T named(ObjectFactory objects, Class<T> type, String name) {
return objects.named(type, name);
}
@DisableCachingByDefault
public abstract static class ConversionTask extends DefaultTask {
private final Pattern pattern;
private final String replacement;
@Inject
public ConversionTask(Type type) {
setGroup("nms");
getInputs().property("type", type);
JavaPluginExtension java = getProject().getExtensions().findByType(JavaPluginExtension.class);
if (java == null) {
throw new GradleException("Java plugin not found");
}
Provider<FileTree> source = java.getSourceSets().named(SourceSet.MAIN_SOURCE_SET_NAME).map(SourceSet::getAllJava);
getInputs().files(source);
getOutputs().files(source);
if (type == Type.USER_DEV) {
this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\." + getProject().getName());
this.replacement = "org.bukkit.craftbukkit";
} else {
this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\.(?!" + getProject().getName() + ")");
this.replacement = "org.bukkit.craftbukkit." + getProject().getName() + ".";
}
}
@TaskAction
public void process() {
ExecutorService executor = Executors.newFixedThreadPool(16);
try {
Set<File> files = getInputs().getFiles().getFiles();
List<Future<?>> futures = new ArrayList<>(files.size());
for (File file : files) {
if (!file.getName().endsWith(".java")) {
continue;
}
futures.add(executor.submit(() -> processFile(file)));
}
for (Future<?> future : futures) {
future.get();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
} finally {
executor.shutdown();
}
}
private void processFile(File file) {
List<String> output = new ArrayList<>();
boolean changed = false;
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("package") || line.isBlank()) {
output.add(line);
continue;
}
if (!line.startsWith("import")) {
if (!changed) {
return;
}
output.add(line);
continue;
}
Matcher matcher = pattern.matcher(line);
if (!matcher.find()) {
output.add(line);
continue;
}
output.add(matcher.replaceAll(replacement));
changed = true;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!changed) {
return;
}
try {
if (hasTrailingNewLine(file)) {
output.add("");
}
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
for (int i = 0; i < output.size(); i++) {
writer.append(output.get(i));
if (i + 1 < output.size()) {
writer.append(NEW_LINE);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean hasTrailingNewLine(File file) throws IOException {
if (NEW_LINE_BYTES.length == 0) {
return false;
}
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
if (raf.length() < NEW_LINE_BYTES.length) {
return false;
}
byte[] bytes = new byte[NEW_LINE_BYTES.length];
raf.seek(raf.length() - bytes.length);
raf.readFully(bytes);
return Arrays.equals(bytes, NEW_LINE_BYTES);
}
}
}
public enum Type {
USER_DEV,
BUILD_TOOLS,
DIRECT
}
}
+121
View File
@@ -0,0 +1,121 @@
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.tasks.Jar
import org.objectweb.asm.*
import java.io.File
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
class ApiGenerator : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins.apply("maven-publish")
val task = tasks.register("irisApi", GenerateApiTask::class.java)
extensions.findByType(PublishingExtension::class.java)!!.apply {
repositories.maven {
it.name = "deployDir"
it.url = targetDirectory.toURI()
}
publications.create("maven", MavenPublication::class.java) {
it.groupId = name
it.version = version.toString()
it.artifact(task)
}
}
}
}
abstract class GenerateApiTask : DefaultTask() {
init {
group = "iris"
dependsOn("jar")
finalizedBy("publishMavenPublicationToDeployDirRepository")
doLast {
logger.lifecycle("The API is located at ${outputFile.absolutePath}")
}
}
@InputFile
val inputFile: File = project.tasks
.named("jar", Jar::class.java)
.get()
.archiveFile
.get()
.asFile
@OutputFile
val outputFile: File = project.targetDirectory.resolve(inputFile.name)
@TaskAction
fun generate() {
JarFile(inputFile).use { jar ->
JarOutputStream(outputFile.apply { parentFile?.mkdirs() }.outputStream()).use { out ->
jar.stream()
.parallel()
.filter { !it.isDirectory }
.filter { it.name.endsWith(".class") }
.forEach {
val bytes = jar.getInputStream(it).use { input ->
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
val visitor = MethodClearingVisitor(writer)
ClassReader(input).accept(visitor, 0)
writer.toByteArray()
}
synchronized(out) {
out.putNextEntry(it)
out.write(bytes)
out.closeEntry()
}
}
}
}
}
}
val Project.targetDirectory: File get() {
val dir = System.getenv("DEPLOY_DIR") ?: return project.layout.buildDirectory.dir("api").get().asFile
return File(dir)
}
private class MethodClearingVisitor(
cv: ClassVisitor
) : ClassVisitor(Opcodes.ASM9, cv) {
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
) = ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions))
}
private class ExceptionThrowingMethodVisitor(
mv: MethodVisitor
) : MethodVisitor(Opcodes.ASM9, mv) {
override fun visitCode() {
if (mv == null) return
mv.visitCode()
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException")
mv.visitInsn(Opcodes.DUP)
mv.visitLdcInsn("Only API")
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"java/lang/IllegalStateException",
"<init>", "(Ljava/lang/String;)V", false
)
mv.visitInsn(Opcodes.ATHROW)
mv.visitMaxs(0, 0)
mv.visitEnd()
}
}
-14
View File
@@ -1,14 +0,0 @@
import org.gradle.jvm.tasks.Jar
plugins {
id 'java'
}
tasks.named('jar', Jar).configure {
manifest.attributes(
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
'Premain-Class': 'art.arcane.iris.util.project.agent.Installer',
'Can-Redefine-Classes': true,
'Can-Retransform-Classes': true
)
}
+12
View File
@@ -0,0 +1,12 @@
plugins {
java
}
tasks.jar {
manifest.attributes(
"Agent-Class" to "com.volmit.iris.util.agent.Installer",
"Premain-Class" to "com.volmit.iris.util.agent.Installer",
"Can-Redefine-Classes" to true,
"Can-Retransform-Classes" to true
)
}
@@ -1,4 +1,4 @@
package art.arcane.iris.util.project.agent;
package com.volmit.iris.util.agent;
import java.lang.instrument.Instrumentation;
-298
View File
@@ -1,298 +0,0 @@
import io.github.slimjar.resolver.data.Mirror
import org.ajoberstar.grgit.Grgit
import org.gradle.api.Task
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.net.URI
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 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/>.
*/
plugins {
id 'java'
id 'java-library'
alias(libs.plugins.shadow)
alias(libs.plugins.sentry)
alias(libs.plugins.slimjar)
alias(libs.plugins.grgit)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.lombok)
}
def apiVersion = '1.21'
def mainClass = 'art.arcane.iris.Iris'
def lib = 'art.arcane.iris.util'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
.get()
String sentryAuthToken = findProperty('sentry.auth.token') as String ?: System.getenv('SENTRY_AUTH_TOKEN')
boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank()
/**
* Dependencies.
*
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
*
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
* need to be shaded into the jar (increasing binary size)
*
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
* these dependencies if they are available on mvn central.
*/
dependencies {
// Provided or Classpath
compileOnly(libs.spigot)
compileOnly(libs.log4j.api)
compileOnly(libs.log4j.core)
// Third Party Integrations
compileOnly(libs.nexo)
compileOnly(libs.itemsadder)
compileOnly(libs.placeholderApi)
compileOnly(libs.score)
compileOnly(libs.mmoitems)
compileOnly(libs.ecoitems)
compileOnly(libs.mythic)
compileOnly(libs.mythicChrucible)
compileOnly(libs.kgenerators) {
transitive = false
}
compileOnly(libs.multiverseCore)
compileOnly(libs.craftengine.core)
compileOnly(libs.craftengine.bukkit)
// Shaded
implementation('de.crazydev22.slimjar.helper:spigot:2.1.9')
implementation(volmLibCoordinate) {
changing = true
transitive = false
}
// Dynamically Loaded
slim(libs.paralithic)
slim(libs.paperlib)
slim(libs.adventure.api)
slim(libs.adventure.minimessage)
slim(libs.adventure.platform)
slim(libs.bstats)
slim(libs.sentry)
slim(libs.commons.io)
slim(libs.commons.lang)
slim(libs.commons.lang3)
slim(libs.commons.math3)
slim(libs.oshi)
slim(libs.lz4)
slim(libs.fastutil)
slim(libs.lru)
slim(libs.zip)
slim(libs.gson)
slim(libs.asm)
slim(libs.caffeine)
slim(libs.byteBuddy.core)
slim(libs.byteBuddy.agent)
slim(libs.dom4j)
slim(libs.jaxen)
// Script Engine
slim(libs.kotlin.stdlib)
slim(libs.kotlin.coroutines)
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.16.1')
testImplementation(libs.spigot)
testRuntimeOnly(libs.spigot)
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
kotlin {
jvmToolchain(25)
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget('25'))
}
}
sentry {
url = 'http://sentry.volmit.com:8080'
autoInstallation.enabled = false
includeSourceContext = true
org = 'sentry'
projectName = 'iris'
authToken = sentryAuthToken
}
slimJar {
mirrors = [
new Mirror(
URI.create('https://maven-central.storage-download.googleapis.com/maven2').toURL(),
URI.create('https://repo.maven.apache.org/maven2/').toURL()
)
]
relocate('com.dfsek.paralithic', "${lib}.paralithic")
relocate('io.papermc.lib', "${lib}.paper")
relocate('net.kyori', "${lib}.kyori")
relocate('org.bstats', "${lib}.metrics")
relocate('io.sentry', "${lib}.sentry")
relocate('org.apache.maven', "${lib}.maven")
relocate('org.codehaus.plexus', "${lib}.plexus")
relocate('org.eclipse.sisu', "${lib}.sisu")
relocate('org.eclipse.aether', "${lib}.aether")
relocate('com.google.inject', "${lib}.guice")
relocate('org.dom4j', "${lib}.dom4j")
relocate('org.jaxen', "${lib}.jaxen")
relocate('com.github.benmanes.caffeine', "${lib}.caffeine")
}
def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar)
def templateSource = file('src/main/templates')
def templateDest = layout.buildDirectory.dir('generated/sources/templates')
def generateTemplates = tasks.register('generateTemplates', Copy) {
inputs.properties([
environment: providers.provider {
if (project.hasProperty('release')) {
return 'production'
}
if (project.hasProperty('argghh')) {
return 'Argghh!'
}
return 'development'
},
commit: providers.provider {
String commitId = null
Exception failure = null
try {
commitId = project.extensions.getByType(Grgit).head().id
} catch (Exception ex) {
failure = ex
}
if (commitId != null && commitId.length() == 40) {
return commitId
}
logger.error('Git commit hash not found', failure)
return 'unknown'
},
])
from(templateSource)
into(templateDest)
rename { String fileName -> "art/arcane/iris/${fileName}" }
expand(inputs.properties)
}
tasks.named('compileJava', JavaCompile).configure {
/**
* We need parameter meta for the decree command system
*/
options.compilerArgs.add('-parameters')
options.encoding = 'UTF-8'
options.debugOptions.debugLevel = 'none'
}
tasks.named('processResources').configure {
/**
* Expand properties into plugin yml
*/
def pluginProperties = [
name : rootProject.name,
version : rootProject.version,
apiVersion: apiVersion,
main : mainClass,
]
inputs.properties(pluginProperties)
filesMatching('**/plugin.yml') {
expand(pluginProperties)
}
}
def runningTestTasks = gradle.startParameter.taskNames.any { String taskName -> taskName.toLowerCase().contains('test') }
if (runningTestTasks) {
TaskProvider<Task> processResourcesTask = tasks.named('processResources')
tasks.named('classes').configure { Task classesTask ->
Set<Object> dependencies = new LinkedHashSet<Object>(classesTask.getDependsOn())
dependencies.removeIf { Object dependency ->
if (dependency instanceof TaskProvider) {
return ((TaskProvider<?>) dependency).name == processResourcesTask.name
}
if (dependency instanceof Task) {
return ((Task) dependency).name == processResourcesTask.name
}
String dependencyName = String.valueOf(dependency)
return dependencyName == 'processResources' || dependencyName.endsWith(':processResources')
}
classesTask.setDependsOn(dependencies)
}
processResourcesTask.configure { Task task ->
task.enabled = false
}
}
tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure {
dependsOn(embeddedAgentJar)
mergeServiceFiles()
//minimize()
relocate('io.github.slimjar', "${lib}.slimjar")
exclude('modules/loader-agent.isolated-jar')
from(embeddedAgentJar.map { it.archiveFile }) {
rename { String ignored -> 'agent.jar' }
}
}
tasks.named('sentryCollectSourcesJava').configure {
dependsOn(generateTemplates)
}
tasks.named('generateSentryBundleIdJava').configure {
dependsOn(generateTemplates)
}
tasks.matching { Task task ->
task.name.startsWith('sentry') || task.name.startsWith('generateSentry')
}.configureEach {
onlyIf {
hasSentryAuthToken
}
}
rootProject.tasks.matching {
it.name == 'prepareKotlinBuildScriptModel'
}.configureEach {
dependsOn(generateTemplates)
}
sourceSets {
main {
java {
srcDir(generateTemplates.map { it.outputs })
}
}
}
+219
View File
@@ -0,0 +1,219 @@
import io.github.slimjar.func.slimjarHelper
import io.github.slimjar.resolver.data.Mirror
import org.ajoberstar.grgit.Grgit
import java.net.URI
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 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/>.
*/
plugins {
java
`java-library`
alias(libs.plugins.shadow)
alias(libs.plugins.sentry)
alias(libs.plugins.slimjar)
alias(libs.plugins.grgit)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.lombok)
}
val apiVersion = "1.19"
val main = "com.volmit.iris.Iris"
val lib = "com.volmit.iris.util"
/**
* Dependencies.
*
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
*
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
* need to be shaded into the jar (increasing binary size)
*
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
* these dependencies if they are available on mvn central.
*/
dependencies {
// Provided or Classpath
compileOnly(libs.spigot)
compileOnly(libs.log4j.api)
compileOnly(libs.log4j.core)
// Third Party Integrations
compileOnly(libs.nexo)
compileOnly(libs.itemsadder)
compileOnly(libs.placeholderApi)
compileOnly(libs.score)
compileOnly(libs.mmoitems)
compileOnly(libs.ecoitems)
compileOnly(libs.mythic)
compileOnly(libs.mythicChrucible)
compileOnly(libs.kgenerators) {
isTransitive = false
}
compileOnly(libs.multiverseCore)
compileOnly(libs.worlds)
// Shaded
implementation(slimjarHelper("spigot"))
implementation(rootProject.libs.platformUtils) {
isTransitive = false
}
// Dynamically Loaded
slim(libs.paralithic)
slim(libs.paperlib)
slim(libs.adventure.minimessage)
slim(libs.adventure.platform)
slim(libs.adventure.gson)
slim(libs.adventure.legacy)
slim(libs.bstats)
slim(libs.sentry)
slim(libs.commons.io)
slim(libs.commons.lang)
slim(libs.commons.lang3)
slim(libs.commons.math3)
slim(libs.oshi)
slim(libs.lz4)
slim(libs.fastutil)
slim(libs.lru)
slim(libs.zip)
slim(libs.gson)
slim(libs.asm)
slim(libs.caffeine)
slim(libs.byteBuddy.core)
slim(libs.byteBuddy.agent)
slim(libs.dom4j)
slim(libs.jaxen)
// Script Engine
slim(libs.kotlin.stdlib)
slim(libs.kotlin.coroutines)
slim(libs.kotlin.scripting.common)
slim(libs.kotlin.scripting.jvm)
slim(libs.kotlin.scripting.jvm.host)
slim(libs.kotlin.scripting.dependencies.maven) {
constraints {
slim(libs.mavenCore)
}
}
constraints {
slim(libs.gson)
compileOnly(libs.gson)
}
}
java {
disableAutoTargetJvm()
}
sentry {
url = "http://sentry.volmit.com:8080/"
autoInstallation.enabled = false
includeSourceContext = true
org = "sentry"
projectName = "iris"
authToken = findProperty("sentry.auth.token") as String? ?: System.getenv("SENTRY_AUTH_TOKEN")
}
slimJar {
mirrors = listOf(Mirror(
URI.create("https://maven-central.storage-download.googleapis.com/maven2").toURL(),
URI.create("https://repo.maven.apache.org/maven2/").toURL()
))
relocate("com.dfsek.paralithic", "$lib.paralithic")
relocate("net.kyori.audience", "$lib.audience")
relocate("org.bstats", "$lib.metrics")
relocate("io.sentry", "$lib.sentry")
relocate("org.apache.maven", "$lib.maven")
relocate("org.codehaus.plexus", "$lib.plexus")
relocate("org.eclipse.sisu", "$lib.sisu")
relocate("org.eclipse.aether", "$lib.aether")
relocate("com.google.inject", "$lib.guice")
relocate("org.dom4j", "$lib.dom4j")
relocate("org.jaxen", "$lib.jaxen")
}
tasks {
/**
* We need parameter meta for the decree command system
*/
compileJava {
options.compilerArgs.add("-parameters")
options.encoding = "UTF-8"
}
/**
* Expand properties into plugin yml
*/
processResources {
inputs.properties(
"name" to rootProject.name,
"version" to rootProject.version,
"apiVersion" to apiVersion,
"main" to main,
)
filesMatching("**/plugin.yml") {
expand(inputs.properties)
}
}
shadowJar {
mergeServiceFiles()
//minimize()
relocate("io.github.slimjar", "$lib.slimjar")
exclude("modules/loader-agent.isolated-jar")
}
}
val templateSource = file("src/main/templates")
val templateDest = layout.buildDirectory.dir("generated/sources/templates")
val generateTemplates = tasks.register<Copy>("generateTemplates") {
inputs.properties(
"environment" to if (project.hasProperty("release")) "production" else "development",
"commit" to provider {
val res = runCatching { project.extensions.getByType<Grgit>().head().id }
res.getOrDefault("")
.takeIf { it.length == 40 } ?: {
logger.error("Git commit hash not found", res.exceptionOrNull())
"unknown"
}()
},
)
from(templateSource)
into(templateDest)
rename { "com/volmit/iris/$it" }
expand(inputs.properties)
}
tasks.generateSentryBundleIdJava {
dependsOn(generateTemplates)
}
rootProject.tasks.named("prepareKotlinBuildScriptModel") {
dependsOn(generateTemplates)
}
sourceSets.main {
java.srcDir(generateTemplates.map { it.outputs })
}
-1
View File
@@ -1 +0,0 @@
1435163759
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
package art.arcane.iris.core;
public enum IrisPaperLikeBackendMode {
AUTO,
TICKET,
SERVICE
}
@@ -1,73 +0,0 @@
package art.arcane.iris.core;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import java.util.Locale;
public enum IrisRuntimeSchedulerMode {
AUTO,
PAPER_LIKE,
FOLIA;
public static IrisRuntimeSchedulerMode resolve(IrisSettings.IrisSettingsPregen pregen) {
Server server = Bukkit.getServer();
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
if (regionizedRuntime) {
return FOLIA;
}
IrisRuntimeSchedulerMode configuredMode = pregen == null ? null : pregen.getRuntimeSchedulerMode();
if (configuredMode != null && configuredMode != AUTO) {
if (configuredMode == FOLIA) {
return PAPER_LIKE;
}
return configuredMode;
}
String bukkitName = Bukkit.getName();
String bukkitVersion = Bukkit.getVersion();
String serverClassName = server == null ? "" : server.getClass().getName();
if (containsIgnoreCase(bukkitName, "folia")
|| containsIgnoreCase(bukkitVersion, "folia")
|| containsIgnoreCase(serverClassName, "folia")) {
return FOLIA;
}
if (containsIgnoreCase(bukkitName, "purpur")
|| containsIgnoreCase(bukkitVersion, "purpur")
|| containsIgnoreCase(serverClassName, "purpur")
|| containsIgnoreCase(bukkitName, "canvas")
|| containsIgnoreCase(bukkitVersion, "canvas")
|| containsIgnoreCase(serverClassName, "canvas")
|| containsIgnoreCase(bukkitName, "paper")
|| containsIgnoreCase(bukkitVersion, "paper")
|| containsIgnoreCase(serverClassName, "paper")
|| containsIgnoreCase(bukkitName, "pufferfish")
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|| containsIgnoreCase(serverClassName, "pufferfish")
|| containsIgnoreCase(bukkitName, "spigot")
|| containsIgnoreCase(bukkitVersion, "spigot")
|| containsIgnoreCase(serverClassName, "spigot")
|| containsIgnoreCase(bukkitName, "craftbukkit")
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|| containsIgnoreCase(serverClassName, "craftbukkit")) {
return PAPER_LIKE;
}
if (regionizedRuntime) {
return FOLIA;
}
return PAPER_LIKE;
}
private static boolean containsIgnoreCase(String value, String contains) {
if (value == null || contains == null || contains.isEmpty()) {
return false;
}
return value.toLowerCase(Locale.ROOT).contains(contains.toLowerCase(Locale.ROOT));
}
}
File diff suppressed because it is too large Load Diff
@@ -1,122 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
import java.awt.*;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something")
public class CommandEdit implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
private boolean noStudio() {
if (!sender().isPlayer()) {
sender().sendMessage(C.RED + "Players only!");
return true;
}
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
sender().sendMessage(C.RED + "No studio world is open!");
return true;
}
if (!engine().isStudio()) {
sender().sendMessage(C.RED + "You must be in a studio world!");
return true;
}
if (GraphicsEnvironment.isHeadless()) {
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
return true;
}
if (!Desktop.isDesktopSupported()) {
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
return true;
}
return false;
}
@Director(description = "Edit the biome you specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) {
if (noStudio()) {
return;
}
try {
if (biome == null || biome.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(biome.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
@Director(description = "Edit the region you specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) {
if (noStudio()) {
return;
}
try {
if (region == null || region.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(region.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
@Director(description = "Edit the dimension you specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) {
if (noStudio()) {
return;
}
try {
if (dimension == null || dimension.getLoadFile() == null) {
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
return;
}
Desktop.getDesktop().open(dimension.getLoadFile());
sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
}
}
}
@@ -1,793 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.ChunkReplacementListener;
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.iris.util.common.parallel.SyncExecutor;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.*;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import static art.arcane.iris.core.service.EditSVC.deletingWorld;
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
import static org.bukkit.Bukkit.getServer;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
public class CommandIris implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
private CommandStudio studio;
private CommandPregen pregen;
private CommandSettings settings;
private CommandObject object;
private CommandWhat what;
private CommandEdit edit;
private CommandDeveloper developer;
private CommandPack pack;
private CommandFind find;
public static boolean worldCreation = false;
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
String WorldEngine;
String worldNameToCheck = "YourWorldName";
VolmitSender sender = Iris.getSender();
@Director(description = "Create a new world", aliases = {"+", "c"})
public void create(
@Param(aliases = "world-name", description = "The name of the world to create")
String name,
@Param(
aliases = {"dimension", "pack"},
description = "The dimension/pack to create the world with",
defaultValue = "default",
customHandler = PackDimensionTypeHandler.class
)
String type,
@Param(description = "The seed to generate the world with", defaultValue = "1337")
long seed,
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
boolean main,
@Param(aliases = {"remove-others", "removeothers"}, description = "When main-world is true, remove other Iris worlds from bukkit.yml and queue deletion on startup", defaultValue = "false")
boolean removeOthers,
@Param(aliases = {"remove-worlds", "removeworlds"}, description = "Comma-separated world names to remove from Iris control and delete on next startup (main-world only)", defaultValue = "none")
String removeWorlds
) {
if (name.equalsIgnoreCase("iris")) {
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (name.equalsIgnoreCase("benchmark")) {
sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (new File(Bukkit.getWorldContainer(), name).exists()) {
sender().sendMessage(C.RED + "That folder already exists!");
return;
}
String resolvedType = type.equalsIgnoreCase("default")
? IrisSettings.get().getGenerator().getDefaultWorldType()
: type;
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
if (dimension == null) {
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
sender().sendMessage(C.YELLOW + "Try one of: overworld, vanilla, flat, theend");
sender().sendMessage(C.YELLOW + "Or download manually: /iris download IrisDimensions/" + resolvedType);
return;
}
if (!main && (removeOthers || hasExplicitCleanupWorlds(removeWorlds))) {
sender().sendMessage(C.YELLOW + "remove-others/remove-worlds only apply when main-world=true. Ignoring cleanup options.");
removeOthers = false;
removeWorlds = "none";
}
if (J.isFolia()) {
if (stageFoliaWorldCreation(name, dimension, seed, main, removeOthers, removeWorlds)) {
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
}
return;
}
try {
worldCreation = true;
IrisToolbelt.createWorld()
.dimension(dimension.getLoadKey())
.name(name)
.seed(seed)
.sender(sender())
.studio(false)
.create();
if (main) {
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
return new Thread(() -> updateMainWorld(name));
}));
}
} catch (Throwable e) {
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
worldCreation = false;
return;
}
if (main && !applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
worldCreation = false;
return;
}
worldCreation = false;
sender().sendMessage(C.GREEN + "Successfully created your world!");
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
}
private boolean updateMainWorld(String newName) {
try {
File worlds = Bukkit.getWorldContainer();
var data = ServerProperties.DATA;
try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
data.load(in);
}
File oldWorldFolder = new File(worlds, ServerProperties.LEVEL_NAME);
File newWorldFolder = new File(worlds, newName);
if (!newWorldFolder.exists() && !newWorldFolder.mkdirs()) {
Iris.warn("Could not create target main world folder: " + newWorldFolder.getAbsolutePath());
}
for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) {
File source = new File(oldWorldFolder, sub);
if (!source.exists()) {
continue;
}
IO.copyDirectory(source.toPath(), new File(newWorldFolder, sub).toPath());
}
data.setProperty("level-name", newName);
try (var out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) {
data.store(out, null);
}
return true;
} catch (Throwable e) {
Iris.error("Failed to update server.properties main world to \"" + newName + "\"");
Iris.reportError(e);
return false;
}
}
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main, boolean removeOthers, String removeWorlds) {
sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia.");
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
File worldFolder = new File(Bukkit.getWorldContainer(), name);
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension.getLoadKey(), worldFolder);
if (installed == null) {
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
return false;
}
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
return false;
}
if (main) {
if (updateMainWorld(name)) {
sender().sendMessage(C.GREEN + "Updated server.properties level-name to \"" + name + "\".");
} else {
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
return false;
}
if (!applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
sender().sendMessage(C.RED + "World was staged, but failed to apply main-world cleanup options.");
return false;
}
}
sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + ".");
if (main) {
sender().sendMessage(C.GREEN + "This world is now configured as main for next restart.");
}
return true;
}
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
if (worlds == null) {
worlds = yml.createSection("worlds");
}
ConfigurationSection worldSection = worlds.getConfigurationSection(worldName);
if (worldSection == null) {
worldSection = worlds.createSection(worldName);
}
String generator = "Iris:" + dimension;
worldSection.set("generator", generator);
if (seed != null) {
worldSection.set("seed", seed);
}
try {
yml.save(BUKKIT_YML);
Iris.info("Registered \"" + worldName + "\" in bukkit.yml");
return true;
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to update bukkit.yml: " + e.getMessage());
Iris.error("Failed to update bukkit.yml!");
Iris.reportError(e);
return false;
}
}
private boolean applyMainWorldCleanup(String mainWorld, boolean removeOthers, String removeWorlds) {
Set<String> targets = resolveCleanupTargets(mainWorld, removeOthers, removeWorlds);
if (targets.isEmpty()) {
return true;
}
sender().sendMessage(C.YELLOW + "Applying main-world cleanup for " + targets.size() + " world(s).");
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
Set<String> removedFromBukkit = new LinkedHashSet<>();
Set<String> notRemoved = new LinkedHashSet<>();
for (String target : targets) {
String key = findWorldKeyIgnoreCase(worlds, target);
if (key == null) {
notRemoved.add(target);
continue;
}
String generator = worlds.getString(key + ".generator");
if (generator == null || !(generator.equalsIgnoreCase("iris") || generator.startsWith("Iris:"))) {
notRemoved.add(key);
continue;
}
worlds.set(key, null);
removedFromBukkit.add(key);
}
try {
if (worlds != null && worlds.getKeys(false).isEmpty()) {
yml.set("worlds", null);
}
if (!removedFromBukkit.isEmpty()) {
yml.save(BUKKIT_YML);
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to update bukkit.yml while applying cleanup: " + e.getMessage());
Iris.reportError(e);
return false;
}
try {
int queued = Iris.queueWorldDeletionOnStartup(targets);
if (queued > 0) {
sender().sendMessage(C.GREEN + "Queued " + queued + " world folder(s) for deletion on next startup.");
} else {
sender().sendMessage(C.YELLOW + "Cleanup queue already contained the requested world folder(s).");
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to queue startup world deletions: " + e.getMessage());
Iris.reportError(e);
return false;
}
if (!removedFromBukkit.isEmpty()) {
sender().sendMessage(C.GREEN + "Removed from Iris control in bukkit.yml: " + String.join(", ", removedFromBukkit));
}
if (!notRemoved.isEmpty()) {
sender().sendMessage(C.YELLOW + "Skipped from bukkit.yml removal (not found or non-Iris generator): " + String.join(", ", notRemoved));
}
return true;
}
private Set<String> resolveCleanupTargets(String mainWorld, boolean removeOthers, String removeWorlds) {
Set<String> targets = new LinkedHashSet<>();
if (removeOthers) {
IrisWorlds.readBukkitWorlds().keySet().stream()
.filter(world -> !world.equalsIgnoreCase(mainWorld))
.forEach(targets::add);
}
if (hasExplicitCleanupWorlds(removeWorlds)) {
for (String raw : removeWorlds.split("[,;\\s]+")) {
if (raw == null || raw.isBlank()) {
continue;
}
if (raw.equalsIgnoreCase(mainWorld)) {
continue;
}
targets.add(raw.trim());
}
}
return targets;
}
private static boolean hasExplicitCleanupWorlds(String removeWorlds) {
if (removeWorlds == null) {
return false;
}
String trimmed = removeWorlds.trim();
return !trimmed.isEmpty() && !trimmed.equalsIgnoreCase("none");
}
private static String findWorldKeyIgnoreCase(ConfigurationSection worlds, String requested) {
if (worlds == null || requested == null) {
return null;
}
if (worlds.contains(requested)) {
return requested;
}
for (String key : worlds.getKeys(false)) {
if (key.equalsIgnoreCase(requested)) {
return key;
}
}
return null;
}
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
public void teleport(
@Param(description = "World to teleport to")
World world,
@Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
Player player
) {
if (player == null && sender().isPlayer())
player = sender().player();
final Player target = player;
if (target == null) {
sender().sendMessage(C.RED + "The specified player does not exist.");
return;
}
new BukkitRunnable() {
@Override
public void run() {
target.teleport(world.getSpawnLocation());
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
}
}.runTask(Iris.instance);
}
@Director(description = "Print version information")
public void version() {
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
}
/*
/todo
@Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE)
public void packbenchmark(
@Param(description = "Dimension to benchmark")
IrisDimension type
) throws InterruptedException {
BenchDimension = type.getLoadKey();
IrisPackBenchmarking.runBenchmark();
} */
@Director(description = "Print world height information", origin = DirectorOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
sender().sendMessage(C.GREEN + "Total Height: " + (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight()));
} else {
World mainWorld = getServer().getWorlds().get(0);
Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight());
Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight()));
}
}
@Director(description = "Check access of all worlds.", aliases = {"accesslist"})
public void worlds() {
KList<World> IrisWorlds = new KList<>();
KList<World> BukkitWorlds = new KList<>();
for (World w : Bukkit.getServer().getWorlds()) {
try {
Engine engine = IrisToolbelt.access(w).getEngine();
if (engine != null) {
IrisWorlds.add(w);
}
} catch (Exception e) {
BukkitWorlds.add(w);
}
}
if (sender().isPlayer()) {
sender().sendMessage(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
sender().sendMessage(C.IRIS + "- " +IrisWorld.getName());
}
sender().sendMessage(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
sender().sendMessage(C.GRAY + "- " +BukkitWorld.getName());
}
} else {
Iris.info(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
Iris.info(C.IRIS + "- " +IrisWorld.getName());
}
Iris.info(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
Iris.info(C.GRAY + "- " +BukkitWorld.getName());
}
}
}
@Director(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true)
public void remove(
@Param(description = "The world to remove")
World world,
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true")
boolean delete
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Removing world: " + world.getName());
if (!IrisToolbelt.evacuate(world)) {
sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName());
return;
}
if (!WorldLifecycleService.get().unload(world, false)) {
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
return;
}
try {
if (IrisToolbelt.removeWorld(world)) {
sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml");
} else {
sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml");
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
}
IrisToolbelt.evacuate(world, "Deleting world");
deletingWorld = true;
if (!delete) {
deletingWorld = false;
return;
}
VolmitSender sender = sender();
J.a(() -> {
int retries = 12;
if (deleteDirectory(world.getWorldFolder())) {
sender.sendMessage(C.GREEN + "Successfully removed world folder");
} else {
while(true){
if (deleteDirectory(world.getWorldFolder())){
sender.sendMessage(C.GREEN + "Successfully removed world folder");
break;
}
retries--;
if (retries == 0){
sender.sendMessage(C.RED + "Failed to remove world folder");
break;
}
J.sleep(3000);
}
}
deletingWorld = false;
});
}
public static boolean deleteDirectory(File dir) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDirectory(children[i]);
if (!success) {
return false;
}
}
}
return dir.delete();
}
@Director(description = "Toggle debug")
public void debug(
@Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other")
Boolean on
) {
boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on;
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
sender().sendMessage(C.GREEN + "Set debug to: " + to);
}
@Director(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
String branch,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
if (pack.equals("overworld")) {
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
Iris.service(StudioSVC.class).downloadRelease(sender(), url, overwrite);
} else {
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
}
}
@Director(description = "Get metrics for your world", aliases = "measure", origin = DirectorOrigin.PLAYER)
public void metrics() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(C.RED + "You must be in an Iris world");
return;
}
sender().sendMessage(C.GREEN + "Sending metrics...");
engine().printMetrics(sender());
}
@Director(description = "Reload configuration file (this is also done automatically)")
public void reload() {
IrisSettings.invalidate();
IrisSettings.get();
sender().sendMessage(C.GREEN + "Hotloaded settings");
}
@Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true)
public void unloadWorld(
@Param(description = "The world to unload")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
try {
IrisToolbelt.evacuate(world);
boolean unloaded = WorldLifecycleService.get().unload(world, false);
if (unloaded) {
sender().sendMessage(C.GREEN + "World unloaded successfully.");
} else {
sender().sendMessage(C.RED + "Failed to unload the world.");
}
} catch (Exception e) {
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
}
}
@Director(description = "Load an Iris World", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
public void loadWorld(
@Param(description = "The name of the world to load")
String world
) {
World worldloaded = Bukkit.getWorld(world);
worldNameToCheck = world;
boolean worldExists = doesWorldExist(worldNameToCheck);
WorldEngine = world;
if (!worldExists) {
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
return;
}
String pathtodim = world + File.separator +"iris"+File.separator +"pack"+File.separator +"dimensions"+File.separator;
File directory = new File(Bukkit.getWorldContainer(), pathtodim);
String dimension = null;
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
String fileName = file.getName();
if (fileName.endsWith(".json")) {
dimension = fileName.substring(0, fileName.length() - 5);
sender().sendMessage(C.BLUE + "Generator: " + dimension);
}
}
}
}
} else {
sender().sendMessage(C.GOLD + world + " is not an iris world.");
return;
}
if (dimension == null) {
sender().sendMessage(C.RED + "Could not determine Iris dimension for " + world + ".");
return;
}
sender().sendMessage(C.GREEN + "Loading world: " + world);
if (!registerWorldInBukkitYml(world, dimension, null)) {
return;
}
if (J.isFolia()) {
sender().sendMessage(C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + world + "\".");
return;
}
Iris.instance.checkForBukkitWorlds(world::equals);
sender().sendMessage(C.GREEN + world + " loaded successfully.");
}
@Director(description = "Evacuate an iris world", origin = DirectorOrigin.PLAYER, sync = true)
public void evacuate(
@Param(description = "Evacuate the world")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Evacuating world" + world.getName());
IrisToolbelt.evacuate(world);
}
boolean doesWorldExist(String worldName) {
File worldContainer = Bukkit.getWorldContainer();
File worldDirectory = new File(worldContainer, worldName);
return worldDirectory.exists() && worldDirectory.isDirectory();
}
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> options = new LinkedHashSet<>();
options.add("default");
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles();
if (packs != null) {
for (File pack : packs) {
if (pack == null || !pack.isDirectory()) {
continue;
}
options.add(pack.getName());
try {
IrisData data = IrisData.get(pack);
for (String key : data.getDimensionLoader().getPossibleKeys()) {
options.add(key);
}
} catch (Throwable ex) {
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
pack.getName(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
}
}
}
return new KList<>(options);
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (in == null || in.trim().isEmpty()) {
throw new DirectorParsingException("World type cannot be empty");
}
return in.trim();
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -1,169 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.io.File;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
public class CommandPack implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"})
public void validate(
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
String pack
) {
VolmitSender s = sender();
File packsRoot = Iris.instance.getDataFolder("packs");
if (!packsRoot.isDirectory()) {
s.sendMessage(C.RED + "packs/ folder not found.");
return;
}
if (pack == null || pack.isBlank()) {
File[] dirs = packsRoot.listFiles(File::isDirectory);
if (dirs == null || dirs.length == 0) {
s.sendMessage(C.YELLOW + "No packs to validate.");
return;
}
int broken = 0;
for (File dir : dirs) {
PackValidationResult result = runValidate(s, dir);
if (result != null && !result.isLoadable()) {
broken++;
}
}
s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length);
return;
}
File target = new File(packsRoot, pack);
if (!target.isDirectory()) {
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return;
}
runValidate(s, target);
}
@Director(description = "Restore most recent trashed files for a pack", aliases = {"r", "undelete"})
public void restore(
@Param(description = "The pack folder name to restore")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
s.sendMessage(C.RED + "You must specify a pack name.");
return;
}
File packFolder = new File(Iris.instance.getDataFolder("packs"), pack);
if (!packFolder.isDirectory()) {
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return;
}
int restored = PackValidator.restoreTrash(packFolder);
if (restored == 0) {
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
return;
}
s.sendMessage(C.GREEN + "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check.");
}
@Director(description = "Show cached validation status for a pack", aliases = {"s", "info"})
public void status(
@Param(description = "The pack folder name", defaultValue = "")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
if (PackValidationRegistry.snapshot().isEmpty()) {
s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first.");
return;
}
PackValidationRegistry.snapshot().forEach((name, result) -> {
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
s.sendMessage(tag + C.RESET + " " + name
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
+ ", warnings=" + result.getWarnings().size()
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
});
return;
}
PackValidationResult result = PackValidationRegistry.get(pack);
if (result == null) {
s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
return;
}
reportResult(s, result);
}
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
try {
PackValidationResult result = PackValidator.validate(packFolder);
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage());
return null;
}
}
private void reportResult(VolmitSender s, PackValidationResult result) {
if (result.isLoadable()) {
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
+ C.GRAY + " (warnings=" + result.getWarnings().size()
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
} else {
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
for (String reason : result.getBlockingErrors()) {
s.sendMessage(C.RED + " - " + reason);
}
}
int wMax = Math.min(10, result.getWarnings().size());
for (int i = 0; i < wMax; i++) {
s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i));
}
if (result.getWarnings().size() > wMax) {
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
}
int tMax = Math.min(10, result.getRemovedUnusedFiles().size());
for (int i = 0; i < tMax; i++) {
s.sendMessage(C.GRAY + " ~ trashed " + result.getRemovedUnusedFiles().get(i));
}
if (result.getRemovedUnusedFiles().size() > tMax) {
s.sendMessage(C.GRAY + " ... and " + (result.getRemovedUnusedFiles().size() - tMax) + " more trashed file(s).");
}
}
}
@@ -1,554 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.*;
import java.util.List;
import java.util.function.Supplier;
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BG = new Color(24, 24, 28);
private static final Color SIDEBAR_BG = new Color(20, 20, 24);
private static final Color SIDEBAR_SELECTED = new Color(40, 50, 70);
private static final Color SIDEBAR_ITEM_COLOR = new Color(170, 170, 185);
private static final Color SEARCH_BG = new Color(30, 30, 38);
private static final Color SEARCH_FG = new Color(180, 180, 190);
private static final Color STATUS_BG = new Color(32, 32, 38, 230);
private static final Color STATUS_TEXT = new Color(180, 180, 190);
private static final Color ACCENT = new Color(90, 140, 255);
private static final Color SEPARATOR = new Color(40, 40, 50);
private static final Font STATUS_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12);
private static final Font SIDEBAR_HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
private static final Font SIDEBAR_ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
private static final Font SEARCH_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
private static final int SIDEBAR_WIDTH = 240;
private static final int[] HSB_LUT = new int[256];
private static final String[] CATEGORY_ORDER = {
"Pack Generators", "Simplex", "Perlin", "Cellular", "Iris", "Clover",
"Hexagon", "Vascular", "Globe", "Cubic", "Fractal", "Static",
"Nowhere", "Sierpinski", "Utility", "Other"
};
static {
for (int i = 0; i < 256; i++) {
float n = i / 255f;
HSB_LUT[i] = Color.HSBtoRGB(0.666f - n * 0.666f, 1f, 1f - n * 0.8f);
}
}
private final RollingSequence fpsHistory = new RollingSequence(60);
private final boolean colorMode = IrisSettings.get().getGui().colorMode;
private final MultiBurst gx = MultiBurst.burst;
private double scale = 1;
private double animScale = 10;
private double ox = 0;
private double oz = 0;
private double animOx = 0;
private double animOz = 0;
private double lastMouseX = Double.MAX_VALUE;
private double lastMouseZ = Double.MAX_VALUE;
private double time = 0;
private double animTime = 0;
private int imgWidth = 0;
private int imgHeight = 0;
private BufferedImage img;
private CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong()));
private Function2<Double, Double, Double> generator;
private Supplier<Function2<Double, Double, Double>> loader;
private String currentName = "STATIC";
public NoiseExplorerGUI() {
Iris.instance.registerListener(this);
setBackground(BG);
addMouseWheelListener(this);
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
Point cp = e.getPoint();
lastMouseX = cp.getX();
lastMouseZ = cp.getY();
}
@Override
public void mouseDragged(MouseEvent e) {
Point cp = e.getPoint();
ox += (lastMouseX - cp.getX()) * scale;
oz += (lastMouseZ - cp.getY()) * scale;
lastMouseX = cp.getX();
lastMouseZ = cp.getY();
}
});
}
public static void launch() {
Engine engine = findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
buildFrame("Noise Explorer", nv, engine, null, null);
});
}
public static void launch(Supplier<Function2<Double, Double, Double>> gen, String genName) {
Engine engine = findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
nv.loader = gen;
nv.generator = gen.get();
nv.currentName = genName;
buildFrame("Noise Explorer: " + genName, nv, engine, gen, genName);
});
}
private static Engine findActiveEngine() {
try {
for (World w : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(w);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {}
}
} catch (Throwable ignored) {}
return null;
}
private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.getContentPane().setBackground(BG);
frame.setLayout(new BorderLayout());
JPanel sidebar = buildSidebar(nv, engine, customGen, customName);
frame.add(sidebar, BorderLayout.WEST);
frame.add(nv, BorderLayout.CENTER);
frame.setSize(1440, 820);
frame.setMinimumSize(new Dimension(640, 480));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
Iris.instance.unregisterListener(nv);
}
});
return frame;
}
private static JPanel buildSidebar(NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JPanel sidebar = new JPanel(new BorderLayout());
sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0));
sidebar.setBackground(SIDEBAR_BG);
sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, SEPARATOR));
JTextField search = new JTextField();
search.setBackground(SEARCH_BG);
search.setForeground(SEARCH_FG);
search.setCaretColor(SEARCH_FG);
search.setFont(SEARCH_FONT);
search.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, SEPARATOR),
BorderFactory.createEmptyBorder(8, 10, 8, 10)
));
search.putClientProperty("JTextField.placeholderText", "Search...");
LinkedHashMap<String, List<ListItem>> categories = buildCategoryMap(nv, engine, customGen, customName);
DefaultListModel<ListItem> model = new DefaultListModel<>();
populateModel(model, categories, "");
JList<ListItem> list = new JList<>(model);
list.setBackground(SIDEBAR_BG);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setCellRenderer(new SidebarCellRenderer());
list.setFixedCellHeight(-1);
list.addListSelectionListener(e -> {
if (e.getValueIsAdjusting()) return;
ListItem selected = list.getSelectedValue();
if (selected != null && !selected.header && selected.action != null) {
selected.action.run();
}
});
search.getDocument().addDocumentListener(new DocumentListener() {
private void filter() {
String text = search.getText().trim();
populateModel(model, categories, text);
}
public void insertUpdate(DocumentEvent e) { filter(); }
public void removeUpdate(DocumentEvent e) { filter(); }
public void changedUpdate(DocumentEvent e) { filter(); }
});
JScrollPane scrollPane = new JScrollPane(list);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.getVerticalScrollBar().setUnitIncrement(16);
scrollPane.getVerticalScrollBar().setBackground(SIDEBAR_BG);
sidebar.add(search, BorderLayout.NORTH);
sidebar.add(scrollPane, BorderLayout.CENTER);
return sidebar;
}
private static void populateModel(DefaultListModel<ListItem> model, LinkedHashMap<String, List<ListItem>> categories, String filter) {
model.clear();
String lower = filter.toLowerCase();
for (Map.Entry<String, List<ListItem>> entry : categories.entrySet()) {
List<ListItem> matching = new ArrayList<>();
for (ListItem item : entry.getValue()) {
if (lower.isEmpty() || item.text.toLowerCase().contains(lower) || item.rawName.toLowerCase().contains(lower)) {
matching.add(item);
}
}
if (!matching.isEmpty()) {
model.addElement(new ListItem(entry.getKey(), entry.getKey(), true, null));
for (ListItem item : matching) {
model.addElement(item);
}
}
}
}
private static LinkedHashMap<String, List<ListItem>> buildCategoryMap(NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
LinkedHashMap<String, List<ListItem>> categories = new LinkedHashMap<>();
if (customGen != null && customName != null) {
List<ListItem> custom = new ArrayList<>();
custom.add(new ListItem(customName, customName, false, () -> {
nv.generator = customGen.get();
nv.loader = customGen;
nv.currentName = customName;
}));
categories.put("Custom", custom);
}
Map<String, List<NoiseStyle>> styleGroups = new LinkedHashMap<>();
for (NoiseStyle style : NoiseStyle.values()) {
String cat = categorize(style);
styleGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(style);
}
if (engine != null && !engine.isClosed()) {
List<ListItem> genItems = new ArrayList<>();
try {
IrisData data = engine.getData();
String[] keys = data.getGeneratorLoader().getPossibleKeys();
Arrays.sort(keys);
for (String key : keys) {
IrisGenerator gen = data.getGeneratorLoader().load(key);
if (gen != null) {
long seed = new RNG(12345).nextParallelRNG(3245).lmax();
genItems.add(new ListItem(formatName(key), key, false, () -> {
nv.generator = (x, z) -> gen.getHeight(x, z, seed);
nv.loader = null;
nv.currentName = key;
}));
}
}
} catch (Throwable ignored) {}
if (!genItems.isEmpty()) {
categories.put("Pack Generators", genItems);
}
}
for (String cat : CATEGORY_ORDER) {
if ("Pack Generators".equals(cat)) continue;
List<NoiseStyle> styles = styleGroups.get(cat);
if (styles != null && !styles.isEmpty()) {
List<ListItem> items = new ArrayList<>();
for (NoiseStyle style : styles) {
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
nv.generator = null;
nv.loader = null;
nv.currentName = style.name();
}));
}
categories.put(cat, items);
}
}
for (Map.Entry<String, List<NoiseStyle>> entry : styleGroups.entrySet()) {
if (!categories.containsKey(entry.getKey())) {
List<ListItem> items = new ArrayList<>();
for (NoiseStyle style : entry.getValue()) {
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
nv.generator = null;
nv.loader = null;
nv.currentName = style.name();
}));
}
categories.put(entry.getKey(), items);
}
}
return categories;
}
private static String categorize(NoiseStyle style) {
String n = style.name();
if (n.startsWith("STATIC")) return "Static";
if (n.startsWith("IRIS")) return "Iris";
if (n.startsWith("CLOVER")) return "Clover";
if (n.startsWith("VASCULAR")) return "Vascular";
if (n.equals("FLAT")) return "Utility";
if (n.startsWith("CELLULAR")) return "Cellular";
if (n.startsWith("HEX") || n.equals("HEXAGON")) return "Hexagon";
if (n.startsWith("SIERPINSKI")) return "Sierpinski";
if (n.startsWith("NOWHERE")) return "Nowhere";
if (n.startsWith("GLOB")) return "Globe";
if (n.startsWith("PERLIN")) return "Perlin";
if (n.startsWith("CUBIC") || (n.startsWith("FRACTAL") && n.contains("CUBIC"))) return "Cubic";
if (n.contains("SIMPLEX") && !n.startsWith("FRACTAL")) return "Simplex";
if (n.startsWith("FRACTAL")) return "Fractal";
return "Other";
}
private static String formatName(String enumName) {
String lower = enumName.toLowerCase().replace('_', ' ');
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
}
@EventHandler
public void on(IrisEngineHotloadEvent e) {
if (generator != null && loader != null) {
generator = loader.get();
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
if (e.isControlDown()) {
time = time + ((0.0025 * time) * notches);
return;
}
scale = scale + ((0.044 * scale) * notches);
scale = Math.max(scale, 0.00001);
}
private double lerp(double current, double target, double speed) {
double diff = target - current;
if (Math.abs(diff) < 0.001) return target;
return current + diff * speed;
}
@Override
public void paint(Graphics g) {
animScale = lerp(animScale, scale, 0.16);
animTime = lerp(animTime, time, 0.29);
animOx = lerp(animOx, ox, 0.16);
animOz = lerp(animOz, oz, 0.16);
PrecisionStopwatch p = PrecisionStopwatch.start();
if (g instanceof Graphics2D gg) {
gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
int pw = getWidth();
int ph = getHeight();
if (pw != imgWidth || ph != imgHeight || img == null) {
imgWidth = pw;
imgHeight = ph;
img = null;
}
int accuracy = M.clip((fpsHistory.getAverage() / 14D), 1D, 64D).intValue();
int rw = Math.max(1, pw / accuracy);
int rh = Math.max(1, ph / accuracy);
if (img == null || img.getWidth() != rw || img.getHeight() != rh) {
img = new BufferedImage(rw, rh, BufferedImage.TYPE_INT_RGB);
}
int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
BurstExecutor burst = gx.burst(rw);
for (int x = 0; x < rw; x++) {
int xx = x;
burst.queue(() -> {
for (int z = 0; z < rh; z++) {
double worldX = (xx * accuracy * animScale) + animOx;
double worldZ = (z * accuracy * animScale) + animOz;
double n = generator != null
? generator.apply(worldX, worldZ)
: cng.noise(worldX, worldZ);
n = Math.max(0, Math.min(1, n));
int rgb;
if (colorMode) {
rgb = HSB_LUT[(int) (n * 255)];
} else {
int v = (int) (n * 255);
rgb = (v << 16) | (v << 8) | v;
}
pixels[z * rw + xx] = rgb;
}
});
}
burst.complete();
gg.setColor(BG);
gg.fillRect(0, 0, pw, ph);
gg.drawImage(img, 0, 0, pw, ph, null);
renderStatusBar(gg, pw, ph, p.getMilliseconds());
renderCrosshair(gg, pw, ph);
}
p.end();
time += 1D;
fpsHistory.put(p.getMilliseconds());
if (!isVisible() || !getParent().isVisible()) {
return;
}
long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds());
EventQueue.invokeLater(() -> {
J.sleep(sleepMs);
repaint();
});
}
private void renderCrosshair(Graphics2D g, int w, int h) {
int cx = w / 2;
int cy = h / 2;
g.setColor(new Color(255, 255, 255, 40));
g.drawLine(cx - 8, cy, cx + 8, cy);
g.drawLine(cx, cy - 8, cx, cy + 8);
}
private void renderStatusBar(Graphics2D g, int w, int h, double frameMs) {
int barHeight = 28;
int y = h - barHeight;
g.setColor(STATUS_BG);
g.fillRect(0, y, w, barHeight);
g.setColor(new Color(50, 50, 60));
g.drawLine(0, y, w, y);
g.setFont(STATUS_FONT);
g.setColor(STATUS_TEXT);
double worldX = (w / 2.0 * animScale) + animOx;
double worldZ = (h / 2.0 * animScale) + animOz;
double noiseVal = generator != null
? generator.apply(worldX, worldZ)
: cng.noise(worldX, worldZ);
noiseVal = Math.max(0, Math.min(1, noiseVal));
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
String status = String.format(" %s | X: %.1f Z: %.1f | Zoom: %.4f | Value: %.4f | %d FPS",
currentName, worldX, worldZ, animScale, noiseVal, fps);
g.drawString(status, 8, y + 18);
int barW = 60;
int barX = w - barW - 12;
int barY = y + 6;
int barH = barHeight - 12;
g.setColor(new Color(40, 40, 48));
g.fillRoundRect(barX, barY, barW, barH, 4, 4);
int fillW = (int) (noiseVal * (barW - 2));
g.setColor(ACCENT);
g.fillRoundRect(barX + 1, barY + 1, fillW, barH - 2, 3, 3);
}
private static final class ListItem {
final String text;
final String rawName;
final boolean header;
final Runnable action;
ListItem(String text, String rawName, boolean header, Runnable action) {
this.text = text;
this.rawName = rawName;
this.header = header;
this.action = action;
}
@Override
public String toString() {
return text;
}
}
private static final class SidebarCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focus) {
ListItem item = (ListItem) value;
super.getListCellRendererComponent(list, item.text, index, !item.header && selected, false);
setOpaque(true);
if (item.header) {
setFont(SIDEBAR_HEADER_FONT);
setForeground(ACCENT);
setBackground(SIDEBAR_BG);
setBorder(BorderFactory.createEmptyBorder(10, 10, 4, 10));
} else {
setFont(SIDEBAR_ITEM_FONT);
setForeground(selected ? Color.WHITE : SIDEBAR_ITEM_COLOR);
setBackground(selected ? SIDEBAR_SELECTED : SIDEBAR_BG);
setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 10));
}
return this;
}
}
}
@@ -1,915 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.components.IrisRenderer;
import art.arcane.iris.core.gui.components.RenderType;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import javax.swing.*;
import javax.swing.event.MouseInputListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BG = new Color(18, 18, 22);
private static final Color CARD_BG = new Color(28, 28, 36, 220);
private static final Color CARD_BORDER = new Color(60, 60, 75, 180);
private static final Color TEXT_PRIMARY = new Color(220, 220, 230);
private static final Color TEXT_SECONDARY = new Color(140, 140, 155);
private static final Color TEXT_DIM = new Color(90, 90, 105);
private static final Color ACCENT = new Color(90, 140, 255);
private static final Color ACCENT_DIM = new Color(60, 100, 200, 100);
private static final Color PLAYER_COLOR = new Color(80, 200, 120);
private static final Color MOB_COLOR = new Color(220, 80, 80);
private static final Color STATUS_BG = new Color(24, 24, 30, 240);
private static final Color GRID_COLOR = new Color(255, 255, 255, 12);
private static final Font FONT_STATUS = new Font(Font.MONOSPACED, Font.PLAIN, 12);
private static final Font FONT_CARD_TITLE = new Font(Font.SANS_SERIF, Font.BOLD, 13);
private static final Font FONT_CARD_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
private static final Font FONT_HELP_KEY = new Font(Font.MONOSPACED, Font.BOLD, 12);
private static final Font FONT_NOTIFICATION = new Font(Font.SANS_SERIF, Font.BOLD, 14);
private static final int CARD_RADIUS = 8;
private static final int CARD_PAD = 12;
private static final int STATUS_HEIGHT = 26;
private final KList<LivingEntity> lastEntities = new KList<>();
private final KMap<String, Long> notifications = new KMap<>();
private final ChronoLatch centities = new ChronoLatch(1000);
private final RollingSequence rs = new RollingSequence(512);
private final O<Integer> m = new O<>();
private final KMap<BlockPosition, BufferedImage> positions = new KMap<>();
private final KMap<BlockPosition, BufferedImage> fastpositions = new KMap<>();
private final KSet<BlockPosition> working = new KSet<>();
private final KSet<BlockPosition> workingfast = new KSet<>();
private RenderType currentType = RenderType.BIOME;
private boolean help = true;
private boolean helpIgnored = false;
private boolean shift = false;
private Player player = null;
private boolean debug = false;
private boolean control = false;
private boolean eco = false;
private boolean lowtile = false;
private boolean follow = false;
private boolean alt = false;
private boolean grid = false;
private IrisRenderer renderer;
private IrisWorld world;
private double velocity = 0;
private int lowq = 12;
private double scale = 128;
private double mscale = 4D;
private int w = 0;
private int h = 0;
private double lx = 0;
private double lz = 0;
private double ox = 0;
private double oz = 0;
private double hx = 0;
private double hz = 0;
private double oxp = 0;
private double ozp = 0;
private Engine engine;
private int tid = 0;
private Map<RenderType, JToggleButton> modeButtons;
private final ExecutorService e = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
tid++;
Thread t = new Thread(r);
t.setName("Iris HD Renderer " + tid);
t.setPriority(Thread.MIN_PRIORITY);
t.setUncaughtExceptionHandler((et, ex) -> {
Iris.info("Exception encountered in " + et.getName());
ex.printStackTrace();
});
return t;
});
private final ExecutorService eh = Executors.newFixedThreadPool(3, r -> {
tid++;
Thread t = new Thread(r);
t.setName("Iris Renderer " + tid);
t.setPriority(Thread.NORM_PRIORITY);
t.setUncaughtExceptionHandler((et, ex) -> {
Iris.info("Exception encountered in " + et.getName());
ex.printStackTrace();
});
return t;
});
public VisionGUI(JFrame frame) {
m.set(8);
rs.put(1);
setBackground(BG);
addMouseWheelListener(this);
addMouseMotionListener(this);
addMouseListener(this);
frame.addKeyListener(this);
J.a(() -> {
J.sleep(10000);
if (!helpIgnored && help) {
help = false;
}
});
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent windowEvent) {
e.shutdown();
eh.shutdown();
}
});
}
private static void createAndShowGUI(Engine r, int s, IrisWorld world) {
JFrame frame = new JFrame("Iris Vision");
VisionGUI nv = new VisionGUI(frame);
nv.world = world;
nv.engine = r;
nv.renderer = new IrisRenderer(r);
frame.getContentPane().setBackground(BG);
frame.setLayout(new BorderLayout());
frame.add(buildToolbar(nv), BorderLayout.NORTH);
frame.add(nv, BorderLayout.CENTER);
frame.setSize(1440, 820);
frame.setMinimumSize(new Dimension(640, 480));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static JPanel buildToolbar(VisionGUI nv) {
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 3));
toolbar.setBackground(new Color(22, 22, 28));
toolbar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(45, 45, 55)));
JLabel modeLabel = new JLabel("View:");
modeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11));
modeLabel.setForeground(TEXT_SECONDARY);
modeLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 2));
toolbar.add(modeLabel);
ButtonGroup modeGroup = new ButtonGroup();
Map<RenderType, JToggleButton> modeButtons = new LinkedHashMap<>();
for (RenderType type : RenderType.values()) {
JToggleButton btn = createToolbarToggle(modeName(type), type == nv.currentType);
btn.addActionListener(e -> {
nv.setRenderType(type);
for (Map.Entry<RenderType, JToggleButton> entry : modeButtons.entrySet()) {
entry.getValue().setSelected(entry.getKey() == type);
}
});
modeGroup.add(btn);
modeButtons.put(type, btn);
toolbar.add(btn);
}
nv.modeButtons = modeButtons;
toolbar.add(createToolbarSeparator());
JToggleButton gridBtn = createToolbarToggle("Grid", nv.grid);
gridBtn.addActionListener(e -> { nv.toggleGrid(); gridBtn.setSelected(nv.grid); });
toolbar.add(gridBtn);
JToggleButton followBtn = createToolbarToggle("Follow", nv.follow);
followBtn.addActionListener(e -> { nv.toggleFollow(); followBtn.setSelected(nv.follow); });
toolbar.add(followBtn);
JToggleButton qualityBtn = createToolbarToggle("LQ", nv.lowtile);
qualityBtn.addActionListener(e -> { nv.toggleQuality(); qualityBtn.setSelected(nv.lowtile); });
toolbar.add(qualityBtn);
return toolbar;
}
private static JToggleButton createToolbarToggle(String text, boolean selected) {
JToggleButton btn = new JToggleButton(text, selected);
btn.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11));
btn.setFocusable(false);
btn.setForeground(new Color(170, 170, 185));
btn.setBackground(new Color(32, 32, 40));
btn.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(50, 50, 60)),
BorderFactory.createEmptyBorder(3, 8, 3, 8)
));
btn.setOpaque(true);
btn.addChangeListener(e -> {
if (btn.isSelected()) {
btn.setBackground(new Color(50, 60, 85));
btn.setForeground(Color.WHITE);
} else {
btn.setBackground(new Color(32, 32, 40));
btn.setForeground(new Color(170, 170, 185));
}
});
return btn;
}
private static JSeparator createToolbarSeparator() {
JSeparator sep = new JSeparator(SwingConstants.VERTICAL);
sep.setPreferredSize(new Dimension(1, 24));
sep.setForeground(new Color(50, 50, 60));
sep.setBackground(new Color(22, 22, 28));
return sep;
}
public static void launch(Engine g, int i) {
J.a(() -> createAndShowGUI(g, i, g.getWorld()));
}
public boolean updateEngine() {
if (engine.isClosed()) {
if (world.hasRealWorld()) {
try {
engine = IrisToolbelt.access(world.realWorld()).getEngine();
return !engine.isClosed();
} catch (Throwable ignored) {
}
}
}
return false;
}
@Override
public void mouseMoved(MouseEvent e) {
Point cp = e.getPoint();
lx = cp.getX();
lz = cp.getY();
}
@Override
public void mouseDragged(MouseEvent e) {
Point cp = e.getPoint();
ox += (lx - cp.getX()) * scale;
oz += (lz - cp.getY()) * scale;
lx = cp.getX();
lz = cp.getY();
}
public void notify(String s) {
notifications.put(s, M.ms() + 2500);
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = true;
if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = true;
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = true;
if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = true; helpIgnored = true; }
if (e.getKeyCode() == KeyEvent.VK_ALT) alt = true;
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = false;
if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = false;
if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = false;
if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = false; helpIgnored = true; }
if (e.getKeyCode() == KeyEvent.VK_ALT) alt = false;
if (e.getKeyCode() == KeyEvent.VK_F) { toggleFollow(); return; }
if (e.getKeyCode() == KeyEvent.VK_R) { dump(); notify("Refreshing"); return; }
if (e.getKeyCode() == KeyEvent.VK_P) { toggleQuality(); return; }
if (e.getKeyCode() == KeyEvent.VK_E) { eco = !eco; dump(); notify((eco ? "30" : "60") + " FPS"); return; }
if (e.getKeyCode() == KeyEvent.VK_G) { toggleGrid(); return; }
if (e.getKeyCode() == KeyEvent.VK_EQUALS) {
mscale = mscale + ((0.044 * mscale) * -3);
mscale = Math.max(mscale, 0.00001);
dump();
return;
}
if (e.getKeyCode() == KeyEvent.VK_MINUS) {
mscale = mscale + ((0.044 * mscale) * 3);
mscale = Math.max(mscale, 0.00001);
dump();
return;
}
if (e.getKeyCode() == KeyEvent.VK_BACK_SLASH) {
mscale = 1D;
dump();
notify("Zoom Reset");
return;
}
int currentMode = currentType.ordinal();
for (RenderType i : RenderType.values()) {
if (e.getKeyChar() == String.valueOf(i.ordinal() + 1).charAt(0)) {
if (i.ordinal() != currentMode) {
setRenderType(i);
syncModeButtons();
return;
}
}
}
if (e.getKeyCode() == KeyEvent.VK_M) {
setRenderType(RenderType.values()[(currentMode + 1) % RenderType.values().length]);
syncModeButtons();
}
}
private static String modeName(RenderType type) {
return Form.capitalizeWords(type.name().toLowerCase().replaceAll("\\Q_\\E", " "));
}
void setRenderType(RenderType type) {
currentType = type;
dump();
notify(modeName(type));
}
void toggleGrid() {
grid = !grid;
notify("Grid " + (grid ? "On" : "Off"));
}
void toggleFollow() {
follow = !follow;
if (player != null && follow) {
notify("Following " + player.getName());
} else if (follow) {
notify("No player in world");
follow = false;
} else {
notify("Follow disabled");
}
}
void toggleQuality() {
lowtile = !lowtile;
dump();
notify((lowtile ? "Low" : "High") + " Quality");
}
private void syncModeButtons() {
if (modeButtons == null) return;
for (Map.Entry<RenderType, JToggleButton> entry : modeButtons.entrySet()) {
entry.getValue().setSelected(entry.getKey() == currentType);
}
}
private void dump() {
positions.clear();
fastpositions.clear();
}
public BufferedImage getTile(KSet<BlockPosition> fg, int div, int x, int z, O<Integer> m) {
BlockPosition key = new BlockPosition((int) mscale, Math.floorDiv(x, div), Math.floorDiv(z, div));
fg.add(key);
if (positions.containsKey(key)) {
return positions.get(key);
}
if (fastpositions.containsKey(key)) {
if (!working.contains(key) && working.size() < 9) {
m.set(m.get() - 1);
if (m.get() >= 0 && velocity < 50) {
working.add(key);
double mk = mscale;
double mkd = scale;
e.submit(() -> {
PrecisionStopwatch ps = PrecisionStopwatch.start();
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType);
rs.put(ps.getMilliseconds());
working.remove(key);
if (mk == mscale && mkd == scale) {
positions.put(key, b);
}
});
}
}
return fastpositions.get(key);
}
if (workingfast.contains(key) || workingfast.size() > Runtime.getRuntime().availableProcessors()) {
return null;
}
workingfast.add(key);
double mk = mscale;
double mkd = scale;
eh.submit(() -> {
PrecisionStopwatch ps = PrecisionStopwatch.start();
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType);
rs.put(ps.getMilliseconds());
workingfast.remove(key);
if (mk == mscale && mkd == scale) {
fastpositions.put(key, b);
}
});
return null;
}
private double getWorldX(double screenX) {
return (mscale * screenX) + ((oxp / scale) * mscale);
}
private double getWorldZ(double screenZ) {
return (mscale * screenZ) + ((ozp / scale) * mscale);
}
private double getScreenX(double x) {
return (x / mscale) - (oxp / scale);
}
private double getScreenZ(double z) {
return (z / mscale) - (ozp / scale);
}
private double lerp(double current, double target, double speed) {
double diff = target - current;
if (Math.abs(diff) < 0.5) return target;
return current + diff * speed;
}
@Override
public void paint(Graphics gx) {
if (engine.isClosed()) {
EventQueue.invokeLater(() -> {
try { setVisible(false); } catch (Throwable ignored) { }
});
return;
}
if (updateEngine()) {
dump();
}
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
oxp = lerp(oxp, ox, 0.36);
ozp = lerp(ozp, oz, 0.36);
hx = lerp(hx, lx, 0.36);
hz = lerp(hz, lz, 0.36);
if (centities.flip()) {
J.s(() -> {
synchronized (lastEntities) {
lastEntities.clear();
lastEntities.addAll(world.getEntitiesByClass(LivingEntity.class));
}
});
}
lowq = Math.max(Math.min((int) M.lerp(8, 28, velocity / 1000D), 28), 8);
PrecisionStopwatch p = PrecisionStopwatch.start();
Graphics2D g = (Graphics2D) gx;
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
w = getWidth();
h = getHeight();
double vscale = scale;
scale = w / 12D;
if (scale != vscale) {
positions.clear();
}
KSet<BlockPosition> gg = new KSet<>();
int iscale = (int) scale;
g.setColor(BG);
g.fillRect(0, 0, w, h);
double offsetX = oxp / scale;
double offsetZ = ozp / scale;
m.set(3);
for (int r = 0; r < Math.max(w, h); r += iscale) {
for (int i = -iscale; i < w + iscale; i += iscale) {
for (int j = -iscale; j < h + iscale; j += iscale) {
int a = i - (w / 2);
int b = j - (h / 2);
if (a * a + b * b <= r * r) {
int tx = (int) (Math.floor((offsetX + i) / iscale) * iscale);
int tz = (int) (Math.floor((offsetZ + j) / iscale) * iscale);
BufferedImage t = getTile(gg, iscale, tx, tz, m);
if (t != null) {
int rx = Math.floorMod((int) Math.floor(offsetX), iscale);
int rz = Math.floorMod((int) Math.floor(offsetZ), iscale);
g.drawImage(t, i - rx, j - rz, iscale, iscale, null);
}
}
}
}
}
if (grid) {
renderGrid(g, iscale, offsetX, offsetZ);
}
p.end();
for (BlockPosition i : positions.k()) {
if (!gg.contains(i)) {
positions.remove(i);
}
}
handleFollow();
renderOverlays(g, p.getMilliseconds());
if (!isVisible() || !getParent().isVisible()) {
return;
}
long targetMs = eco ? 32 : 16;
long sleepMs = Math.max(1, targetMs - (long) p.getMilliseconds());
J.a(() -> {
J.sleep(sleepMs);
repaint();
});
}
private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) {
g.setColor(GRID_COLOR);
int rx = Math.floorMod((int) Math.floor(offsetX), tileSize);
int rz = Math.floorMod((int) Math.floor(offsetZ), tileSize);
for (int i = -tileSize; i < w + tileSize; i += tileSize) {
g.drawLine(i - rx, 0, i - rx, h);
}
for (int j = -tileSize; j < h + tileSize; j += tileSize) {
g.drawLine(0, j - rz, w, j - rz);
}
}
private void handleFollow() {
if (follow && player != null) {
animateTo(player.getLocation().getX(), player.getLocation().getZ());
}
}
private void renderOverlays(Graphics2D g, double frameMs) {
renderEntities(g);
if (help) {
renderOverlayHelp(g);
} else if (debug) {
renderOverlayDebug(g);
}
renderStatusBar(g, frameMs);
renderHoverOverlay(g, shift);
if (!notifications.isEmpty()) {
renderNotification(g);
}
}
private void renderStatusBar(Graphics2D g, double frameMs) {
int y = h - STATUS_HEIGHT;
g.setColor(STATUS_BG);
g.fillRect(0, y, w, STATUS_HEIGHT);
g.setColor(CARD_BORDER);
g.drawLine(0, y, w, y);
g.setFont(FONT_STATUS);
g.setColor(TEXT_SECONDARY);
double wx = getWorldX(w / 2.0);
double wz = getWorldZ(h / 2.0);
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
String left = String.format(" %s | %.1f bpp | %s x %s blocks",
modeName(currentType), mscale,
Form.f((int) (mscale * w)), Form.f((int) (mscale * h)));
g.drawString(left, 8, y + 17);
String right = String.format("X: %s Z: %s | %d FPS ",
Form.f((int) wx), Form.f((int) wz), fps);
int rw = g.getFontMetrics().stringWidth(right);
g.drawString(right, w - rw - 8, y + 17);
g.setColor(ACCENT);
int modeW = g.getFontMetrics().stringWidth(" " + modeName(currentType));
g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1);
}
private void renderEntities(Graphics2D g) {
Player b = null;
for (Player i : world.getPlayers()) {
b = i;
renderPlayerMarker(g, i.getLocation().getX(), i.getLocation().getZ(), i.getName());
}
synchronized (lastEntities) {
double dist = Double.MAX_VALUE;
LivingEntity nearest = null;
for (LivingEntity i : lastEntities) {
if (i instanceof Player) continue;
renderMobMarker(g, i.getLocation().getX(), i.getLocation().getZ());
if (shift) {
double d = i.getLocation().distanceSquared(
new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz)));
if (d < dist) {
dist = d;
nearest = i;
}
}
}
if (nearest != null && shift) {
double sx = getScreenX(nearest.getLocation().getX());
double sz = getScreenZ(nearest.getLocation().getZ());
g.setColor(MOB_COLOR);
g.fillOval((int) sx - 6, (int) sz - 6, 12, 12);
g.setColor(new Color(220, 80, 80, 60));
g.fillOval((int) sx - 10, (int) sz - 10, 20, 20);
KList<String> k = new KList<>();
k.add(Form.capitalizeWords(nearest.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")));
k.add("Pos: " + nearest.getLocation().getBlockX() + ", " + nearest.getLocation().getBlockY() + ", " + nearest.getLocation().getBlockZ());
k.add("HP: " + Form.f(nearest.getHealth(), 1) + " / " + Form.f(nearest.getAttribute(MAX_HEALTH).getValue(), 1));
drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k);
}
}
player = b;
}
private void renderPlayerMarker(Graphics2D g, double x, double z, String name) {
int sx = (int) getScreenX(x);
int sz = (int) getScreenZ(z);
g.setColor(new Color(80, 200, 120, 40));
g.fillOval(sx - 12, sz - 12, 24, 24);
g.setColor(PLAYER_COLOR);
g.fillOval(sx - 5, sz - 5, 10, 10);
g.setColor(new Color(40, 160, 80));
g.drawOval(sx - 5, sz - 5, 10, 10);
g.setFont(FONT_CARD_BODY);
g.setColor(TEXT_PRIMARY);
int nw = g.getFontMetrics().stringWidth(name);
g.drawString(name, sx - nw / 2, sz - 14);
}
private void renderMobMarker(Graphics2D g, double x, double z) {
int sx = (int) getScreenX(x);
int sz = (int) getScreenZ(z);
g.setColor(MOB_COLOR);
g.fillRect(sx - 2, sz - 2, 4, 4);
}
private void animateTo(double wx, double wz) {
double cx = getWorldX(getWidth() / 2.0);
double cz = getWorldZ(getHeight() / 2.0);
ox += ((wx - cx) / mscale) * scale;
oz += ((wz - cz) / mscale) * scale;
}
private void renderHoverOverlay(Graphics2D g, boolean detailed) {
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz));
IrisRegion region = engine.getComplex().getRegionStream().get(getWorldX(hx), getWorldZ(hz));
KList<String> l = new KList<>();
l.add(biome.getName());
l.add(region.getName());
l.add("Block " + (int) getWorldX(hx) + ", " + (int) getWorldZ(hz));
if (detailed) {
l.add("Chunk " + ((int) getWorldX(hx) >> 4) + ", " + ((int) getWorldZ(hz) >> 4));
l.add("Region " + (((int) getWorldX(hx) >> 4) >> 5) + ", " + (((int) getWorldZ(hz) >> 4) >> 5));
l.add("Key: " + biome.getLoadKey());
l.add("File: " + biome.getLoadFile());
}
drawCard((float) hx + 16, (float) hz, 0, 0, g, l);
}
private void renderOverlayDebug(Graphics2D g) {
KList<String> l = new KList<>();
l.add("Velocity: " + (int) velocity);
l.add("Tiles: " + positions.size() + " HD / " + fastpositions.size() + " LQ");
l.add("Workers: " + working.size() + " HD / " + workingfast.size() + " LQ");
l.add("Center: " + Form.f((int) getWorldX(getWidth() / 2.0)) + ", " + Form.f((int) getWorldZ(getHeight() / 2.0)));
drawCard(CARD_PAD, h - STATUS_HEIGHT - CARD_PAD, 0, 1, g, l);
}
private void renderOverlayHelp(Graphics2D g) {
KList<String> keys = new KList<>();
KList<String> descs = new KList<>();
keys.add("/"); descs.add("Toggle help");
keys.add("R"); descs.add("Refresh tiles");
keys.add("F"); descs.add("Follow player");
keys.add("+/-"); descs.add("Zoom in/out");
keys.add("\\"); descs.add("Reset zoom");
keys.add("M"); descs.add("Cycle render mode");
keys.add("P"); descs.add("Toggle tile quality");
keys.add("E"); descs.add("Toggle 30/60 FPS");
keys.add("G"); descs.add("Toggle grid");
int ff = 0;
for (RenderType i : RenderType.values()) {
ff++;
keys.add(String.valueOf(ff));
descs.add(modeName(i));
}
keys.add("Shift"); descs.add("Detailed biome info");
keys.add("Ctrl+Click"); descs.add("Teleport to cursor");
keys.add("Alt+Click"); descs.add("Open biome in editor");
int maxKeyW = 0;
g.setFont(FONT_HELP_KEY);
for (String k : keys) {
maxKeyW = Math.max(maxKeyW, g.getFontMetrics().stringWidth(k));
}
int lineH = 20;
int totalH = keys.size() * lineH + CARD_PAD * 2 + 4;
int totalW = maxKeyW + 180 + CARD_PAD * 2;
drawCardBackground(g, CARD_PAD, CARD_PAD, totalW, totalH);
for (int i = 0; i < keys.size(); i++) {
int y = CARD_PAD + 16 + i * lineH;
g.setFont(FONT_HELP_KEY);
g.setColor(ACCENT);
g.drawString(keys.get(i), CARD_PAD * 2, y);
g.setFont(FONT_CARD_BODY);
g.setColor(TEXT_SECONDARY);
g.drawString(descs.get(i), CARD_PAD * 2 + maxKeyW + 16, y);
}
}
private void renderNotification(Graphics2D g) {
int y = h - STATUS_HEIGHT - 50;
g.setFont(FONT_NOTIFICATION);
KList<String> active = new KList<>();
for (String i : notifications.k()) {
if (M.ms() > notifications.get(i)) {
notifications.remove(i);
} else {
active.add(i);
}
}
if (active.isEmpty()) return;
String text = String.join(" | ", active);
int tw = g.getFontMetrics().stringWidth(text);
int th = g.getFontMetrics().getHeight();
int px = (w - tw) / 2 - 16;
int py = y - th / 2 - 8;
int bw = tw + 32;
int bh = th + 16;
drawCardBackground(g, px, py, bw, bh);
g.setColor(TEXT_PRIMARY);
g.drawString(text, px + 16, py + th + 4);
}
private void drawCardBackground(Graphics2D g, int x, int y, int w, int h) {
RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, w, h, CARD_RADIUS, CARD_RADIUS);
g.setColor(CARD_BG);
g.fill(rect);
g.setColor(CARD_BORDER);
g.draw(rect);
}
private void drawCard(float x, float y, double pushX, double pushZ, Graphics2D g, KList<String> text) {
g.setFont(FONT_CARD_BODY);
int lineH = g.getFontMetrics().getHeight();
int cardW = 0;
for (String i : text) {
cardW = Math.max(cardW, g.getFontMetrics().stringWidth(i));
}
cardW += CARD_PAD * 2;
int cardH = text.size() * lineH + CARD_PAD * 2 - 4;
int cx = (int) (x - cardW * pushX);
int cy = (int) (y - cardH * pushZ);
drawCardBackground(g, cx, cy, cardW, cardH);
int ty = cy + CARD_PAD + lineH - 4;
for (int i = 0; i < text.size(); i++) {
g.setColor(i == 0 ? TEXT_PRIMARY : TEXT_SECONDARY);
g.setFont(i == 0 ? FONT_CARD_TITLE : FONT_CARD_BODY);
g.drawString(text.get(i), cx + CARD_PAD, ty + i * lineH);
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
if (e.isControlDown()) return;
double m0 = mscale;
double m1 = m0 + ((0.25 * m0) * notches);
m1 = Math.max(m1, 0.00001);
if (m1 == m0) return;
positions.clear();
fastpositions.clear();
Point p = e.getPoint();
double sx = p.getX();
double sz = p.getY();
double newOxp = scale * ((m0 / m1) * (sx + (oxp / scale)) - sx);
double newOzp = scale * ((m0 / m1) * (sz + (ozp / scale)) - sz);
mscale = m1;
oxp = newOxp;
ozp = newOzp;
ox = oxp;
oz = ozp;
}
@Override
public void mouseClicked(MouseEvent e) {
if (control) teleport();
else if (alt) open();
}
@Override public void mousePressed(MouseEvent e) { }
@Override public void mouseReleased(MouseEvent e) { }
@Override public void mouseEntered(MouseEvent e) { }
@Override public void mouseExited(MouseEvent e) { }
private void open() {
IrisComplex complex = engine.getComplex();
File r = null;
switch (currentType) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
}
if (r != null) {
notify("Opened " + r.getName());
}
}
private void teleport() {
J.s(() -> {
if (player != null) {
int xx = (int) getWorldX(hx);
int zz = (int) getWorldZ(hz);
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
notify("Teleported to " + xx + ", " + yy + ", " + zz);
} else {
notify("No player in world");
}
});
}
}
@@ -1,58 +0,0 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import java.util.concurrent.CompletableFuture;
final class BukkitPublicBackend implements WorldLifecycleBackend {
private final CapabilitySnapshot capabilities;
BukkitPublicBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
return true;
}
@Override
public CompletableFuture<World> create(WorldLifecycleRequest request) {
World existing = Bukkit.getWorld(request.worldName());
if (existing != null) {
return CompletableFuture.completedFuture(existing);
}
WorldCreator creator = request.toWorldCreator();
if (request.generator() != null) {
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
}
try {
World world = creator.createWorld();
return CompletableFuture.completedFuture(world);
} catch (Throwable e) {
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
} finally {
WorldLifecycleStaging.clearAll(request.worldName());
}
}
@Override
public boolean unload(World world, boolean save) {
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
}
@Override
public String backendName() {
return "bukkit_public";
}
@Override
public String describeSelectionReason() {
return "public Bukkit world lifecycle path";
}
}
@@ -1,235 +0,0 @@
package art.arcane.iris.core.lifecycle;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.function.Predicate;
final class CapabilityResolution {
private CapabilityResolution() {
}
static Method resolveCreateLevelMethod(Class<?> owner) throws NoSuchMethodException {
Method current = resolveMethod(owner, "createLevel", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& "LevelStem".equals(params[0].getSimpleName())
&& "WorldLoadingInfoAndData".equals(params[1].getSimpleName())
&& "WorldDataAndGenSettings".equals(params[2].getSimpleName());
});
if (current != null) {
return current;
}
Method legacy = resolveMethod(owner, "createLevel", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 4
&& "LevelStem".equals(params[0].getSimpleName())
&& "WorldLoadingInfo".equals(params[1].getSimpleName())
&& "LevelStorageAccess".equals(params[2].getSimpleName())
&& "PrimaryLevelData".equals(params[3].getSimpleName());
});
if (legacy != null) {
return legacy;
}
throw new NoSuchMethodException(owner.getName() + "#createLevel");
}
static Method resolveLevelStorageAccessMethod(Class<?> owner) throws NoSuchMethodException {
Method exactValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2
&& String.class.equals(params[0])
&& "ResourceKey".equals(params[1].getSimpleName())
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (exactValidate != null) {
return exactValidate;
}
Method oneArgValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1
&& String.class.equals(params[0])
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (oneArgValidate != null) {
return oneArgValidate;
}
Method exactCreate = resolveMethod(owner, "createAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2
&& String.class.equals(params[0])
&& "ResourceKey".equals(params[1].getSimpleName())
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (exactCreate != null) {
return exactCreate;
}
Method oneArgCreate = resolveMethod(owner, "createAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1
&& String.class.equals(params[0])
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (oneArgCreate != null) {
return oneArgCreate;
}
throw new NoSuchMethodException(owner.getName() + "#validateAndCreateAccess/createAccess");
}
static Method resolvePaperWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
Method current = resolveMethod(owner, "loadWorldData", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& "MinecraftServer".equals(params[0].getSimpleName())
&& "ResourceKey".equals(params[1].getSimpleName())
&& String.class.equals(params[2])
&& "LoadedWorldData".equals(method.getReturnType().getSimpleName());
});
if (current != null) {
return current;
}
Method legacy = resolveMethod(owner, "getLevelData", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && "LevelStorageAccess".equals(params[0].getSimpleName());
});
if (legacy != null) {
return legacy;
}
throw new NoSuchMethodException(owner.getName() + "#loadWorldData/getLevelData");
}
static Constructor<?> resolveWorldLoadingInfoConstructor(Class<?> owner) throws NoSuchMethodException {
Constructor<?> current = resolveConstructor(owner, constructor -> {
Class<?>[] params = constructor.getParameterTypes();
return params.length == 4
&& "Environment".equals(params[0].getSimpleName())
&& "ResourceKey".equals(params[1].getSimpleName())
&& "ResourceKey".equals(params[2].getSimpleName())
&& boolean.class.equals(params[3]);
});
if (current != null) {
return current;
}
Constructor<?> legacy = resolveConstructor(owner, constructor -> {
Class<?>[] params = constructor.getParameterTypes();
return params.length == 5
&& int.class.equals(params[0])
&& String.class.equals(params[1])
&& String.class.equals(params[2])
&& "ResourceKey".equals(params[3].getSimpleName())
&& boolean.class.equals(params[4]);
});
if (legacy != null) {
return legacy;
}
throw new NoSuchMethodException(owner.getName() + "#<init>");
}
static Constructor<?> resolveWorldLoadingInfoAndDataConstructor(Class<?> owner) throws NoSuchMethodException {
Constructor<?> constructor = resolveConstructor(owner, candidate -> {
Class<?>[] params = candidate.getParameterTypes();
return params.length == 2
&& "WorldLoadingInfo".equals(params[0].getSimpleName())
&& "LoadedWorldData".equals(params[1].getSimpleName());
});
if (constructor == null) {
throw new NoSuchMethodException(owner.getName() + "#<init>");
}
return constructor;
}
static Method resolveCreateNewWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
Method method = resolveMethod(owner, "createNewWorldData", candidate -> {
Class<?>[] params = candidate.getParameterTypes();
return params.length == 5
&& "DedicatedServerSettings".equals(params[0].getSimpleName())
&& "DataLoadContext".equals(params[1].getSimpleName())
&& "Registry".equals(params[2].getSimpleName())
&& boolean.class.equals(params[3])
&& boolean.class.equals(params[4]);
});
if (method == null) {
throw new NoSuchMethodException(owner.getName() + "#createNewWorldData");
}
return method;
}
static Method resolveServerRegistryAccessMethod(Class<?> owner) throws NoSuchMethodException {
Method method = resolveMethod(owner, "registryAccess", candidate -> candidate.getParameterCount() == 0
&& !void.class.equals(candidate.getReturnType()));
if (method == null) {
throw new NoSuchMethodException(owner.getName() + "#registryAccess");
}
return method;
}
static Method resolveMethod(Class<?> owner, String name, Predicate<Method> predicate) {
Method selected = scanMethods(owner.getMethods(), name, predicate);
if (selected != null) {
return selected;
}
Class<?> current = owner;
while (current != null) {
selected = scanMethods(current.getDeclaredMethods(), name, predicate);
if (selected != null) {
selected.setAccessible(true);
return selected;
}
current = current.getSuperclass();
}
return null;
}
static Field resolveField(Class<?> owner, String name) throws NoSuchFieldException {
Class<?> current = owner;
while (current != null) {
try {
Field field = current.getDeclaredField(name);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(owner.getName() + "#" + name);
}
private static Method scanMethods(Method[] methods, String name, Predicate<Method> predicate) {
for (Method method : methods) {
if (!method.getName().equals(name)) {
continue;
}
if (predicate.test(method)) {
return method;
}
}
return null;
}
private static Constructor<?> resolveConstructor(Class<?> owner, Predicate<Constructor<?>> predicate) {
for (Constructor<?> constructor : owner.getConstructors()) {
if (predicate.test(constructor)) {
return constructor;
}
}
for (Constructor<?> constructor : owner.getDeclaredConstructors()) {
if (predicate.test(constructor)) {
constructor.setAccessible(true);
return constructor;
}
}
return null;
}
}
@@ -1,612 +0,0 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Locale;
public final class CapabilitySnapshot {
public enum PaperLikeFlavor {
CURRENT_INFO_AND_DATA,
LEGACY_STORAGE_ACCESS,
UNSUPPORTED
}
private final ServerFamily serverFamily;
private final boolean regionizedRuntime;
private final Object worldsProvider;
private final Class<?> worldsLevelStemClass;
private final Class<?> worldsGeneratorTypeClass;
private final String worldsProviderResolution;
private final Object bukkitServer;
private final Object minecraftServer;
private final Method createLevelMethod;
private final PaperLikeFlavor paperLikeFlavor;
private final Class<?> paperWorldLoaderClass;
private final Method paperWorldDataMethod;
private final Constructor<?> worldLoadingInfoConstructor;
private final Constructor<?> worldLoadingInfoAndDataConstructor;
private final Method createNewWorldDataMethod;
private final Method levelStorageAccessMethod;
private final Field worldLoaderContextField;
private final Method serverRegistryAccessMethod;
private final Field settingsField;
private final Field optionsField;
private final Method isDemoMethod;
private final Method unloadWorldAsyncMethod;
private final Method chunkAtAsyncMethod;
private final Method removeLevelMethod;
private final String paperLikeResolution;
private CapabilitySnapshot(
ServerFamily serverFamily,
boolean regionizedRuntime,
Object worldsProvider,
Class<?> worldsLevelStemClass,
Class<?> worldsGeneratorTypeClass,
String worldsProviderResolution,
Object bukkitServer,
Object minecraftServer,
Method createLevelMethod,
PaperLikeFlavor paperLikeFlavor,
Class<?> paperWorldLoaderClass,
Method paperWorldDataMethod,
Constructor<?> worldLoadingInfoConstructor,
Constructor<?> worldLoadingInfoAndDataConstructor,
Method createNewWorldDataMethod,
Method levelStorageAccessMethod,
Field worldLoaderContextField,
Method serverRegistryAccessMethod,
Field settingsField,
Field optionsField,
Method isDemoMethod,
Method unloadWorldAsyncMethod,
Method chunkAtAsyncMethod,
Method removeLevelMethod,
String paperLikeResolution
) {
this.serverFamily = serverFamily;
this.regionizedRuntime = regionizedRuntime;
this.worldsProvider = worldsProvider;
this.worldsLevelStemClass = worldsLevelStemClass;
this.worldsGeneratorTypeClass = worldsGeneratorTypeClass;
this.worldsProviderResolution = worldsProviderResolution;
this.bukkitServer = bukkitServer;
this.minecraftServer = minecraftServer;
this.createLevelMethod = createLevelMethod;
this.paperLikeFlavor = paperLikeFlavor;
this.paperWorldLoaderClass = paperWorldLoaderClass;
this.paperWorldDataMethod = paperWorldDataMethod;
this.worldLoadingInfoConstructor = worldLoadingInfoConstructor;
this.worldLoadingInfoAndDataConstructor = worldLoadingInfoAndDataConstructor;
this.createNewWorldDataMethod = createNewWorldDataMethod;
this.levelStorageAccessMethod = levelStorageAccessMethod;
this.worldLoaderContextField = worldLoaderContextField;
this.serverRegistryAccessMethod = serverRegistryAccessMethod;
this.settingsField = settingsField;
this.optionsField = optionsField;
this.isDemoMethod = isDemoMethod;
this.unloadWorldAsyncMethod = unloadWorldAsyncMethod;
this.chunkAtAsyncMethod = chunkAtAsyncMethod;
this.removeLevelMethod = removeLevelMethod;
this.paperLikeResolution = paperLikeResolution;
}
public static CapabilitySnapshot probe() {
Server server = Bukkit.getServer();
Object bukkitServer = server;
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
ServerFamily serverFamily = detectServerFamily(server, regionizedRuntime);
Object worldsProvider = null;
Class<?> worldsLevelStemClass = null;
Class<?> worldsGeneratorTypeClass = null;
String worldsProviderResolution = "inactive";
try {
Object[] worldsProviderData = resolveWorldsProvider();
worldsProvider = worldsProviderData[0];
worldsLevelStemClass = (Class<?>) worldsProviderData[1];
worldsGeneratorTypeClass = (Class<?>) worldsProviderData[2];
worldsProviderResolution = (String) worldsProviderData[3];
} catch (Throwable e) {
worldsProviderResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
}
Object minecraftServer = null;
Method createLevelMethod = null;
PaperLikeFlavor paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
Class<?> paperWorldLoaderClass = null;
Method paperWorldDataMethod = null;
Constructor<?> worldLoadingInfoConstructor = null;
Constructor<?> worldLoadingInfoAndDataConstructor = null;
Method createNewWorldDataMethod = null;
Method levelStorageAccessMethod = null;
Field worldLoaderContextField = null;
Method serverRegistryAccessMethod = null;
Field settingsField = null;
Field optionsField = null;
Method isDemoMethod = null;
Method removeLevelMethod = null;
String paperLikeResolution = "inactive";
try {
if (bukkitServer != null) {
Method getServerMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "getServer", method -> method.getParameterCount() == 0);
if (getServerMethod != null) {
minecraftServer = getServerMethod.invoke(bukkitServer);
}
}
if (minecraftServer != null) {
Class<?> minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer");
if (!minecraftServerClass.isInstance(minecraftServer)) {
throw new IllegalStateException("resolved server is not a MinecraftServer: " + minecraftServer.getClass().getName());
}
createLevelMethod = CapabilityResolution.resolveCreateLevelMethod(minecraftServer.getClass());
removeLevelMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "removeLevel", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && "ServerLevel".equals(params[0].getSimpleName());
});
worldLoaderContextField = CapabilityResolution.resolveField(minecraftServer.getClass(), "worldLoaderContext");
serverRegistryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(minecraftServer.getClass());
settingsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "settings");
optionsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "options");
isDemoMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "isDemo", method -> method.getParameterCount() == 0 && boolean.class.equals(method.getReturnType()));
Class<?> mainClass = Class.forName("net.minecraft.server.Main");
createNewWorldDataMethod = CapabilityResolution.resolveCreateNewWorldDataMethod(mainClass);
Class<?> paperLoaderCandidate = Class.forName("io.papermc.paper.world.PaperWorldLoader");
paperWorldLoaderClass = paperLoaderCandidate;
paperWorldDataMethod = CapabilityResolution.resolvePaperWorldDataMethod(paperLoaderCandidate);
Class<?> worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo");
worldLoadingInfoConstructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(worldLoadingInfoClass);
if (createLevelMethod.getParameterCount() == 3) {
Class<?> worldLoadingInfoAndDataClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfoAndData");
worldLoadingInfoAndDataConstructor = CapabilityResolution.resolveWorldLoadingInfoAndDataConstructor(worldLoadingInfoAndDataClass);
paperLikeFlavor = PaperLikeFlavor.CURRENT_INFO_AND_DATA;
} else {
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
levelStorageAccessMethod = CapabilityResolution.resolveLevelStorageAccessMethod(levelStorageSourceClass);
paperLikeFlavor = PaperLikeFlavor.LEGACY_STORAGE_ACCESS;
}
paperLikeResolution = "available(flavor=" + paperLikeFlavor.name().toLowerCase(Locale.ROOT)
+ ", createLevel=" + createLevelMethod.toGenericString() + ")";
}
} catch (Throwable e) {
paperLikeResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
createLevelMethod = null;
paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
paperWorldLoaderClass = null;
paperWorldDataMethod = null;
worldLoadingInfoConstructor = null;
worldLoadingInfoAndDataConstructor = null;
createNewWorldDataMethod = null;
levelStorageAccessMethod = null;
worldLoaderContextField = null;
serverRegistryAccessMethod = null;
settingsField = null;
optionsField = null;
isDemoMethod = null;
removeLevelMethod = null;
}
Method unloadWorldAsyncMethod = null;
try {
if (bukkitServer != null) {
unloadWorldAsyncMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "unloadWorldAsync", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& World.class.equals(params[0])
&& boolean.class.equals(params[1])
&& "Consumer".equals(params[2].getSimpleName());
});
}
} catch (Throwable ignored) {
unloadWorldAsyncMethod = null;
}
Method chunkAtAsyncMethod = null;
try {
chunkAtAsyncMethod = CapabilityResolution.resolveMethod(World.class, "getChunkAtAsync", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& int.class.equals(params[0])
&& int.class.equals(params[1])
&& boolean.class.equals(params[2]);
});
} catch (Throwable ignored) {
chunkAtAsyncMethod = null;
}
return new CapabilitySnapshot(
serverFamily,
regionizedRuntime,
worldsProvider,
worldsLevelStemClass,
worldsGeneratorTypeClass,
worldsProviderResolution,
bukkitServer,
minecraftServer,
createLevelMethod,
paperLikeFlavor,
paperWorldLoaderClass,
paperWorldDataMethod,
worldLoadingInfoConstructor,
worldLoadingInfoAndDataConstructor,
createNewWorldDataMethod,
levelStorageAccessMethod,
worldLoaderContextField,
serverRegistryAccessMethod,
settingsField,
optionsField,
isDemoMethod,
unloadWorldAsyncMethod,
chunkAtAsyncMethod,
removeLevelMethod,
paperLikeResolution
);
}
public static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) {
Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null;
Method createLevelMethod = null;
Field worldLoaderContextField = null;
Method serverRegistryAccessMethod = null;
try {
createLevelMethod = paperLikeRuntimeHealthy
? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class)
: null;
worldLoaderContextField = paperLikeRuntimeHealthy
? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext")
: null;
serverRegistryAccessMethod = paperLikeRuntimeHealthy
? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class)
: null;
} catch (NoSuchMethodException | NoSuchFieldException e) {
throw new IllegalStateException(e);
}
return new CapabilitySnapshot(
serverFamily,
regionizedRuntime,
worldsProviderHealthy ? new Object() : null,
worldsProviderHealthy ? Object.class : null,
worldsProviderHealthy ? Object.class : null,
worldsProviderHealthy ? "test-provider" : "inactive",
null,
minecraftServer,
createLevelMethod,
paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED,
null,
null,
null,
null,
null,
null,
worldLoaderContextField,
serverRegistryAccessMethod,
null,
null,
null,
null,
null,
null,
paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)"
);
}
public static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) {
TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess);
Method createLevelMethod;
Field worldLoaderContextField;
Method registryAccessMethod;
try {
createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class);
worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext");
registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class);
} catch (NoSuchMethodException | NoSuchFieldException e) {
throw new IllegalStateException(e);
}
return new CapabilitySnapshot(
serverFamily,
regionizedRuntime,
null,
null,
null,
"inactive",
null,
minecraftServer,
createLevelMethod,
PaperLikeFlavor.CURRENT_INFO_AND_DATA,
null,
null,
null,
null,
null,
null,
worldLoaderContextField,
registryAccessMethod,
null,
null,
null,
null,
null,
null,
"available(test-runtime-registries)"
);
}
public ServerFamily serverFamily() {
return serverFamily;
}
public boolean regionizedRuntime() {
return regionizedRuntime;
}
public Object worldsProvider() {
return worldsProvider;
}
public Class<?> worldsLevelStemClass() {
return worldsLevelStemClass;
}
public Class<?> worldsGeneratorTypeClass() {
return worldsGeneratorTypeClass;
}
public Object bukkitServer() {
return bukkitServer;
}
public Object minecraftServer() {
return minecraftServer;
}
public Method createLevelMethod() {
return createLevelMethod;
}
public PaperLikeFlavor paperLikeFlavor() {
return paperLikeFlavor;
}
public Class<?> paperWorldLoaderClass() {
return paperWorldLoaderClass;
}
public Method paperWorldDataMethod() {
return paperWorldDataMethod;
}
public Constructor<?> worldLoadingInfoConstructor() {
return worldLoadingInfoConstructor;
}
public Constructor<?> worldLoadingInfoAndDataConstructor() {
return worldLoadingInfoAndDataConstructor;
}
public Method createNewWorldDataMethod() {
return createNewWorldDataMethod;
}
public Method levelStorageAccessMethod() {
return levelStorageAccessMethod;
}
public Field worldLoaderContextField() {
return worldLoaderContextField;
}
public Method serverRegistryAccessMethod() {
return serverRegistryAccessMethod;
}
public Field settingsField() {
return settingsField;
}
public Field optionsField() {
return optionsField;
}
public Method isDemoMethod() {
return isDemoMethod;
}
public Method unloadWorldAsyncMethod() {
return unloadWorldAsyncMethod;
}
public Method chunkAtAsyncMethod() {
return chunkAtAsyncMethod;
}
public Method removeLevelMethod() {
return removeLevelMethod;
}
public boolean hasWorldsProvider() {
return worldsProvider != null && worldsLevelStemClass != null && worldsGeneratorTypeClass != null;
}
public boolean hasPaperLikeRuntime() {
return minecraftServer != null
&& createLevelMethod != null
&& serverRegistryAccessMethod != null
&& paperLikeFlavor != PaperLikeFlavor.UNSUPPORTED;
}
public String worldsProviderResolution() {
return worldsProviderResolution;
}
public String paperLikeResolution() {
return paperLikeResolution;
}
public String describe() {
return "family=" + serverFamily.id()
+ ", regionizedRuntime=" + regionizedRuntime
+ ", worldsProvider=" + worldsProviderResolution
+ ", paperLike=" + paperLikeResolution
+ ", serverRegistryAccess=" + (serverRegistryAccessMethod != null)
+ ", unloadAsync=" + (unloadWorldAsyncMethod != null)
+ ", chunkAsync=" + (chunkAtAsyncMethod != null);
}
private static ServerFamily detectServerFamily(Server server, boolean regionizedRuntime) {
String bukkitName = server == null ? "" : server.getName();
String bukkitVersion = server == null ? "" : server.getVersion();
String serverClassName = server == null ? "" : server.getClass().getName();
boolean canvasRuntime = hasCanvasRuntime();
if (containsIgnoreCase(bukkitName, "folia")
|| containsIgnoreCase(bukkitVersion, "folia")
|| containsIgnoreCase(serverClassName, "folia")) {
return ServerFamily.FOLIA;
}
if (canvasRuntime
|| containsIgnoreCase(bukkitName, "canvas")
|| containsIgnoreCase(bukkitVersion, "canvas")
|| containsIgnoreCase(serverClassName, "canvas")) {
return regionizedRuntime ? ServerFamily.CANVAS : ServerFamily.CANVAS;
}
if (containsIgnoreCase(bukkitName, "purpur")
|| containsIgnoreCase(bukkitVersion, "purpur")
|| containsIgnoreCase(serverClassName, "purpur")) {
return ServerFamily.PURPUR;
}
if (containsIgnoreCase(bukkitName, "paper")
|| containsIgnoreCase(bukkitVersion, "paper")
|| containsIgnoreCase(serverClassName, "paper")
|| containsIgnoreCase(bukkitName, "pufferfish")
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|| containsIgnoreCase(serverClassName, "pufferfish")) {
return ServerFamily.PAPER;
}
if (containsIgnoreCase(bukkitName, "spigot")
|| containsIgnoreCase(bukkitVersion, "spigot")
|| containsIgnoreCase(serverClassName, "spigot")) {
return ServerFamily.SPIGOT;
}
if (containsIgnoreCase(bukkitName, "craftbukkit")
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|| containsIgnoreCase(serverClassName, "craftbukkit")
|| containsIgnoreCase(bukkitName, "bukkit")
|| containsIgnoreCase(bukkitVersion, "bukkit")) {
return ServerFamily.BUKKIT;
}
if (regionizedRuntime || J.isFolia()) {
return ServerFamily.FOLIA;
}
return ServerFamily.UNKNOWN;
}
private static boolean hasCanvasRuntime() {
try {
Class.forName("io.canvasmc.canvas.region.WorldRegionizer");
return true;
} catch (Throwable ignored) {
return false;
}
}
private static boolean containsIgnoreCase(String value, String needle) {
if (value == null || needle == null || needle.isEmpty()) {
return false;
}
return value.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT));
}
private static Object[] resolveWorldsProvider() throws Throwable {
try {
Class<?> worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider");
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem");
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType");
Object provider = Bukkit.getServicesManager().load(worldsProviderClass);
String resolution = provider == null ? "inactive(service not registered)" : "active(service=" + provider.getClass().getName() + ")";
return new Object[]{provider, levelStemClass, generatorTypeClass, resolution};
} catch (Throwable ignored) {
}
Collection<Class<?>> knownServices = Bukkit.getServicesManager().getKnownServices();
for (Class<?> serviceClass : knownServices) {
if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) {
continue;
}
RegisteredServiceProvider<?> registration = Bukkit.getServicesManager().getRegistration(serviceClass);
if (registration == null) {
continue;
}
Object provider = registration.getProvider();
ClassLoader loader = serviceClass.getClassLoader();
if (loader == null && provider != null) {
loader = provider.getClass().getClassLoader();
}
if (loader == null) {
continue;
}
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader);
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader);
return new Object[]{provider, levelStemClass, generatorTypeClass, "active(service-scan=" + provider.getClass().getName() + ")"};
}
return new Object[]{null, null, null, "inactive(service scan found nothing)"};
}
private static final class TestingPaperLikeServer {
private final TestingWorldLoaderContext worldLoaderContext;
private final Object registryAccess;
private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) {
this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions);
this.registryAccess = registryAccess;
}
@SuppressWarnings("unused")
private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) {
}
@SuppressWarnings("unused")
private Object registryAccess() {
return registryAccess;
}
}
private static final class TestingWorldLoaderContext {
private final Object datapackDimensions;
private TestingWorldLoaderContext(Object datapackDimensions) {
this.datapackDimensions = datapackDimensions;
}
@SuppressWarnings("unused")
private Object datapackDimensions() {
return datapackDimensions;
}
}
}
@@ -1,89 +0,0 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.Iris;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
private final CapabilitySnapshot capabilities;
PaperLikeRuntimeBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
return request.studio()
&& capabilities.serverFamily().isPaperLike()
&& capabilities.hasPaperLikeRuntime();
}
@Override
public CompletableFuture<World> create(WorldLifecycleRequest request) {
Object legacyStorageAccess = null;
try {
World existing = Bukkit.getWorld(request.worldName());
if (existing != null) {
return CompletableFuture.completedFuture(existing);
}
if (request.generator() == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Runtime world creation requires a non-null chunk generator."));
}
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
WorldLifecycleSupport.stageRuntimeConfiguration(request.worldName());
Iris.info("WorldLifecycle runtime LevelStem: world=" + request.worldName()
+ ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT)
+ ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request));
Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request);
Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldName());
if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) {
Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey);
Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName());
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio());
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
} else {
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName());
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, !request.studio());
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData);
}
World loadedWorld = Bukkit.getWorld(request.worldName());
if (loadedWorld == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Paper-like runtime backend did not load world \"" + request.worldName() + "\"."));
}
return CompletableFuture.completedFuture(loadedWorld);
} catch (Throwable e) {
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
} finally {
WorldLifecycleStaging.clearGenerator(request.worldName());
WorldLifecycleSupport.closeLevelStorageAccess(legacyStorageAccess);
}
}
@Override
public boolean unload(World world, boolean save) {
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
}
@Override
public String backendName() {
return "paper_like_runtime";
}
@Override
public String describeSelectionReason() {
return "server family " + capabilities.serverFamily().id() + " exposes paper-like runtime world lifecycle capabilities";
}
}
@@ -1,21 +0,0 @@
package art.arcane.iris.core.lifecycle;
import java.util.Locale;
public enum ServerFamily {
BUKKIT,
SPIGOT,
PAPER,
PURPUR,
FOLIA,
CANVAS,
UNKNOWN;
public boolean isPaperLike() {
return this == PAPER || this == PURPUR || this == FOLIA || this == CANVAS;
}
public String id() {
return name().toLowerCase(Locale.ROOT);
}
}
@@ -1,17 +0,0 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.World;
import java.util.concurrent.CompletableFuture;
public interface WorldLifecycleBackend {
boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities);
CompletableFuture<World> create(WorldLifecycleRequest request);
boolean unload(World world, boolean save);
String backendName();
String describeSelectionReason();
}
@@ -1,7 +0,0 @@
package art.arcane.iris.core.lifecycle;
public enum WorldLifecycleCaller {
STUDIO,
CREATE,
BENCHMARK
}
@@ -1,51 +0,0 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
public record WorldLifecycleRequest(
String worldName,
World.Environment environment,
ChunkGenerator generator,
BiomeProvider biomeProvider,
WorldType worldType,
boolean generateStructures,
boolean hardcore,
long seed,
boolean studio,
boolean benchmark,
WorldLifecycleCaller callerKind
) {
public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) {
return new WorldLifecycleRequest(
creator.name(),
creator.environment(),
creator.generator(),
creator.biomeProvider(),
creator.type(),
creator.generateStructures(),
creator.hardcore(),
creator.seed(),
studio,
benchmark,
callerKind
);
}
public WorldCreator toWorldCreator() {
WorldCreator creator = new WorldCreator(worldName)
.environment(environment)
.generateStructures(generateStructures)
.hardcore(hardcore)
.type(worldType)
.seed(seed)
.generator(generator);
if (biomeProvider != null) {
creator.biomeProvider(biomeProvider);
}
return creator;
}
}
@@ -1,178 +0,0 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
public final class WorldLifecycleService {
private static volatile WorldLifecycleService instance;
private final CapabilitySnapshot capabilities;
private final WorldsProviderBackend worldsProviderBackend;
private final PaperLikeRuntimeBackend paperLikeRuntimeBackend;
private final BukkitPublicBackend bukkitPublicBackend;
private final List<WorldLifecycleBackend> backends;
private final Map<String, String> worldBackendByName;
public WorldLifecycleService(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
this.worldsProviderBackend = new WorldsProviderBackend(capabilities);
this.paperLikeRuntimeBackend = new PaperLikeRuntimeBackend(capabilities);
this.bukkitPublicBackend = new BukkitPublicBackend(capabilities);
this.backends = List.of(worldsProviderBackend, paperLikeRuntimeBackend, bukkitPublicBackend);
this.worldBackendByName = new ConcurrentHashMap<>();
}
public static WorldLifecycleService get() {
WorldLifecycleService current = instance;
if (current != null) {
return current;
}
synchronized (WorldLifecycleService.class) {
if (instance != null) {
return instance;
}
CapabilitySnapshot capabilities = CapabilitySnapshot.probe();
instance = new WorldLifecycleService(capabilities);
Iris.info("WorldLifecycle capabilities: %s", capabilities.describe());
return instance;
}
}
public CapabilitySnapshot capabilities() {
return capabilities;
}
public CompletableFuture<World> create(WorldLifecycleRequest request) {
WorldLifecycleBackend backend;
try {
backend = selectCreateBackend(request);
} catch (Throwable e) {
Iris.reportError("WorldLifecycle create backend selection failed for world=\"" + request.worldName()
+ "\", caller=" + request.callerKind().name().toLowerCase() + ".", e);
return CompletableFuture.failedFuture(e);
}
Iris.info("WorldLifecycle create: world=%s, caller=%s, backend=%s, reason=%s",
request.worldName(),
request.callerKind().name().toLowerCase(),
backend.backendName(),
backend.describeSelectionReason());
return backend.create(request).whenComplete((world, throwable) -> {
if (throwable != null) {
Throwable cause = WorldLifecycleSupport.unwrap(throwable);
Iris.reportError("WorldLifecycle create failed: world=\"" + request.worldName()
+ "\", caller=" + request.callerKind().name().toLowerCase()
+ ", backend=" + backend.backendName()
+ ", family=" + capabilities.serverFamily().id() + ".", cause);
return;
}
if (world != null) {
worldBackendByName.put(world.getName(), backend.backendName());
}
});
}
public World createBlocking(WorldLifecycleRequest request) {
try {
return create(request).join();
} catch (CompletionException e) {
throw new IllegalStateException(WorldLifecycleSupport.unwrap(e));
}
}
public boolean unload(World world, boolean save) {
if (!J.isPrimaryThread()) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
J.s(() -> {
try {
future.complete(unloadDirect(world, save));
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
return future.join();
}
return unloadDirect(world, save);
}
private boolean unloadDirect(World world, boolean save) {
WorldLifecycleBackend backend = selectUnloadBackend(world.getName());
Iris.info("WorldLifecycle unload: world=%s, backend=%s, reason=%s",
world.getName(),
backend.backendName(),
backend.describeSelectionReason());
boolean unloaded;
try {
unloaded = backend.unload(world, save);
} catch (Throwable e) {
Iris.reportError("WorldLifecycle unload failed: world=\"" + world.getName()
+ "\", backend=" + backend.backendName()
+ ", family=" + capabilities.serverFamily().id() + ".", e);
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error error) {
throw error;
}
throw new IllegalStateException(e);
}
if (unloaded) {
worldBackendByName.remove(world.getName());
}
return unloaded;
}
public String backendNameForWorld(String worldName) {
return selectUnloadBackend(worldName).backendName();
}
WorldLifecycleBackend selectCreateBackend(WorldLifecycleRequest request) {
if (worldsProviderBackend.supports(request, capabilities)) {
return worldsProviderBackend;
}
if (request.studio() && capabilities.serverFamily().isPaperLike()) {
if (!paperLikeRuntimeBackend.supports(request, capabilities)) {
throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for studio create on "
+ capabilities.serverFamily().id() + ": " + capabilities.paperLikeResolution());
}
return paperLikeRuntimeBackend;
}
for (WorldLifecycleBackend backend : backends) {
if (backend.supports(request, capabilities)) {
return backend;
}
}
throw new IllegalStateException("No world lifecycle backend supports request for \"" + request.worldName() + "\".");
}
WorldLifecycleBackend selectUnloadBackend(String worldName) {
String backendName = worldBackendByName.get(worldName);
if (backendName == null) {
return bukkitPublicBackend;
}
for (WorldLifecycleBackend backend : backends) {
if (backend.backendName().equals(backendName)) {
return backend;
}
}
return bukkitPublicBackend;
}
void rememberBackend(String worldName, String backendName) {
worldBackendByName.put(worldName, backendName);
}
}
@@ -1,60 +0,0 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class WorldLifecycleStaging {
private static final Map<String, ChunkGenerator> stagedGenerators = new ConcurrentHashMap<>();
private static final Map<String, BiomeProvider> stagedBiomeProviders = new ConcurrentHashMap<>();
private static final Map<String, ChunkGenerator> stagedStemGenerators = new ConcurrentHashMap<>();
private WorldLifecycleStaging() {
}
public static void stageGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator, @Nullable BiomeProvider biomeProvider) {
stagedGenerators.put(worldName, generator);
if (biomeProvider != null) {
stagedBiomeProviders.put(worldName, biomeProvider);
} else {
stagedBiomeProviders.remove(worldName);
}
}
public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) {
stagedStemGenerators.put(worldName, generator);
}
@Nullable
public static ChunkGenerator consumeGenerator(@NotNull String worldName) {
return stagedGenerators.remove(worldName);
}
@Nullable
public static BiomeProvider consumeBiomeProvider(@NotNull String worldName) {
return stagedBiomeProviders.remove(worldName);
}
@Nullable
public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) {
return stagedStemGenerators.remove(worldName);
}
public static void clearGenerator(@NotNull String worldName) {
stagedGenerators.remove(worldName);
stagedBiomeProviders.remove(worldName);
}
public static void clearStem(@NotNull String worldName) {
stagedStemGenerators.remove(worldName);
}
public static void clearAll(@NotNull String worldName) {
clearGenerator(worldName);
clearStem(worldName);
}
}
@@ -1,520 +0,0 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.Iris;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
final class WorldLifecycleSupport {
private WorldLifecycleSupport() {
}
static Throwable unwrap(Throwable throwable) {
if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) {
return unwrap(invocationTargetException.getCause());
}
if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) {
return unwrap(completionException.getCause());
}
if (throwable instanceof ExecutionException executionException && executionException.getCause() != null) {
return unwrap(executionException.getCause());
}
return throwable;
}
static Object invoke(Method method, Object target, Object... args) throws ReflectiveOperationException {
return method.invoke(target, args);
}
static Object invokeNamed(Object target, String methodName, Class<?>[] parameterTypes, Object... args) throws ReflectiveOperationException {
Method method = target.getClass().getMethod(methodName, parameterTypes);
return method.invoke(target, args);
}
static Object read(Field field, Object target) throws IllegalAccessException {
return field.get(target);
}
static void stageRuntimeConfiguration(String worldName) throws ReflectiveOperationException {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
throw new IllegalStateException("Bukkit server is unavailable.");
}
Field configurationField = CapabilityResolution.resolveField(bukkitServer.getClass(), "configuration");
Object rawConfiguration = configurationField.get(bukkitServer);
if (!(rawConfiguration instanceof YamlConfiguration configuration)) {
throw new IllegalStateException("CraftServer configuration field is unavailable.");
}
ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds");
if (worldsSection == null) {
worldsSection = configuration.createSection("worlds");
}
ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName);
if (worldSection == null) {
worldSection = worldsSection.createSection(worldName);
}
worldSection.set("generator", "Iris:runtime");
}
static Object getRuntimeDatapackDimensions(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
Method datapackDimensionsMethod = CapabilityResolution.resolveMethod(worldLoaderContext.getClass(), "datapackDimensions", method -> method.getParameterCount() == 0);
if (datapackDimensionsMethod == null) {
throw new IllegalStateException("DataLoadContext does not expose datapackDimensions().");
}
Object datapackDimensions = datapackDimensionsMethod.invoke(worldLoaderContext);
if (datapackDimensions == null) {
throw new IllegalStateException("DataLoadContext.datapackDimensions() returned null.");
}
return datapackDimensions;
}
static Object getRuntimeServerRegistryAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
Method registryAccessMethod = capabilities.serverRegistryAccessMethod();
if (registryAccessMethod == null) {
throw new IllegalStateException("MinecraftServer does not expose registryAccess().");
}
Object registryAccess = registryAccessMethod.invoke(capabilities.minecraftServer());
if (registryAccess == null) {
throw new IllegalStateException("MinecraftServer.registryAccess() returned null.");
}
return registryAccess;
}
static Object getRuntimeLevelStemRegistry(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
Object datapackDimensions = getRuntimeDatapackDimensions(capabilities);
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Method lookupMethod = CapabilityResolution.resolveMethod(datapackDimensions.getClass(), "lookupOrThrow", method -> method.getParameterCount() == 1);
if (lookupMethod == null) {
throw new IllegalStateException("Registry access does not expose lookupOrThrow(...).");
}
return lookupMethod.invoke(datapackDimensions, levelStemRegistryKey);
}
static Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException {
String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_");
String path = "runtime/" + sanitized;
Identifier identifier = new Identifier("iris", path);
Object rawIdentifier = Class.forName("net.minecraft.resources.Identifier")
.getMethod("fromNamespaceAndPath", String.class, String.class)
.invoke(null, identifier.namespace(), identifier.key());
Object registryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Method createMethod = Class.forName("net.minecraft.resources.ResourceKey")
.getMethod("create", registryKey.getClass(), rawIdentifier.getClass());
return createMethod.invoke(null, registryKey, rawIdentifier);
}
static Object createDimensionKey(Object stemKey) throws ReflectiveOperationException {
Class<?> resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey");
Method identifierMethod = CapabilityResolution.resolveMethod(resourceKeyClass, "identifier", method -> method.getParameterCount() == 0);
Object identifier = identifierMethod.invoke(stemKey);
Object dimensionRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("DIMENSION")
.get(null);
Method createMethod = resourceKeyClass.getMethod("create", dimensionRegistryKey.getClass(), identifier.getClass());
return createMethod.invoke(null, dimensionRegistryKey, identifier);
}
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request) throws ReflectiveOperationException {
return resolveRuntimeLevelStem(capabilities, request, INMS.get());
}
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request, INMSBinding binding) throws ReflectiveOperationException {
ChunkGenerator generator = request.generator();
if (generator instanceof PlatformChunkGenerator) {
Object registryAccess = getRuntimeServerRegistryAccess(capabilities);
try {
Object levelStem = binding.createRuntimeLevelStem(registryAccess, generator);
if (levelStem == null) {
throw new IllegalStateException("Iris NMS binding returned null runtime LevelStem.");
}
return levelStem;
} catch (Throwable e) {
throw new IllegalStateException("Failed to create runtime LevelStem from full server registry access for world \"" + request.worldName() + "\".", unwrap(e));
}
}
try {
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
Object overworldKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
.getField("OVERWORLD")
.get(null);
Method getValueMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "getValue", method -> method.getParameterCount() == 1);
if (getValueMethod != null) {
Object resolved = getValueMethod.invoke(levelStemRegistry, overworldKey);
if (resolved != null) {
return resolved;
}
}
Method getMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "get", method -> method.getParameterCount() == 1);
if (getMethod == null) {
throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry.");
}
Object raw = getMethod.invoke(levelStemRegistry, overworldKey);
return extractRegistryValue(raw);
} catch (Throwable e) {
throw new IllegalStateException("Failed to resolve fallback OVERWORLD LevelStem from datapack registry access for world \"" + request.worldName() + "\".", unwrap(e));
}
}
static String runtimeLevelStemRegistrySource(WorldLifecycleRequest request) {
if (request.generator() instanceof PlatformChunkGenerator) {
return "full_server_registry";
}
return "datapack_level_stem_registry";
}
static Object extractRegistryValue(Object raw) throws ReflectiveOperationException {
if (raw == null) {
return null;
}
if (raw instanceof Optional<?> optional) {
Object nested = optional.orElse(null);
if (nested == null) {
return null;
}
return extractRegistryValue(nested);
}
Method valueMethod = CapabilityResolution.resolveMethod(raw.getClass(), "value", method -> method.getParameterCount() == 0);
if (valueMethod != null) {
return valueMethod.invoke(raw);
}
return raw;
}
static void applyWorldDataNameAndModInfo(CapabilitySnapshot capabilities, Object worldDataAndGenSettings, String worldName) throws ReflectiveOperationException {
Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0);
if (dataMethod == null) {
return;
}
Object worldData = dataMethod.invoke(worldDataAndGenSettings);
if (worldData == null) {
return;
}
Method checkNameMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "checkName", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
if (checkNameMethod != null) {
checkNameMethod.invoke(worldData, worldName);
}
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
if (getModdedStatusMethod == null || getServerModNameMethod == null) {
return;
}
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "setModdedInfo", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
});
if (shouldReportAsModifiedMethod == null || setModdedInfoMethod == null) {
return;
}
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
setModdedInfoMethod.invoke(worldData, modName, modified);
}
static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
if (cookieMethod == null) {
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
}
Object worldDataAndGenSettings = cookieMethod.invoke(dataLoadOutput);
applyWorldDataNameAndModInfo(capabilities, worldDataAndGenSettings, worldName);
return worldDataAndGenSettings;
}
static Object createLegacyPrimaryLevelData(CapabilitySnapshot capabilities, Object levelStorageAccess, String worldName) throws ReflectiveOperationException {
Object levelDataResult = capabilities.paperWorldDataMethod().invoke(null, levelStorageAccess);
Method fatalErrorMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "fatalError", method -> method.getParameterCount() == 0);
Method dataTagMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "dataTag", method -> method.getParameterCount() == 0);
if (fatalErrorMethod != null && Boolean.TRUE.equals(fatalErrorMethod.invoke(levelDataResult))) {
throw new IllegalStateException("Paper runtime world-data helper reported a fatal error for \"" + worldName + "\".");
}
if (dataTagMethod != null && dataTagMethod.invoke(levelDataResult) != null) {
throw new IllegalStateException("Runtime world \"" + worldName + "\" already contains level data.");
}
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
if (cookieMethod == null) {
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
}
Object primaryLevelData = cookieMethod.invoke(dataLoadOutput);
Method checkNameMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "checkName", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
if (checkNameMethod != null) {
checkNameMethod.invoke(primaryLevelData, worldName);
}
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
if (getModdedStatusMethod != null && getServerModNameMethod != null) {
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "setModdedInfo", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
});
if (shouldReportAsModifiedMethod != null && setModdedInfoMethod != null) {
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
setModdedInfoMethod.invoke(primaryLevelData, modName, modified);
}
}
return primaryLevelData;
}
static Object createLegacyStorageAccess(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
Method createDefaultMethod = levelStorageSourceClass.getMethod("createDefault", Path.class);
Object levelStorageSource = createDefaultMethod.invoke(null, Bukkit.getWorldContainer().toPath());
Method storageAccessMethod = capabilities.levelStorageAccessMethod();
if (storageAccessMethod.getParameterCount() == 1) {
return storageAccessMethod.invoke(levelStorageSource, worldName);
}
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
.getField("OVERWORLD")
.get(null);
return storageAccessMethod.invoke(levelStorageSource, worldName, overworldStemKey);
}
static void closeLevelStorageAccess(Object levelStorageAccess) {
if (levelStorageAccess == null) {
return;
}
try {
Method closeMethod = levelStorageAccess.getClass().getMethod("close");
closeMethod.invoke(levelStorageAccess);
} catch (Throwable ignored) {
}
}
static boolean unloadWorld(CapabilitySnapshot capabilities, World world, boolean save) {
if (world == null) {
return false;
}
CompletableFuture<Boolean> asyncUnload = unloadWorldViaAsyncApi(capabilities, world, save);
if (asyncUnload != null) {
return resolveAsyncUnload(asyncUnload);
}
try {
return Bukkit.unloadWorld(world, save);
} catch (UnsupportedOperationException unsupported) {
if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) {
throw unsupported;
}
}
try {
if (save) {
world.save();
}
Method getHandleMethod = world.getClass().getMethod("getHandle");
Object serverLevel = getHandleMethod.invoke(world);
closeServerLevel(world, serverLevel);
detachServerLevel(capabilities, serverLevel, world.getName());
return Bukkit.getWorld(world.getName()) == null;
} catch (Throwable e) {
throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" through the selected world lifecycle backend.", unwrap(e));
}
}
private static CompletableFuture<Boolean> unloadWorldViaAsyncApi(CapabilitySnapshot capabilities, World world, boolean save) {
if (capabilities.unloadWorldAsyncMethod() == null || capabilities.bukkitServer() == null) {
return null;
}
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
Runnable invokeTask = () -> {
Consumer<Boolean> callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result));
try {
capabilities.unloadWorldAsyncMethod().invoke(capabilities.bukkitServer(), world, save, callback);
} catch (Throwable e) {
callbackFuture.completeExceptionally(unwrap(e));
}
};
if (J.isFolia() && !isGlobalTickThread()) {
CompletableFuture<Void> scheduled = J.sfut(invokeTask);
if (scheduled == null) {
callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global unload task."));
return callbackFuture;
}
scheduled.whenComplete((unused, throwable) -> {
if (throwable != null) {
callbackFuture.completeExceptionally(unwrap(throwable));
}
});
return callbackFuture;
}
invokeTask.run();
return callbackFuture;
}
private static boolean resolveAsyncUnload(CompletableFuture<Boolean> asyncUnload) {
if (J.isPrimaryThread()) {
if (!asyncUnload.isDone()) {
return true;
}
try {
return Boolean.TRUE.equals(asyncUnload.join());
} catch (Throwable e) {
throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e));
}
}
try {
return Boolean.TRUE.equals(asyncUnload.get(120, TimeUnit.SECONDS));
} catch (Throwable e) {
throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e));
}
}
private static void closeServerLevel(World world, Object serverLevel) throws Throwable {
Method closeMethod = CapabilityResolution.resolveMethod(serverLevel.getClass(), "close", method -> method.getParameterCount() == 0);
if (closeMethod == null) {
return;
}
if (!J.isFolia()) {
closeMethod.invoke(serverLevel);
return;
}
Location spawn = world.getSpawnLocation();
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
closeMethod.invoke(serverLevel);
closeFuture.complete(null);
} catch (Throwable e) {
closeFuture.completeExceptionally(unwrap(e));
}
});
if (!scheduled) {
throw new IllegalStateException("Failed to schedule region close task for world \"" + world.getName() + "\".");
}
closeFuture.get(90, TimeUnit.SECONDS);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
return;
}
Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds");
Object rawWorlds = worldsField.get(bukkitServer);
if (rawWorlds instanceof Map map) {
map.remove(worldName);
map.remove(worldName.toLowerCase(Locale.ROOT));
}
}
private static void detachServerLevel(CapabilitySnapshot capabilities, Object serverLevel, String worldName) throws Throwable {
Runnable detachTask = () -> {
try {
capabilities.removeLevelMethod().invoke(capabilities.minecraftServer(), serverLevel);
removeWorldFromCraftServerMap(worldName);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
if (!J.isFolia() || isGlobalTickThread()) {
detachTask.run();
return;
}
CompletableFuture<Void> detachFuture = J.sfut(detachTask);
if (detachFuture == null) {
throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\".");
}
detachFuture.get(15, TimeUnit.SECONDS);
}
static boolean isGlobalTickThread() {
Object server = Bukkit.getServer();
if (server == null) {
return false;
}
try {
Method method = server.getClass().getMethod("isGlobalTickThread");
return Boolean.TRUE.equals(method.invoke(server));
} catch (Throwable ignored) {
return false;
}
}
}
@@ -1,95 +0,0 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldType;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
final class WorldsProviderBackend implements WorldLifecycleBackend {
private final CapabilitySnapshot capabilities;
WorldsProviderBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
return request.studio() && capabilities.hasWorldsProvider();
}
@Override
@SuppressWarnings("unchecked")
public CompletableFuture<World> create(WorldLifecycleRequest request) {
try {
Path worldPath = new File(Bukkit.getWorldContainer(), request.worldName()).toPath();
Object builder = WorldLifecycleSupport.invokeNamed(capabilities.worldsProvider(), "levelBuilder", new Class[]{Path.class}, worldPath);
builder = WorldLifecycleSupport.invokeNamed(builder, "name", new Class[]{String.class}, request.worldName());
builder = WorldLifecycleSupport.invokeNamed(builder, "seed", new Class[]{long.class}, request.seed());
builder = WorldLifecycleSupport.invokeNamed(builder, "levelStem", new Class[]{capabilities.worldsLevelStemClass()}, resolveLevelStem(request.environment()));
builder = WorldLifecycleSupport.invokeNamed(builder, "chunkGenerator", new Class[]{org.bukkit.generator.ChunkGenerator.class}, request.generator());
builder = WorldLifecycleSupport.invokeNamed(builder, "biomeProvider", new Class[]{org.bukkit.generator.BiomeProvider.class}, request.biomeProvider());
builder = WorldLifecycleSupport.invokeNamed(builder, "generatorType", new Class[]{capabilities.worldsGeneratorTypeClass()}, resolveGeneratorType(request.worldType()));
builder = WorldLifecycleSupport.invokeNamed(builder, "structures", new Class[]{boolean.class}, request.generateStructures());
builder = WorldLifecycleSupport.invokeNamed(builder, "hardcore", new Class[]{boolean.class}, request.hardcore());
Object levelBuilder = WorldLifecycleSupport.invokeNamed(builder, "build", new Class[0]);
Object async = WorldLifecycleSupport.invokeNamed(levelBuilder, "createAsync", new Class[0]);
if (async instanceof CompletableFuture<?> future) {
return future.thenApply(world -> (World) world);
}
return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture."));
} catch (Throwable e) {
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
}
}
@Override
public boolean unload(World world, boolean save) {
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
}
@Override
public String backendName() {
return "worlds_provider";
}
@Override
public String describeSelectionReason() {
return "external Worlds provider is registered and healthy";
}
@SuppressWarnings({"rawtypes", "unchecked"})
private Object resolveLevelStem(World.Environment environment) {
String key;
if (environment == World.Environment.NETHER) {
key = "NETHER";
} else if (environment == World.Environment.THE_END) {
key = "END";
} else {
key = "OVERWORLD";
}
Class<? extends Enum> enumClass = capabilities.worldsLevelStemClass().asSubclass(Enum.class);
return Enum.valueOf(enumClass, key);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private Object resolveGeneratorType(WorldType worldType) {
String typeName = worldType == null ? "NORMAL" : worldType.getName();
String key;
if ("FLAT".equalsIgnoreCase(typeName)) {
key = "FLAT";
} else if ("AMPLIFIED".equalsIgnoreCase(typeName)) {
key = "AMPLIFIED";
} else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) {
key = "LARGE_BIOMES";
} else {
key = "NORMAL";
}
Class<? extends Enum> enumClass = capabilities.worldsGeneratorTypeClass().asSubclass(Enum.class);
return Enum.valueOf(enumClass, key.toUpperCase(Locale.ROOT));
}
}
@@ -1,182 +0,0 @@
package art.arcane.iris.core.link.data;
import art.arcane.iris.core.link.ExternalDataProvider;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.properties.BooleanProperty;
import net.momirealms.craftengine.core.block.properties.IntegerProperty;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.function.Function;
import java.util.stream.Stream;
public class CraftEngineDataProvider extends ExternalDataProvider {
private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{
BlockProperty.ofBoolean("randomYaw", false),
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
BlockProperty.ofBoolean("randomPitch", false),
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true),
};
public CraftEngineDataProvider() {
super("CraftEngine");
}
@Override
public void init() {
}
@Override
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
Key key = Key.of(blockId.namespace(), blockId.key());
net.momirealms.craftengine.core.block.CustomBlock block = CraftEngineBlocks.byId(key);
if (block != null) {
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
}
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
if (furniture != null) {
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
properties[4] = new BlockProperty(
"variant",
String.class,
furniture.anyVariantName(),
furniture.variants().keySet(),
Function.identity()
);
return List.of(properties);
}
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
}
@Override
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
net.momirealms.craftengine.core.item.CustomItem<ItemStack> item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
if (item == null) {
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
}
return item.buildItemStack();
}
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
Key key = Key.of(blockId.namespace(), blockId.key());
if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) {
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
}
return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state));
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
art.arcane.iris.core.nms.container.Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
Identifier baseBlockId = statePair.getA();
KMap<String, String> state = statePair.getB();
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
net.momirealms.craftengine.core.block.CustomBlock customBlock = CraftEngineBlocks.byId(key);
if (customBlock != null) {
ImmutableBlockState blockState = customBlock.defaultState();
for (Map.Entry<String, String> entry : state.entrySet()) {
Property<?> property = customBlock.getProperty(entry.getKey());
if (property == null) {
continue;
}
Comparable<?> tag = property.optional(entry.getValue()).orElse(null);
if (tag == null) {
continue;
}
blockState = ImmutableBlockState.with(blockState, property, tag);
}
CraftEngineBlocks.place(block.getLocation(), blockState, false);
return;
}
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
if (furniture == null) {
return;
}
Location location = parseYawAndPitch(engine, block, state);
String variant = state.getOrDefault("variant", furniture.anyVariantName());
CraftEngineFurniture.place(location, furniture, variant, false);
}
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
Location location = block.getLocation();
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
RNG rng = new RNG(seed);
if ("true".equals(state.get("randomYaw"))) {
location.setYaw(rng.f(0, 360));
} else if (state.containsKey("yaw")) {
location.setYaw(Float.parseFloat(state.get("yaw")));
}
if ("true".equals(state.get("randomPitch"))) {
location.setPitch(rng.f(0, 360));
} else if (state.containsKey("pitch")) {
location.setPitch(Float.parseFloat(state.get("pitch")));
}
return location;
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
Stream<Key> keys = switch (dataType) {
case ENTITY -> Stream.<Key>empty();
case ITEM -> CraftEngineItems.loadedItems().keySet().stream();
case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(),
CraftEngineFurniture.loadedFurniture().keySet().stream());
};
return keys.map(key -> new Identifier(key.namespace(), key.value())).toList();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
Key key = Key.of(id.namespace(), id.key());
return switch (dataType) {
case ENTITY -> false;
case ITEM -> CraftEngineItems.byId(key) != null;
case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null;
};
}
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
return switch (raw) {
case BooleanProperty property -> BlockProperty.ofBoolean(property.name(), property.defaultValue());
case IntegerProperty property -> BlockProperty.ofLong(property.name(), property.defaultValue(), property.min, property.max, false, false);
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
};
}
}
@@ -1,203 +0,0 @@
package art.arcane.iris.core.loader;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.json.JSONObject;
import com.google.gson.annotations.SerializedName;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class JsonSchemaValidator {
private static final ConcurrentHashMap<Class<?>, Set<String>> FIELD_CACHE = new ConcurrentHashMap<>();
private static final int SUGGESTION_MAX_DISTANCE = 4;
private JsonSchemaValidator() {
}
static void validateTopLevelKeys(JSONObject parsed, String rawText, File file, String resourceTypeName, Class<?> objectClass) {
if (parsed == null || objectClass == null) {
return;
}
Set<String> known = FIELD_CACHE.computeIfAbsent(objectClass, JsonSchemaValidator::collectFieldNames);
for (String key : parsed.keySet()) {
if (known.contains(key)) {
continue;
}
reportUnknownKey(key, rawText, file, resourceTypeName, known);
}
}
static void reportLoadFailure(File file, String rawText, String resourceTypeName, Throwable error) {
String message = error.getMessage();
if (message == null || message.isBlank()) {
message = error.getClass().getSimpleName();
}
int line = extractLineFromMessage(message);
String location = file.getPath();
if (line > 0) {
location = location + ":" + line;
}
StringBuilder out = new StringBuilder();
out.append("Couldn't load ").append(resourceTypeName)
.append(C.RED).append(" in ").append(C.WHITE).append(location).append(C.RED)
.append(" -> ").append(message);
String snippet = buildSnippet(rawText, line);
if (snippet != null) {
out.append('\n').append(snippet);
}
Iris.warn(out.toString());
}
private static void reportUnknownKey(String key, String rawText, File file, String resourceTypeName, Set<String> known) {
int line = findLineForKey(rawText, key);
String suggestion = closestMatch(key, known);
StringBuilder out = new StringBuilder();
out.append("Unknown ").append(resourceTypeName).append(" field ")
.append(C.WHITE).append('"').append(key).append('"').append(C.YELLOW)
.append(" in ").append(C.WHITE).append(file.getPath());
if (line > 0) {
out.append(":").append(line);
}
out.append(C.YELLOW).append(" (Gson will silently ignore this)");
if (suggestion != null) {
out.append(". Did you mean ").append(C.WHITE).append('"').append(suggestion).append('"').append(C.YELLOW).append("?");
}
String snippet = buildSnippet(rawText, line);
if (snippet != null) {
out.append('\n').append(snippet);
}
Iris.warn(out.toString());
}
private static Set<String> collectFieldNames(Class<?> cls) {
Set<String> names = new LinkedHashSet<>();
Class<?> c = cls;
while (c != null && c != Object.class) {
for (Field field : c.getDeclaredFields()) {
int mods = field.getModifiers();
if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) {
continue;
}
if (field.isSynthetic()) {
continue;
}
SerializedName serialized = field.getAnnotation(SerializedName.class);
if (serialized != null) {
names.add(serialized.value());
Collections.addAll(names, serialized.alternate());
} else {
names.add(field.getName());
}
}
c = c.getSuperclass();
}
return Collections.unmodifiableSet(names);
}
private static int findLineForKey(String rawText, String key) {
if (rawText == null || key == null) {
return -1;
}
Pattern pattern = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:");
Matcher matcher = pattern.matcher(rawText);
if (!matcher.find()) {
return -1;
}
int index = matcher.start();
int line = 1;
for (int i = 0; i < index; i++) {
if (rawText.charAt(i) == '\n') {
line++;
}
}
return line;
}
private static int extractLineFromMessage(String message) {
if (message == null) {
return -1;
}
Matcher m = Pattern.compile("line\\s+(\\d+)").matcher(message);
if (m.find()) {
try {
return Integer.parseInt(m.group(1));
} catch (NumberFormatException ignored) {
}
}
return -1;
}
private static String buildSnippet(String rawText, int line) {
if (rawText == null || line <= 0) {
return null;
}
String[] lines = rawText.split("\n", -1);
if (line > lines.length) {
return null;
}
int from = Math.max(0, line - 2);
int to = Math.min(lines.length, line + 1);
StringBuilder out = new StringBuilder();
int width = String.valueOf(to).length();
for (int i = from; i < to; i++) {
int n = i + 1;
boolean focus = n == line;
out.append(focus ? C.RED + "> " : C.GRAY + " ");
out.append(String.format("%" + width + "d", n)).append(" | ");
String content = lines[i];
if (content.length() > 200) {
content = content.substring(0, 200) + "...";
}
out.append(content);
if (i < to - 1) {
out.append('\n');
}
}
return out.toString();
}
private static String closestMatch(String key, Set<String> known) {
String lowerKey = key.toLowerCase();
String best = null;
int bestDistance = Integer.MAX_VALUE;
for (String candidate : known) {
int d = levenshtein(lowerKey, candidate.toLowerCase());
if (d < bestDistance) {
bestDistance = d;
best = candidate;
}
}
if (best == null) {
return null;
}
int threshold = Math.min(SUGGESTION_MAX_DISTANCE, Math.max(1, key.length() / 2));
return bestDistance <= threshold ? best : null;
}
private static int levenshtein(String a, String b) {
int[] prev = new int[b.length() + 1];
int[] curr = new int[b.length() + 1];
for (int j = 0; j <= b.length(); j++) {
prev[j] = j;
}
for (int i = 1; i <= a.length(); i++) {
curr[0] = i;
for (int j = 1; j <= b.length(); j++) {
int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost);
}
int[] tmp = prev;
prev = curr;
curr = tmp;
}
return prev[b.length()];
}
}
@@ -1,163 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.nms;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
import org.bukkit.Bukkit;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
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);
private static final List<Version> REVISION = List.of(
new Version(21, 11, "v1_21_R7")
);
private static final List<Version> PACKS = List.of(
new Version(21, 11, "31100")
);
//@done
private static final INMSBinding binding = bind();
public static final String OVERWORLD_TAG = getTag(PACKS, "31100");
public static INMSBinding get() {
return binding;
}
public static String getNMSTag() {
if (IrisSettings.get().getGeneral().isDisableNMS()) {
return "BUKKIT";
}
try {
String name = Bukkit.getServer().getClass().getCanonicalName();
if (name.equals("org.bukkit.craftbukkit.CraftServer")) {
return getTag(REVISION, "BUKKIT");
} else {
return name.split("\\Q.\\E")[3];
}
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to determine server nms version!");
e.printStackTrace();
}
return "BUKKIT";
}
private static INMSBinding bind() {
String code = getNMSTag();
boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS();
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes(code, disableNms, getFallbackBindingCodes());
if ("BUKKIT".equals(code) && !disableNms) {
Iris.info("NMS tag resolution fell back to Bukkit; probing supported revision bindings.");
}
for (int i = 0; i < probeCodes.size(); i++) {
INMSBinding resolvedBinding = tryBind(probeCodes.get(i), i == 0);
if (resolvedBinding != null) {
return resolvedBinding;
}
}
if (disableNms) {
Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
Iris.warn("Note: NMS support is disabled. Iris is running in limited Bukkit fallback mode.");
return new NMSBinding1X();
}
MinecraftVersion detectedVersion = getMinecraftVersion();
String serverVersion = detectedVersion == null ? Bukkit.getServer().getVersion() : detectedVersion.value();
throw new IllegalStateException("Iris requires Minecraft 1.21.11 or newer. Detected server version: " + serverVersion);
}
private static String getTag(List<Version> versions, String def) {
MinecraftVersion detectedVersion = getMinecraftVersion();
if (detectedVersion == null) {
return def;
}
if (detectedVersion.isNewerThan(CURRENT.major, CURRENT.minor)) {
return versions.getFirst().tag;
}
for (Version p : versions) {
if (!detectedVersion.isAtLeast(p.major, p.minor)) {
continue;
}
return p.tag;
}
return def;
}
private static MinecraftVersion getMinecraftVersion() {
try {
return MinecraftVersion.detect(Bukkit.getServer());
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to determine server minecraft version!");
e.printStackTrace();
return null;
}
}
private static INMSBinding tryBind(String code, boolean announce) {
if (announce) {
Iris.info("Locating NMS Binding for " + code);
} else {
Iris.info("Probing NMS Binding for " + code);
}
try {
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
Object candidate = clazz.getConstructor().newInstance();
if (candidate instanceof INMSBinding binding) {
Iris.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
return binding;
}
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
return null;
}
private static Set<String> getFallbackBindingCodes() {
Set<String> codes = new LinkedHashSet<>();
for (Version version : REVISION) {
if (version.tag != null && !version.tag.isBlank()) {
codes.add(version.tag);
}
}
return codes;
}
private record Version(int major, int minor, String tag) {}
}
@@ -1,114 +0,0 @@
package art.arcane.iris.core.nms;
import org.bukkit.Server;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class MinecraftVersion {
private static final Pattern DECORATED_VERSION_PATTERN = Pattern.compile("\\(MC: ([0-9]+(?:\\.[0-9]+){0,2})\\)");
private final String value;
private final int major;
private final int minor;
private MinecraftVersion(String value, int major, int minor) {
this.value = value;
this.major = major;
this.minor = minor;
}
public static MinecraftVersion detect(Server server) {
if (server == null) {
return null;
}
MinecraftVersion runtimeVersion = fromRuntimeMinecraftVersion(server);
if (runtimeVersion != null) {
return runtimeVersion;
}
MinecraftVersion decoratedVersion = fromDecoratedVersion(server.getVersion());
if (decoratedVersion != null) {
return decoratedVersion;
}
return fromBukkitVersion(server.getBukkitVersion());
}
static MinecraftVersion fromRuntimeMinecraftVersion(Server server) {
try {
Method method = server.getClass().getMethod("getMinecraftVersion");
Object value = method.invoke(server);
if (value instanceof String version) {
return fromVersionToken(version);
}
} catch (ReflectiveOperationException ignored) {
return null;
}
return null;
}
static MinecraftVersion fromDecoratedVersion(String input) {
if (input == null || input.isBlank()) {
return null;
}
Matcher matcher = DECORATED_VERSION_PATTERN.matcher(input);
if (!matcher.find()) {
return null;
}
return fromVersionToken(matcher.group(1));
}
static MinecraftVersion fromBukkitVersion(String input) {
if (input == null || input.isBlank()) {
return null;
}
String versionToken = input.split("-", 2)[0].trim();
return fromVersionToken(versionToken);
}
private static MinecraftVersion fromVersionToken(String input) {
if (input == null || input.isBlank()) {
return null;
}
String[] parts = input.split("\\.");
if (parts.length < 2 || !"1".equals(parts[0])) {
return null;
}
try {
int major = Integer.parseInt(parts[1]);
int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
return new MinecraftVersion(input, major, minor);
} catch (NumberFormatException ignored) {
return null;
}
}
public String value() {
return value;
}
public int major() {
return major;
}
public int minor() {
return minor;
}
public boolean isAtLeast(int major, int minor) {
return this.major > major || (this.major == major && this.minor >= minor);
}
public boolean isNewerThan(int major, int minor) {
return this.major > major || (this.major == major && this.minor > minor);
}
}
@@ -1,29 +0,0 @@
package art.arcane.iris.core.nms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class NmsBindingProbeSupport {
private NmsBindingProbeSupport() {
}
static List<String> getBindingProbeCodes(String code, boolean disableNms, Collection<String> fallbackCodes) {
List<String> probeCodes = new ArrayList<>();
if (code == null || code.isBlank()) {
return probeCodes;
}
if (!"BUKKIT".equals(code)) {
probeCodes.add(code);
return probeCodes;
}
if (disableNms || fallbackCodes == null) {
return probeCodes;
}
probeCodes.addAll(fallbackCodes);
return probeCodes;
}
}
@@ -1,60 +0,0 @@
package art.arcane.iris.core.nms.datapack;
import art.arcane.iris.Iris;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
import art.arcane.iris.core.nms.datapack.v1206.DataFixerV1206;
import art.arcane.iris.core.nms.datapack.v1213.DataFixerV1213;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
import art.arcane.volmlib.util.collection.KMap;
import lombok.AccessLevel;
import lombok.Getter;
import java.util.function.Supplier;
//https://minecraft.wiki/w/Pack_format
@Getter
public enum DataVersion {
UNSUPPORTED("0.0.0", 0, () -> null),
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);
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
@Getter(AccessLevel.NONE)
private final Supplier<IDataFixer> constructor;
private final String version;
private final int packFormat;
DataVersion(String version, int packFormat, Supplier<IDataFixer> constructor) {
this.constructor = constructor;
this.packFormat = packFormat;
this.version = version;
}
public IDataFixer get() {
return cache.computeIfAbsent(this, k -> constructor.get());
}
public static IDataFixer getDefault() {
DataVersion version = INMS.get().getDataVersion();
if (version == null || version == UNSUPPORTED) {
DataVersion fallback = getLatest();
Iris.warn("Unsupported datapack version mapping detected, falling back to latest fixer: " + fallback.getVersion());
return fallback.get();
}
IDataFixer fixer = version.get();
if (fixer == null) {
DataVersion fallback = getLatest();
Iris.warn("Null datapack fixer for " + version.getVersion() + ", falling back to latest fixer: " + fallback.getVersion());
return fallback.get();
}
return fixer;
}
public static DataVersion getLatest() {
return values()[values().length - 1];
}
}
@@ -1,173 +0,0 @@
package art.arcane.iris.core.nms.datapack.v1217;
import art.arcane.iris.core.nms.datapack.v1213.DataFixerV1213;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.util.Map;
public class DataFixerV1217 extends DataFixerV1213 {
private static final Map<Dimension, String> DIMENSIONS = Map.of(
Dimension.OVERWORLD, """
{
"ambient_light": 0.0,
"has_ender_dragon_fight": false,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
"block_search_extent": 8,
"offset": 2.0,
"sound": "minecraft:ambient.cave",
"tick_delay": 6000
}
},
"minecraft:audio/background_music": {
"creative": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.creative"
},
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.game"
}
},
"minecraft:visual/cloud_color": "#ccffffff",
"minecraft:visual/fog_color": "#c0d8ff",
"minecraft:visual/sky_color": "#78a7ff"
},
"timelines": "#minecraft:in_overworld"
}""",
Dimension.NETHER, """
{
"ambient_light": 0.1,
"has_ender_dragon_fight": false,
"attributes": {
"minecraft:gameplay/sky_light_level": 4.0,
"minecraft:gameplay/snow_golem_melts": true,
"minecraft:visual/fog_end_distance": 96.0,
"minecraft:visual/fog_start_distance": 10.0,
"minecraft:visual/sky_light_color": "#7a7aff",
"minecraft:visual/sky_light_factor": 0.0
},
"cardinal_light": "nether",
"skybox": "none",
"timelines": "#minecraft:in_nether"
}""",
Dimension.END, """
{
"ambient_light": 0.25,
"has_ender_dragon_fight": true,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
"block_search_extent": 8,
"offset": 2.0,
"sound": "minecraft:ambient.cave",
"tick_delay": 6000
}
},
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 6000,
"replace_current_music": true,
"sound": "minecraft:music.end"
}
},
"minecraft:visual/fog_color": "#181318",
"minecraft:visual/sky_color": "#000000",
"minecraft:visual/sky_light_color": "#e580ff",
"minecraft:visual/sky_light_factor": 0.0
},
"skybox": "end",
"timelines": "#minecraft:in_end"
}"""
);
@Override
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
json = super.fixCustomBiome(biome, json);
var effects = json.getJSONObject("effects");
var attributes = new JSONObject();
attributes.put("minecraft:visual/fog_color", effects.remove("fog_color"));
attributes.put("minecraft:visual/sky_color", effects.remove("sky_color"));
attributes.put("minecraft:visual/water_fog_color", effects.remove("water_fog_color"));
JSONObject particle = (JSONObject) effects.remove("particle");
if (particle != null) {
particle.put("particle", particle.remove("options"));
attributes.put("minecraft:visual/ambient_particles", new JSONArray()
.put(particle));
}
json.put("attributes", attributes);
return json;
}
@Override
public void fixDimension(Dimension dimension, JSONObject json) {
super.fixDimension(dimension, json);
var attributes = new JSONObject();
if ((Boolean) json.remove("ultrawarm")) {
attributes.put("minecraft:gameplay/water_evaporates", true);
attributes.put("minecraft:gameplay/fast_lava", true);
attributes.put("minecraft:gameplay/snow_golem_melts", true);
attributes.put("minecraft:visual/default_dripstone_particle", new JSONObject()
.put("type", "minecraft:dripping_dripstone_lava"));
}
if ((Boolean) json.remove("bed_works")) {
attributes.put("minecraft:gameplay/bed_rule", new JSONObject()
.put("can_set_spawn", "always")
.put("can_sleep", "when_dark")
.put("error_message", new JSONObject()
.put("translate", "block.minecraft.bed.no_sleep")));
} else {
attributes.put("minecraft:gameplay/bed_rule", new JSONObject()
.put("can_set_spawn", "never")
.put("can_sleep", "never")
.put("explodes", true));
}
attributes.put("minecraft:gameplay/respawn_anchor_works", json.remove("respawn_anchor_works"));
attributes.put("minecraft:gameplay/piglins_zombify", !(Boolean) json.remove("piglin_safe"));
attributes.put("minecraft:gameplay/can_start_raid", json.remove("has_raids"));
var cloud_height = json.remove("cloud_height");
if (cloud_height != null) attributes.put("minecraft:visual/cloud_height", cloud_height);
boolean natural = (Boolean) json.remove("natural");
attributes.put("minecraft:gameplay/nether_portal_spawns_piglin", natural);
if (natural != (dimension == Dimension.OVERWORLD)) {
attributes.put("minecraft:gameplay/eyeblossom_open", natural);
attributes.put("minecraft:gameplay/creaking_active", natural);
}
//json.put("has_fixed_time", json.remove("fixed_time") != null); //TODO investigate
json.put("attributes", attributes);
json.remove("effects");
var defaults = new JSONObject(DIMENSIONS.get(dimension));
merge(json, defaults);
}
private void merge(JSONObject base, JSONObject override) {
for (String key : override.keySet()) {
switch (base.opt(key)) {
case null -> base.put(key, override.opt(key));
case JSONObject base1 when override.opt(key) instanceof JSONObject override1 -> merge(base1, override1);
case JSONArray base1 when override.opt(key) instanceof JSONArray override1 -> {
for (Object o : override1) {
base1.put(o);
}
}
default -> {}
}
}
}
}
@@ -1,56 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pack;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class BrokenPackException extends RuntimeException {
private final String packName;
private final List<String> reasons;
public BrokenPackException(String packName, List<String> reasons) {
super(buildMessage(packName, reasons));
this.packName = packName;
this.reasons = reasons == null ? new ArrayList<>() : new ArrayList<>(reasons);
}
public String getPackName() {
return packName;
}
public List<String> getReasons() {
return Collections.unmodifiableList(reasons);
}
private static String buildMessage(String packName, List<String> reasons) {
StringBuilder sb = new StringBuilder();
sb.append("Iris pack '").append(packName).append("' is broken and cannot be used for world or studio creation.");
if (reasons != null) {
for (String reason : reasons) {
if (reason == null || reason.isBlank()) {
continue;
}
sb.append(System.lineSeparator()).append(" - ").append(reason);
}
}
return sb.toString();
}
}
@@ -1,64 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pack;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class PackValidationRegistry {
private static final Map<String, PackValidationResult> RESULTS = new ConcurrentHashMap<>();
private PackValidationRegistry() {
}
public static void publish(PackValidationResult result) {
if (result == null || result.getPackName() == null || result.getPackName().isBlank()) {
return;
}
RESULTS.put(result.getPackName(), result);
}
public static PackValidationResult get(String packName) {
if (packName == null || packName.isBlank()) {
return null;
}
return RESULTS.get(packName);
}
public static boolean isBroken(String packName) {
PackValidationResult result = get(packName);
return result != null && !result.isLoadable();
}
public static Map<String, PackValidationResult> snapshot() {
return Collections.unmodifiableMap(RESULTS);
}
public static void remove(String packName) {
if (packName == null || packName.isBlank()) {
return;
}
RESULTS.remove(packName);
}
public static void clear() {
RESULTS.clear();
}
}
@@ -1,67 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pack;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class PackValidationResult {
private final String packName;
private final List<String> blockingErrors;
private final List<String> warnings;
private final List<String> removedUnusedFiles;
private final long validatedAtMillis;
public PackValidationResult(String packName,
List<String> blockingErrors,
List<String> warnings,
List<String> removedUnusedFiles,
long validatedAtMillis) {
this.packName = packName;
this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors);
this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings);
this.removedUnusedFiles = removedUnusedFiles == null ? new ArrayList<>() : new ArrayList<>(removedUnusedFiles);
this.validatedAtMillis = validatedAtMillis;
}
public String getPackName() {
return packName;
}
public boolean isLoadable() {
return blockingErrors.isEmpty();
}
public List<String> getBlockingErrors() {
return Collections.unmodifiableList(blockingErrors);
}
public List<String> getWarnings() {
return Collections.unmodifiableList(warnings);
}
public List<String> getRemovedUnusedFiles() {
return Collections.unmodifiableList(removedUnusedFiles);
}
public long getValidatedAtMillis() {
return validatedAtMillis;
}
}
@@ -1,381 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pack;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
public final class PackValidator {
private static final String TRASH_ROOT = ".iris-trash";
private static final String DATAPACK_IMPORTS = "datapack-imports";
private static final String EXTERNAL_DATAPACKS = "externaldatapacks";
private static final String INTERNAL_DATAPACKS = "internaldatapacks";
private static final String DATAPACKS_FOLDER = "datapacks";
private static final String CACHE_FOLDER = "cache";
private static final String OBJECTS_FOLDER = "objects";
private static final String DIMENSIONS_FOLDER = "dimensions";
private static final List<String> MANAGED_RESOURCE_FOLDERS = List.of(
"biomes",
"regions",
"entities",
"spawners",
"loot",
"generators",
"expressions",
"markers",
"blocks",
"mods"
);
private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
private PackValidator() {
}
public static PackValidationResult validate(File packFolder) {
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
List<String> blockingErrors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
List<String> removedUnusedFiles = new ArrayList<>();
long validatedAt = System.currentTimeMillis();
if (packFolder == null || !packFolder.isDirectory()) {
blockingErrors.add("Pack folder does not exist or is not a directory.");
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
if (!dimensionsFolder.isDirectory()) {
blockingErrors.add("Missing dimensions/ folder.");
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) {
blockingErrors.add("No dimension JSON files under dimensions/.");
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
try {
String packTextCorpus = buildPackTextCorpus(packFolder);
runUnusedResourceGc(packFolder, packTextCorpus, removedUnusedFiles, warnings);
} catch (Throwable e) {
Iris.reportError("PackValidator GC pass failed for pack '" + packName + "'", e);
warnings.add("Unused-resource GC pass failed: " + e.getMessage());
}
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
private static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
File regionsFolder = new File(packFolder, "regions");
File biomesFolder = new File(packFolder, "biomes");
for (File dimFile : dimensionFiles) {
String dimensionKey = stripExtension(dimFile.getName());
JSONObject dimJson;
try {
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage());
continue;
}
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions.");
continue;
}
int resolvedRegions = 0;
for (int i = 0; i < regionsArray.length(); i++) {
String regionKey = regionsArray.optString(i, null);
if (regionKey == null || regionKey.isBlank()) {
warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + ".");
continue;
}
File regionFile = new File(regionsFolder, regionKey + ".json");
if (!regionFile.isFile()) {
blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'.");
continue;
}
JSONObject regionJson;
try {
regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage());
continue;
}
int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings);
if (anyBiome == 0) {
blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes.");
}
resolvedRegions++;
}
if (resolvedRegions == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions.");
}
}
}
private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List<String> warnings) {
JSONArray arr = regionJson.optJSONArray(field);
if (arr == null) {
return 0;
}
int resolved = 0;
for (int i = 0; i < arr.length(); i++) {
String biomeKey = arr.optString(i, null);
if (biomeKey == null || biomeKey.isBlank()) {
continue;
}
File biomeFile = new File(biomesFolder, biomeKey + ".json");
if (!biomeFile.isFile()) {
warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + ".");
continue;
}
resolved++;
}
return resolved;
}
private static String buildPackTextCorpus(File packFolder) {
StringBuilder sb = new StringBuilder(1 << 16);
try (Stream<Path> stream = Files.walk(packFolder.toPath())) {
stream.filter(Files::isRegularFile)
.filter(PackValidator::isScannableJsonPath)
.forEach(p -> {
try {
sb.append(Files.readString(p, StandardCharsets.UTF_8));
sb.append('\n');
} catch (Throwable ignored) {
}
});
} catch (Throwable e) {
Iris.reportError("PackValidator failed to walk pack folder for corpus scan", e);
}
return sb.toString();
}
private static boolean isScannableJsonPath(Path path) {
String name = path.getFileName().toString();
if (!name.endsWith(".json")) {
return false;
}
String str = path.toString().replace(File.separatorChar, '/');
if (str.contains("/" + TRASH_ROOT + "/")) {
return false;
}
if (str.contains("/" + DATAPACK_IMPORTS + "/")) {
return false;
}
if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + INTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + DATAPACKS_FOLDER + "/")) {
return false;
}
if (str.contains("/" + CACHE_FOLDER + "/")) {
return false;
}
if (str.contains("/" + OBJECTS_FOLDER + "/")) {
return false;
}
if (str.contains("/.iris/")) {
return false;
}
return true;
}
private static void runUnusedResourceGc(File packFolder, String corpus, List<String> removedUnusedFiles, List<String> warnings) {
if (corpus == null || corpus.isEmpty()) {
return;
}
File trashRoot = new File(packFolder, TRASH_ROOT + File.separator + LocalDateTime.now().format(TRASH_STAMP));
Set<File> scheduledForTrash = new LinkedHashSet<>();
for (String folderName : MANAGED_RESOURCE_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> files = listJsonRecursive(resourceFolder);
for (File resourceFile : files) {
String key = deriveKey(resourceFolder, resourceFile);
if (key == null || key.isBlank()) {
continue;
}
if (isReferenced(corpus, key)) {
continue;
}
scheduledForTrash.add(resourceFile);
}
}
if (scheduledForTrash.isEmpty()) {
return;
}
for (File file : scheduledForTrash) {
try {
Path src = file.toPath();
Path relative = packFolder.toPath().relativize(src);
Path dest = trashRoot.toPath().resolve(relative);
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
removedUnusedFiles.add(relative.toString().replace(File.separatorChar, '/'));
} catch (Throwable e) {
Iris.reportError("PackValidator failed to move unused file " + file.getPath() + " to trash", e);
warnings.add("Failed to quarantine unused file " + file.getName() + ": " + e.getMessage());
}
}
}
private static boolean isReferenced(String corpus, String key) {
String needleQuoted = "\"" + key + "\"";
if (corpus.contains(needleQuoted)) {
return true;
}
int slash = key.indexOf('/');
if (slash > 0) {
String tail = key.substring(slash + 1);
if (!tail.isBlank() && corpus.contains("\"" + tail + "\"")) {
return true;
}
}
return false;
}
private static List<File> listJsonRecursive(File root) {
List<File> out = new ArrayList<>();
try (Stream<Path> stream = Files.walk(root.toPath())) {
stream.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".json"))
.forEach(p -> out.add(p.toFile()));
} catch (Throwable ignored) {
}
return out;
}
private static String deriveKey(File resourceFolder, File resourceFile) {
Path relative = resourceFolder.toPath().relativize(resourceFile.toPath());
String str = relative.toString().replace(File.separatorChar, '/');
if (!str.endsWith(".json")) {
return null;
}
return str.substring(0, str.length() - ".json".length());
}
private static String stripExtension(String name) {
int dot = name.lastIndexOf('.');
return dot <= 0 ? name : name.substring(0, dot);
}
public static int restoreTrash(File packFolder) {
if (packFolder == null || !packFolder.isDirectory()) {
return 0;
}
File trashRoot = new File(packFolder, TRASH_ROOT);
if (!trashRoot.isDirectory()) {
return 0;
}
File[] dumps = trashRoot.listFiles(File::isDirectory);
if (dumps == null || dumps.length == 0) {
return 0;
}
Arrays.sort(dumps, Comparator.comparing(File::getName));
File latestDump = dumps[dumps.length - 1];
int restored = 0;
try (Stream<Path> stream = Files.walk(latestDump.toPath())) {
List<Path> files = stream.filter(Files::isRegularFile).toList();
for (Path src : files) {
Path relative = latestDump.toPath().relativize(src);
Path dest = packFolder.toPath().resolve(relative);
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
restored++;
}
} catch (Throwable e) {
Iris.reportError("PackValidator failed to restore trash for pack " + packFolder.getName(), e);
}
deleteFolderQuiet(latestDump);
return restored;
}
private static void deleteFolderQuiet(File folder) {
if (folder == null || !folder.exists()) {
return;
}
try (Stream<Path> stream = Files.walk(folder.toPath())) {
stream.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
} catch (Throwable ignored) {
}
}
public static Set<String> listReferencedKeysFromCorpus(String corpus) {
Set<String> keys = new HashSet<>();
if (corpus == null) {
return keys;
}
int i = 0;
while (i < corpus.length()) {
int start = corpus.indexOf('"', i);
if (start < 0) {
break;
}
int end = corpus.indexOf('"', start + 1);
if (end < 0) {
break;
}
keys.add(corpus.substring(start + 1, end));
i = end + 1;
}
return keys;
}
}
@@ -1,226 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pregenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.Spiraled;
import art.arcane.volmlib.util.math.Spiraler;
import lombok.Builder;
import lombok.Data;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Builder
@Data
public class PregenTask {
private static final KMap<Long, int[]> ORDERS = new KMap<>();
@Builder.Default
private final boolean gui = false;
@Builder.Default
private final Position2 center = new Position2(0, 0);
@Builder.Default
private final int radiusX = 1;
@Builder.Default
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) {
iterateRegion(xr, zr, s, pull.getX(), pull.getZ());
}
public static void iterateRegion(int xr, int zr, Spiraled s, int pullX, int pullZ) {
for (int packed : orderForPull(pullX, pullZ)) {
s.on(PowerOfTwoCoordinates.unpackLocal32X(packed) + PowerOfTwoCoordinates.regionToChunk(xr), PowerOfTwoCoordinates.unpackLocal32Z(packed) + PowerOfTwoCoordinates.regionToChunk(zr));
}
}
public static void iterateRegion(int xr, int zr, Spiraled s) {
iterateRegion(xr, zr, s, -PowerOfTwoCoordinates.regionToChunk(xr), -PowerOfTwoCoordinates.regionToChunk(zr));
}
private static int[] orderForPull(int pullX, int pullZ) {
long key = orderKey(pullX, pullZ);
return ORDERS.computeIfAbsent(key, PregenTask::computeOrder);
}
private static int[] computeOrder(long key) {
int pullX = (int) (key >> 32);
int pullZ = (int) key;
Position2 pull = new Position2(pullX, pullZ);
KList<Position2> p = new KList<>();
new Spiraler(33, 33, (x, z) -> {
int xx = (x + 15);
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(pull)));
int[] packed = new int[p.size()];
for (int index = 0; index < p.size(); index++) {
Position2 position = p.get(index);
packed[index] = PowerOfTwoCoordinates.packLocal32(position.getX(), position.getZ());
}
return packed;
}
private static long orderKey(int pullX, int pullZ) {
long high = (long) pullX << 32;
long low = pullZ & 0xFFFFFFFFL;
return high | low;
}
public void iterateRegions(Spiraled s) {
Bound bound = bounds.region();
new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> {
if (bound.check(x, z)) s.on(x, z);
})).setOffset(PowerOfTwoCoordinates.blockToRegionFloor(center.getX()), PowerOfTwoCoordinates.blockToRegionFloor(center.getZ())).drain();
}
public void iterateChunks(int rX, int rZ, Spiraled s) {
Bound bound = bounds.chunk();
iterateRegion(rX, rZ, ((x, z) -> {
if (bound.check(x, z)) s.on(x, z);
}));
}
@FunctionalInterface
public interface InterleavedChunkConsumer {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
}
public void iterateAllChunks(Spiraled s) {
iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s)));
}
public void iterateAllChunksInterleaved(InterleavedChunkConsumer consumer) {
List<int[]> regions = new ArrayList<>();
iterateRegions((rX, rZ) -> regions.add(new int[]{rX, rZ}));
List<List<int[]>> regionChunks = new ArrayList<>();
for (int[] region : regions) {
List<int[]> chunks = new ArrayList<>();
iterateChunks(region[0], region[1], (cx, cz) -> chunks.add(new int[]{region[0], region[1], cx, cz}));
if (!chunks.isEmpty()) {
regionChunks.add(chunks);
}
}
int[] indices = new int[regionChunks.size()];
boolean anyRemaining = true;
while (anyRemaining) {
anyRemaining = false;
for (int r = 0; r < regionChunks.size(); r++) {
List<int[]> chunks = regionChunks.get(r);
int idx = indices[r];
if (idx >= chunks.size()) {
continue;
}
anyRemaining = true;
int[] entry = chunks.get(idx);
boolean first = idx == 0;
boolean last = idx == chunks.size() - 1;
indices[r]++;
if (!consumer.on(entry[0], entry[1], entry[2], entry[3], first, last)) {
return;
}
}
}
}
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(
PowerOfTwoCoordinates.blockToChunkFloor(minX),
PowerOfTwoCoordinates.blockToChunkFloor(minZ),
PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.CHUNK_BITS),
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.CHUNK_BITS)
);
region = new Bound(
PowerOfTwoCoordinates.blockToRegionFloor(minX),
PowerOfTwoCoordinates.blockToRegionFloor(minZ),
PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.REGION_BITS),
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.REGION_BITS)
);
}
public Bound chunk() {
if (chunk == null) update();
return chunk;
}
public Bound region() {
if (region == null) update();
return region;
}
}
private record Bound(int minX, int minZ, int maxX, int maxZ, int sizeX, int sizeZ) {
private Bound(int minX, int minZ, int maxX, int maxZ) {
this(minX, minZ, maxX, maxZ, maxX - minX + 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");
}
}
}
@@ -1,376 +0,0 @@
package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.data.Varint;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.documentation.RegionCoordinates;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import net.jpountz.lz4.LZ4BlockInputStream;
import net.jpountz.lz4.LZ4BlockOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PregenCacheImpl implements PregenCache {
private static final ExecutorService DISPATCHER = Executors.newFixedThreadPool(4);
private static final short SIZE = 1024;
private final File directory;
private final int maxSize;
private final Long2ObjectLinkedOpenHashMap<Plate> cache = new Long2ObjectLinkedOpenHashMap<>();
public PregenCacheImpl(File directory, int maxSize) {
this.directory = directory;
this.maxSize = maxSize;
}
@Override
@ChunkCoordinates
public boolean isChunkCached(int x, int z) {
return getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).isCached(
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
region -> region.isCached(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS)
)
);
}
@Override
@RegionCoordinates
public boolean isRegionCached(int x, int z) {
return getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).isCached(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
Region::isCached
);
}
@Override
@ChunkCoordinates
public void cacheChunk(int x, int z) {
getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).cache(
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
region -> region.cache(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS)
)
);
}
@Override
@RegionCoordinates
public void cacheRegion(int x, int z) {
getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).cache(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
Region::cache
);
}
@Override
public void write() {
if (cache.isEmpty()) {
return;
}
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size());
for (Plate plate : cache.values()) {
if (!plate.dirty) {
continue;
}
futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER));
}
for (CompletableFuture<Void> future : futures) {
future.join();
}
}
@Override
public void trim(long unloadDuration) {
if (cache.isEmpty()) {
return;
}
long threshold = System.currentTimeMillis() - unloadDuration;
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size());
Iterator<Plate> iterator = cache.values().iterator();
while (iterator.hasNext()) {
Plate plate = iterator.next();
if (plate.lastAccess < threshold) {
iterator.remove();
}
futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER));
}
for (CompletableFuture<Void> future : futures) {
future.join();
}
}
private Plate getPlate(int x, int z) {
long key = key(x, z);
Plate plate = cache.getAndMoveToFirst(key);
if (plate != null) {
return plate;
}
Plate loaded = readPlate(x, z);
cache.putAndMoveToFirst(key, loaded);
if (cache.size() > maxSize) {
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size() - maxSize);
while (cache.size() > maxSize) {
Plate evicted = cache.removeLast();
futures.add(CompletableFuture.runAsync(() -> writePlate(evicted), DISPATCHER));
}
for (CompletableFuture<Void> future : futures) {
future.join();
}
}
return loaded;
}
private Plate readPlate(int x, int z) {
File file = fileForPlate(x, z);
if (!file.exists()) {
return new Plate(x, z);
}
try (DataInputStream input = new DataInputStream(new LZ4BlockInputStream(new FileInputStream(file)))) {
return readPlate(x, z, input);
} catch (IOException e) {
Iris.error("Failed to read pregen cache " + file);
e.printStackTrace();
Iris.reportError(e);
}
return new Plate(x, z);
}
private void writePlate(Plate plate) {
if (!plate.dirty) {
return;
}
File file = fileForPlate(plate.x, plate.z);
try {
IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write);
plate.dirty = false;
} catch (IOException e) {
Iris.error("Failed to write preen cache " + file);
e.printStackTrace();
Iris.reportError(e);
}
}
private File fileForPlate(int x, int z) {
if (!directory.exists() && !directory.mkdirs()) {
throw new IllegalStateException("Cannot create directory: " + directory.getAbsolutePath());
}
return new File(directory, "c." + x + "." + z + ".lz4b");
}
private static long key(int x, int z) {
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
private interface RegionPredicate {
boolean test(Region region);
}
private static class Plate {
private final int x;
private final int z;
private short count;
private Region[] regions;
private boolean dirty;
private long lastAccess;
private Plate(int x, int z) {
this(x, z, (short) 0, new Region[1024]);
}
private Plate(int x, int z, short count, Region[] regions) {
this.x = x;
this.z = z;
this.count = count;
this.regions = regions;
this.lastAccess = System.currentTimeMillis();
}
private boolean cache(int x, int z, RegionPredicate predicate) {
lastAccess = System.currentTimeMillis();
if (count == SIZE) {
return false;
}
int index = PowerOfTwoCoordinates.packLocal32(x, z);
Region region = regions[index];
if (region == null) {
region = new Region();
regions[index] = region;
}
if (!predicate.test(region)) {
return false;
}
count++;
if (count == SIZE) {
regions = null;
}
dirty = true;
return true;
}
private boolean isCached(int x, int z, RegionPredicate predicate) {
lastAccess = System.currentTimeMillis();
if (count == SIZE) {
return true;
}
Region region = regions[PowerOfTwoCoordinates.packLocal32(x, z)];
if (region == null) {
return false;
}
return predicate.test(region);
}
private void write(DataOutput output) throws IOException {
Varint.writeSignedVarInt(count, output);
if (regions == null) {
return;
}
for (Region region : regions) {
output.writeBoolean(region == null);
if (region != null) {
region.write(output);
}
}
}
}
private static class Region {
private short count;
private long[] words;
private Region() {
this((short) 0, new long[64]);
}
private Region(short count, long[] words) {
this.count = count;
this.words = words;
}
private boolean cache() {
if (count == SIZE) {
return false;
}
count = SIZE;
words = null;
return true;
}
private boolean cache(int x, int z) {
if (count == SIZE) {
return false;
}
long[] value = words;
if (value == null) {
return false;
}
int index = PowerOfTwoCoordinates.packLocal32(x, z);
int wordIndex = index >> 6;
long bit = 1L << (index & 63);
boolean current = (value[wordIndex] & bit) != 0L;
if (current) {
return false;
}
count++;
if (count == SIZE) {
words = null;
return true;
}
value[wordIndex] = value[wordIndex] | bit;
return false;
}
private boolean isCached() {
return count == SIZE;
}
private boolean isCached(int x, int z) {
int index = PowerOfTwoCoordinates.packLocal32(x, z);
if (count == SIZE) {
return true;
}
return (words[index >> 6] & (1L << (index & 63))) != 0L;
}
private void write(DataOutput output) throws IOException {
Varint.writeSignedVarInt(count, output);
if (words == null) {
return;
}
for (long word : words) {
Varint.writeUnsignedVarLong(word, output);
}
}
}
private static Plate readPlate(int x, int z, DataInput input) throws IOException {
int count = Varint.readSignedVarInt(input);
if (count == 1024) {
return new Plate(x, z, SIZE, null);
}
Region[] regions = new Region[1024];
for (int i = 0; i < regions.length; i++) {
boolean isNull = input.readBoolean();
if (!isNull) {
regions[i] = readRegion(input);
}
}
return new Plate(x, z, (short) count, regions);
}
private static Region readRegion(DataInput input) throws IOException {
int count = Varint.readSignedVarInt(input);
if (count == 1024) {
return new Region(SIZE, null);
}
long[] words = new long[64];
for (int i = 0; i < words.length; i++) {
words[i] = Varint.readUnsignedVarLong(input);
}
return new Region((short) count, words);
}
}
@@ -1,74 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import io.papermc.lib.PaperLib;
import org.bukkit.World;
public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
private final PregeneratorMethod method;
public AsyncOrMedievalPregenMethod(World world, int threads) {
method = PaperLib.isPaper() ? new AsyncPregenMethod(world, threads) : new MedievalPregenMethod(world);
}
@Override
public void init() {
method.init();
}
@Override
public void close() {
method.close();
}
@Override
public void save() {
method.save();
}
@Override
public String getMethod(int x, int z) {
return method.getMethod(x, z);
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
method.generateChunk(x, z, listener);
}
@Override
public Mantle getMantle() {
return method.getMantle();
}
}
@@ -1,849 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisPaperLikeBackendMode;
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CHUNK_CLEANUP_INTERVAL_MS = 15_000L;
private static final long CHUNK_CLEANUP_MIN_AGE_MS = 5_000L;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
private final boolean foliaRuntime;
private final String backendMode;
private final int workerPoolThreads;
private final int runtimeCpuThreads;
private final int effectiveWorkerThreads;
private final int recommendedRuntimeConcurrencyCap;
private final int configuredMaxConcurrency;
private final Method directChunkAtAsyncUrgentMethod;
private final Method directChunkAtAsyncMethod;
private final String chunkAccessMode;
private final Executor executor;
private final Semaphore semaphore;
private final int threads;
private final int timeoutSeconds;
private final int timeoutWarnIntervalMs;
private final boolean urgent;
private final Map<Chunk, Long> lastUse;
private final AtomicInteger adaptiveInFlightLimit;
private final int adaptiveMinInFlightLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
private final AtomicInteger inFlight = new AtomicInteger();
private final AtomicLong submitted = new AtomicLong();
private final AtomicLong completed = new AtomicLong();
private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final AtomicLong lastPermitWaitLog = new AtomicLong(0L);
private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms());
private final Object permitMonitor = new Object();
private volatile Engine metricsEngine;
public AsyncPregenMethod(World world, int unusedThreads) {
if (!PaperLib.isPaper()) {
throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!");
}
this.world = world;
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
this.runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(pregen);
this.foliaRuntime = runtimeSchedulerMode == IrisRuntimeSchedulerMode.FOLIA;
ChunkAsyncMethodSelection chunkAsyncMethodSelection = resolveChunkAsyncMethodSelection(world);
this.directChunkAtAsyncUrgentMethod = chunkAsyncMethodSelection.urgentMethod();
this.directChunkAtAsyncMethod = chunkAsyncMethodSelection.standardMethod();
this.chunkAccessMode = chunkAsyncMethodSelection.mode();
int detectedWorkerPoolThreads = resolveWorkerPoolThreads();
int detectedCpuThreads = Math.max(1, Runtime.getRuntime().availableProcessors());
int configuredWorldGenThreads = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads());
int workerThreadsForCap = foliaRuntime
? resolveFoliaConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, configuredWorldGenThreads)
: resolvePaperLikeConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, configuredWorldGenThreads);
if (foliaRuntime) {
this.paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO;
this.backendMode = "folia-region";
this.executor = new FoliaRegionExecutor();
} else {
this.paperLikeBackendMode = resolvePaperLikeBackendMode(pregen);
if (paperLikeBackendMode == IrisPaperLikeBackendMode.SERVICE) {
this.executor = new ServiceExecutor();
this.backendMode = "paper-service";
} else {
this.executor = new TicketExecutor();
this.backendMode = "paper-ticket";
}
}
int runtimeMaxConcurrency = foliaRuntime
? pregen.getFoliaMaxConcurrency()
: pregen.getPaperLikeMaxConcurrency();
int configuredThreads = applyRuntimeConcurrencyCap(
runtimeMaxConcurrency,
foliaRuntime,
workerThreadsForCap
);
this.configuredMaxConcurrency = Math.max(1, pregen.getMaxConcurrency());
this.threads = Math.max(1, configuredThreads);
this.workerPoolThreads = detectedWorkerPoolThreads;
this.runtimeCpuThreads = detectedCpuThreads;
this.effectiveWorkerThreads = workerThreadsForCap;
this.recommendedRuntimeConcurrencyCap = foliaRuntime
? computeFoliaRecommendedCap(workerThreadsForCap)
: computePaperLikeRecommendedCap(workerThreadsForCap);
this.semaphore = new Semaphore(this.threads, true);
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.urgent = IrisSettings.get().getPregen().useHighPriority;
this.lastUse = new ConcurrentHashMap<>();
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
}
private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) {
IrisPaperLikeBackendMode configuredMode = pregen.getPaperLikeBackendMode();
if (configuredMode != IrisPaperLikeBackendMode.AUTO) {
return configuredMode;
}
return pregen.isUseVirtualThreads() ? IrisPaperLikeBackendMode.SERVICE : IrisPaperLikeBackendMode.TICKET;
}
private int resolveWorkerPoolThreads() {
try {
Class<?> moonriseCommonClass = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon");
java.lang.reflect.Field workerPoolField = moonriseCommonClass.getDeclaredField("WORKER_POOL");
Object workerPool = workerPoolField.get(null);
Object coreThreads = workerPool.getClass().getDeclaredMethod("getCoreThreads").invoke(workerPool);
if (coreThreads instanceof Thread[] threadsArray) {
return threadsArray.length;
}
} catch (Throwable ignored) {
}
return -1;
}
private void unloadAndSaveAllChunks() {
if (foliaRuntime) {
lastUse.clear();
return;
}
if (lastUse.isEmpty()) {
return;
}
try {
J.sfut(() -> {
if (world == null) {
Iris.warn("World was null somehow...");
return;
}
long minTime = M.ms() - 10_000;
AtomicBoolean unloaded = new AtomicBoolean(false);
lastUse.entrySet().removeIf(i -> {
final Chunk chunk = i.getKey();
final Long lastUseTime = i.getValue();
if (!chunk.isLoaded() || lastUseTime == null)
return true;
if (lastUseTime < minTime) {
chunk.unload();
unloaded.set(true);
return true;
}
return false;
});
if (unloaded.get()) {
world.save();
}
}).get();
} catch (Throwable e) {
e.printStackTrace();
}
}
private void periodicChunkCleanup() {
long now = M.ms();
long lastCleanup = lastChunkCleanup.get();
if (now - lastCleanup < CHUNK_CLEANUP_INTERVAL_MS) {
return;
}
if (!lastChunkCleanup.compareAndSet(lastCleanup, now)) {
return;
}
if (foliaRuntime) {
int sizeBefore = lastUse.size();
if (sizeBefore > 0) {
lastUse.clear();
Iris.info("Periodic chunk cleanup: cleared " + sizeBefore + " Folia chunk references");
}
return;
}
int sizeBefore = lastUse.size();
if (sizeBefore == 0) {
return;
}
long minTime = now - CHUNK_CLEANUP_MIN_AGE_MS;
AtomicInteger removed = new AtomicInteger();
lastUse.entrySet().removeIf(entry -> {
Long lastUseTime = entry.getValue();
if (lastUseTime == null || lastUseTime < minTime) {
removed.incrementAndGet();
return true;
}
return false;
});
int removedCount = removed.get();
if (removedCount > 0) {
Iris.info("Periodic chunk cleanup: removed " + removedCount + "/" + sizeBefore + " stale chunk references");
}
}
private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) {
Throwable root = throwable;
while (root.getCause() != null) {
root = root.getCause();
}
if (root instanceof java.util.concurrent.TimeoutException) {
onTimeout(x, z);
} else {
Iris.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
}
Iris.reportError(throwable);
return null;
}
private void onTimeout(int x, int z) {
int streak = timeoutStreak.incrementAndGet();
if (streak % ADAPTIVE_TIMEOUT_STEP == 0) {
lowerAdaptiveInFlightLimit();
}
long now = M.ms();
long last = lastTimeoutLogAt.get();
if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) {
suppressedTimeoutLogs.incrementAndGet();
return;
}
int suppressed = suppressedTimeoutLogs.getAndSet(0);
String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed;
Iris.warn("Timed out async pregen chunk load at " + x + "," + z
+ " after " + timeoutSeconds + "s."
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ suppressedText + " " + metricsSnapshot());
}
private void onSuccess() {
int streak = timeoutStreak.get();
if (streak > 0) {
int newStreak = Math.max(0, streak - 2);
timeoutStreak.compareAndSet(streak, newStreak);
if (newStreak > 0) {
return;
}
}
if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) {
raiseAdaptiveInFlightLimit();
}
}
private void lowerAdaptiveInFlightLimit() {
while (true) {
int current = adaptiveInFlightLimit.get();
if (current <= adaptiveMinInFlightLimit) {
return;
}
int next = Math.max(adaptiveMinInFlightLimit, current - 1);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("decrease", next);
notifyPermitWaiters();
return;
}
}
}
private void raiseAdaptiveInFlightLimit() {
while (true) {
int current = adaptiveInFlightLimit.get();
if (current >= threads) {
return;
}
int deficit = threads - current;
int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1;
int next = Math.min(threads, current + step);
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
logAdaptiveLimit("increase", next);
notifyPermitWaiters();
return;
}
}
}
private void logAdaptiveLimit(String mode, int value) {
long now = M.ms();
long last = lastAdaptiveLogAt.get();
if (now - last < 5000L) {
return;
}
if (lastAdaptiveLogAt.compareAndSet(last, now)) {
Iris.info("Async pregen adaptive limit " + mode + " -> " + value + " " + metricsSnapshot());
}
}
static int computePaperLikeRecommendedCap(int workerThreads) {
int normalizedWorkers = Math.max(1, workerThreads);
int recommendedCap = normalizedWorkers * 2;
if (recommendedCap < 8) {
return 8;
}
if (recommendedCap > 96) {
return 96;
}
return recommendedCap;
}
static int resolvePaperLikeConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) {
if (detectedWorkerPoolThreads > 0) {
return detectedWorkerPoolThreads;
}
return Math.max(1, Math.max(detectedCpuThreads, configuredWorldGenThreads));
}
static int computeFoliaRecommendedCap(int workerThreads) {
int normalizedWorkers = Math.max(1, workerThreads);
int recommendedCap = normalizedWorkers * 4;
if (recommendedCap < 64) {
return 64;
}
if (recommendedCap > 192) {
return 192;
}
return recommendedCap;
}
static int resolveFoliaConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) {
return Math.max(detectedCpuThreads, Math.max(configuredWorldGenThreads, Math.max(1, detectedWorkerPoolThreads)));
}
static int applyRuntimeConcurrencyCap(int maxConcurrency, boolean foliaRuntime, int workerThreads) {
int normalizedMaxConcurrency = Math.max(1, maxConcurrency);
int recommendedCap = foliaRuntime
? computeFoliaRecommendedCap(workerThreads)
: computePaperLikeRecommendedCap(workerThreads);
return Math.min(normalizedMaxConcurrency, recommendedCap);
}
private String metricsSnapshot() {
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
return "world=" + world.getName()
+ " permits=" + semaphore.availablePermits() + "/" + threads
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ " inFlight=" + inFlight.get()
+ " submitted=" + submitted.get()
+ " completed=" + completed.get()
+ " failed=" + failed.get()
+ " stalledForMs=" + stalledFor;
}
private void markSubmitted() {
submitted.incrementAndGet();
inFlight.incrementAndGet();
}
private void markFinished(boolean success) {
if (success) {
completed.incrementAndGet();
onSuccess();
} else {
failed.incrementAndGet();
}
lastProgressAt.set(M.ms());
int after = inFlight.decrementAndGet();
if (after < 0) {
inFlight.compareAndSet(after, 0);
}
notifyPermitWaiters();
}
private void notifyPermitWaiters() {
synchronized (permitMonitor) {
permitMonitor.notifyAll();
}
}
private void recordAdaptiveWait(long waitedMs) {
Engine engine = resolveMetricsEngine();
if (engine != null) {
engine.getMetrics().getPregenWaitAdaptive().put(waitedMs);
}
}
private void recordPermitWait(long waitedMs) {
Engine engine = resolveMetricsEngine();
if (engine != null) {
engine.getMetrics().getPregenWaitPermit().put(waitedMs);
}
}
private void cleanupMantleChunk(int x, int z) {
Engine engine = resolveMetricsEngine();
if (engine != null) {
try {
engine.getMantle().forceCleanupChunk(x, z);
} catch (Throwable ignored) {
}
}
}
private Engine resolveMetricsEngine() {
Engine cachedEngine = metricsEngine;
if (cachedEngine != null) {
return cachedEngine;
}
if (!IrisToolbelt.isIrisWorld(world)) {
return null;
}
try {
Engine resolvedEngine = IrisToolbelt.access(world).getEngine();
if (resolvedEngine != null) {
metricsEngine = resolvedEngine;
}
return resolvedEngine;
} catch (Throwable ignored) {
return null;
}
}
private void logPermitWaitIfNeeded(int x, int z, long waitedMs) {
long now = M.ms();
long last = lastPermitWaitLog.get();
if (now - last < 5000L) {
return;
}
if (lastPermitWaitLog.compareAndSet(last, now)) {
Iris.warn("Async pregen waiting for permit at chunk " + x + "," + z + " waitedMs=" + waitedMs + " " + metricsSnapshot());
}
}
@Override
public void init() {
Iris.info("Async pregen init: world=" + world.getName()
+ ", mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
+ ", backend=" + backendMode
+ ", chunkAccess=" + chunkAccessMode
+ ", threads=" + threads
+ ", adaptiveLimit=" + adaptiveInFlightLimit.get()
+ ", workerPoolThreads=" + workerPoolThreads
+ ", cpuThreads=" + runtimeCpuThreads
+ ", effectiveWorkerThreads=" + effectiveWorkerThreads
+ ", maxConcurrency=" + configuredMaxConcurrency
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
+ ", urgent=" + urgent
+ ", timeout=" + timeoutSeconds + "s");
unloadAndSaveAllChunks();
increaseWorkerThreads();
}
@Override
public String getMethod(int x, int z) {
return "Async";
}
@Override
public boolean isAsyncChunkMode() {
return true;
}
@Override
public void close() {
semaphore.acquireUninterruptibly(threads);
unloadAndSaveAllChunks();
executor.shutdown();
resetWorkerThreads();
}
@Override
public void save() {
unloadAndSaveAllChunks();
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
periodicChunkCleanup();
try {
long waitStart = M.ms();
synchronized (permitMonitor) {
while (inFlight.get() >= adaptiveInFlightLimit.get()) {
long waited = Math.max(0L, M.ms() - waitStart);
logPermitWaitIfNeeded(x, z, waited);
permitMonitor.wait(500L);
}
}
long adaptiveWait = Math.max(0L, M.ms() - waitStart);
if (adaptiveWait > 0L) {
recordAdaptiveWait(adaptiveWait);
}
long permitWaitStart = M.ms();
while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart));
}
long permitWait = Math.max(0L, M.ms() - permitWaitStart);
if (permitWait > 0L) {
recordPermitWait(permitWait);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
markSubmitted();
executor.generate(x, z, listener);
}
private CompletableFuture<Chunk> requestChunkAsync(int x, int z) {
Throwable failure = null;
if (directChunkAtAsyncUrgentMethod != null) {
try {
return invokeChunkFuture(directChunkAtAsyncUrgentMethod, x, z, true, urgent);
} catch (Throwable e) {
failure = e;
}
}
if (directChunkAtAsyncMethod != null) {
try {
return invokeChunkFuture(directChunkAtAsyncMethod, x, z, true, urgent);
} catch (Throwable e) {
if (failure == null) {
failure = e;
}
}
}
try {
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, x, z, true, urgent);
if (future != null) {
return future;
}
} catch (Throwable e) {
if (failure == null) {
failure = e;
}
}
if (failure == null) {
failure = new IllegalStateException("Chunk async access returned no future.");
}
return CompletableFuture.failedFuture(new IllegalStateException("Failed to request async chunk " + x + "," + z + " in world " + world.getName(), failure));
}
@SuppressWarnings("unchecked")
private CompletableFuture<Chunk> invokeChunkFuture(Method method, int x, int z, boolean generate, boolean urgentRequest) throws Throwable {
Object result;
try {
if (method.getParameterCount() == 4) {
result = method.invoke(world, x, z, generate, urgentRequest);
} else {
result = method.invoke(world, x, z, generate);
}
} catch (InvocationTargetException e) {
throw e.getCause() == null ? e : e.getCause();
}
if (result instanceof CompletableFuture<?>) {
return (CompletableFuture<Chunk>) result;
}
throw new IllegalStateException("Chunk async method returned a non-future result.");
}
private static ChunkAsyncMethodSelection resolveChunkAsyncMethodSelection(World world) {
if (world == null) {
return new ChunkAsyncMethodSelection(null, null, "paperlib");
}
Class<?> worldClass = world.getClass();
Method urgentMethod = resolveChunkAsyncMethod(worldClass, int.class, int.class, boolean.class, boolean.class);
Method standardMethod = resolveChunkAsyncMethod(worldClass, int.class, int.class, boolean.class);
if (urgentMethod != null) {
return new ChunkAsyncMethodSelection(urgentMethod, standardMethod, "world#getChunkAtAsync(int,int,boolean,boolean)");
}
if (standardMethod != null) {
return new ChunkAsyncMethodSelection(null, standardMethod, "world#getChunkAtAsync(int,int,boolean)");
}
return new ChunkAsyncMethodSelection(null, null, "paperlib");
}
private static Method resolveChunkAsyncMethod(Class<?> worldClass, Class<?>... parameterTypes) {
try {
return worldClass.getMethod("getChunkAtAsync", parameterTypes);
} catch (NoSuchMethodException ignored) {
}
try {
return World.class.getMethod("getChunkAtAsync", parameterTypes);
} catch (NoSuchMethodException ignored) {
}
return null;
}
@Override
public Mantle getMantle() {
if (IrisToolbelt.isIrisWorld(world)) {
return IrisToolbelt.access(world).getEngine().getMantle().getMantle();
}
return null;
}
public static void increaseWorkerThreads() {
THREAD_COUNT.updateAndGet(i -> {
if (i > 0) return 1;
var adjusted = IrisSettings.get().getConcurrency().getWorldGenThreads();
try {
var field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL");
var pool = field.get(null);
var threads = ((Thread[]) pool.getClass().getDeclaredMethod("getCoreThreads").invoke(pool)).length;
if (threads >= adjusted) return 0;
pool.getClass().getDeclaredMethod("adjustThreadCount", int.class).invoke(pool, adjusted);
return threads;
} catch (Throwable e) {
Iris.warn("Failed to increase worker threads, if you are on paper or a fork of it please increase it manually to " + adjusted);
Iris.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads");
if (e instanceof InvocationTargetException) {
Iris.reportError(e);
e.printStackTrace();
}
}
return 0;
});
}
public static void resetWorkerThreads() {
THREAD_COUNT.updateAndGet(i -> {
if (i == 0) return 0;
try {
var field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL");
var pool = field.get(null);
var method = pool.getClass().getDeclaredMethod("adjustThreadCount", int.class);
method.invoke(pool, i);
return 0;
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to reset worker threads");
e.printStackTrace();
}
return i;
});
}
private interface Executor {
void generate(int x, int z, PregenListener listener);
default void shutdown() {}
}
private class FoliaRegionExecutor implements Executor {
@Override
public void generate(int x, int z, PregenListener listener) {
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable));
return;
} catch (Throwable ignored) {
}
if (!J.runRegion(world, x, z, () -> requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)))) {
markFinished(false);
semaphore.release();
Iris.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot());
}
}
private void completeFoliaChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) {
boolean success = false;
try {
if (throwable != null) {
onChunkFutureFailure(x, z, throwable);
return;
}
if (chunk == null) {
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
lastUse.put(chunk, M.ms());
success = true;
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
} finally {
markFinished(success);
semaphore.release();
}
}
}
private class ServiceExecutor implements Executor {
private final ExecutorService service = IrisSettings.get().getPregen().isUseVirtualThreads() ?
Executors.newVirtualThreadPerTaskExecutor() :
new MultiBurst("Iris Async Pregen");
public void generate(int x, int z, PregenListener listener) {
service.submit(() -> {
boolean success = false;
try {
Chunk i = requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.exceptionally(e -> onChunkFutureFailure(x, z, e))
.get();
if (i == null) {
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
lastUse.put(i, M.ms());
success = true;
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
} finally {
markFinished(success);
semaphore.release();
}
});
}
@Override
public void shutdown() {
service.shutdown();
}
}
private class TicketExecutor implements Executor {
@Override
public void generate(int x, int z, PregenListener listener) {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.exceptionally(e -> onChunkFutureFailure(x, z, e))
.thenAccept(i -> {
boolean success = false;
try {
if (i == null) {
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
lastUse.put(i, M.ms());
success = true;
} finally {
markFinished(success);
semaphore.release();
}
});
}
}
private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) {
}
}
@@ -1,75 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.World;
public class HybridPregenMethod implements PregeneratorMethod {
private final PregeneratorMethod inWorld;
private final World world;
public HybridPregenMethod(World world, int threads) {
this.world = world;
inWorld = new AsyncOrMedievalPregenMethod(world, threads);
}
@Override
public String getMethod(int x, int z) {
return "Hybrid<" + inWorld.getMethod(x, z) + ">";
}
@Override
public void init() {
inWorld.init();
}
@Override
public void close() {
inWorld.close();
}
@Override
public void save() {
inWorld.save();
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return inWorld.supportsRegions(x, z, listener);
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
inWorld.generateRegion(x, z, listener);
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
inWorld.generateChunk(x, z, listener);
}
@Override
public Mantle getMantle() {
return inWorld.getMantle();
}
}
@@ -1,144 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class MedievalPregenMethod implements PregeneratorMethod {
private final World world;
private final KList<CompletableFuture<?>> futures;
private final Map<Chunk, Long> lastUse;
public MedievalPregenMethod(World world) {
this.world = world;
futures = new KList<>();
this.lastUse = new ConcurrentHashMap<>();
}
private void waitForChunks() {
for (CompletableFuture<?> i : futures) {
try {
i.get();
} catch (Throwable e) {
e.printStackTrace();
}
}
futures.clear();
}
private void unloadAndSaveAllChunks() {
if (J.isFolia()) {
lastUse.clear();
return;
}
try {
J.sfut(() -> {
if (world == null) {
Iris.warn("World was null somehow...");
return;
}
for (Chunk i : new ArrayList<>(lastUse.keySet())) {
Long lastUseTime = lastUse.get(i);
if (lastUseTime != null && M.ms() - lastUseTime >= 10) {
i.unload();
lastUse.remove(i);
}
}
world.save();
}).get();
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public void init() {
unloadAndSaveAllChunks();
}
@Override
public void close() {
unloadAndSaveAllChunks();
}
@Override
public void save() {
unloadAndSaveAllChunks();
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
throw new UnsupportedOperationException();
}
@Override
public String getMethod(int x, int z) {
return "Medieval";
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
if (futures.size() > IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())) {
waitForChunks();
}
listener.onChunkGenerating(x, z);
futures.add(J.sfut(() -> {
world.getChunkAt(x, z);
Chunk c = Bukkit.getWorld(world.getUID()).getChunkAt(x, z);
lastUse.put(c, M.ms());
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
}));
}
@Override
public Mantle getMantle() {
if (IrisToolbelt.isIrisWorld(world)) {
return IrisToolbelt.access(world).getEngine().getMantle().getMantle();
}
return null;
}
}
@@ -1,77 +0,0 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import io.papermc.lib.PaperLib;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
final class BukkitPublicRuntimeControlBackend implements WorldRuntimeControlBackend {
private final CapabilitySnapshot capabilities;
BukkitPublicRuntimeControlBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public String backendName() {
return "bukkit_public_runtime";
}
@Override
public String describeCapabilities() {
String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib";
return "time=bukkit_world#setTime, chunkAsync=" + chunkAsync + ", teleport=entity_scheduler";
}
@Override
public OptionalLong readDayTime(World world) {
if (world == null) {
return OptionalLong.empty();
}
return OptionalLong.of(world.getTime());
}
@Override
public boolean writeDayTime(World world, long dayTime) {
if (world == null) {
return false;
}
world.setTime(dayTime);
return true;
}
@Override
public void syncTime(World world) {
}
@Override
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
if (world == null) {
return CompletableFuture.failedFuture(new IllegalStateException("World is null."));
}
if (capabilities.chunkAtAsyncMethod() != null) {
try {
Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate);
if (result instanceof CompletableFuture<?>) {
@SuppressWarnings("unchecked")
CompletableFuture<Chunk> future = (CompletableFuture<Chunk>) result;
return future;
}
} catch (Throwable ignored) {
}
}
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
if (future == null) {
return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future."));
}
return future;
}
}
@@ -1,67 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.runtime;
import art.arcane.iris.core.loader.IrisData;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ObjectStudioActivation {
private static final Set<String> ACTIVE = Collections.newSetFromMap(new ConcurrentHashMap<>());
private static final Map<String, Map<String, IrisData>> SOURCES = new ConcurrentHashMap<>();
private ObjectStudioActivation() {
}
public static void activate(String packKey) {
if (packKey == null) return;
ACTIVE.add(normalize(packKey));
}
public static void deactivate(String packKey) {
if (packKey == null) return;
String norm = normalize(packKey);
ACTIVE.remove(norm);
SOURCES.remove(norm);
}
public static boolean isActive(String packKey) {
if (packKey == null) return false;
return ACTIVE.contains(normalize(packKey));
}
public static void setSources(String packKey, Map<String, IrisData> sources) {
if (packKey == null || sources == null || sources.isEmpty()) return;
SOURCES.put(normalize(packKey), new LinkedHashMap<>(sources));
}
public static Map<String, IrisData> getSources(String packKey) {
if (packKey == null) return null;
return SOURCES.get(normalize(packKey));
}
private static String normalize(String key) {
return key.toLowerCase(Locale.ROOT);
}
}
@@ -1,258 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.bukkit.util.BlockVector;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class ObjectStudioLayout {
public static final int FLOOR_Y = 64;
private static final int DEFAULT_ROW_WIDTH_CAP = 160;
private final int padding;
private final int rowWidthCap;
private final List<GridCell> cells;
private final Map<String, GridCell> byKey;
private ObjectStudioLayout(int padding, int rowWidthCap, List<GridCell> cells) {
this.padding = padding;
this.rowWidthCap = rowWidthCap;
this.cells = Collections.unmodifiableList(cells);
Map<String, GridCell> index = new LinkedHashMap<>();
for (GridCell cell : cells) {
index.putIfAbsent(cell.key(), cell);
index.putIfAbsent(cell.pack() + "/" + cell.key(), cell);
}
this.byKey = Collections.unmodifiableMap(index);
}
public static ObjectStudioLayout build(IrisData data, int padding) {
Map<String, IrisData> sources = new LinkedHashMap<>();
sources.put(data.getDataFolder().getName(), data);
return build(sources, padding, DEFAULT_ROW_WIDTH_CAP);
}
public static ObjectStudioLayout build(Map<String, IrisData> sources, int padding) {
return build(sources, padding, DEFAULT_ROW_WIDTH_CAP);
}
public static ObjectStudioLayout build(Map<String, IrisData> sources, int padding, int rowWidthCap) {
List<PackEntry> entries = new ArrayList<>();
for (Map.Entry<String, IrisData> entry : sources.entrySet()) {
String packName = entry.getKey();
IrisData data = entry.getValue();
String[] possible = data.getObjectLoader().getPossibleKeys();
if (possible == null) continue;
List<String> sorted = new ArrayList<>(Arrays.asList(possible));
Collections.sort(sorted);
for (String key : sorted) {
entries.add(new PackEntry(packName, data, key));
}
}
entries.sort((a, b) -> {
int c = a.pack.compareTo(b.pack);
if (c != 0) return c;
return a.key.compareTo(b.key);
});
List<GridCell> packed = new ArrayList<>(entries.size());
int cursorX = 0;
int rowZ = 0;
int rowMaxDepth = 0;
for (PackEntry entry : entries) {
File file = entry.data.getObjectLoader().findFile(entry.key);
if (file == null) {
continue;
}
BlockVector size;
try {
size = IrisObject.sampleSize(file);
} catch (Throwable e) {
Iris.reportError(e);
continue;
}
int w = Math.max(1, size.getBlockX());
int h = Math.max(1, size.getBlockY());
int d = Math.max(1, size.getBlockZ());
int cellWidth = w + padding * 2;
int cellDepth = d + padding * 2;
if (cursorX > 0 && cursorX + cellWidth > rowWidthCap) {
rowZ += rowMaxDepth;
cursorX = 0;
rowMaxDepth = 0;
}
int originX = cursorX + padding;
int originZ = rowZ + padding;
int originY = FLOOR_Y + 1;
packed.add(new GridCell(entry.pack, entry.key, originX, originY, originZ, w, h, d));
cursorX += cellWidth;
rowMaxDepth = Math.max(rowMaxDepth, cellDepth);
}
return new ObjectStudioLayout(padding, rowWidthCap, packed);
}
public static ObjectStudioLayout load(File file, Map<String, IrisData> sources, int padding) {
if (file == null || !file.exists()) {
return null;
}
try {
String raw = Files.readString(file.toPath());
JSONObject root = new JSONObject(raw);
int storedPadding = root.optInt("padding", padding);
int storedCap = root.optInt("rowWidthCap", DEFAULT_ROW_WIDTH_CAP);
JSONArray arr = root.getJSONArray("cells");
List<GridCell> stored = new ArrayList<>();
Set<String> storedIds = new HashSet<>();
for (int i = 0; i < arr.length(); i++) {
JSONObject c = arr.getJSONObject(i);
String pack = c.optString("pack", null);
if (pack == null || pack.isEmpty()) {
return null;
}
GridCell cell = new GridCell(
pack,
c.getString("key"),
c.getInt("x"),
c.getInt("y"),
c.getInt("z"),
c.getInt("w"),
c.getInt("h"),
c.getInt("d")
);
stored.add(cell);
storedIds.add(cell.pack() + "/" + cell.key());
}
Set<String> liveIds = new HashSet<>();
for (Map.Entry<String, IrisData> entry : sources.entrySet()) {
String[] live = entry.getValue().getObjectLoader().getPossibleKeys();
if (live == null) continue;
for (String k : live) {
liveIds.add(entry.getKey() + "/" + k);
}
}
if (liveIds.size() == storedIds.size() && liveIds.containsAll(storedIds)) {
return new ObjectStudioLayout(storedPadding, storedCap, stored);
}
return null;
} catch (Throwable e) {
Iris.reportError(e);
return null;
}
}
public void save(File file) {
if (file == null) {
return;
}
try {
if (file.getParentFile() != null) {
file.getParentFile().mkdirs();
}
JSONObject root = new JSONObject();
root.put("padding", padding);
root.put("rowWidthCap", rowWidthCap);
JSONArray arr = new JSONArray();
for (GridCell cell : cells) {
JSONObject c = new JSONObject();
c.put("pack", cell.pack());
c.put("key", cell.key());
c.put("x", cell.originX());
c.put("y", cell.originY());
c.put("z", cell.originZ());
c.put("w", cell.w());
c.put("h", cell.h());
c.put("d", cell.d());
arr.put(c);
}
root.put("cells", arr);
Files.writeString(file.toPath(), root.toString(2));
} catch (Throwable e) {
Iris.reportError(e);
}
}
public int padding() {
return padding;
}
public List<GridCell> cells() {
return cells;
}
public GridCell findAt(int worldX, int worldZ) {
for (GridCell cell : cells) {
if (worldX >= cell.originX() && worldX < cell.originX() + cell.w()
&& worldZ >= cell.originZ() && worldZ < cell.originZ() + cell.d()) {
return cell;
}
}
return null;
}
public GridCell get(String key) {
return byKey.get(key);
}
private record PackEntry(String pack, IrisData data, String key) {
}
public record GridCell(String pack, String key, int originX, int originY, int originZ, int w, int h, int d) {
public int chunkMinX() {
return originX >> 4;
}
public int chunkMaxX() {
return (originX + w - 1) >> 4;
}
public int chunkMinZ() {
return originZ >> 4;
}
public int chunkMaxZ() {
return (originZ + d - 1) >> 4;
}
}
}
@@ -1,319 +0,0 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.lang.reflect.Method;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
final class PaperLikeRuntimeControlBackend implements WorldRuntimeControlBackend {
private final CapabilitySnapshot capabilities;
private final AtomicReference<TimeAccessStrategy> timeAccessStrategy;
PaperLikeRuntimeControlBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
this.timeAccessStrategy = new AtomicReference<>();
}
@Override
public String backendName() {
return "paper_like_runtime";
}
@Override
public String describeCapabilities() {
TimeAccessStrategy strategy = timeAccessStrategy.get();
String timeAccess = strategy == null ? "deferred" : strategy.description();
String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib";
return "time=" + timeAccess + ", chunkAsync=" + chunkAsync + ", teleport=entity_scheduler";
}
@Override
public OptionalLong readDayTime(World world) {
if (world == null) {
return OptionalLong.empty();
}
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
if (strategy == null) {
return OptionalLong.empty();
}
try {
Object handle = strategy.handleMethod().invoke(world);
if (handle == null) {
return OptionalLong.empty();
}
Object value = strategy.readMethod().invoke(strategy.readOwner(handle), strategy.readArguments(handle));
if (value instanceof Long longValue) {
return OptionalLong.of(longValue.longValue());
}
if (value instanceof Number number) {
return OptionalLong.of(number.longValue());
}
} catch (Throwable ignored) {
}
return OptionalLong.empty();
}
@Override
public boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException {
if (world == null) {
return false;
}
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
if (strategy == null || !strategy.writable()) {
return false;
}
Object handle = strategy.handleMethod().invoke(world);
if (handle == null) {
return false;
}
Object writeOwner = strategy.writeOwner(handle);
if (writeOwner == null) {
return false;
}
strategy.writeMethod().invoke(writeOwner, dayTime);
return true;
}
@Override
public void syncTime(World world) {
TimeAccessStrategy strategy = timeAccessStrategy.get();
if (strategy == null || strategy.syncMethod() == null) {
return;
}
try {
Object craftServer = Bukkit.getServer();
if (craftServer == null) {
return;
}
Object serverHandle = strategy.serverHandleMethod() == null ? null : strategy.serverHandleMethod().invoke(craftServer);
if (serverHandle == null) {
return;
}
strategy.syncMethod().invoke(serverHandle);
} catch (Throwable ignored) {
}
}
@Override
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
if (world == null) {
return CompletableFuture.failedFuture(new IllegalStateException("World is null."));
}
if (capabilities.chunkAtAsyncMethod() != null) {
try {
Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate);
if (result instanceof CompletableFuture<?>) {
@SuppressWarnings("unchecked")
CompletableFuture<Chunk> future = (CompletableFuture<Chunk>) result;
return future;
}
} catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
}
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
if (future == null) {
return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future."));
}
return future;
}
private TimeAccessStrategy resolveTimeAccessStrategy(World world) {
TimeAccessStrategy current = timeAccessStrategy.get();
if (current != null) {
return current;
}
synchronized (timeAccessStrategy) {
current = timeAccessStrategy.get();
if (current != null) {
return current;
}
TimeAccessStrategy resolved = probeTimeAccessStrategy(world);
timeAccessStrategy.set(resolved);
return resolved;
}
}
private TimeAccessStrategy probeTimeAccessStrategy(World world) {
if (world == null) {
return TimeAccessStrategy.unsupported();
}
try {
Method handleMethod = resolveZeroArgMethod(world.getClass(), "getHandle");
if (handleMethod == null) {
return TimeAccessStrategy.unsupported();
}
Object handle = handleMethod.invoke(world);
if (handle == null) {
return TimeAccessStrategy.unsupported();
}
Method readMethod = resolveZeroArgMethod(handle.getClass(), "getDayTime");
Method writeMethod = resolveLongArgMethod(handle.getClass(), "setDayTime");
if (readMethod != null && writeMethod != null) {
return TimeAccessStrategy.forHandle(handleMethod, readMethod, writeMethod, "runtime_handle#setDayTime");
}
Method levelDataMethod = resolveZeroArgMethod(handle.getClass(), "serverLevelData");
if (levelDataMethod == null) {
levelDataMethod = resolveZeroArgMethod(handle.getClass(), "getLevelData");
}
if (levelDataMethod != null) {
Object levelData = levelDataMethod.invoke(handle);
if (levelData != null) {
Method levelDataReadMethod = resolveZeroArgMethod(levelData.getClass(), "getDayTime");
Method levelDataWriteMethod = resolveLongArgMethod(levelData.getClass(), "setDayTime");
if (levelDataReadMethod != null && levelDataWriteMethod != null) {
return TimeAccessStrategy.forLevelData(handleMethod, levelDataMethod, levelDataReadMethod, levelDataWriteMethod, "world_data#setDayTime");
}
}
}
return TimeAccessStrategy.unsupported(handleMethod);
} catch (Throwable ignored) {
return TimeAccessStrategy.unsupported();
}
}
private static Method resolveZeroArgMethod(Class<?> type, String name) {
Class<?> current = type;
while (current != null) {
try {
Method method = current.getDeclaredMethod(name);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException ignored) {
current = current.getSuperclass();
}
}
return null;
}
private static Method resolveLongArgMethod(Class<?> type, String name) {
Class<?> current = type;
while (current != null) {
try {
Method method = current.getDeclaredMethod(name, long.class);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException ignored) {
current = current.getSuperclass();
}
}
return null;
}
private record TimeAccessStrategy(
Method handleMethod,
Method levelDataMethod,
Method readMethod,
Method writeMethod,
Method serverHandleMethod,
Method syncMethod,
String description
) {
static TimeAccessStrategy forHandle(Method handleMethod, Method readMethod, Method writeMethod, String description) {
Method serverHandleMethod = resolveCraftServerMethod("getHandle");
Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization");
return new TimeAccessStrategy(handleMethod, null, readMethod, writeMethod, serverHandleMethod, syncMethod, description);
}
static TimeAccessStrategy forLevelData(Method handleMethod, Method levelDataMethod, Method readMethod, Method writeMethod, String description) {
Method serverHandleMethod = resolveCraftServerMethod("getHandle");
Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization");
return new TimeAccessStrategy(handleMethod, levelDataMethod, readMethod, writeMethod, serverHandleMethod, syncMethod, description);
}
static TimeAccessStrategy unsupported() {
return new TimeAccessStrategy(null, null, null, null, null, null, "unsupported");
}
static TimeAccessStrategy unsupported(Method handleMethod) {
return new TimeAccessStrategy(handleMethod, null, null, null, null, null, "unsupported");
}
boolean writable() {
return handleMethod != null && readMethod != null && writeMethod != null;
}
Object readOwner(Object handle) throws ReflectiveOperationException {
if (levelDataMethod == null) {
return handle;
}
return levelDataMethod.invoke(handle);
}
Object[] readArguments(Object handle) {
return new Object[0];
}
Object writeOwner(Object handle) throws ReflectiveOperationException {
if (levelDataMethod == null) {
return handle;
}
return levelDataMethod.invoke(handle);
}
private static Method resolveCraftServerMethod(String name) {
try {
Method method = Bukkit.getServer().getClass().getMethod(name);
method.setAccessible(true);
return method;
} catch (Throwable ignored) {
return null;
}
}
private static Method resolveServerMethod(Method serverHandleMethod, String name) {
if (serverHandleMethod == null) {
return null;
}
try {
Object craftServer = Bukkit.getServer();
if (craftServer == null) {
return null;
}
Object serverHandle = serverHandleMethod.invoke(craftServer);
if (serverHandle == null) {
return null;
}
Method method = serverHandle.getClass().getMethod(name);
method.setAccessible(true);
return method;
} catch (Throwable ignored) {
return null;
}
}
}
}
@@ -1,475 +0,0 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.io.IO;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
public final class StudioOpenCoordinator {
private static volatile StudioOpenCoordinator instance;
private StudioOpenCoordinator() {
}
public static StudioOpenCoordinator get() {
StudioOpenCoordinator current = instance;
if (current != null) {
return current;
}
synchronized (StudioOpenCoordinator.class) {
if (instance != null) {
return instance;
}
instance = new StudioOpenCoordinator();
return instance;
}
}
public CompletableFuture<StudioOpenResult> open(StudioOpenRequest request) {
CompletableFuture<StudioOpenResult> future = new CompletableFuture<>();
J.aBukkit(() -> executeOpen(request, future));
return future;
}
public CompletableFuture<StudioCloseResult> closeProject(IrisProject project) {
CompletableFuture<StudioCloseResult> future = new CompletableFuture<>();
J.aBukkit(() -> future.complete(executeClose(project)));
return future;
}
private StudioCloseResult executeClose(IrisProject project) {
if (project == null) {
return new StudioCloseResult(null, true, true, false, null);
}
PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) {
return new StudioCloseResult(null, true, true, false, null);
}
World world = provider.getTarget().getWorld().realWorld();
String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName();
try {
return closeWorld(provider, worldName, world, true, project);
} catch (Throwable e) {
project.setActiveProvider(null);
return new StudioCloseResult(worldName, false, false, false, e);
}
}
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
World world = null;
PlatformChunkGenerator provider = null;
try {
updateStage(request, "resolve_dimension", 0.04D);
if (IrisToolbelt.getDimension(request.dimensionKey()) == null) {
throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + ".");
}
updateStage(request, "prepare_world_pack", 0.10D);
cleanupStaleTransientWorlds(request.worldName());
updateStage(request, "install_datapacks", 0.18D);
IrisCreator creator = IrisToolbelt.createWorld()
.seed(request.seed())
.sender(request.sender())
.studio(true)
.name(request.worldName())
.dimension(request.dimensionKey())
.studioProgressConsumer((progress, stage) -> updateStage(request, mapCreatorStage(stage), progress));
world = creator.create();
provider = IrisToolbelt.access(world);
if (provider == null) {
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
}
updateStage(request, "apply_world_rules", 0.72D);
WorldRuntimeControlService.get().applyStudioWorldRules(world);
updateStage(request, "prepare_generator", 0.78D);
WorldRuntimeControlService.get().prepareGenerator(world);
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
if (entryAnchor == null) {
throw new IllegalStateException("Studio entry anchor could not be resolved.");
}
updateStage(request, "resolve_safe_entry", 0.84D);
Location safeEntry;
try {
safeEntry = WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor)
.get(5L, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Studio entry point resolution timed out — region thread may be stalled.");
}
if (safeEntry == null) {
throw new IllegalStateException("Studio entry point could not be resolved for world \"" + request.worldName() + "\".");
}
if (request.playerName() != null && !request.playerName().isBlank()) {
updateStage(request, "teleport_player", 0.96D);
Player player = resolvePlayer(request.playerName());
if (player == null) {
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
}
Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(10L, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(teleported)) {
throw new IllegalStateException("Studio teleport did not complete successfully.");
}
}
updateStage(request, "finalize_open", 1.00D);
if (request.project() != null) {
request.project().setActiveProvider(provider);
}
if (request.openWorkspace() && request.project() != null) {
request.project().openVSCode(request.sender());
}
if (request.onDone() != null) {
request.onDone().accept(world);
}
future.complete(new StudioOpenResult(world, safeEntry));
} catch (Throwable e) {
Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) {
try {
updateStage(request, "cleanup", 1.00D);
closeWorld(provider, request.worldName(), world, true, request.project());
} catch (Throwable cleanupError) {
Iris.reportError("Studio cleanup failed for world \"" + request.worldName() + "\".", cleanupError);
}
}
future.completeExceptionally(e);
}
}
private StudioCloseResult closeWorld(
PlatformChunkGenerator provider,
String worldName,
World world,
boolean deleteFolder,
IrisProject project
) {
Throwable failure = null;
boolean unloadCompletedLive = world == null || !isWorldFamilyLoaded(worldName);
boolean folderDeletionCompletedLive = !deleteFolder;
boolean startupCleanupQueued = false;
CompletableFuture<Void> closeFuture = CompletableFuture.completedFuture(null);
if (world != null) {
try {
evacuatePlayers(world);
} catch (Throwable e) {
failure = e;
}
}
if (world != null) {
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
}
try {
if (project != null) {
project.setActiveProvider(null);
}
if (provider != null) {
closeFuture = provider.closeAsync();
}
if (worldName != null && !worldName.isBlank()) {
requestWorldFamilyUnload(worldName);
}
if (worldName != null && !worldName.isBlank()) {
long unloadDeadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20L);
CompletableFuture<Void> unloadFuture = waitForWorldFamilyUnload(worldName, unloadDeadline);
try {
unloadFuture.get(Math.max(1000L, unloadDeadline - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
unloadCompletedLive = true;
} catch (TimeoutException e) {
unloadCompletedLive = !isWorldFamilyLoaded(worldName);
} catch (Throwable e) {
failure = failure == null ? unwrapFailure(e) : failure;
}
}
try {
closeFuture.get(20L, TimeUnit.SECONDS);
} catch (Throwable e) {
Throwable cause = unwrapFailure(e);
if (failure == null) {
failure = cause;
}
}
if (deleteFolder && worldName != null && !worldName.isBlank()) {
WorldFamilyDeleteResult deleteResult = deleteWorldFamily(worldName, unloadCompletedLive);
folderDeletionCompletedLive = deleteResult.liveDeleted();
startupCleanupQueued = deleteResult.startupCleanupQueued();
}
} finally {
if (world != null) {
IrisToolbelt.endWorldMaintenance(world, "studio-close");
}
}
return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure);
}
private void evacuatePlayers(World world) throws Exception {
if (world == null) {
return;
}
CompletableFuture<Void> future = J.sfut(() -> {
IrisToolbelt.evacuate(world);
return null;
});
if (future != null) {
future.get(10L, TimeUnit.SECONDS);
}
}
private void requestWorldFamilyUnload(String worldName) {
if (worldName == null || worldName.isBlank()) {
return;
}
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
World familyWorld = Bukkit.getWorld(familyWorldName);
if (familyWorld == null) {
continue;
}
Iris.linkMultiverseCore.removeFromConfig(familyWorld);
WorldLifecycleService.get().unload(familyWorld, false);
}
}
private WorldFamilyDeleteResult deleteWorldFamily(String worldName, boolean unloadCompletedLive) {
if (worldName == null || worldName.isBlank()) {
return new WorldFamilyDeleteResult(true, false);
}
File container = Bukkit.getWorldContainer();
boolean liveDeleted = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File folder = new File(container, familyWorldName);
if (!folder.exists()) {
continue;
}
try {
deleteWorldFolderAsync(folder, 40).get(15L, TimeUnit.SECONDS);
} catch (Throwable e) {
liveDeleted = false;
Iris.reportError("Studio folder deletion retries failed for \"" + folder.getAbsolutePath() + "\".", unwrapFailure(e));
}
if (folder.exists()) {
liveDeleted = false;
}
}
if (liveDeleted) {
return new WorldFamilyDeleteResult(true, false);
}
try {
Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName));
return new WorldFamilyDeleteResult(false, true);
} catch (IOException e) {
if (unloadCompletedLive) {
Iris.reportError("Failed to queue deferred deletion for world \"" + worldName + "\".", e);
}
return new WorldFamilyDeleteResult(false, false);
}
}
private void cleanupStaleTransientWorlds(String worldName) {
File container = Bukkit.getWorldContainer();
LinkedHashSet<String> staleWorldNames = TransientWorldCleanupSupport.collectTransientStudioWorldNames(container);
String requestedBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName);
if (requestedBaseName != null) {
staleWorldNames.add(requestedBaseName);
}
for (String staleWorldName : staleWorldNames) {
if (Bukkit.getWorld(staleWorldName) != null) {
continue;
}
deleteWorldFamily(staleWorldName, true);
}
}
private void updateStage(StudioOpenRequest request, String stage, double progress) {
if (request.progressConsumer() != null) {
request.progressConsumer().accept(new StudioOpenProgress(progress, stage));
}
}
private String mapCreatorStage(String stage) {
if (stage == null || stage.isBlank()) {
return "create_world";
}
String normalized = stage.trim().toLowerCase();
return switch (normalized) {
case "resolve_dimension", "resolving dimension" -> "resolve_dimension";
case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack";
case "install_datapacks", "installing datapacks", "datapacks ready" -> "install_datapacks";
case "create_world", "creating world", "world created" -> "create_world";
default -> normalized.replace(' ', '_');
};
}
private CompletableFuture<Void> waitForWorldFamilyUnload(String worldName, long deadline) {
if (worldName == null || !isWorldFamilyLoaded(worldName) || System.currentTimeMillis() >= deadline) {
return CompletableFuture.completedFuture(null);
}
return delayFuture(100L).thenCompose(ignored -> waitForWorldFamilyUnload(worldName, deadline));
}
private CompletableFuture<Void> deleteWorldFolderAsync(File folder, int attemptsRemaining) {
if (folder == null || !folder.exists()) {
return CompletableFuture.completedFuture(null);
}
IO.delete(folder);
if (!folder.exists()) {
return CompletableFuture.completedFuture(null);
}
if (attemptsRemaining <= 1) {
return CompletableFuture.failedFuture(new IllegalStateException("World folder still exists after deletion retries: " + folder.getAbsolutePath()));
}
return delayFuture(250L).thenCompose(ignored -> deleteWorldFolderAsync(folder, attemptsRemaining - 1));
}
private CompletableFuture<Void> delayFuture(long delayMillis) {
long safeDelay = Math.max(0L, delayMillis);
return CompletableFuture.runAsync(() -> {
}, CompletableFuture.delayedExecutor(safeDelay, TimeUnit.MILLISECONDS));
}
private Throwable unwrapFailure(Throwable throwable) {
Throwable cursor = throwable;
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
if (cursor.getCause() == null) {
break;
}
cursor = cursor.getCause();
}
return cursor;
}
private Player resolvePlayer(String playerName) {
Player exact = Bukkit.getPlayerExact(playerName);
if (exact != null) {
return exact;
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().equalsIgnoreCase(playerName)) {
return player;
}
}
return null;
}
private boolean isWorldFamilyLoaded(String worldName) {
if (worldName == null || worldName.isBlank()) {
return false;
}
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
if (Bukkit.getWorld(familyWorldName) != null) {
return true;
}
}
return false;
}
public record StudioOpenRequest(
String dimensionKey,
IrisProject project,
VolmitSender sender,
long seed,
String worldName,
String playerName,
boolean openWorkspace,
boolean retainOnFailure,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone
) {
public static StudioOpenRequest studioProject(IrisProject project, VolmitSender sender, long seed, Consumer<StudioOpenProgress> progressConsumer, Consumer<World> onDone) {
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
return new StudioOpenRequest(
project.getName(),
project,
sender,
seed,
"iris-" + UUID.randomUUID(),
playerName,
true,
false,
progressConsumer,
onDone
);
}
}
public record StudioOpenProgress(double progress, String stage) {
}
public record StudioOpenResult(World world, Location entryLocation) {
}
public record StudioCloseResult(
String worldName,
boolean unloadCompletedLive,
boolean folderDeletionCompletedLive,
boolean startupCleanupQueued,
Throwable failureCause
) {
public boolean successful() {
return failureCause == null;
}
}
private record WorldFamilyDeleteResult(boolean liveDeleted, boolean startupCleanupQueued) {
}
}
@@ -1,91 +0,0 @@
package art.arcane.iris.core.runtime;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
public final class TransientWorldCleanupSupport {
private static final Pattern TRANSIENT_STUDIO_WORLD_PATTERN = Pattern.compile("^iris-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
private TransientWorldCleanupSupport() {
}
public static boolean isTransientStudioWorldName(String worldName) {
return transientStudioBaseWorldName(worldName) != null;
}
public static String transientStudioBaseWorldName(String worldName) {
if (worldName == null || worldName.isBlank()) {
return null;
}
String candidate = worldName.trim();
if (candidate.endsWith("_nether")) {
candidate = candidate.substring(0, candidate.length() - "_nether".length());
} else if (candidate.endsWith("_the_end")) {
candidate = candidate.substring(0, candidate.length() - "_the_end".length());
}
if (!TRANSIENT_STUDIO_WORLD_PATTERN.matcher(candidate).matches()) {
return null;
}
return candidate;
}
public static List<String> worldFamilyNames(String worldName) {
ArrayList<String> names = new ArrayList<>();
String normalized = normalizeWorldName(worldName);
if (normalized == null) {
return names;
}
names.add(normalized);
names.add(normalized + "_nether");
names.add(normalized + "_the_end");
return names;
}
public static LinkedHashSet<String> collectTransientStudioWorldNames(File worldContainer) {
LinkedHashSet<String> names = new LinkedHashSet<>();
if (worldContainer == null) {
return names;
}
File[] children = worldContainer.listFiles();
if (children == null) {
return names;
}
for (File child : children) {
if (child == null || !child.isDirectory()) {
continue;
}
String baseName = transientStudioBaseWorldName(child.getName());
if (baseName == null) {
continue;
}
names.add(baseName);
}
return names;
}
private static String normalizeWorldName(String worldName) {
if (worldName == null) {
return null;
}
String normalized = worldName.trim();
if (normalized.isEmpty()) {
return null;
}
return normalized.toLowerCase(Locale.ROOT).equals(normalized) ? normalized : normalized;
}
}
@@ -1,21 +0,0 @@
package art.arcane.iris.core.runtime;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
interface WorldRuntimeControlBackend {
String backendName();
String describeCapabilities();
OptionalLong readDayTime(World world);
boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException;
void syncTime(World world);
CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate);
}
@@ -1,524 +0,0 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import art.arcane.iris.core.lifecycle.ServerFamily;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.GameRule;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
import org.bukkit.plugin.PluginManager;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public final class WorldRuntimeControlService {
private static volatile WorldRuntimeControlService instance;
private final CapabilitySnapshot capabilities;
private final WorldRuntimeControlBackend backend;
private final String capabilityDescription;
private WorldRuntimeControlService(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
this.backend = selectBackend(capabilities);
this.capabilityDescription = "family=" + capabilities.serverFamily().id()
+ ", backend=" + backend.backendName()
+ ", " + backend.describeCapabilities();
}
public static WorldRuntimeControlService get() {
WorldRuntimeControlService current = instance;
if (current != null) {
return current;
}
synchronized (WorldRuntimeControlService.class) {
if (instance != null) {
return instance;
}
CapabilitySnapshot capabilities = WorldLifecycleService.get().capabilities();
instance = new WorldRuntimeControlService(capabilities);
Iris.info("WorldRuntimeControl capabilities: %s", instance.capabilityDescription);
return instance;
}
}
public String backendName() {
return backend.backendName();
}
public String capabilityDescription() {
return capabilityDescription;
}
public OptionalLong readDayTime(World world) {
return backend.readDayTime(world);
}
public boolean applyStudioWorldRules(World world) {
if (world == null) {
return false;
}
Iris.linkMultiverseCore.removeFromConfig(world);
if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
return true;
}
setBooleanGameRule(world, false, "ADVANCE_WEATHER", "DO_WEATHER_CYCLE", "WEATHER_CYCLE", "doWeatherCycle", "weatherCycle");
setBooleanGameRule(world, false, "ADVANCE_TIME", "DO_DAYLIGHT_CYCLE", "DAYLIGHT_CYCLE", "doDaylightCycle", "daylightCycle");
applyNoonTimeLock(world);
return true;
}
public boolean applyObjectStudioWorldRules(World world) {
if (world == null) {
return false;
}
applyStudioWorldRules(world);
setBooleanGameRule(world, false, "DO_FIRE_TICK", "doFireTick");
setBooleanGameRule(world, false, "DO_MOB_SPAWNING", "doMobSpawning");
setBooleanGameRule(world, false, "DO_MOB_LOOT", "doMobLoot");
setBooleanGameRule(world, false, "DO_TRADER_SPAWNING", "doTraderSpawning");
setBooleanGameRule(world, false, "DO_PATROL_SPAWNING", "doPatrolSpawning");
setBooleanGameRule(world, false, "DO_INSOMNIA", "doInsomnia");
setBooleanGameRule(world, true, "DO_IMMEDIATE_RESPAWN", "doImmediateRespawn");
setBooleanGameRule(world, false, "FALL_DAMAGE", "fallDamage");
setBooleanGameRule(world, false, "FIRE_DAMAGE", "fireDamage");
setBooleanGameRule(world, false, "DROWNING_DAMAGE", "drowningDamage");
setBooleanGameRule(world, false, "FREEZE_DAMAGE", "freezeDamage");
setBooleanGameRule(world, false, "DO_WARDEN_SPAWNING", "doWardenSpawning");
setBooleanGameRule(world, false, "MOB_GRIEFING", "mobGriefing");
setBooleanGameRule(world, false, "DO_TILE_DROPS", "doTileDrops");
setBooleanGameRule(world, true, "KEEP_INVENTORY", "keepInventory");
setIntGameRule(world, 0, "RANDOM_TICK_SPEED", "randomTickSpeed");
setIntGameRule(world, 0, "SPAWN_RADIUS", "spawnRadius");
setIntGameRule(world, 0, "MAX_ENTITY_CRAMMING", "maxEntityCramming");
applyNoonTimeLock(world);
return true;
}
public boolean applyNoonTimeLock(World world) {
if (world == null) {
return false;
}
if (!hasMutableClock(world)) {
return false;
}
OptionalLong currentTime = readDayTime(world);
if (currentTime.isEmpty()) {
return false;
}
long skipAmount = (6000L - currentTime.getAsLong()) % 24000L;
if (skipAmount < 0L) {
skipAmount += 24000L;
}
TimeSkipEvent event = new TimeSkipEvent(world, TimeSkipEvent.SkipReason.CUSTOM, skipAmount);
PluginManager pluginManager = Bukkit.getPluginManager();
if (pluginManager != null) {
pluginManager.callEvent(event);
}
if (event.isCancelled()) {
return false;
}
try {
boolean written = backend.writeDayTime(world, currentTime.getAsLong() + event.getSkipAmount());
if (!written) {
return false;
}
backend.syncTime(world);
return true;
} catch (Throwable e) {
Iris.reportError("Runtime time control failed for world \"" + world.getName() + "\".", e);
return false;
}
}
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
return backend.requestChunkAsync(world, chunkX, chunkZ, generate);
}
public void prepareGenerator(World world) {
if (world == null) {
return;
}
try {
art.arcane.iris.engine.platform.PlatformChunkGenerator provider = art.arcane.iris.core.tools.IrisToolbelt.access(world);
if (provider == null) {
return;
}
art.arcane.iris.engine.framework.Engine engine = provider.getEngine();
if (engine == null) {
return;
}
engine.getMantle().getComponents();
engine.getMantle().getRealRadius();
} catch (Throwable e) {
Iris.reportError("Failed to prepare generator state for world \"" + world.getName() + "\".", e);
}
}
public Location resolveEntryAnchor(World world) {
if (world == null) {
return null;
}
PlatformChunkGenerator provider = IrisToolbelt.access(world);
return resolveEntryAnchor(world, provider);
}
static Location resolveEntryAnchor(World world, PlatformChunkGenerator provider) {
if (world == null) {
return null;
}
if (provider != null && provider.isStudio()) {
Location initialSpawn = provider.getInitialSpawnLocation(world);
if (initialSpawn != null) {
return initialSpawn.clone();
}
}
Location spawnLocation = world.getSpawnLocation();
if (spawnLocation != null) {
return spawnLocation.clone();
}
int minY = world.getMinHeight() + 1;
int y = Math.max(minY, 96);
return new Location(world, 0.5D, y, 0.5D);
}
public CompletableFuture<Location> resolveSafeEntry(World world, Location source) {
if (world == null || source == null) {
return CompletableFuture.completedFuture(null);
}
int chunkX = source.getBlockX() >> 4;
int chunkZ = source.getBlockZ() >> 4;
CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
future.complete(findTopSafeLocation(world, source));
} catch (Throwable t) {
future.completeExceptionally(t);
}
});
if (!scheduled) {
future.completeExceptionally(new IllegalStateException(
"Failed to schedule safe-entry resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + "."));
}
return future;
}
public CompletableFuture<Boolean> teleport(Player player, Location location) {
if (player == null || location == null) {
return CompletableFuture.completedFuture(false);
}
CompletableFuture<Boolean> future = new CompletableFuture<>();
boolean scheduled = J.runEntity(player, () -> {
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
if (teleportFuture == null) {
future.complete(false);
return;
}
teleportFuture.whenComplete((success, throwable) -> {
if (throwable != null) {
future.completeExceptionally(throwable);
return;
}
if (Boolean.TRUE.equals(success)) {
J.runEntity(player, () -> Iris.service(BoardSVC.class).updatePlayer(player));
future.complete(true);
return;
}
future.complete(false);
});
});
if (!scheduled) {
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
}
return future;
}
public boolean hasMutableClock(World world) {
try {
Object handle = invokeNoArg(world, "getHandle");
if (handle == null) {
return false;
}
Object dimensionTypeHolder = invokeNoArg(handle, "dimensionTypeRegistration");
Object dimensionType = unwrapDimensionType(dimensionTypeHolder);
if (dimensionType == null) {
return false;
}
return !dimensionTypeHasFixedTime(dimensionType);
} catch (Throwable e) {
return false;
}
}
private static WorldRuntimeControlBackend selectBackend(CapabilitySnapshot capabilities) {
ServerFamily family = capabilities.serverFamily();
if (family.isPaperLike()) {
return new PaperLikeRuntimeControlBackend(capabilities);
}
return new BukkitPublicRuntimeControlBackend(capabilities);
}
static Location findTopSafeLocation(World world, Location source) {
int x = source.getBlockX();
int z = source.getBlockZ();
float yaw = source.getYaw();
float pitch = source.getPitch();
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
if (world.isChunkLoaded(x >> 4, z >> 4)) {
int raw = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING_NO_LEAVES);
int y = Math.max(minY, Math.min(maxY, raw + 1));
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
int y = Math.max(minY, Math.min(maxY, source.getBlockY()));
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
@SuppressWarnings("unchecked")
private static void setBooleanGameRule(World world, boolean value, String... names) {
GameRule<Boolean> gameRule = resolveBooleanGameRule(world, names);
if (gameRule != null) {
world.setGameRule(gameRule, value);
}
}
@SuppressWarnings("unchecked")
private static void setIntGameRule(World world, int value, String... names) {
GameRule<Integer> gameRule = resolveIntGameRule(world, names);
if (gameRule != null) {
world.setGameRule(gameRule, value);
}
}
@SuppressWarnings("unchecked")
private static GameRule<Integer> resolveIntGameRule(World world, String... names) {
if (world == null || names == null || names.length == 0) {
return null;
}
Set<String> candidates = buildRuleNameCandidates(names);
for (String name : candidates) {
if (name == null || name.isBlank()) {
continue;
}
try {
Field field = GameRule.class.getField(name);
Object value = field.get(null);
if (value instanceof GameRule<?> gameRule && Integer.class.equals(gameRule.getType())) {
return (GameRule<Integer>) gameRule;
}
} catch (Throwable ignored) {
}
try {
GameRule<?> byName = GameRule.getByName(name);
if (byName != null && Integer.class.equals(byName.getType())) {
return (GameRule<Integer>) byName;
}
} catch (Throwable ignored) {
}
}
String[] availableRules = world.getGameRules();
if (availableRules == null || availableRules.length == 0) {
return null;
}
Set<String> normalizedCandidates = new LinkedHashSet<>();
for (String candidate : candidates) {
if (candidate != null && !candidate.isBlank()) {
normalizedCandidates.add(normalizeRuleName(candidate));
}
}
for (String availableRule : availableRules) {
String normalizedAvailable = normalizeRuleName(availableRule);
if (!normalizedCandidates.contains(normalizedAvailable)) {
continue;
}
try {
GameRule<?> byName = GameRule.getByName(availableRule);
if (byName != null && Integer.class.equals(byName.getType())) {
return (GameRule<Integer>) byName;
}
} catch (Throwable ignored) {
}
}
return null;
}
@SuppressWarnings("unchecked")
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
if (world == null || names == null || names.length == 0) {
return null;
}
Set<String> candidates = buildRuleNameCandidates(names);
for (String name : candidates) {
if (name == null || name.isBlank()) {
continue;
}
try {
Field field = GameRule.class.getField(name);
Object value = field.get(null);
if (value instanceof GameRule<?> gameRule && Boolean.class.equals(gameRule.getType())) {
return (GameRule<Boolean>) gameRule;
}
} catch (Throwable ignored) {
}
try {
GameRule<?> byName = GameRule.getByName(name);
if (byName != null && Boolean.class.equals(byName.getType())) {
return (GameRule<Boolean>) byName;
}
} catch (Throwable ignored) {
}
}
String[] availableRules = world.getGameRules();
if (availableRules == null || availableRules.length == 0) {
return null;
}
Set<String> normalizedCandidates = new LinkedHashSet<>();
for (String candidate : candidates) {
if (candidate != null && !candidate.isBlank()) {
normalizedCandidates.add(normalizeRuleName(candidate));
}
}
for (String availableRule : availableRules) {
String normalizedAvailable = normalizeRuleName(availableRule);
if (!normalizedCandidates.contains(normalizedAvailable)) {
continue;
}
try {
GameRule<?> byName = GameRule.getByName(availableRule);
if (byName != null && Boolean.class.equals(byName.getType())) {
return (GameRule<Boolean>) byName;
}
} catch (Throwable ignored) {
}
}
return null;
}
private static Set<String> buildRuleNameCandidates(String... names) {
Set<String> candidates = new LinkedHashSet<>();
for (String name : names) {
if (name == null || name.isBlank()) {
continue;
}
candidates.add(name);
candidates.add(name.toUpperCase());
candidates.add(name.toLowerCase());
}
return candidates;
}
private static String normalizeRuleName(String name) {
if (name == null) {
return "";
}
StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
char current = name.charAt(i);
if (Character.isLetterOrDigit(current)) {
builder.append(Character.toLowerCase(current));
}
}
return builder.toString();
}
private static boolean dimensionTypeHasFixedTime(Object dimensionType) throws ReflectiveOperationException {
Object fixedTimeFlag;
try {
fixedTimeFlag = invokeNoArg(dimensionType, "hasFixedTime");
} catch (NoSuchMethodException ignored) {
Object fixedTime = invokeNoArg(dimensionType, "fixedTime");
if (fixedTime instanceof OptionalLong optionalLong) {
return optionalLong.isPresent();
}
if (fixedTime instanceof Optional<?> optional) {
return optional.isPresent();
}
return false;
}
return fixedTimeFlag instanceof Boolean && (Boolean) fixedTimeFlag;
}
private static Object unwrapDimensionType(Object dimensionTypeHolder) throws ReflectiveOperationException {
if (dimensionTypeHolder == null) {
return null;
}
Class<?> holderClass = dimensionTypeHolder.getClass();
if (holderClass.getName().startsWith("net.minecraft.world.level.dimension.")) {
return dimensionTypeHolder;
}
Method valueMethod = holderClass.getMethod("value");
return valueMethod.invoke(dimensionTypeHolder);
}
private static Object invokeNoArg(Object instance, String methodName) throws ReflectiveOperationException {
Method method = instance.getClass().getMethod(methodName);
return method.invoke(instance);
}
}
@@ -1,129 +0,0 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.Iris;
import art.arcane.iris.core.safeguard.task.Diagnostic;
import art.arcane.iris.core.safeguard.task.Task;
import art.arcane.iris.core.safeguard.task.Tasks;
import art.arcane.iris.core.safeguard.task.ValueWithDiagnostics;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.scheduling.J;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class IrisSafeguard {
private static volatile boolean forceShutdown = false;
private static Map<Task, ValueWithDiagnostics<Mode>> results = Collections.emptyMap();
private static Map<String, String> context = Collections.emptyMap();
private static Map<String, List<String>> attachment = Collections.emptyMap();
private static Mode mode = Mode.STABLE;
private static int count = 0;
private IrisSafeguard() {
}
public static void execute() {
List<Task> tasks = Tasks.getTasks();
LinkedHashMap<Task, ValueWithDiagnostics<Mode>> resultValues = new LinkedHashMap<>(tasks.size());
LinkedHashMap<String, String> contextValues = new LinkedHashMap<>(tasks.size());
LinkedHashMap<String, List<String>> attachmentValues = new LinkedHashMap<>(tasks.size());
Mode currentMode = Mode.STABLE;
int issueCount = 0;
for (Task task : tasks) {
ValueWithDiagnostics<Mode> result;
try {
result = task.run();
} catch (Throwable e) {
Iris.reportError(e);
result = new ValueWithDiagnostics<>(
Mode.WARNING,
new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e)
);
}
currentMode = currentMode.highest(result.getValue());
resultValues.put(task, result);
contextValues.put(task.getId(), result.getValue().getId());
List<String> lines = new ArrayList<>();
for (Diagnostic diagnostic : result.getDiagnostics()) {
String[] split = diagnostic.toString().split("\\n");
Collections.addAll(lines, split);
}
attachmentValues.put(task.getId(), lines);
if (result.getValue() != Mode.STABLE) {
issueCount++;
}
}
results = Collections.unmodifiableMap(resultValues);
context = Collections.unmodifiableMap(contextValues);
attachment = Collections.unmodifiableMap(attachmentValues);
mode = currentMode;
count = issueCount;
}
public static Mode mode() {
return mode;
}
public static Map<String, String> asContext() {
return context;
}
public static Map<String, List<String>> asAttachment() {
return attachment;
}
public static void splash() {
Iris.instance.splash();
printReports();
printFooter();
}
public static void printReports() {
switch (mode) {
case STABLE -> Iris.info(C.BLUE + "0 Conflicts found");
case WARNING -> Iris.warn(C.GOLD + "%s Issues found", count);
case UNSTABLE -> Iris.error(C.DARK_RED + "%s Issues found", count);
}
for (ValueWithDiagnostics<Mode> value : results.values()) {
value.log(true, true);
}
}
public static void printFooter() {
switch (mode) {
case STABLE -> Iris.info(C.BLUE + "Iris is running Stable");
case WARNING -> warning();
case UNSTABLE -> unstable();
}
}
public static boolean isForceShutdown() {
return forceShutdown;
}
private static void warning() {
Iris.warn(C.GOLD + "Iris is running in Warning Mode");
Iris.warn(C.GRAY + "Some startup checks need attention. Review the messages above for tuning suggestions.");
Iris.warn(C.GRAY + "Iris will continue startup normally.");
Iris.warn("");
}
private static void unstable() {
Iris.error(C.DARK_RED + "Iris is running in Danger Mode");
Iris.error("");
Iris.error(C.DARK_GRAY + "--==<" + C.RED + " IMPORTANT " + C.DARK_GRAY + ">==--");
Iris.error("Critical startup checks failed. Iris will continue startup in 10 seconds.");
Iris.error("Review and resolve the errors above as soon as possible.");
J.sleep(10000L);
Iris.info("");
}
}
@@ -1,141 +0,0 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Bukkit;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public enum Mode {
STABLE(C.IRIS),
WARNING(C.GOLD),
UNSTABLE(C.RED);
private final C color;
private final String id;
Mode(C color) {
this.color = color;
this.id = name().toLowerCase(Locale.ROOT);
}
public String getId() {
return id;
}
public Mode highest(Mode mode) {
if (mode.ordinal() > ordinal()) {
return mode;
}
return this;
}
public String tag(String subTag) {
if (subTag == null || subTag.isBlank()) {
return wrap("Iris") + C.GRAY + ": ";
}
return wrap("Iris") + " " + wrap(subTag) + C.GRAY + ": ";
}
public void trySplash() {
if (!IrisSettings.get().getGeneral().isSplashLogoStartup()) {
return;
}
splash();
}
public void splash() {
String padd = Form.repeat(" ", 8);
String padd2 = Form.repeat(" ", 4);
String version = Iris.instance.getDescription().getVersion();
String releaseTrain = getReleaseTrain(version);
String serverVersion = getServerVersion();
String startupDate = getStartupDate();
int javaVersion = getJavaVersion();
String[] splash = new String[]{
padd + C.GRAY + " @@@@@@@@@@@@@@" + C.DARK_GRAY + "@@@",
padd + C.GRAY + " @@&&&&&&&&&" + C.DARK_GRAY + "&&&&&&" + color + " .(((()))). ",
padd + C.GRAY + "@@@&&&&&&&&" + C.DARK_GRAY + "&&&&&" + color + " .((((((())))))). ",
padd + C.GRAY + "@@@&&&&&" + C.DARK_GRAY + "&&&&&&&" + color + " ((((((((())))))))) " + C.GRAY + " @",
padd + C.GRAY + "@@@&&&&" + C.DARK_GRAY + "@@@@@&" + color + " ((((((((-))))))))) " + C.GRAY + " @@",
padd + C.GRAY + "@@@&&" + color + " ((((((({ })))))))) " + C.GRAY + " &&@@@",
padd + C.GRAY + "@@" + color + " ((((((((-))))))))) " + C.DARK_GRAY + "&@@@@@" + C.GRAY + "&&&&@@@",
padd + C.GRAY + "@" + color + " ((((((((())))))))) " + C.DARK_GRAY + "&&&&&" + C.GRAY + "&&&&&&&@@@",
padd + C.GRAY + "" + color + " '((((((()))))))' " + C.DARK_GRAY + "&&&&&" + C.GRAY + "&&&&&&&&@@@",
padd + C.GRAY + "" + color + " '(((())))' " + C.DARK_GRAY + "&&&&&&&&" + C.GRAY + "&&&&&&&@@",
padd + C.GRAY + " " + C.DARK_GRAY + "@@@" + C.GRAY + "@@@@@@@@@@@@@@"
};
String[] info = new String[]{
"",
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1.1.6]",
padd2 + C.GRAY + " Version: " + color + version,
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
padd2 + C.GRAY + " Server: " + color + serverVersion,
padd2 + C.GRAY + " Java: " + color + javaVersion + C.GRAY + " | Date: " + color + startupDate,
padd2 + C.GRAY + " Commit: " + color + BuildConstants.COMMIT + C.GRAY + "/" + color + BuildConstants.ENVIRONMENT,
"",
"",
"",
""
};
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(splash[i]);
builder.append(info[i]);
builder.append("\n");
}
Iris.info(builder.toString());
}
private String wrap(String tag) {
return C.BOLD.toString() + C.DARK_GRAY + "[" + C.BOLD + color + tag + C.BOLD + C.DARK_GRAY + "]" + C.RESET;
}
private String getServerVersion() {
String version = Bukkit.getVersion();
int marker = version.indexOf(" (MC:");
if (marker != -1) {
return version.substring(0, marker);
}
return version;
}
private int getJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
private String getStartupDate() {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
private String getReleaseTrain(String version) {
String value = version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
}
@@ -1,112 +0,0 @@
package art.arcane.iris.core.safeguard.task;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.format.C;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.function.Consumer;
public class Diagnostic {
private final Logger logger;
private final String message;
private final Throwable exception;
public Diagnostic(String message) {
this(Logger.ERROR, message, null);
}
public Diagnostic(Logger logger, String message) {
this(logger, message, null);
}
public Diagnostic(Logger logger, String message, Throwable exception) {
this.logger = logger;
this.message = message;
this.exception = exception;
}
public Logger getLogger() {
return logger;
}
public String getMessage() {
return message;
}
public Throwable getException() {
return exception;
}
public void log() {
log(true, false);
}
public void log(boolean withException) {
log(withException, false);
}
public void log(boolean withException, boolean withStackTrace) {
logger.print(render(withException, withStackTrace));
}
public String render() {
return render(true, false);
}
public String render(boolean withException) {
return render(withException, false);
}
public String render(boolean withException, boolean withStackTrace) {
StringBuilder builder = new StringBuilder();
builder.append(message);
if (withException && exception != null) {
builder.append(": ");
builder.append(exception);
if (withStackTrace) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(os);
exception.printStackTrace(ps);
ps.flush();
builder.append("\n");
builder.append(os);
}
}
return builder.toString();
}
@Override
public String toString() {
return C.strip(render());
}
public enum Logger {
DEBUG(Iris::debug),
RAW(Iris::msg),
INFO(Iris::info),
WARN(Iris::warn),
ERROR(Iris::error);
private final Consumer<String> logger;
Logger(Consumer<String> logger) {
this.logger = logger;
}
public void print(String message) {
String[] lines = message.split("\\n");
for (String line : lines) {
logger.accept(line);
}
}
public Diagnostic create(String message) {
return create(message, null);
}
public Diagnostic create(String message, Throwable exception) {
return new Diagnostic(this, message, exception);
}
}
}
@@ -1,49 +0,0 @@
package art.arcane.iris.core.safeguard.task;
import art.arcane.iris.core.safeguard.Mode;
import art.arcane.volmlib.util.format.Form;
import java.util.Locale;
import java.util.function.Supplier;
public abstract class Task {
private final String id;
private final String name;
public Task(String id) {
this(id, Form.capitalizeWords(id.replace(" ", "_").toLowerCase(Locale.ROOT)));
}
public Task(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public abstract ValueWithDiagnostics<Mode> run();
public static Task of(String id, String name, Supplier<ValueWithDiagnostics<Mode>> action) {
return new Task(id, name) {
@Override
public ValueWithDiagnostics<Mode> run() {
return action.get();
}
};
}
public static Task of(String id, Supplier<ValueWithDiagnostics<Mode>> action) {
return new Task(id) {
@Override
public ValueWithDiagnostics<Mode> run() {
return action.get();
}
};
}
}
@@ -1,228 +0,0 @@
package art.arcane.iris.core.safeguard.task;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
import art.arcane.iris.core.safeguard.Mode;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.common.misc.getHardware;
import art.arcane.iris.util.project.agent.Agent;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
public final class Tasks {
private static final Task MEMORY = Task.of("memory", () -> {
long mem = getHardware.getProcessMemory();
if (mem >= 3072L) {
return withDiagnostics(Mode.STABLE);
}
if (mem > 2048L) {
return withDiagnostics(Mode.STABLE,
Diagnostic.Logger.INFO.create("Memory Recommendation"),
Diagnostic.Logger.INFO.create("- 3GB+ process memory is recommended for Iris."),
Diagnostic.Logger.INFO.create("- Process Memory: " + mem + " MB"));
}
return withDiagnostics(Mode.WARNING,
Diagnostic.Logger.WARN.create("Low Memory"),
Diagnostic.Logger.WARN.create("- Iris is running with 2GB or less process memory."),
Diagnostic.Logger.WARN.create("- 3GB+ process memory is recommended for Iris."),
Diagnostic.Logger.WARN.create("- Process Memory: " + mem + " MB"));
});
private static final Task INCOMPATIBILITIES = Task.of("incompatibilities", () -> {
Set<String> plugins = new HashSet<>(Set.of("dynmap", "Stratos"));
plugins.removeIf(name -> server().getPluginManager().getPlugin(name) == null);
if (plugins.isEmpty()) {
return withDiagnostics(Mode.STABLE);
}
List<Diagnostic> diagnostics = new ArrayList<>();
if (plugins.contains("dynmap")) {
addAllDiagnostics(diagnostics,
Diagnostic.Logger.ERROR.create("Dynmap"),
Diagnostic.Logger.ERROR.create("- The plugin Dynmap is not compatible with the server."),
Diagnostic.Logger.ERROR.create("- If you want to have a map plugin like Dynmap, consider Bluemap."));
}
if (plugins.contains("Stratos")) {
addAllDiagnostics(diagnostics,
Diagnostic.Logger.ERROR.create("Stratos"),
Diagnostic.Logger.ERROR.create("- Iris is not compatible with other worldgen plugins."));
}
return withDiagnostics(Mode.WARNING, diagnostics);
});
private static final Task SOFTWARE = Task.of("software", () -> {
Set<String> supported = Set.of("canvas", "folia", "purpur", "pufferfish", "paper", "spigot", "bukkit");
String serverName = server().getName().toLowerCase(Locale.ROOT);
boolean supportedServer = isCanvasServer();
if (!supportedServer) {
for (String candidate : supported) {
if (serverName.contains(candidate)) {
supportedServer = true;
break;
}
}
}
if (supportedServer) {
return withDiagnostics(Mode.STABLE);
}
return withDiagnostics(Mode.WARNING,
Diagnostic.Logger.WARN.create("Unsupported Server Software"),
Diagnostic.Logger.WARN.create("- Please consider using Canvas, Folia, Paper, or Purpur instead."));
});
private static final Task VERSION = Task.of("version", () -> {
String[] parts = Iris.instance.getDescription().getVersion().split("-");
String supportedVersions;
if (parts.length >= 3) {
String minVersion = parts[1];
String maxVersion = parts[2];
supportedVersions = minVersion.equals(maxVersion) ? minVersion : minVersion + " - " + maxVersion;
} else if (parts.length >= 2) {
supportedVersions = parts[1];
} else {
supportedVersions = "1.21.11";
}
if (!(INMS.get() instanceof NMSBinding1X)) {
return withDiagnostics(Mode.STABLE);
}
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Server Version"),
Diagnostic.Logger.ERROR.create("- Iris only supports " + supportedVersions));
});
private static final Task INJECTION = Task.of("injection", () -> {
if (!isPaperPreferredServer() && !Agent.isInstalled()) {
return withDiagnostics(Mode.WARNING,
Diagnostic.Logger.WARN.create("Java Agent"),
Diagnostic.Logger.WARN.create("- Skipping dynamic Java agent attach on Spigot/Bukkit to avoid runtime agent warnings."),
Diagnostic.Logger.WARN.create("- For full runtime injection support, run with -javaagent:"
+ Agent.AGENT_JAR.getPath() + " or use Canvas/Folia/Paper/Purpur."));
}
if (!Agent.install()) {
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Java Agent"),
Diagnostic.Logger.ERROR.create("- Please enable dynamic agent loading by adding -XX:+EnableDynamicAgentLoading to your jvm arguments."),
Diagnostic.Logger.ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.getPath()));
}
if (!INMS.get().injectBukkit()) {
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Code Injection"),
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
}
return withDiagnostics(Mode.STABLE);
});
private static final Task DIMENSION_TYPES = Task.of("dimensionTypes", () -> {
Set<String> keys = IrisWorlds.get().getDimensions().map(IrisDimension::getDimensionTypeKey).collect(Collectors.toSet());
if (!INMS.get().missingDimensionTypes(keys.toArray(String[]::new))) {
return withDiagnostics(Mode.STABLE);
}
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Dimension Types"),
Diagnostic.Logger.ERROR.create("- Required Iris dimension types were not loaded."),
Diagnostic.Logger.ERROR.create("- If this still happens after a restart please contact support."));
});
private static final Task DISK_SPACE = Task.of("diskSpace", () -> {
double freeGiB = server().getWorldContainer().getFreeSpace() / (double) 0x4000_0000;
if (freeGiB > 3.0) {
return withDiagnostics(Mode.STABLE);
}
return withDiagnostics(Mode.WARNING,
Diagnostic.Logger.WARN.create("Insufficient Disk Space"),
Diagnostic.Logger.WARN.create("- 3GB of free space is required for Iris to function."));
});
private static final Task JAVA = Task.of("java", () -> {
int version = Iris.getJavaVersion();
if (version == 25) {
return withDiagnostics(Mode.STABLE);
}
if (version > 25) {
return withDiagnostics(Mode.STABLE,
Diagnostic.Logger.INFO.create("Java Runtime"),
Diagnostic.Logger.INFO.create("- Running Java " + version + ". Iris is tested primarily on Java 25."));
}
return withDiagnostics(Mode.WARNING,
Diagnostic.Logger.WARN.create("Unsupported Java version"),
Diagnostic.Logger.WARN.create("- Java 25+ is recommended. Current runtime: Java " + version));
});
private static final List<Task> TASKS = List.of(
MEMORY,
INCOMPATIBILITIES,
SOFTWARE,
VERSION,
INJECTION,
DIMENSION_TYPES,
DISK_SPACE,
JAVA
);
private Tasks() {
}
public static List<Task> getTasks() {
return TASKS;
}
private static Server server() {
return Bukkit.getServer();
}
private static boolean isPaperPreferredServer() {
String name = server().getName().toLowerCase(Locale.ROOT);
return isCanvasServer()
|| name.contains("folia")
|| name.contains("paper")
|| name.contains("purpur")
|| name.contains("pufferfish");
}
private static boolean isCanvasServer() {
ClassLoader loader = server().getClass().getClassLoader();
try {
Class.forName("io.canvasmc.canvas.region.WorldRegionizer", false, loader);
return true;
} catch (Throwable ignored) {
return server().getName().toLowerCase(Locale.ROOT).contains("canvas");
}
}
private static void addAllDiagnostics(List<Diagnostic> diagnostics, Diagnostic... values) {
for (Diagnostic value : values) {
diagnostics.add(value);
}
}
private static ValueWithDiagnostics<Mode> withDiagnostics(Mode mode, Diagnostic... diagnostics) {
return new ValueWithDiagnostics<>(mode, diagnostics);
}
private static ValueWithDiagnostics<Mode> withDiagnostics(Mode mode, List<Diagnostic> diagnostics) {
return new ValueWithDiagnostics<>(mode, diagnostics);
}
}
@@ -1,40 +0,0 @@
package art.arcane.iris.core.safeguard.task;
import java.util.List;
public class ValueWithDiagnostics<T> {
private final T value;
private final List<Diagnostic> diagnostics;
public ValueWithDiagnostics(T value, List<Diagnostic> diagnostics) {
this.value = value;
this.diagnostics = List.copyOf(diagnostics);
}
public ValueWithDiagnostics(T value, Diagnostic... diagnostics) {
this.value = value;
this.diagnostics = List.of(diagnostics);
}
public T getValue() {
return value;
}
public List<Diagnostic> getDiagnostics() {
return diagnostics;
}
public void log() {
log(true, false);
}
public void log(boolean withException) {
log(withException, false);
}
public void log(boolean withException, boolean withStackTrace) {
for (Diagnostic diagnostic : diagnostics) {
diagnostic.log(withException, withStackTrace);
}
}
}
@@ -1,257 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorSystem;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
import art.arcane.volmlib.util.director.runtime.DirectorSender;
import art.arcane.volmlib.util.director.visual.DirectorVisualCommand;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
private static final String ROOT_COMMAND = "iris";
private static final String ROOT_PERMISSION = "iris.all";
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
private final transient AtomicCache<DirectorVisualCommand> helpCache = new AtomicCache<>();
@Override
public void onEnable() {
PluginCommand command = Iris.instance.getCommand(ROOT_COMMAND);
if (command == null) {
Iris.warn("Failed to find command '" + ROOT_COMMAND + "'");
return;
}
command.setExecutor(this);
command.setTabCompleter(this);
J.a(this::getDirector);
}
@Override
public void onDisable() {
}
public DirectorRuntimeEngine getDirector() {
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
new CommandIris(),
null,
buildDirectorContexts(),
this::dispatchDirector,
this,
DirectorSystem.handlers
));
}
private DirectorContextRegistry buildDirectorContexts() {
DirectorContextRegistry contexts = new DirectorContextRegistry();
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
registerContextHandler(contexts, entry.getKey(), entry.getValue());
}
return contexts;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
contexts.register((Class) type, (invocation, map) -> {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
}
return null;
});
}
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
if (mode == DirectorExecutionMode.SYNC) {
J.s(runnable);
} else {
runnable.run();
}
}
@Override
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
DirectorContext.touch(new VolmitSender(sender.sender()));
}
}
@Override
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
DirectorContext.remove();
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return List.of();
}
List<String> v = runDirectorTab(sender, alias, args);
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
}
return v;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
return false;
}
if (!sender.hasPermission(ROOT_PERMISSION)) {
sender.sendMessage("You lack the Permission '" + ROOT_PERMISSION + "'");
return true;
}
J.aBukkit(() -> executeCommand(sender, label, args));
return true;
}
private void executeCommand(CommandSender sender, String label, String[] args) {
if (sendHelpIfRequested(sender, args)) {
playSuccessSound(sender);
return;
}
DirectorExecutionResult result = runDirector(sender, label, args);
if (result.isSuccess()) {
playSuccessSound(sender);
return;
}
playFailureSound(sender);
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
new VolmitSender(sender).sendMessage(C.RED + "Unknown Iris Command");
}
}
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
Optional<DirectorVisualCommand.HelpRequest> request = DirectorVisualCommand.resolveHelp(getHelpRoot(), Arrays.asList(args));
if (request.isEmpty()) {
return false;
}
VolmitSender volmitSender = new VolmitSender(sender);
volmitSender.sendDirectorHelp(request.get().command(), request.get().page());
return true;
}
private DirectorVisualCommand getHelpRoot() {
return helpCache.aquireNastyPrint(() -> DirectorVisualCommand.createRoot(getDirector()));
}
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
try {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
}
}
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
try {
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return List.of();
}
}
private void playFailureSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f);
player.playSound(player.getLocation(), Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f);
}
}
private void playSuccessSound(CommandSender sender) {
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
return;
}
if (sender instanceof Player player) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f);
player.playSound(player.getLocation(), Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f);
}
}
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
@Override
public String getName() {
return sender.getName();
}
@Override
public boolean isPlayer() {
return sender instanceof Player;
}
@Override
public void sendMessage(String message) {
if (message != null && !message.trim().isEmpty()) {
sender.sendMessage(message);
}
}
}
}
@@ -1,438 +0,0 @@
package art.arcane.iris.core.service;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.Looper;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
import art.arcane.iris.core.gui.PregeneratorJob;
import lombok.Synchronized;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class IrisEngineSVC implements IrisService {
private static final int TRIM_PERIOD = 2_000;
private static final long ACTIVE_PREGEN_IDLE_MILLIS = 500L;
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
private final AtomicInteger tectonicPlates = new AtomicInteger();
private final AtomicInteger queuedTectonicPlates = new AtomicInteger();
private final AtomicInteger trimmerAlive = new AtomicInteger();
private final AtomicInteger unloaderAlive = new AtomicInteger();
private final AtomicInteger totalWorlds = new AtomicInteger();
private final AtomicDouble maxIdleDuration = new AtomicDouble();
private final AtomicDouble minIdleDuration = new AtomicDouble();
private final AtomicLong loadedChunks = new AtomicLong();
private final KMap<World, Registered> worlds = new KMap<>();
private ScheduledExecutorService service;
private Looper updateTicker;
@Override
public void onEnable() {
var settings = IrisSettings.get().getPerformance();
var engine = settings.getEngineSVC();
service = Executors.newScheduledThreadPool(0,
(engine.isUseVirtualThreads()
? Thread.ofVirtual()
: Thread.ofPlatform().priority(engine.getPriority()))
.name("Iris EngineSVC-", 0)
.factory());
tectonicLimit.set(settings.getTectonicPlateSize());
Bukkit.getWorlds().forEach(this::add);
setup();
}
@Override
public void onDisable() {
for (World world : worlds.keySet()) {
PlatformChunkGenerator gen = IrisToolbelt.access(world);
if (gen == null) continue;
try {
gen.close();
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (service != null) {
service.shutdown();
}
if (updateTicker != null) {
updateTicker.interrupt();
}
worlds.keySet().forEach(this::remove);
worlds.clear();
}
public int getQueuedTectonicPlateCount() {
return queuedTectonicPlates.get();
}
public double getAverageIdleDuration() {
double min = minIdleDuration.get();
double max = maxIdleDuration.get();
if (!Double.isFinite(min) || !Double.isFinite(max) || min < 0D || max < 0D) {
return 0D;
}
if (max < min) {
return max;
}
return (min + max) / 2D;
}
public double getBiomeCacheUsageRatio() {
PreservationSVC preservation = Iris.service(PreservationSVC.class);
if (preservation == null) {
return 0D;
}
double total = 0D;
int count = 0;
for (var cache : preservation.getCaches()) {
if (!(cache instanceof CachedStream2D<?>) && !(cache instanceof CachedDoubleStream2D)) {
continue;
}
double usage = cache.getUsage();
if (!Double.isFinite(usage)) {
continue;
}
total += Math.max(0D, Math.min(1D, usage));
count++;
}
if (count <= 0) {
return 0D;
}
return total / count;
}
public void engineStatus(VolmitSender sender) {
long[] sizes = new long[4];
long[] count = new long[4];
for (var cache : Iris.service(PreservationSVC.class).getCaches()) {
var type = switch (cache) {
case ResourceLoader<?> ignored -> 0;
case CachedStream2D<?> ignored -> 1;
case CachedDoubleStream2D ignored -> 1;
case CachedStream3D<?> ignored -> 2;
default -> 3;
};
sizes[type] += cache.getSize();
count[type]++;
}
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
sender.sendMessage(C.DARK_PURPLE + "Status:");
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (service.isShutdown() ? "Shutdown" : "Running"));
sender.sendMessage(C.DARK_PURPLE + "- Updater: " + C.LIGHT_PURPLE + (updateTicker.isAlive() ? "Running" : "Stopped"));
sender.sendMessage(C.DARK_PURPLE + "- Period: " + C.LIGHT_PURPLE + Form.duration(TRIM_PERIOD));
sender.sendMessage(C.DARK_PURPLE + "- Trimmers: " + C.LIGHT_PURPLE + trimmerAlive.get());
sender.sendMessage(C.DARK_PURPLE + "- Unloaders: " + C.LIGHT_PURPLE + unloaderAlive.get());
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
sender.sendMessage(C.DARK_PURPLE + "- Limit: " + C.LIGHT_PURPLE + tectonicLimit.get());
sender.sendMessage(C.DARK_PURPLE + "- Total: " + C.LIGHT_PURPLE + tectonicPlates.get());
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + queuedTectonicPlates.get());
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(maxIdleDuration.get(), 2));
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(minIdleDuration.get(), 2));
sender.sendMessage(C.DARK_PURPLE + "Caches:");
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + sizes[0] + " (" + count[0] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + sizes[1] + " (" + count[1] + ")");
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + sizes[2] + " (" + count[2] + ")");
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + sizes[3] + " (" + count[3] + ")");
sender.sendMessage(C.DARK_PURPLE + "Other:");
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + totalWorlds.get());
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + loadedChunks.get());
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
}
@EventHandler
public void onWorldUnload(WorldUnloadEvent event) {
remove(event.getWorld());
}
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
add(event.getWorld());
}
private void remove(World world) {
var entry = worlds.remove(world);
if (entry == null) return;
entry.close();
}
private void add(World world) {
var access = IrisToolbelt.access(world);
if (access == null) return;
worlds.put(world, new Registered(world.getName(), access));
}
private synchronized void setup() {
if (updateTicker != null && updateTicker.isAlive())
return;
updateTicker = new Looper() {
@Override
protected long loop() {
try {
int queuedPlates = 0;
int totalPlates = 0;
long chunks = 0;
int unloaders = 0;
int trimmers = 0;
int iris = 0;
double maxDuration = Long.MIN_VALUE;
double minDuration = Long.MAX_VALUE;
for (var entry : worlds.entrySet()) {
var registered = entry.getValue();
if (registered.closed) continue;
iris++;
if (registered.unloaderAlive()) unloaders++;
if (registered.trimmerAlive()) trimmers++;
var engine = registered.getEngine();
if (engine == null) continue;
queuedPlates += engine.getMantle().getUnloadRegionCount();
totalPlates += engine.getMantle().getLoadedRegionCount();
chunks += entry.getKey().getLoadedChunks().length;
double duration = engine.getMantle().getAdjustedIdleDuration();
if (duration > maxDuration) maxDuration = duration;
if (duration < minDuration) minDuration = duration;
}
trimmerAlive.set(trimmers);
unloaderAlive.set(unloaders);
tectonicPlates.set(totalPlates);
queuedTectonicPlates.set(queuedPlates);
maxIdleDuration.set(maxDuration);
minIdleDuration.set(minDuration);
loadedChunks.set(chunks);
totalWorlds.set(iris);
worlds.values().forEach(Registered::update);
} catch (Throwable e) {
e.printStackTrace();
}
return 1000;
}
};
updateTicker.start();
}
private static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
return true;
}
current = current.getCause();
}
return false;
}
static boolean shouldSkipMantleReductionForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
return maintenanceActive && !pregeneratorTargetsWorld;
}
private final class Registered {
private final String name;
private final PlatformChunkGenerator access;
private final int offset = RNG.r.nextInt(TRIM_PERIOD);
private transient ScheduledFuture<?> trimmer;
private transient ScheduledFuture<?> unloader;
private transient boolean closed;
private Registered(String name, PlatformChunkGenerator access) {
this.name = name;
this.access = access;
update();
}
private boolean unloaderAlive() {
return unloader != null && !unloader.isDone() && !unloader.isCancelled();
}
private boolean trimmerAlive() {
return trimmer != null && !trimmer.isDone() && !trimmer.isCancelled();
}
@Synchronized
private void update() {
if (closed || service == null || service.isShutdown())
return;
if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) {
trimmer = service.scheduleAtFixedRate(() -> {
Engine engine = getEngine();
if (engine == null
|| engine.isClosed()
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = engine.getWorld().realWorld();
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
try {
engine.getMantle().trim(activeIdleDuration(engineWorld), activeTectonicLimit(engineWorld));
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
return;
}
Iris.reportError(e);
Iris.error("EngineSVC: Failed to trim for " + name);
e.printStackTrace();
}
}, offset, TRIM_PERIOD, TimeUnit.MILLISECONDS);
}
if (unloader == null || unloader.isDone() || unloader.isCancelled()) {
unloader = service.scheduleAtFixedRate(() -> {
Engine engine = getEngine();
if (engine == null
|| engine.isClosed()
|| engine.getMantle().getMantle().isClosed()
|| !shouldReduce(engine))
return;
World engineWorld = engine.getWorld().realWorld();
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
try {
long unloadStart = System.currentTimeMillis();
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engineWorld));
if (count > 0) {
Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
}
} catch (Throwable e) {
if (isMantleClosed(e)) {
close();
return;
}
Iris.reportError(e);
Iris.error("EngineSVC: Failed to unload for " + name);
e.printStackTrace();
}
}, offset + TRIM_PERIOD / 2, TRIM_PERIOD, TimeUnit.MILLISECONDS);
}
}
private int tectonicLimit() {
return tectonicLimit.get() / Math.max(worlds.size(), 1);
}
private int activeTectonicLimit(@Nullable World world) {
int limit = tectonicLimit();
if (world == null) {
return limit;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return limit;
}
return Math.max(1, Math.min(limit, Math.max(2, limit / 8)));
}
private long activeIdleDuration(@Nullable World world) {
if (world == null) {
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
}
return ACTIVE_PREGEN_IDLE_MILLIS;
}
@Synchronized
private void close() {
if (closed) return;
closed = true;
if (trimmer != null) {
trimmer.cancel(false);
trimmer = null;
}
if (unloader != null) {
unloader.cancel(false);
unloader = null;
}
}
@Nullable
private Engine getEngine() {
if (closed) return null;
return access.getEngine();
}
private boolean shouldReduce(Engine engine) {
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
return true;
}
World world = engine.getWorld().realWorld();
if (world == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
}
private boolean shouldSkipForMaintenance(@Nullable World world) {
if (world == null) {
return false;
}
boolean maintenanceActive = IrisToolbelt.isWorldMaintenanceActive(world);
if (!maintenanceActive) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
}
@@ -1,210 +0,0 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
import art.arcane.volmlib.integration.IntegrationHeartbeat;
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
import art.arcane.volmlib.integration.IntegrationMetricSample;
import art.arcane.volmlib.integration.IntegrationMetricSchema;
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
import art.arcane.volmlib.integration.IntegrationServiceContract;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
new IntegrationProtocolVersion(1, 0),
new IntegrationProtocolVersion(1, 1)
);
private static final Set<String> CAPABILITIES = Set.of(
"handshake",
"heartbeat",
"metrics",
"iris-engine-metrics"
);
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
@Override
public void onEnable() {
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
Iris.verbose("Integration provider registered for Iris");
}
@Override
public void onDisable() {
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
}
@Override
public String pluginId() {
return "iris";
}
@Override
public String pluginVersion() {
return Iris.instance.getDescription().getVersion();
}
@Override
public Set<IntegrationProtocolVersion> supportedProtocols() {
return SUPPORTED_PROTOCOLS;
}
@Override
public Set<String> capabilities() {
return CAPABILITIES;
}
@Override
public Set<IntegrationMetricDescriptor> metricDescriptors() {
return IntegrationMetricSchema.descriptors().stream()
.filter(descriptor -> descriptor.key().startsWith("iris."))
.collect(java.util.stream.Collectors.toSet());
}
@Override
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
long now = System.currentTimeMillis();
if (request == null) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"missing request",
now
);
}
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
SUPPORTED_PROTOCOLS,
request.supportedProtocols()
);
if (negotiated.isEmpty()) {
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
false,
null,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"no-common-protocol",
now
);
}
negotiatedProtocol = negotiated.get();
return new IntegrationHandshakeResponse(
pluginId(),
pluginVersion(),
true,
negotiatedProtocol,
SUPPORTED_PROTOCOLS,
CAPABILITIES,
"ok",
now
);
}
@Override
public IntegrationHeartbeat heartbeat() {
long now = System.currentTimeMillis();
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
}
@Override
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
? IntegrationMetricSchema.irisKeys()
: metricKeys;
long now = System.currentTimeMillis();
Map<String, IntegrationMetricSample> out = new HashMap<>();
for (String key : requested) {
switch (key) {
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
default -> out.put(key, IntegrationMetricSample.unavailable(
IntegrationMetricSchema.descriptor(key),
"unsupported-key",
now
));
}
}
return out;
}
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
if (chunksPerSecond > 0D) {
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
double idle = engineService.getAverageIdleDuration();
if (idle > 0D && Double.isFinite(idle)) {
return IntegrationMetricSample.available(descriptor, idle, now);
}
}
return IntegrationMetricSample.available(descriptor, 0D, now);
}
private IntegrationMetricSample samplePregenQueueMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
long totalQueue = 0L;
boolean hasAnySource = false;
long pregenRemaining = PregeneratorJob.chunksRemaining();
if (pregenRemaining >= 0L) {
totalQueue += pregenRemaining;
hasAnySource = true;
}
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService != null) {
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
hasAnySource = true;
}
if (!hasAnySource) {
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
}
return IntegrationMetricSample.available(descriptor, totalQueue, now);
}
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
if (engineService == null) {
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
}
double ratio = engineService.getBiomeCacheUsageRatio();
if (!Double.isFinite(ratio)) {
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
}
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
}
}
@@ -1,347 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.ObjectStudioLayout;
import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class ObjectStudioSaveService implements IrisService {
private static ObjectStudioSaveService INSTANCE;
private final Map<UUID, ActiveStudio> studios = new ConcurrentHashMap<>();
public static ObjectStudioSaveService get() {
ObjectStudioSaveService svc = INSTANCE;
if (svc != null) return svc;
svc = Iris.service(ObjectStudioSaveService.class);
return svc;
}
@Override
public void onEnable() {
INSTANCE = this;
}
@Override
public void onDisable() {
studios.clear();
INSTANCE = null;
}
public void register(Engine engine, ObjectStudioGenerator generator) {
World world = engine.getTarget().getWorld().realWorld();
if (world == null) return;
ObjectStudioLayout layout = generator.getLayout();
if (layout == null) return;
Map<String, IrisData> sources = generator.getPackData();
if (sources == null || sources.isEmpty()) {
Iris.warn("Object Studio save disabled: no pack data sources available for world %s", world.getName());
return;
}
Map<String, File> objectsDirs = new ConcurrentHashMap<>();
for (Map.Entry<String, IrisData> e : sources.entrySet()) {
File dir = resolveObjectsDir(e.getValue());
if (dir != null) {
objectsDirs.put(e.getKey(), dir);
}
}
if (objectsDirs.isEmpty()) {
Iris.warn("Object Studio save disabled: no resolvable objects folders for world %s", world.getName());
return;
}
ActiveStudio existing = studios.get(world.getUID());
if (existing != null && existing.layout == layout) {
return;
}
String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey();
studios.put(world.getUID(), new ActiveStudio(world.getUID(), layout, objectsDirs, packKey));
Iris.info("Object Studio live-save registered: world=%s cells=%d packs=%d",
world.getName(), layout.cells().size(), objectsDirs.size());
}
public void unregister(World world) {
if (world == null) return;
ActiveStudio removed = studios.remove(world.getUID());
if (removed != null) {
if (removed.packKey != null) {
ObjectStudioActivation.deactivate(removed.packKey);
}
Iris.info("Object Studio live-save unregistered: world=%s", world.getName());
}
}
@EventHandler
public void onWorldUnload(WorldUnloadEvent event) {
unregister(event.getWorld());
}
@EventHandler(ignoreCancelled = true)
public void onCreatureSpawn(CreatureSpawnEvent event) {
if (!studios.containsKey(event.getLocation().getWorld().getUID())) return;
CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason();
if (reason == CreatureSpawnEvent.SpawnReason.CUSTOM
|| reason == CreatureSpawnEvent.SpawnReason.COMMAND
|| reason == CreatureSpawnEvent.SpawnReason.SPAWNER_EGG) {
return;
}
event.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onEntitySpawn(EntitySpawnEvent event) {
if (event instanceof CreatureSpawnEvent) return;
if (!studios.containsKey(event.getLocation().getWorld().getUID())) return;
if (event.getEntity() instanceof org.bukkit.entity.Player) return;
event.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getHand() != EquipmentSlot.HAND) return;
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return;
Block clicked = event.getClickedBlock();
if (clicked == null) return;
World world = clicked.getWorld();
ActiveStudio studio = studios.get(world.getUID());
if (studio == null) return;
Player player = event.getPlayer();
GridCell cell = findCellNear(studio, clicked.getX(), clicked.getZ());
if (cell == null) {
player.sendMessage(C.GRAY + "Object Studio: no cell under click (x=" + clicked.getX() + " z=" + clicked.getZ() + ").");
return;
}
player.sendMessage(C.AQUA + "Object Studio: saving " + C.WHITE + cell.pack() + "/" + cell.key() + C.GRAY + " (" + cell.w() + "x" + cell.h() + "x" + cell.d() + ")");
Iris.info("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key());
J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> {
try {
captureAndSave(studio, world, cell, player);
} catch (Throwable e) {
Iris.reportError(e);
}
});
}
private static GridCell findCellNear(ActiveStudio studio, int x, int z) {
GridCell inside = studio.layout.findAt(x, z);
if (inside != null) return inside;
int reach = Math.max(1, studio.layout.padding() + 1);
GridCell best = null;
int bestDist = Integer.MAX_VALUE;
for (GridCell cell : studio.layout.cells()) {
int dx = 0;
if (x < cell.originX()) dx = cell.originX() - x;
else if (x >= cell.originX() + cell.w()) dx = x - (cell.originX() + cell.w() - 1);
int dz = 0;
if (z < cell.originZ()) dz = cell.originZ() - z;
else if (z >= cell.originZ() + cell.d()) dz = z - (cell.originZ() + cell.d() - 1);
int dist = Math.max(dx, dz);
if (dist <= reach && dist < bestDist) {
bestDist = dist;
best = cell;
}
}
return best;
}
public boolean teleportTo(Player player, String objectKey) {
if (player == null || objectKey == null) return false;
for (ActiveStudio studio : studios.values()) {
GridCell cell = studio.layout.get(objectKey);
if (cell == null) continue;
World world = Bukkit.getWorld(studio.worldId);
if (world == null) continue;
double targetX = cell.originX() + cell.w() / 2.0D + 0.5D;
double targetZ = cell.originZ() + cell.d() / 2.0D + 0.5D;
double targetY = cell.originY() + cell.h() + 2.0D;
Location location = new Location(world, targetX, targetY, targetZ);
J.runEntity(player, () -> PaperLib.teleportAsync(player, location));
Iris.info("Object Studio goto: %s -> %s at %.0f,%.0f,%.0f",
player.getName(), objectKey, location.getX(), location.getY(), location.getZ());
return true;
}
return false;
}
private static File resolveObjectsDir(IrisData data) {
File root = data.getDataFolder();
if (root == null) return null;
File objects = new File(root, "objects");
if (!objects.exists()) {
objects.mkdirs();
}
return objects;
}
private void captureAndSave(ActiveStudio studio, World world, GridCell cell, Player notify) {
if (!allChunksLoaded(world, cell)) {
return;
}
IrisObject snapshot = new IrisObject(cell.w(), cell.h(), cell.d());
int originX = cell.originX();
int originY = cell.originY();
int originZ = cell.originZ();
boolean anyBlock = false;
for (int dx = 0; dx < cell.w(); dx++) {
for (int dy = 0; dy < cell.h(); dy++) {
for (int dz = 0; dz < cell.d(); dz++) {
Block block = world.getBlockAt(originX + dx, originY + dy, originZ + dz);
if (block.getType() == Material.AIR) continue;
snapshot.setUnsigned(dx, dy, dz, block, false);
anyBlock = true;
}
}
}
String hashKey = cell.pack() + "/" + cell.key();
long hash = hashOf(snapshot);
Long prior = studio.hashes.get(hashKey);
if (prior != null && prior == hash) {
if (notify != null) {
notify.sendMessage(C.GRAY + "Object Studio: no changes for " + cell.pack() + "/" + cell.key() + ".");
}
return;
}
if (!anyBlock && prior == null) {
studio.hashes.put(hashKey, hash);
if (notify != null) {
notify.sendMessage(C.GRAY + "Object Studio: empty cell " + cell.pack() + "/" + cell.key() + " (nothing to write).");
}
return;
}
studio.hashes.put(hashKey, hash);
File targetFile = objectFileFor(studio, cell);
if (targetFile == null) {
if (notify != null) {
notify.sendMessage(C.RED + "Object Studio: no target file for " + cell.pack() + "/" + cell.key() + ".");
}
return;
}
J.a(() -> {
try {
File parent = targetFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
snapshot.write(targetFile);
Iris.info("Object Studio saved: %s/%s (%dx%dx%d)",
cell.pack(), cell.key(), cell.w(), cell.h(), cell.d());
if (notify != null) {
J.runEntity(notify, () -> notify.sendMessage(C.GREEN + "Object Studio: saved " + C.WHITE + cell.pack() + "/" + cell.key()));
}
} catch (Throwable e) {
Iris.reportError(e);
if (notify != null) {
J.runEntity(notify, () -> notify.sendMessage(C.RED + "Object Studio: save failed for " + cell.pack() + "/" + cell.key() + " (" + e.getMessage() + ")"));
}
}
});
}
private boolean allChunksLoaded(World world, GridCell cell) {
for (int cx = cell.chunkMinX(); cx <= cell.chunkMaxX(); cx++) {
for (int cz = cell.chunkMinZ(); cz <= cell.chunkMaxZ(); cz++) {
if (!world.isChunkLoaded(cx, cz)) {
return false;
}
}
}
return true;
}
private static long hashOf(IrisObject snapshot) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
snapshot.write(baos);
byte[] bytes = baos.toByteArray();
long h = 1125899906842597L;
for (byte b : bytes) {
h = 31 * h + b;
}
return h;
} catch (Throwable e) {
Iris.reportError(e);
return System.nanoTime();
}
}
private static File objectFileFor(ActiveStudio studio, GridCell cell) {
File objectsDir = studio.objectsDirs.get(cell.pack());
if (objectsDir == null) return null;
String relative = cell.key().replace('\\', '/');
return new File(objectsDir, relative + ".iob");
}
private static final class ActiveStudio {
final UUID worldId;
final ObjectStudioLayout layout;
final Map<String, File> objectsDirs;
final String packKey;
final Map<String, Long> hashes = new ConcurrentHashMap<>();
ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map<String, File> objectsDirs, String packKey) {
this.worldId = worldId;
this.layout = layout;
this.objectsDirs = objectsDirs;
this.packKey = packKey;
}
}
}
@@ -1,157 +0,0 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.object.*;
import art.arcane.volmlib.util.data.Varint;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.nbt.io.NBTUtil;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.tag.*;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.apache.commons.io.FileUtils;
import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData;
import java.io.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class IrisConverter {
public static void convertSchematics(VolmitSender sender) {
File folder = Iris.instance.getDataFolder("convert");
FilenameFilter filter = (dir, name) -> name.endsWith(".schem");
File[] fileList = folder.listFiles(filter);
if (fileList == null) {
sender.sendMessage("No schematic files to convert found in " + folder.getAbsolutePath());
return;
}
AtomicInteger counter = new AtomicInteger(0);
var stopwatch = PrecisionStopwatch.start();
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(() -> {
for (File schem : fileList) {
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
IrisObject object;
boolean largeObject = false;
NamedTag tag;
try {
tag = NBTUtil.read(schem);
} catch (IOException e) {
Iris.info(C.RED + "Failed to read: " + schem.getName());
throw new RuntimeException(e);
}
CompoundTag compound = (CompoundTag) tag.getTag();
int version = resolveVersion(compound);
if (!(version == 2 || version == 3))
throw new RuntimeException(C.RED + "Unsupported schematic version: " + version);
compound = version == 3 ? (CompoundTag) compound.get("Schematic") : compound;
int objW = ((ShortTag) compound.get("Width")).getValue();
int objH = ((ShortTag) compound.get("Height")).getValue();
int objD = ((ShortTag) compound.get("Length")).getValue();
int i = -1;
int mv = objW * objH * objD;
AtomicInteger v = new AtomicInteger(0);
if (mv > 2_000_000) {
largeObject = true;
Iris.info(C.GRAY + "Converting.. " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob"));
Iris.info(C.GRAY + "- It may take a while");
if (sender.isPlayer()) {
i = J.ar(() -> {
sender.sendProgress((double) v.get() / mv, "Converting");
}, 0);
}
}
compound = version == 3 ? (CompoundTag) compound.get("Blocks") : compound;
CompoundTag paletteTag = (CompoundTag) compound.get("Palette");
Map<Integer, BlockData> blockmap = new HashMap<>(paletteTag.size(), 0.9f);
for (Map.Entry<String, Tag<?>> entry : paletteTag.getValue().entrySet()) {
String blockName = entry.getKey();
BlockData bd = Bukkit.createBlockData(blockName);
Tag<?> blockTag = entry.getValue();
int blockId = ((IntTag) blockTag).getValue();
blockmap.put(blockId, bd);
}
boolean isBytes = version == 3 ? compound.getByteArrayTag("Data").length() < 128 : ((IntTag) compound.get("PaletteMax")).getValue() < 128;
ByteArrayTag byteArray = version == 3 ? (ByteArrayTag) compound.get("Data") : (ByteArrayTag) compound.get("BlockData");
byte[] originalBlockArray = byteArray.getValue();
var din = new DataInputStream(new ByteArrayInputStream(originalBlockArray));
object = new IrisObject(objW, objH, objD);
for (int h = 0; h < objH; h++) {
for (int d = 0; d < objD; d++) {
for (int w = 0; w < objW; w++) {
int blockIndex = isBytes ? din.read() & 0xFF : Varint.readUnsignedVarInt(din);
BlockData bd = blockmap.get(blockIndex);
if (!bd.getMaterial().isAir()) {
object.setUnsigned(w, h, d, bd);
}
v.getAndAdd(1);
}
}
}
if (i != -1) J.car(i);
try {
object.shrinkwrap();
object.write(new File(folder, schem.getName().replace(".schem", ".iob")));
counter.incrementAndGet();
if (sender.isPlayer()) {
if (largeObject) {
sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis()));
} else {
sender.sendMessage(C.IRIS + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob"));
}
}
if (largeObject) {
Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob") + " in " + Form.duration(p.getMillis()));
} else {
Iris.info(C.GRAY + "Converted " + schem.getName() + " -> " + schem.getName().replace(".schem", ".iob"));
}
FileUtils.delete(schem);
} catch (IOException e) {
sender.sendMessage(C.RED + "Failed to save: " + schem.getName());
throw new IOException(e);
}
} catch (Exception e) {
sender.sendMessage(C.RED + "Failed to convert: " + schem.getName());
e.printStackTrace();
}
}
stopwatch.end();
if (counter.get() != 0) {
sender.sendMessage(C.GRAY + "Converted: " + counter.get() + " in " + Form.duration(stopwatch.getMillis()));
}
if (counter.get() < fileList.length) {
sender.sendMessage(C.RED + "Some schematics failed to convert. Check the console for details.");
}
});
}
private static int resolveVersion(CompoundTag compound) throws Exception {
try {
IntTag root = compound.getIntTag("Version");
if (root != null) {
return root.getValue();
}
CompoundTag schematic = (CompoundTag) compound.get("Schematic");
return schematic.getIntTag("Version").getValue();
} catch (NullPointerException e) {
throw new Exception("Cannot resolve schematic version", e);
}
}
}
@@ -1,380 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.tools;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import io.papermc.lib.PaperLib;
import lombok.Data;
import lombok.experimental.Accessors;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
/**
* Makes it a lot easier to setup an engine, world, studio or whatever
*/
@Data
@Accessors(fluent = true, chain = true)
public class IrisCreator {
/**
* Specify an area to pregenerate during creation
*/
private PregenTask pregen;
/**
* Specify a sender to get updates & progress info + tp when world is created.
*/
private VolmitSender sender;
/**
* The seed to use for this generator
*/
private long seed = 1337;
/**
* The dimension to use. This can be any online dimension, or a dimension in the
* packs folder
*/
private String dimension = IrisSettings.get().getGenerator().getDefaultWorldType();
/**
* The name of this world.
*/
private String name = "irisworld";
/**
* Studio mode makes the engine hotloadable and uses the dimension in
* your Iris/packs folder instead of copying the dimension files into
* the world itself. Studio worlds are deleted when they are unloaded.
*/
private boolean studio = false;
/**
* Benchmark mode
*/
private boolean benchmark = false;
private BiConsumer<Double, String> studioProgressConsumer;
public static boolean removeFromBukkitYml(String name) throws IOException {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
ConfigurationSection section = yml.getConfigurationSection("worlds");
if (section == null) {
return false;
}
section.set(name, null);
if (section.getValues(false).keySet().stream().noneMatch(k -> section.get(k) != null)) {
yml.set("worlds", null);
}
yml.save(BUKKIT_YML);
return true;
}
public static boolean worldLoaded(){
return true;
}
/**
* Create the IrisAccess (contains the world)
*
* @return the IrisAccess
* @throws IrisException shit happens
*/
public World create() throws IrisException {
if (Bukkit.isPrimaryThread()) {
throw new IrisException("You cannot invoke create() on the main thread.");
}
reportStudioProgress(0.02D, "resolve_dimension");
reportStudioProgress(0.08D, "resolve_dimension");
IrisDimension d = IrisToolbelt.getDimension(dimension());
if (d == null) {
throw new IrisException("Dimension cannot be found null for id " + dimension());
}
if (sender == null)
sender = Iris.getSender();
reportStudioProgress(0.16D, "prepare_world_pack");
if (!studio() || benchmark) {
Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
}
if (studio()) {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
Iris.info("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
+ ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer()));
}
reportStudioProgress(0.28D, "install_datapacks");
AtomicDouble pp = new AtomicDouble(0);
AtomicBoolean done = new AtomicBoolean(false);
WorldCreator wc = new IrisWorldCreator()
.dimension(dimension)
.name(name)
.seed(seed)
.studio(studio)
.create();
if (!studio()) {
IrisWorlds.get().put(name(), dimension());
}
ServerConfigurator.installDataPacksIfChanged(!studio());
reportStudioProgress(0.40D, "install_datapacks");
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
if (access == null) throw new IrisException("Access is null. Something bad happened.");
AtomicInteger createProgressTask = startCreateProgressReporter(access, done);
World world;
reportStudioProgress(0.46D, "create_world");
try {
WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE;
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(wc, studio(), benchmark, callerKind);
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
.thenCompose(Function.identity())
.get();
} catch (Throwable e) {
done.set(true);
cancelRepeatingTask(createProgressTask);
if (J.isFolia() && containsCreateWorldUnsupportedOperation(e)) {
throw new IrisException("Runtime world creation is blocked and the selected world lifecycle backend could not create the world.", e);
}
throw new IrisException("Failed to create world with backend family " + WorldLifecycleService.get().capabilities().serverFamily().id() + "!", e);
}
done.set(true);
cancelRepeatingTask(createProgressTask);
reportStudioProgress(0.86D, "create_world");
if (!studio && !benchmark) {
addToBukkitYml();
J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension));
}
if (pregen != null) {
CompletableFuture<Boolean> ff = new CompletableFuture<>();
IrisToolbelt.pregenerate(pregen, access)
.onProgress(pp::set)
.whenDone(() -> ff.complete(true));
AtomicBoolean dx = new AtomicBoolean(false);
AtomicInteger pregenProgressTask = startPregenProgressReporter(pp, dx);
try {
ff.get();
dx.set(true);
cancelRepeatingTask(pregenProgressTask);
} catch (Throwable e) {
dx.set(true);
cancelRepeatingTask(pregenProgressTask);
e.printStackTrace();
}
}
return world;
}
private void reportStudioProgress(double progress, String stage) {
BiConsumer<Double, String> consumer = studioProgressConsumer;
if (consumer == null) {
return;
}
double clamped = Math.max(0D, Math.min(1D, progress));
try {
consumer.accept(clamped, stage);
} catch (Throwable e) {
Iris.reportError("Studio progress consumer failed for world \"" + name() + "\".", e);
}
}
private AtomicInteger startCreateProgressReporter(PlatformChunkGenerator access, AtomicBoolean done) {
AtomicInteger taskId = new AtomicInteger(-1);
if (benchmark) {
return taskId;
}
IntSupplier generatedSupplier = () -> {
if (access.getEngine() == null) {
return 0;
}
return access.getEngine().getGenerated();
};
access.getSpawnChunks().whenComplete((required, throwable) -> {
if (throwable != null) {
Iris.reportError("Failed to resolve studio spawn chunk target for world \"" + name() + "\".", throwable);
return;
}
if (done.get() || required == null || required <= 0) {
return;
}
int interval = studioProgressConsumer != null || sender.isPlayer() ? 1 : 20;
taskId.set(J.ar(() -> {
if (done.get()) {
cancelRepeatingTask(taskId);
return;
}
int generated = generatedSupplier.getAsInt();
if (generated >= required) {
cancelRepeatingTask(taskId);
return;
}
double progress = (double) generated / required;
if (studioProgressConsumer != null) {
reportStudioProgress(0.40D + (0.42D * progress), "create_world");
return;
}
int percent = (int) Math.round(progress * 100.0D);
int remaining = required - generated;
if (sender.isPlayer()) {
int barWidth = 44;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * barWidth);
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int bi = 0; bi < barWidth; bi++) {
bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
sender.sendAction(bar.toString() + C.GRAY + " " + C.YELLOW + percent + "%" + C.DARK_GRAY + " " + Form.f(generated) + "/" + Form.f(required) + " chunks");
return;
}
sender.sendMessage(C.GOLD + "Generating " + C.YELLOW + percent + "%" + C.GRAY + " " + Form.f(generated) + "/" + Form.f(required) + " chunks" + C.DARK_GRAY + " (" + remaining + " left)");
}, interval));
});
return taskId;
}
private AtomicInteger startPregenProgressReporter(AtomicDouble progress, AtomicBoolean done) {
AtomicInteger taskId = new AtomicInteger(-1);
int interval = sender.isPlayer() ? 1 : 20;
taskId.set(J.ar(() -> {
if (done.get()) {
cancelRepeatingTask(taskId);
return;
}
double p = progress.get();
int percent = (int) Math.round(p * 100.0D);
if (sender.isPlayer()) {
int barWidth = 44;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth);
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int bi = 0; bi < barWidth; bi++) {
bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
sender.sendAction(bar.toString() + C.GRAY + " " + C.YELLOW + percent + "%" + C.GRAY + " | " + C.WHITE + "Pregenerating");
return;
}
sender.sendMessage(C.GOLD + "Pregenerating " + C.YELLOW + percent + "%");
}, interval));
return taskId;
}
private void cancelRepeatingTask(AtomicInteger taskId) {
if (taskId == null) {
return;
}
int id = taskId.getAndSet(-1);
if (id >= 0) {
J.car(id);
}
}
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
Throwable cursor = throwable;
while (cursor != null) {
if (cursor instanceof UnsupportedOperationException) {
for (StackTraceElement element : cursor.getStackTrace()) {
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
&& "createWorld".equals(element.getMethodName())) {
return true;
}
}
}
cursor = cursor.getCause();
}
return false;
}
private void addToBukkitYml() {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
String gen = "Iris:" + dimension;
ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds");
if (!section.contains(name)) {
section.createSection(name).set("generator", gen);
try {
yml.save(BUKKIT_YML);
Iris.info("Registered \"" + name + "\" in bukkit.yml");
} catch (IOException e) {
Iris.error("Failed to update bukkit.yml!");
e.printStackTrace();
}
}
}
}
@@ -1,527 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.tools;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod;
import art.arcane.iris.core.pregenerator.methods.HybridPregenMethod;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.plugin.VolmitSender;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Something you really want to wear if working on Iris. Shit gets pretty hectic down there.
* Hope you packed snacks & road sodas.
*/
public class IrisToolbelt {
@ApiStatus.Internal
public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>();
private static final Map<String, AtomicInteger> worldMaintenanceDepth = new ConcurrentHashMap<>();
private static final Map<String, AtomicInteger> worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
private static final AtomicBoolean PREGEN_PROFILE_JVM_HINT_LOGGED = new AtomicBoolean(false);
/**
* Will find / download / search for the dimension or return null
* <p>
* - You can provide a dimenson in the packs folder by the folder name
* - You can provide a github repo by using (assumes branch is master unless specified)
* - GithubUsername/repository
* - GithubUsername/repository/branch
*
* @param dimension the dimension id such as overworld or flat
* @return the IrisDimension or null
*/
public static IrisDimension getDimension(String dimension) {
if (dimension == null) {
return null;
}
String requested = dimension.trim();
if (requested.isEmpty()) {
return null;
}
File packsFolder = Iris.instance.getDataFolder("packs");
File pack = new File(packsFolder, requested);
if (!pack.exists()) {
File found = findCaseInsensitivePack(packsFolder, requested);
if (found != null) {
pack = found;
}
}
if (!pack.exists()) {
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), Iris.instance.getTag()), requested, false);
File found = findCaseInsensitivePack(packsFolder, requested);
if (found != null) {
pack = found;
}
}
if (!pack.exists()) {
return null;
}
IrisData data = IrisData.get(pack);
IrisDimension resolved = data.getDimensionLoader().load(requested, false);
if (resolved != null) {
return resolved;
}
String packName = pack.getName();
if (!packName.equals(requested)) {
resolved = data.getDimensionLoader().load(packName, false);
if (resolved != null) {
return resolved;
}
}
for (String key : data.getDimensionLoader().getPossibleKeys()) {
if (!key.equalsIgnoreCase(requested) && !key.equalsIgnoreCase(packName)) {
continue;
}
resolved = data.getDimensionLoader().load(key, false);
if (resolved != null) {
return resolved;
}
}
return null;
}
private static File findCaseInsensitivePack(File packsFolder, String requested) {
File[] children = packsFolder.listFiles();
if (children == null) {
return null;
}
for (File child : children) {
if (child.isDirectory() && child.getName().equalsIgnoreCase(requested)) {
return child;
}
}
return null;
}
/**
* Create a world with plenty of options
*
* @return the creator builder
*/
public static IrisCreator createWorld() {
return new IrisCreator();
}
/**
* Checks if the given world is an Iris World (same as access(world) != null)
*
* @param world the world
* @return true if it is an Iris Access world
*/
public static boolean isIrisWorld(World world) {
if (world == null) {
return false;
}
if (world.getGenerator() instanceof PlatformChunkGenerator f) {
f.touch(world);
return true;
}
return false;
}
public static boolean isIrisStudioWorld(World world) {
return isIrisWorld(world) && access(world).isStudio();
}
/**
* Get the Iris generator for the given world
*
* @param world the given world
* @return the IrisAccess or null if it's not an Iris World
*/
public static PlatformChunkGenerator access(World world) {
if (world == null) {
return null;
}
if (isIrisWorld(world)) {
return ((PlatformChunkGenerator) world.getGenerator());
}
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
IrisProject activeProject = studioService.getActiveProject();
if (activeProject != null) {
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
if (activeProvider != null) {
World activeWorld = activeProvider.getTarget().getWorld().realWorld();
if (activeWorld != null && activeWorld.getName().equals(world.getName())) {
activeProvider.touch(world);
return activeProvider;
}
}
}
}
return null;
}
/**
* Start a pregenerator task
*
* @param task the scheduled task
* @param method the method to execute the task
* @return the pregenerator job (already started)
*/
public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) {
return pregenerate(task, method, engine, IrisSettings.get().getPregen().useCacheByDefault);
}
/**
* Start a pregenerator task
*
* @param task the scheduled task
* @param method the method to execute the task
* @return the pregenerator job (already started)
*/
public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) {
applyPregenPerformanceProfile(engine);
boolean useCachedWrapper = false;
if (cached && engine != null) {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
}
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine);
}
public static boolean applyPregenPerformanceProfile() {
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
if (!pregen.isEnablePregenPerformanceProfile()) {
return false;
}
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, Math.max(1, pregen.getPregenProfileNoiseCacheSize()));
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
}
if (pregen.isPregenProfileEnableFastCache() && !fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (pregen.isPregenProfileLogJvmHints()
&& pregen.isPregenProfileEnableFastCache()
&& PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true)
&& !fastCacheEnabledBefore) {
Iris.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
}
public static void applyPregenPerformanceProfile(Engine engine) {
boolean changed = applyPregenPerformanceProfile();
if (changed && engine != null) {
engine.hotloadComplex();
Iris.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
}
/**
* Start a pregenerator task. If the supplied generator is headless, headless mode is used,
* otherwise Hybrid mode is used.
*
* @param task the scheduled task
* @param gen the Iris Generator
* @return the pregenerator job (already started)
*/
public static PregeneratorJob pregenerate(PregenTask task, PlatformChunkGenerator gen) {
return pregenerate(task, new HybridPregenMethod(gen.getEngine().getWorld().realWorld(),
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), gen.getEngine());
}
/**
* Start a pregenerator task. If the supplied generator is headless, headless mode is used,
* otherwise Hybrid mode is used.
*
* @param task the scheduled task
* @param world the World
* @return the pregenerator job (already started)
*/
public static PregeneratorJob pregenerate(PregenTask task, World world) {
if (isIrisWorld(world)) {
return pregenerate(task, access(world));
}
return pregenerate(task, new HybridPregenMethod(world, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())), null);
}
/**
* Evacuate all players from the world into literally any other world.
* If there are no other worlds, kick them! Not the best but what's mine is mine sometimes...
*
* @param world the world to evac
*/
public static boolean evacuate(World world) {
if (world == null || isServerStopping()) {
return false;
}
for (World i : Bukkit.getWorlds()) {
if (!i.getName().equals(world.getName())) {
for (Player j : new ArrayList<>(world.getPlayers())) {
new VolmitSender(j, Iris.instance.getTag()).sendMessage("You have been evacuated from this world.");
Location target = i.getSpawnLocation();
Runnable teleportTask = () -> teleportAsyncSafely(j, target);
if (!J.runEntity(j, teleportTask)) {
teleportTask.run();
}
}
return true;
}
}
return false;
}
/**
* Evacuate all players from the world
*
* @param world the world to leave
* @param m the message
* @return true if it was evacuated.
*/
public static boolean evacuate(World world, String m) {
if (world == null || isServerStopping()) {
return false;
}
for (World i : Bukkit.getWorlds()) {
if (!i.getName().equals(world.getName())) {
for (Player j : new ArrayList<>(world.getPlayers())) {
new VolmitSender(j, Iris.instance.getTag()).sendMessage("You have been evacuated from this world. " + m);
Location target = i.getSpawnLocation();
Runnable teleportTask = () -> teleportAsyncSafely(j, target);
if (!J.runEntity(j, teleportTask)) {
teleportTask.run();
}
}
return true;
}
}
return false;
}
public static boolean isStudio(World i) {
if (!isIrisWorld(i)) {
return false;
}
PlatformChunkGenerator generator = access(i);
return generator != null && generator.isStudio();
}
private static void teleportAsyncSafely(Player player, Location target) {
if (player == null || target == null || isServerStopping()) {
return;
}
try {
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, target);
if (teleportFuture != null) {
teleportFuture.exceptionally(throwable -> {
if (!isServerStopping()) {
Iris.reportError(throwable);
}
return false;
});
}
} catch (Throwable throwable) {
if (!isServerStopping()) {
Iris.reportError(throwable);
}
}
}
public static boolean isServerStopping() {
Method method = BUKKIT_IS_STOPPING_METHOD;
if (method != null) {
try {
Object value = method.invoke(null);
if (value instanceof Boolean) {
return (Boolean) value;
}
} catch (Throwable ignored) {
}
}
Iris iris = Iris.instance;
return iris == null || !iris.isEnabled();
}
private static Method resolveBukkitIsStoppingMethod() {
try {
return Bukkit.class.getMethod("isStopping");
} catch (Throwable ignored) {
return null;
}
}
public static void beginWorldMaintenance(World world, String reason) {
beginWorldMaintenance(world, reason, false);
}
public static void beginWorldMaintenance(World world, String reason, boolean bypassMantleStages) {
if (world == null) {
return;
}
String name = world.getName();
int depth = worldMaintenanceDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
if (bypassMantleStages) {
worldMaintenanceMantleBypassDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
}
if (IrisSettings.get().getGeneral().isDebug()) {
Iris.info("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
} else {
Iris.verbose("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
}
}
public static void endWorldMaintenance(World world, String reason) {
if (world == null) {
return;
}
String name = world.getName();
AtomicInteger depthCounter = worldMaintenanceDepth.get(name);
if (depthCounter == null) {
return;
}
int depth = depthCounter.decrementAndGet();
if (depth <= 0) {
worldMaintenanceDepth.remove(name, depthCounter);
depth = 0;
}
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(name);
int bypassDepth = 0;
if (bypassCounter != null) {
bypassDepth = bypassCounter.decrementAndGet();
if (bypassDepth <= 0) {
worldMaintenanceMantleBypassDepth.remove(name, bypassCounter);
bypassDepth = 0;
}
}
if (IrisSettings.get().getGeneral().isDebug()) {
Iris.info("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
} else {
Iris.verbose("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
}
}
public static boolean isWorldMaintenanceActive(World world) {
if (world == null) {
return false;
}
AtomicInteger counter = worldMaintenanceDepth.get(world.getName());
return counter != null && counter.get() > 0;
}
public static boolean isWorldMaintenanceBypassingMantleStages(World world) {
if (world == null) {
return false;
}
AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(world.getName());
return counter != null && counter.get() > 0;
}
public static void retainMantleDataForSlice(String className) {
toolbeltConfiguration.put("retain.mantle." + className, Boolean.TRUE);
}
public static boolean isRetainingMantleDataForSlice(String className) {
return !toolbeltConfiguration.isEmpty() && toolbeltConfiguration.get("retain.mantle." + className) == Boolean.TRUE;
}
public static <T> T getMantleData(World world, int x, int y, int z, Class<T> of) {
PlatformChunkGenerator e = access(world);
if (e == null) {
return null;
}
return e.getEngine().getMantle().getMantle().get(x, y - world.getMinHeight(), z, of);
}
public static <T> void deleteMantleData(World world, int x, int y, int z, Class<T> of) {
PlatformChunkGenerator e = access(world);
if (e == null) {
return;
}
e.getEngine().getMantle().getMantle().remove(x, y - world.getMinHeight(), z, of);
}
public static boolean removeWorld(World world) throws IOException {
return IrisCreator.removeFromBukkitYml(world.getName());
}
}
@@ -1,684 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.core.tools;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.VectorMap;
import org.bukkit.Axis;
import org.bukkit.Tag;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Orientable;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.util.BlockVector;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class TreePlausibilizer {
public static final int MAX_DISTANCE = 6;
public static final int DEFAULT_SHELL_RADIUS = 2;
private static final int[][] NEIGHBORS = {
{1, 0, 0}, {-1, 0, 0},
{0, 1, 0}, {0, -1, 0},
{0, 0, 1}, {0, 0, -1}
};
private static final BlockData FALLBACK_LOG = B.get("minecraft:oak_log[axis=y]");
private static final BlockData FALLBACK_LEAF = B.get("minecraft:oak_leaves[distance=1,persistent=false,waterlogged=false]");
private TreePlausibilizer() {
}
public record Result(
int totalLeaves,
int persistentLeavesInput,
int reachableBefore,
int logsAdded,
int leavesAdded,
int leavesRemoved,
int leavesNormalized,
int unreachableAfter,
String skipReason
) {
public static Result skipped(String reason) {
return new Result(0, 0, 0, 0, 0, 0, 0, 0, reason);
}
}
public static Result analyze(IrisObject obj, PlausibilizeMode mode, int shellRadius) {
return run(obj, false, mode, shellRadius);
}
public static Result apply(IrisObject obj, PlausibilizeMode mode, int shellRadius) {
return run(obj, true, mode, shellRadius);
}
private static Result run(IrisObject obj, boolean mutate, PlausibilizeMode mode, int shellRadius) {
boolean normalize = mode == PlausibilizeMode.NORMALIZE;
boolean smoke = mode == PlausibilizeMode.SMOKE;
boolean foliageOverature = mode == PlausibilizeMode.FOLIAGE_OVERATURE;
VectorMap<BlockData> blocks = obj.getBlocks();
Map<Long, BlockData> positions = new HashMap<>(blocks.size() * 2);
Set<Long> logPositions = new HashSet<>();
Set<Long> originalLeafPositions = new HashSet<>();
Set<Long> persistentLeafPositions = new HashSet<>();
Map<BlockData, Integer> leafTypeCounts = new HashMap<>();
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
for (Map.Entry<BlockVector, BlockData> entry : blocks) {
BlockVector pos = entry.getKey();
BlockData data = entry.getValue();
long key = packKey(pos);
positions.put(key, data);
int x = pos.getBlockX();
int y = pos.getBlockY();
int z = pos.getBlockZ();
if (x < minX) minX = x;
if (y < minY) minY = y;
if (z < minZ) minZ = z;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
if (z > maxZ) maxZ = z;
if (isLog(data)) {
logPositions.add(key);
continue;
}
if (isLeaf(data)) {
boolean persistent = data instanceof Leaves leaves && leaves.isPersistent();
if (persistent) {
persistentLeafPositions.add(key);
}
originalLeafPositions.add(key);
leafTypeCounts.merge(data, 1, Integer::sum);
}
}
int persistentInput = persistentLeafPositions.size();
int totalLeavesInitial = originalLeafPositions.size();
if (logPositions.isEmpty() && !originalLeafPositions.isEmpty()) {
return Result.skipped("leaves present but no logs to bridge from");
}
if (logPositions.isEmpty() && !smoke) {
return new Result(0, 0, 0, 0, 0, 0, 0, 0, null);
}
Set<Long> leafPositions;
int leavesRemoved = 0;
List<Long> removedLeafKeys = new ArrayList<>();
if (smoke) {
leafPositions = new HashSet<>();
for (long key : originalLeafPositions) {
removedLeafKeys.add(key);
positions.remove(key);
}
leavesRemoved = removedLeafKeys.size();
int r = Math.max(0, Math.min(shellRadius, 5));
BlockData leafTemplate = pickDominantLeaf(leafTypeCounts);
paintShell(
logPositions, positions, leafPositions,
leafTemplate, r,
minX, minY, minZ, maxX, maxY, maxZ
);
} else if (normalize) {
leafPositions = new HashSet<>(originalLeafPositions);
} else {
leafPositions = new HashSet<>(originalLeafPositions);
leafPositions.removeAll(persistentLeafPositions);
}
int reachableBefore;
int logsAdded = 0;
Set<Long> unreached;
Map<Long, Integer> distances;
List<LogInsertion> inserts = new ArrayList<>();
Set<Long> orphanRemovals = new HashSet<>();
List<LeafAddition> leafAdds = new ArrayList<>();
if (!leafPositions.isEmpty() && !logPositions.isEmpty()) {
Set<Long> connectivityLeaves;
if (!normalize && !smoke) {
connectivityLeaves = new HashSet<>(leafPositions);
connectivityLeaves.addAll(persistentLeafPositions);
} else {
connectivityLeaves = leafPositions;
}
distances = seedDistances(logPositions, connectivityLeaves);
reachableBefore = countReachable(leafPositions, distances);
unreached = new HashSet<>(leafPositions);
unreached.removeAll(distances.keySet());
if (foliageOverature && !smoke && !unreached.isEmpty()) {
BlockData bridgeLeaf = pickDominantLeaf(leafTypeCounts);
foliageBridge(
unreached, logPositions, distances,
leafPositions, connectivityLeaves, positions,
bridgeLeaf, leafAdds,
minX, minY, minZ, maxX, maxY, maxZ
);
distances = seedDistances(logPositions, connectivityLeaves);
unreached = new HashSet<>(leafPositions);
unreached.removeAll(distances.keySet());
}
logsAdded = tentacleGrow(
unreached, distances,
logPositions, leafPositions, connectivityLeaves, positions,
inserts, orphanRemovals, !foliageOverature
);
} else {
distances = new HashMap<>();
unreached = new HashSet<>();
reachableBefore = 0;
}
leavesRemoved += orphanRemovals.size();
int leavesAdded = leafAdds.size();
if (smoke) {
BlockData leafTemplate = pickDominantLeaf(leafTypeCounts);
for (long key : leafPositions) {
leafAdds.add(new LeafAddition(key, leafTemplate));
}
leavesAdded = leafAdds.size();
}
int leavesNormalized = 0;
List<LeafRewrite> normalizeRewrites = new ArrayList<>();
if (normalize && !smoke) {
for (long pos : leafPositions) {
BlockData data = positions.get(pos);
if (data instanceof Leaves leaves && leaves.isPersistent()) {
BlockData cloned = data.clone();
((Leaves) cloned).setPersistent(false);
normalizeRewrites.add(new LeafRewrite(pos, cloned));
leavesNormalized++;
}
}
}
if (mutate) {
if (smoke) {
for (long key : removedLeafKeys) {
if (!positions.containsKey(key)) {
blocks.remove(unpackKey(key));
}
}
}
for (long key : orphanRemovals) {
blocks.remove(unpackKey(key));
}
for (LeafAddition addition : leafAdds) {
blocks.put(unpackKey(addition.key()), addition.data());
}
for (LogInsertion insertion : inserts) {
blocks.put(unpackKey(insertion.key()), insertion.data());
}
for (LeafRewrite rewrite : normalizeRewrites) {
blocks.put(unpackKey(rewrite.key()), rewrite.data());
}
}
int finalLeafCount = leafPositions.size();
if (!normalize && !smoke) {
finalLeafCount += persistentInput;
}
return new Result(
smoke ? leavesAdded : totalLeavesInitial,
persistentInput,
reachableBefore,
logsAdded,
leavesAdded,
leavesRemoved,
leavesNormalized,
unreached.size(),
null
);
}
private static void paintShell(
Set<Long> logPositions,
Map<Long, BlockData> positions,
Set<Long> leafPositions,
BlockData leafTemplate,
int radius,
int minX, int minY, int minZ, int maxX, int maxY, int maxZ
) {
if (radius <= 0) {
return;
}
int r2 = radius * radius;
int bxMin = minX;
int byMin = minY;
int bzMin = minZ;
int bxMax = maxX;
int byMax = maxY;
int bzMax = maxZ;
for (long log : logPositions) {
int[] lx = unpack(log);
for (int dx = -radius; dx <= radius; dx++) {
int ax = lx[0] + dx;
if (ax < bxMin || ax > bxMax) continue;
int dx2 = dx * dx;
for (int dy = -radius; dy <= radius; dy++) {
int ay = lx[1] + dy;
if (ay < byMin || ay > byMax) continue;
int dy2 = dy * dy;
int partial = dx2 + dy2;
if (partial > r2) continue;
for (int dz = -radius; dz <= radius; dz++) {
if (partial + dz * dz > r2) continue;
int az = lx[2] + dz;
if (az < bzMin || az > bzMax) continue;
long nk = packXYZ(ax, ay, az);
if (logPositions.contains(nk)) continue;
if (positions.containsKey(nk)) continue;
positions.put(nk, leafTemplate);
leafPositions.add(nk);
}
}
}
}
}
private static int tentacleGrow(
Set<Long> unreached,
Map<Long, Integer> distances,
Set<Long> logPositions,
Set<Long> leafPositions,
Set<Long> connectivityLeaves,
Map<Long, BlockData> positions,
List<LogInsertion> inserts,
Set<Long> orphanRemovals,
boolean deleteOrphans
) {
int logsAdded = 0;
int safetyLimit = unreached.size() * 2 + 32;
long currentTarget = -1L;
while (!unreached.isEmpty() && logsAdded < safetyLimit) {
if (currentTarget == -1L || !unreached.contains(currentTarget)) {
currentTarget = unreached.iterator().next();
}
long extensionLeaf = findWoodAdjacentLeafFrom(currentTarget, connectivityLeaves, logPositions);
if (extensionLeaf == -1L) {
if (deleteOrphans) {
removeOrphanCluster(currentTarget, connectivityLeaves, leafPositions, unreached, distances, positions, orphanRemovals);
} else {
skipOrphanCluster(currentTarget, unreached, connectivityLeaves);
}
currentTarget = -1L;
continue;
}
BlockData logData = pickLogVariant(extensionLeaf, positions, logPositions);
inserts.add(new LogInsertion(extensionLeaf, logData));
logPositions.add(extensionLeaf);
leafPositions.remove(extensionLeaf);
connectivityLeaves.remove(extensionLeaf);
distances.remove(extensionLeaf);
unreached.remove(extensionLeaf);
positions.put(extensionLeaf, logData);
logsAdded++;
propagateFromLog(extensionLeaf, distances, connectivityLeaves, unreached);
}
return logsAdded;
}
private static long findWoodAdjacentLeafFrom(long start, Set<Long> leafPositions, Set<Long> logPositions) {
if (!leafPositions.contains(start)) return -1L;
if (hasLogNeighbor(start, logPositions)) return start;
Set<Long> visited = new HashSet<>();
Deque<Long> queue = new ArrayDeque<>();
queue.add(start);
visited.add(start);
while (!queue.isEmpty()) {
long pos = queue.poll();
int[] xyz = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (!leafPositions.contains(nk) || !visited.add(nk)) continue;
if (hasLogNeighbor(nk, logPositions)) return nk;
queue.add(nk);
}
}
return -1L;
}
private static boolean hasLogNeighbor(long key, Set<Long> logPositions) {
int[] xyz = unpack(key);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (logPositions.contains(nk)) return true;
}
return false;
}
private static void propagateFromLog(
long logKey, Map<Long, Integer> distances,
Set<Long> connectivityLeaves, Set<Long> unreached
) {
int[] cx = unpack(logKey);
Deque<Long> q = new ArrayDeque<>();
for (int[] n : NEIGHBORS) {
long nk = packXYZ(cx[0] + n[0], cx[1] + n[1], cx[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
Integer cur = distances.get(nk);
if (cur == null || cur > 1) {
if (cur == null) unreached.remove(nk);
distances.put(nk, 1);
q.add(nk);
}
}
}
while (!q.isEmpty()) {
long pos = q.poll();
int d = distances.get(pos);
if (d >= MAX_DISTANCE) continue;
int[] px = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(px[0] + n[0], px[1] + n[1], px[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
Integer cur = distances.get(nk);
if (cur == null || cur > d + 1) {
if (cur == null) unreached.remove(nk);
distances.put(nk, d + 1);
q.add(nk);
}
}
}
}
}
private static void foliageBridge(
Set<Long> unreached,
Set<Long> logPositions,
Map<Long, Integer> distances,
Set<Long> leafPositions,
Set<Long> connectivityLeaves,
Map<Long, BlockData> positions,
BlockData leafTemplate,
List<LeafAddition> leafAdds,
int minX, int minY, int minZ, int maxX, int maxY, int maxZ
) {
Set<Long> pending = new HashSet<>(unreached);
while (!pending.isEmpty()) {
long seed = pending.iterator().next();
Set<Long> cluster = new HashSet<>();
Deque<Long> cq = new ArrayDeque<>();
cq.add(seed);
cluster.add(seed);
while (!cq.isEmpty()) {
long p = cq.poll();
int[] xyz = unpack(p);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (pending.contains(nk) && cluster.add(nk)) {
cq.add(nk);
}
}
}
Map<Long, Long> parent = new HashMap<>();
Deque<Long> q = new ArrayDeque<>();
for (long c : cluster) {
parent.put(c, -1L);
q.add(c);
}
long pathEnd = -1L;
while (!q.isEmpty() && pathEnd == -1L) {
long p = q.poll();
int[] xyz = unpack(p);
for (int[] n : NEIGHBORS) {
int nx = xyz[0] + n[0];
int ny = xyz[1] + n[1];
int nz = xyz[2] + n[2];
if (nx < minX || nx > maxX) continue;
if (ny < minY || ny > maxY) continue;
if (nz < minZ || nz > maxZ) continue;
long nk = packXYZ(nx, ny, nz);
if (parent.containsKey(nk)) continue;
if (logPositions.contains(nk) || distances.containsKey(nk)) {
pathEnd = p;
break;
}
if (positions.containsKey(nk)) continue;
parent.put(nk, p);
q.add(nk);
}
}
pending.removeAll(cluster);
if (pathEnd == -1L) {
continue;
}
long cur = pathEnd;
while (cur != -1L && !cluster.contains(cur)) {
if (!positions.containsKey(cur)) {
BlockData clone = leafTemplate.clone();
positions.put(cur, clone);
leafPositions.add(cur);
connectivityLeaves.add(cur);
leafAdds.add(new LeafAddition(cur, clone));
}
cur = parent.get(cur);
}
}
}
private static void skipOrphanCluster(long seed, Set<Long> unreached, Set<Long> connectivityLeaves) {
Deque<Long> queue = new ArrayDeque<>();
Set<Long> visited = new HashSet<>();
queue.add(seed);
visited.add(seed);
while (!queue.isEmpty()) {
long pos = queue.poll();
unreached.remove(pos);
int[] xyz = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (visited.add(nk) && unreached.contains(nk) && connectivityLeaves.contains(nk)) {
queue.add(nk);
}
}
}
}
private static void removeOrphanCluster(
long seed,
Set<Long> connectivityLeaves, Set<Long> leafPositions, Set<Long> unreached,
Map<Long, Integer> distances, Map<Long, BlockData> positions, Set<Long> orphanRemovals
) {
Deque<Long> queue = new ArrayDeque<>();
Set<Long> visited = new HashSet<>();
queue.add(seed);
visited.add(seed);
while (!queue.isEmpty()) {
long pos = queue.poll();
int[] xyz = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (visited.add(nk) && connectivityLeaves.contains(nk)) {
queue.add(nk);
}
}
orphanRemovals.add(pos);
connectivityLeaves.remove(pos);
leafPositions.remove(pos);
unreached.remove(pos);
distances.remove(pos);
positions.remove(pos);
}
}
private static Map<Long, Integer> seedDistances(Set<Long> logPositions, Set<Long> leafPositions) {
Map<Long, Integer> dist = new HashMap<>();
Deque<Long> queue = new ArrayDeque<>();
for (long leaf : leafPositions) {
int[] xyz = unpack(leaf);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (logPositions.contains(nk)) {
dist.put(leaf, 1);
queue.add(leaf);
break;
}
}
}
while (!queue.isEmpty()) {
long pos = queue.poll();
int d = dist.get(pos);
if (d >= MAX_DISTANCE) {
continue;
}
int[] xyz = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (leafPositions.contains(nk) && !dist.containsKey(nk)) {
dist.put(nk, d + 1);
queue.add(nk);
}
}
}
return dist;
}
private static int countReachable(Set<Long> leafPositions, Map<Long, Integer> distances) {
int count = 0;
for (long leaf : leafPositions) {
if (distances.containsKey(leaf)) {
count++;
}
}
return count;
}
private static BlockData pickDominantLeaf(Map<BlockData, Integer> leafTypeCounts) {
BlockData best = null;
int bestCount = -1;
for (Map.Entry<BlockData, Integer> e : leafTypeCounts.entrySet()) {
if (e.getValue() > bestCount) {
bestCount = e.getValue();
best = e.getKey();
}
}
if (best == null) {
return FALLBACK_LEAF.clone();
}
BlockData clone = best.clone();
if (clone instanceof Leaves leaves) {
leaves.setPersistent(false);
}
return clone;
}
private static BlockData pickLogVariant(long target, Map<Long, BlockData> positions, Set<Long> logPositions) {
if (logPositions.isEmpty()) {
return FALLBACK_LOG.clone();
}
int[] tx = unpack(target);
long nearest = -1L;
long nearestDistSq = Long.MAX_VALUE;
for (long lp : logPositions) {
int[] lx = unpack(lp);
long dx = tx[0] - lx[0];
long dy = tx[1] - lx[1];
long dz = tx[2] - lx[2];
long d2 = dx * dx + dy * dy + dz * dz;
if (d2 < nearestDistSq) {
nearestDistSq = d2;
nearest = lp;
}
}
BlockData source = positions.get(nearest);
if (source == null) {
return FALLBACK_LOG.clone();
}
BlockData clone = source.clone();
if (clone instanceof Orientable orientable) {
orientable.setAxis(Axis.Y);
}
return clone;
}
private static boolean isLog(BlockData data) {
return Tag.LOGS.isTagged(data.getMaterial());
}
private static boolean isLeaf(BlockData data) {
return Tag.LEAVES.isTagged(data.getMaterial()) || data instanceof Leaves;
}
private static long packKey(BlockVector pos) {
return packXYZ(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ());
}
private static long packXYZ(int x, int y, int z) {
long lx = (x + 32768L) & 0xFFFFL;
long ly = (y + 32768L) & 0xFFFFL;
long lz = (z + 32768L) & 0xFFFFL;
return (lx << 32) | (ly << 16) | lz;
}
private static int[] unpack(long key) {
int x = (int) ((key >> 32) & 0xFFFFL) - 32768;
int y = (int) ((key >> 16) & 0xFFFFL) - 32768;
int z = (int) (key & 0xFFFFL) - 32768;
return new int[]{x, y, z};
}
private static BlockVector unpackKey(long key) {
int[] xyz = unpack(key);
return new BlockVector(xyz[0], xyz[1], xyz[2]);
}
private record LogInsertion(long key, BlockData data) {
}
private record LeafAddition(long key, BlockData data) {
}
private record LeafRewrite(long key, BlockData data) {
}
}
@@ -1,408 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleFluidBodyComponent;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.util.project.matter.IrisMatterSupport;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.mantle.io.Lz4IOWorkerCodecSupport;
import art.arcane.volmlib.util.mantle.runtime.IOWorker;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.iris.util.common.parallel.HyperLock;
import art.arcane.iris.util.common.parallel.MultiBurst;
import lombok.*;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Data
@EqualsAndHashCode(exclude = "engine")
@ToString(exclude = "engine")
public class IrisEngineMantle implements EngineMantle {
private final Engine engine;
private final Mantle<Matter> mantle;
@Getter(AccessLevel.NONE)
private final KMap<Integer, KList<MantleComponent>> components;
private final KMap<MantleFlag, MantleComponent> registeredComponents = new KMap<>();
private final AtomicCache<List<Pair<List<MantleComponent>, Integer>>> componentsCache = new AtomicCache<>();
private final AtomicCache<Set<MantleFlag>> disabledFlags = new AtomicCache<>();
private final MantleObjectComponent object;
public IrisEngineMantle(Engine engine) {
this.engine = engine;
this.mantle = createMantle(engine);
components = new KMap<>();
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleFluidBodyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
}
@Override
public int getRadius() {
if (components.isEmpty()) return 0;
return getComponents().getFirst().getB();
}
@Override
public int getRealRadius() {
if (components.isEmpty()) return 0;
return getComponents().getLast().getB();
}
@Override
public List<Pair<List<MantleComponent>, Integer>> getComponents() {
return componentsCache.aquire(() -> {
var list = components.keySet()
.stream()
.sorted()
.map(components::get)
.map(components -> {
int radius = components.stream()
.filter(MantleComponent::isEnabled)
.mapToInt(MantleComponent::getRadius)
.max()
.orElse(0);
return new Pair<>(List.copyOf(components), radius);
})
.filter(pair -> !pair.getA().isEmpty())
.toList();
int radius = 0;
for (var pair : list.reversed()) {
radius += pair.getB();
pair.setB(Math.ceilDiv(radius, 16));
}
return list;
});
}
@Override
public Map<MantleFlag, MantleComponent> getRegisteredComponents() {
return Collections.unmodifiableMap(registeredComponents);
}
@Override
public boolean registerComponent(MantleComponent c) {
if (registeredComponents.putIfAbsent(c.getFlag(), c) != null) return false;
c.setEnabled(!getDisabledFlags().contains(c.getFlag()));
components.computeIfAbsent(c.getPriority(), k -> new KList<>()).add(c);
componentsCache.reset();
return true;
}
@Override
public KList<MantleFlag> getComponentFlags() {
return new KList<>(registeredComponents.keySet());
}
@Override
public void hotload() {
disabledFlags.reset();
for (var component : registeredComponents.values()) {
component.hotload();
component.setEnabled(!getDisabledFlags().contains(component.getFlag()));
}
componentsCache.reset();
}
private Set<MantleFlag> getDisabledFlags() {
return disabledFlags.aquire(() -> {
KList<MantleFlag> disabled = new KList<>();
disabled.addAll(getDimension().getDisabledComponents());
if (!getDimension().isCarvingEnabled()) {
disabled.addIfMissing(ReservedFlag.CARVED);
}
return Set.copyOf(disabled);
});
}
@Override
public MantleObjectComponent getObjectComponent() {
return object;
}
private static Mantle<Matter> createMantle(Engine engine) {
IrisMatterSupport.ensureRegistered();
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
int worldHeight = engine.getTarget().getHeight();
MantleDataAdapter<Matter> adapter = createRuntimeDataAdapter();
MantleHooks hooks = createRuntimeHooks();
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
createRegionIO(dataFolder, worldHeight, adapter, hooks);
return new Mantle<>(
dataFolder,
worldHeight,
Short.MAX_VALUE,
new HyperLock(),
MultiBurst.ioBurst,
regionIO,
adapter,
hooks
);
}
public static MantleDataAdapter<Matter> createRuntimeDataAdapter() {
return createDataAdapter();
}
public static MantleHooks createRuntimeHooks() {
return createHooks();
}
private static MantleDataAdapter<Matter> createDataAdapter() {
return new MantleDataAdapter<>() {
@Override
public Matter createSection() {
return new IrisMatter(16, 16, 16);
}
@Override
public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException {
return Matter.readDin(din);
}
@Override
public void writeSection(Matter section, java.io.DataOutputStream dos) throws IOException {
section.writeDos(dos);
}
@Override
public void trimSection(Matter section) {
section.trimSlices();
}
@Override
public boolean isSectionEmpty(Matter section) {
return section.getSliceMap().isEmpty();
}
@Override
public Class<?> classifyValue(Object value) {
if (value instanceof World) {
return World.class;
}
if (value instanceof BlockData) {
return BlockData.class;
}
if (value instanceof Entity) {
return Entity.class;
}
return value.getClass();
}
@Override
@SuppressWarnings("unchecked")
public <T> void set(Matter section, int x, int y, int z, Class<?> type, T value) {
MatterSlice<T> slice = (MatterSlice<T>) section.slice(type);
slice.set(x, y, z, value);
}
@Override
public <T> void remove(Matter section, int x, int y, int z, Class<T> type) {
MatterSlice<T> slice = section.slice(type);
slice.set(x, y, z, null);
}
@Override
public <T> T get(Matter section, int x, int y, int z, Class<T> type) {
MatterSlice<T> slice = section.slice(type);
return slice.get(x, y, z);
}
@Override
public <T> void iterate(Matter section, Class<T> type, art.arcane.volmlib.util.function.Consumer4<Integer, Integer, Integer, T> iterator) {
MatterSlice<T> slice = section.getSlice(type);
if (slice != null) {
slice.iterateSync(iterator);
}
}
@Override
public boolean hasSlice(Matter section, Class<?> type) {
return section.hasSlice(type);
}
@Override
public void deleteSlice(Matter section, Class<?> type) {
section.deleteSlice(type);
}
};
}
private static MantleHooks createHooks() {
return new MantleHooks() {
@Override
public void onBeforeReadSection(int index) {
Iris.addPanic("read.section", "Section[" + index + "]");
}
@Override
public void onReadSectionFailure(int index,
long start,
long end,
art.arcane.volmlib.util.io.CountingDataInputStream din,
IOException error) {
Iris.error("Failed to read chunk section, skipping it.");
Iris.addPanic("read.byte.range", start + " " + end);
Iris.addPanic("read.byte.current", din.count() + "");
Iris.reportError(error);
error.printStackTrace();
Iris.panic();
TectonicPlate.addError();
}
@Override
public void onBeforeReadChunk(int index) {
Iris.addPanic("read-chunk", "Chunk[" + index + "]");
}
@Override
public void onAfterReadChunk(int index) {
EnginePanic.saveLast();
}
@Override
public void onReadChunkFailure(int index,
long start,
long end,
art.arcane.volmlib.util.io.CountingDataInputStream din,
Throwable error) {
Iris.error("Failed to read chunk, creating a new chunk instead.");
Iris.addPanic("read.byte.range", start + " " + end);
Iris.addPanic("read.byte.current", din.count() + "");
Iris.reportError(error);
error.printStackTrace();
Iris.panic();
}
@Override
public boolean shouldRetainSlice(Class<?> sliceType) {
return IrisToolbelt.isRetainingMantleDataForSlice(sliceType.getCanonicalName());
}
@Override
public String formatDuration(double millis) {
return Form.duration(millis, 0);
}
@Override
public void onDebug(String message) {
Iris.debug(message);
}
@Override
public void onWarn(String message) {
Iris.warn(message);
}
@Override
public void onError(Throwable throwable) {
Iris.reportError(throwable);
}
};
}
private static art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> createRegionIO(File root,
int worldHeight,
MantleDataAdapter<Matter> adapter,
MantleHooks hooks) {
IOWorker<TectonicPlate<Matter>> worker = new IOWorker<>(
root,
new Lz4IOWorkerCodecSupport(),
128,
(name, millis) -> {
String threadName = Thread.currentThread().getName();
String message = "Acquired Channel for " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(millis, 2)
+ C.GRAY + " thread=" + threadName;
if (millis >= 1000L) {
Iris.warn(message);
} else {
Iris.debug(message);
}
}
);
return new art.arcane.volmlib.util.mantle.Mantle.RegionIO<>() {
@Override
public TectonicPlate<Matter> read(String name) throws Exception {
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
try {
return worker.read(name, (regionName, in) ->
TectonicPlate.read(worldHeight, in, regionName.startsWith("pv."), adapter, hooks));
} finally {
if (TectonicPlate.hasError() && IrisSettings.get().getGeneral().isDumpMantleOnError()) {
File dump = Iris.instance.getDataFolder("dump", name + ".bin");
worker.dumpDecoded(name, dump.toPath());
} else {
Iris.debug("Read Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(stopwatch.getMilliseconds(), 2));
}
}
}
@Override
public void write(String name, TectonicPlate<Matter> region) throws Exception {
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
worker.write(name, "iris", ".bin", region, TectonicPlate::write);
Iris.debug("Saved Tectonic Plate " + C.DARK_GREEN + name + C.RED + " in " + Form.duration(stopwatch.getMilliseconds(), 2));
}
@Override
public void close() throws Exception {
worker.close();
}
};
}
}
File diff suppressed because it is too large Load Diff
@@ -1,275 +0,0 @@
package art.arcane.iris.engine;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.*;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import org.bukkit.block.data.BlockData;
import java.util.*;
public class UpperDimensionContext implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private final IrisDimension dimension;
private final IrisData data;
private final int chunkHeight;
private final ProceduralStream<Double> heightStream;
private final ProceduralStream<IrisBiome> biomeStream;
private final ProceduralStream<IrisRegion> regionStream;
private final ProceduralStream<BlockData> rockStream;
private final boolean selfReferencing;
private UpperDimensionContext(IrisDimension dimension, IrisData data, int chunkHeight,
ProceduralStream<Double> heightStream,
ProceduralStream<IrisBiome> biomeStream,
ProceduralStream<IrisRegion> regionStream,
ProceduralStream<BlockData> rockStream,
boolean selfReferencing) {
this.dimension = dimension;
this.data = data;
this.chunkHeight = chunkHeight;
this.heightStream = heightStream;
this.biomeStream = biomeStream;
this.regionStream = regionStream;
this.rockStream = rockStream;
this.selfReferencing = selfReferencing;
}
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey());
int chunkHeight = engine.getHeight();
if (selfRef) {
return createSelfReferencing(engine, chunkHeight);
}
return createCrossReferencing(engine, upperDim, chunkHeight);
}
private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) {
IrisComplex complex = engine.getComplex();
return new UpperDimensionContext(
engine.getDimension(),
engine.getData(),
chunkHeight,
complex.getHeightStream(),
complex.getBaseBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
true
);
}
private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) {
IrisData resolvedData = upperDim.getLoader();
if (resolvedData == null) {
resolvedData = engine.getData();
}
IrisData upperData = resolvedData;
long seedOffset = upperDim.getLoadKey().hashCode();
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
double fluidHeight = upperDim.getFluidHeight();
DataProvider dataProvider = () -> upperData;
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
upperDim.getRegions().forEach(regionKey -> {
IrisRegion region = upperData.getRegionLoader().load(regionKey);
if (region != null) {
region.getAllBiomes(dataProvider).forEach(biome -> {
allBiomes.add(biome);
biome.getGenerators().forEach(link -> {
IrisGenerator gen = link.getCachedGenerator(dataProvider);
if (gen != null) {
generators.computeIfAbsent(gen.getInterpolator(), k -> new HashSet<>()).add(gen);
}
});
});
}
});
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
for (IrisBiome biome : allBiomes) {
double min = 0D;
double max = 0D;
for (IrisGenerator gen : entry.getValue()) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
}
max += biome.getGenLinkMax(key, engine);
min += biome.getGenLinkMin(key, engine);
}
interpolatorBounds.put(biome, new NoiseBounds(min, max));
}
generatorBounds.put(entry.getKey(), interpolatorBounds);
}
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
.create(rng.nextParallelRNG(883), upperData).stream()
.zoom(upperDim.getRegionZoom());
ProceduralStream<IrisRegion> regionStream = regionStyleStream
.selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions()));
ProceduralStream<IrisBiome> landBiomeStream = regionStream
.convert(r -> upperDim.getLandBiomeStyle()
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getLandBiomes(),
t -> t.setInferredType(InferredType.LAND))))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
.convert(r -> upperDim.getSeaBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getSeaBiomes(),
t -> t.setInferredType(InferredType.SEA))))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
.convert(r -> upperDim.getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getShoreBiomes(),
t -> t.setInferredType(InferredType.SHORE))))
.convertAware2D(ProceduralStream::get);
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
inferredStreams.put(InferredType.LAND, landBiomeStream);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
ProceduralStream<InferredType> bridgeStream = upperDim.getContinentalStyle()
.create(rng.nextParallelRNG(234234565), upperData)
.bake().scale(1D / upperDim.getContinentZoom()).bake().stream()
.convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND);
ProceduralStream<IrisBiome> baseBiomeStream = bridgeStream
.convertAware2D((t, x, z) -> {
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
});
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
? ProceduralStream.ofDouble((x, z) -> 0.0D)
: ProceduralStream.ofDouble((x, z) -> {
double value = 0D;
for (IrisShapedGeneratorStyle style : overlayNoise) {
value += style.get(rng, upperData, x, z);
}
return value;
});
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
IrisBiome b = baseBiomeStream.get(x, z);
if (b == null) {
return fluidHeight;
}
double interpolatedHeight = 0;
for (Map.Entry<IrisInterpolator, Set<IrisGenerator>> entry : generators.entrySet()) {
IrisInterpolator interpolator = entry.getKey();
Set<IrisGenerator> gens = entry.getValue();
if (gens.isEmpty()) {
continue;
}
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> {
try {
IrisBiome bx = baseBiomeStream.get(xx, zz);
if (bx == null) {
return ZERO_NOISE_BOUNDS;
}
NoiseBounds bounds = cachedBounds != null ? cachedBounds.get(bx) : null;
if (bounds != null) {
return bounds;
}
double bMin = 0D;
double bMax = 0D;
for (IrisGenerator gen : gens) {
String key = gen.getLoadKey();
if (key == null || key.isBlank()) {
continue;
}
bMax += bx.getGenLinkMax(key, engine);
bMin += bx.getGenLinkMin(key, engine);
}
return new NoiseBounds(bMin, bMax);
} catch (Throwable e) {
Iris.reportError(e);
return ZERO_NOISE_BOUNDS;
}
});
double hi = sampledBounds.max();
double lo = sampledBounds.min();
double d = 0;
for (IrisGenerator gen : gens) {
d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945));
}
interpolatedHeight += d / gens.size();
}
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
}, Interpolated.DOUBLE);
ProceduralStream<BlockData> rockStream = upperDim.getRockPalette()
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
.select(upperDim.getRockPalette().getBlockData(upperData));
return new UpperDimensionContext(
upperDim,
upperData,
chunkHeight,
heightStream,
baseBiomeStream,
regionStream,
rockStream,
false
);
}
public int getUpperSurfaceY(int x, int z) {
double rawHeight = heightStream.get((double) x, (double) z);
return chunkHeight - 1 - (int) Math.round(rawHeight);
}
public IrisBiome getUpperBiome(int x, int z) {
return biomeStream.get((double) x, (double) z);
}
public IrisRegion getUpperRegion(int x, int z) {
return regionStream == null ? null : regionStream.get((double) x, (double) z);
}
public BlockData getRockBlock(int x, int z) {
return rockStream.get((double) x, (double) z);
}
public IrisDimension getDimension() {
return dimension;
}
@Override
public IrisData getData() {
return data;
}
public boolean isSelfReferencing() {
return selfReferencing;
}
}
@@ -1,331 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
public class IrisTerrainNormalActuator extends EngineAssignedActuator<BlockData> {
private static final BlockData AIR = Material.AIR.createBlockData();
private static final BlockData BEDROCK = Material.BEDROCK.createBlockData();
private static final BlockData LAVA = Material.LAVA.createBlockData();
private static final BlockData GLASS = Material.GLASS.createBlockData();
private static final BlockData CAVE_AIR = Material.CAVE_AIR.createBlockData();
@Getter
private final RNG rng;
@Getter
private int lastBedrock = -1;
public IrisTerrainNormalActuator(Engine engine) {
super(engine, "Terrain");
rng = new RNG(engine.getSeedManager().getTerrain());
}
@BlockCoordinates
@Override
public void onActuate(int x, int z, Hunk<BlockData> h, boolean multicore, ChunkContext context) {
PrecisionStopwatch p = PrecisionStopwatch.start();
for (int xf = 0; xf < h.getWidth(); xf++) {
terrainSliver(x, z, xf, h, context);
}
getEngine().getMetrics().getTerrain().put(p.getMilliseconds());
}
private int fluidOrHeight(int height) {
return Math.max(getDimension().getFluidHeight(), height);
}
/**
* This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction.
*
* @param x the chunk x in blocks
* @param z the chunk z in blocks
* @param xf the current x slice
* @param h the blockdata
*/
@BlockCoordinates
public void terrainSliver(int x, int z, int xf, Hunk<BlockData> h, ChunkContext context) {
terrainSliverOptimized(x, z, xf, h, context);
}
@BlockCoordinates
private void terrainSliverLegacy(int x, int z, int xf, Hunk<BlockData> h, ChunkContext context) {
int zf, realX, realZ, hf, he;
IrisBiome biome;
IrisRegion region;
int clampedFluidHeight = Math.min(h.getHeight(), getDimension().getFluidHeight());
for (zf = 0; zf < h.getDepth(); zf++) {
realX = xf + x;
realZ = zf + z;
biome = context.getBiome().get(xf, zf);
region = context.getRegion().get(xf, zf);
he = Math.min(h.getHeight(), context.getRoundedHeight(xf, zf));
hf = Math.max(clampedFluidHeight, he);
if (hf < 0) {
continue;
}
KList<BlockData> blocks = null;
KList<BlockData> fblocks = null;
int depth, fdepth;
for (int i = hf; i >= 0; i--) {
if (i >= h.getHeight()) {
continue;
}
if (i == 0) {
if (getDimension().isBedrock()) {
h.setRaw(xf, i, zf, BEDROCK);
lastBedrock = i;
continue;
}
}
BlockData ore = biome.generateOres(realX, i, realZ, rng, getData(), true);
ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore;
ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore;
if (ore != null) {
h.setRaw(xf, i, zf, ore);
continue;
}
if (i > he && i <= hf) {
fdepth = hf - i;
if (fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, getData());
}
if (fblocks.hasIndex(fdepth)) {
h.setRaw(xf, i, zf, fblocks.get(fdepth));
continue;
}
h.setRaw(xf, i, zf, context.getFluid().get(xf, zf));
continue;
}
if (i <= he) {
depth = he - i;
if (blocks == null) {
blocks = biome.generateLayers(getDimension(), realX, realZ, rng,
he,
he,
getData(),
getComplex());
}
if (blocks.hasIndex(depth)) {
h.setRaw(xf, i, zf, blocks.get(depth));
continue;
}
ore = biome.generateOres(realX, i, realZ, rng, getData(), false);
ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), false) : ore;
ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore;
if (ore != null) {
h.setRaw(xf, i, zf, ore);
} else {
h.setRaw(xf, i, zf, context.getRock().get(xf, zf));
}
}
}
UpperDimensionContext upperContext = getEngine().getUpperContext();
if (upperContext != null) {
int chunkHeight = h.getHeight();
boolean bedrockEnabled = getDimension().isBedrock();
int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ);
int upperGap = getDimension().getUpperDimensionGap();
int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap);
if (upperSurfaceY < chunkHeight - 1) {
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
BlockData upperRock = upperContext.getRockBlock(realX, realZ);
int upperThickness = chunkHeight - 1 - upperSurfaceY;
KList<BlockData> upperBlocks = upperBiome != null
? upperBiome.generateLayers(upperContext.getDimension(),
realX, realZ, rng, upperThickness, upperThickness,
upperContext.getData(), getComplex())
: null;
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
if (y == chunkHeight - 1 && bedrockEnabled) {
h.setRaw(xf, y, zf, BEDROCK);
continue;
}
int depthFromFace = y - upperSurfaceY;
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
} else {
h.setRaw(xf, y, zf, upperRock);
}
}
}
}
}
}
@BlockCoordinates
private void terrainSliverOptimized(int x, int z, int xf, Hunk<BlockData> h, ChunkContext context) {
int chunkHeight = h.getHeight();
int chunkDepth = h.getDepth();
IrisDimension dimension = getDimension();
IrisData data = getData();
IrisComplex complex = getComplex();
RNG localRng = rng;
int fluidHeight = dimension.getFluidHeight();
int clampedFluidHeight = Math.min(chunkHeight, fluidHeight);
boolean bedrockEnabled = dimension.isBedrock();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<BlockData> fluidCache = context.getFluid();
ChunkedDataCache<BlockData> rockCache = context.getRock();
int realX = xf + x;
UpperDimensionContext upperContext = getEngine().getUpperContext();
for (int zf = 0; zf < chunkDepth; zf++) {
int realZ = zf + z;
IrisBiome biome = biomeCache.get(xf, zf);
IrisRegion region = regionCache.get(xf, zf);
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
int hf = Math.max(clampedFluidHeight, he);
if (hf < 0) {
continue;
}
int topY = Math.min(hf, chunkHeight - 1);
BlockData fluid = fluidCache.get(xf, zf);
BlockData rock = rockCache.get(xf, zf);
boolean hasSurfaceOres = biome.hasSurfaceOres() || region.hasSurfaceOres() || dimension.hasSurfaceOres();
boolean hasUndergroundOres = biome.hasUndergroundOres() || region.hasUndergroundOres() || dimension.hasUndergroundOres();
KList<BlockData> blocks = null;
KList<BlockData> fblocks = null;
for (int i = topY; i >= 0; i--) {
if (i == 0 && bedrockEnabled) {
h.setRaw(xf, i, zf, BEDROCK);
lastBedrock = i;
continue;
}
BlockData ore = null;
if (hasSurfaceOres) {
ore = biome.generateSurfaceOres(realX, i, realZ, localRng, data);
ore = ore == null ? region.generateSurfaceOres(realX, i, realZ, localRng, data) : ore;
ore = ore == null ? dimension.generateSurfaceOres(realX, i, realZ, localRng, data) : ore;
}
if (ore != null) {
h.setRaw(xf, i, zf, ore);
continue;
}
if (i > he && i <= hf) {
int fdepth = hf - i;
if (fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data);
}
if (fblocks.hasIndex(fdepth)) {
h.setRaw(xf, i, zf, fblocks.get(fdepth));
} else {
h.setRaw(xf, i, zf, fluid);
}
continue;
}
if (i <= he) {
int depth = he - i;
if (blocks == null) {
blocks = biome.generateLayers(dimension, realX, realZ, localRng, he, he, data, complex);
}
if (blocks.hasIndex(depth)) {
h.setRaw(xf, i, zf, blocks.get(depth));
continue;
}
if (hasUndergroundOres) {
ore = biome.generateUndergroundOres(realX, i, realZ, localRng, data);
ore = ore == null ? region.generateUndergroundOres(realX, i, realZ, localRng, data) : ore;
ore = ore == null ? dimension.generateUndergroundOres(realX, i, realZ, localRng, data) : ore;
}
if (ore != null) {
h.setRaw(xf, i, zf, ore);
} else {
h.setRaw(xf, i, zf, rock);
}
}
}
if (upperContext != null) {
int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ);
int upperGap = dimension.getUpperDimensionGap();
int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap);
if (upperSurfaceY < chunkHeight - 1) {
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
BlockData upperRock = upperContext.getRockBlock(realX, realZ);
int upperThickness = chunkHeight - 1 - upperSurfaceY;
KList<BlockData> upperBlocks = upperBiome != null
? upperBiome.generateLayers(upperContext.getDimension(),
realX, realZ, localRng, upperThickness, upperThickness,
upperContext.getData(), complex)
: null;
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
if (y == chunkHeight - 1 && bedrockEnabled) {
h.setRaw(xf, y, zf, BEDROCK);
continue;
}
int depthFromFace = y - upperSurfaceY;
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
} else {
h.setRaw(xf, y, zf, upperRock);
}
}
}
}
}
}
}
@@ -1,99 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.data.chunk;
import art.arcane.iris.util.common.data.IrisCustomData;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData;
public class LinkedTerrainChunk implements TerrainChunk {
private static final int CHUNK_SIZE = 16;
private final ChunkData rawChunkData;
private final int minHeight;
private final int maxHeight;
private final int biomeHeight;
private final Biome[] biomes;
public LinkedTerrainChunk(World world) {
this(Bukkit.createChunkData(world));
}
public LinkedTerrainChunk(ChunkData data) {
rawChunkData = data;
minHeight = data.getMinHeight();
maxHeight = data.getMaxHeight();
biomeHeight = Math.max(1, maxHeight - minHeight);
biomes = new Biome[CHUNK_SIZE * biomeHeight * CHUNK_SIZE];
}
@Override
public Biome getBiome(int x, int y, int z) {
int index = biomeIndex(x, y, z);
Biome biome = biomes[index];
return biome == null ? Biome.PLAINS : biome;
}
@Override
public void setBiome(int x, int y, int z, Biome bio) {
biomes[biomeIndex(x, y, z)] = bio;
}
@Override
public int getMinHeight() {
return minHeight;
}
@Override
public int getMaxHeight() {
return maxHeight;
}
@Override
public synchronized void setBlock(int x, int y, int z, BlockData blockData) {
if (blockData instanceof IrisCustomData data) {
blockData = data.getBase();
}
rawChunkData.setBlock(x, y, z, blockData);
}
@Override
public synchronized void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData) {
rawChunkData.setRegion(xMin, yMin, zMin, xMax, yMax, zMax, blockData);
}
@Override
public BlockData getBlockData(int x, int y, int z) {
return rawChunkData.getBlockData(x, y, z);
}
@Override
public ChunkData getChunkData() {
return rawChunkData;
}
private int biomeIndex(int x, int y, int z) {
int clampedX = x & (CHUNK_SIZE - 1);
int clampedZ = z & (CHUNK_SIZE - 1);
int clampedY = Math.max(minHeight, Math.min(maxHeight - 1, y)) - minHeight;
return (clampedY * CHUNK_SIZE + clampedZ) * CHUNK_SIZE + clampedX;
}
}
@@ -1,44 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.data.chunk;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData;
public interface TerrainChunk {
static TerrainChunk create(World world) {
return new LinkedTerrainChunk(world);
}
static TerrainChunk create(ChunkData raw) {
return new LinkedTerrainChunk(raw);
}
Biome getBiome(int x, int y, int z);
void setBiome(int x, int y, int z, Biome bio);
int getMinHeight();
int getMaxHeight();
void setBlock(int x, int y, int z, BlockData blockData);
void setRegion(int xMin, int yMin, int zMin, int xMax, int yMax, int zMax, BlockData blockData);
BlockData getBlockData(int x, int y, int z);
ChunkData getChunkData();
}
@@ -1,430 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.block.data.type.PointedDripstone;
final class DecoratorCore {
private static final long SEED_OFFSET = 29356788L;
private static final long PART_FACTOR = 10439677L;
static final ThreadLocal<PlaceOpts> SCRATCH_OPTS = ThreadLocal.withInitial(PlaceOpts::new);
static final class PlaceOpts {
boolean caveSkipFluid;
boolean underwater;
int fluidHeight;
void reset() {
caveSkipFluid = false;
underwater = false;
fluidHeight = 0;
}
}
static long partSeed(long baseSeed, int partOrdinal) {
return baseSeed + SEED_OFFSET - (partOrdinal * PART_FACTOR);
}
static long partSeed(long baseSeed, IrisDecorationPart part) {
return partSeed(baseSeed, part.ordinal());
}
static IrisDecorator pickDecorator(IrisBiome biome, IrisDecorationPart part, RNG gRNG,
RNG colRng, IrisData data, double realX, double realZ) {
IrisDecorator[] bucket = biome.getDecoratorBucket(part);
if (bucket.length == 0) {
return null;
}
IrisDecorator picked = null;
int count = 0;
for (IrisDecorator d : bucket) {
try {
if (d.passesChanceGate(gRNG, realX, realZ, data)) {
count++;
if (count == 1 || colRng.nextInt(count) == 0) {
picked = d;
}
}
} catch (Throwable e) {
Iris.reportError(e);
}
}
return picked;
}
static void placeSingleUp(IrisDecorator decorator, int x, int z,
int realX, int height, int realZ, Hunk<BlockData> data,
RNG rng, IrisData irisData, boolean caveSkipFluid, EngineMantle mantle) {
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
return;
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
data.set(x, height + 2, z, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
static void placeSurfaceSingle(IrisDecorator decorator,
int x, int z, int realX, int height, int realZ,
Hunk<BlockData> data, RNG rng, IrisData irisData,
boolean underwater, boolean caveSkipFluid, EngineMantle mantle) {
BlockData bdx = data.get(x, height, z);
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (!underwater && !canGoOn(bd, bdx)
&& !decorator.isForcePlace() && decorator.getForceBlock() == null) {
return;
}
if (decorator.getForceBlock() != null) {
if (caveSkipFluid && B.isFluid(bdx)) {
return;
}
data.set(x, height, z, fixFacesForHunk(
decorator.getForceBlock().getBlockData(irisData), data, x, z, realX, height, realZ, mantle));
return;
}
if (!decorator.isForcePlace()) {
if (decorator.getWhitelist() != null
&& decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(irisData).equals(bdx))) {
return;
}
if (decorator.getBlacklist() != null
&& decorator.getBlacklist().stream().anyMatch(d -> d.getBlockData(irisData).equals(bdx))) {
return;
}
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
data.set(x, height + 2, z, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
static void placeSingleAt(IrisDecorator decorator, int x, int z,
int realX, int height, int realZ, Hunk<BlockData> data,
RNG rng, IrisData irisData, boolean applyFixFaces, EngineMantle mantle) {
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
return;
}
if (applyFixFaces) {
bd = fixFacesForHunk(bd, data, x, z, realX, height, realZ, mantle);
}
data.set(x, height, z, bd);
}
static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int max, Hunk<BlockData> data,
RNG rng, IrisData irisData, PlaceOpts opts) {
int effectiveMax = max;
if (opts.underwater && height < opts.fluidHeight) {
effectiveMax = opts.fluidHeight;
}
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, decorator.pickBlockDataTop(rng, irisData, realX, realZ));
return;
}
BlockData bdx = data.get(x, height, z);
for (int i = 0; i < stack; i++) {
int h = height + i;
double threshold = ((double) i) / (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
break;
}
if (i == 0 && !opts.underwater && !canGoOn(bd, bdx)) {
break;
}
if (opts.underwater && height + 1 + i > opts.fluidHeight) {
break;
}
if (opts.caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
break;
}
if (bd instanceof PointedDripstone) {
bd = dripstoneBlock(stack, i, BlockFace.UP);
}
data.set(x, height + 1 + i, z, bd);
}
}
static void placeStackDown(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int minHeight, Hunk<BlockData> data,
RNG rng, IrisData irisData, int max, PlaceOpts opts, EngineMantle mantle) {
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
if (stack == 1) {
if (opts.caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, fixFacesForHunk(
decorator.pickBlockDataTop(rng, irisData, realX, realZ),
data, x, z, realX, height, realZ, mantle));
return;
}
for (int i = 0; i < stack; i++) {
int h = height - i;
if (h < minHeight) {
continue;
}
double threshold = ((double) i) / (double) (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd instanceof PointedDripstone) {
bd = dripstoneBlock(stack, i, BlockFace.DOWN);
}
if (opts.caveSkipFluid && B.isFluid(data.get(x, h, z))) {
break;
}
data.set(x, h, z, fixFacesForHunk(bd, data, x, z, realX, h, realZ, mantle));
}
}
static void placeFloatingSimple(IrisDecorator decorator,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data,
RNG rng, IrisData irisData) {
BlockData bd = decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
return;
}
if (bd instanceof Bisected) {
BlockData top = bd.clone();
((Bisected) top).setHalf(Bisected.Half.TOP);
try {
if (max > 2) {
data.set(xf, height + 2, zf, top);
}
} catch (Throwable e) {
Iris.reportError(e);
}
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.BOTTOM);
}
if (max > 1) {
data.set(xf, height + 1, zf, bd);
}
}
static int placeFloatingStacked(IrisDecorator decorator,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data,
RNG rng, IrisData irisData) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
int placed = 0;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max) {
break;
}
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
BlockData bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
if (bd == null) {
break;
}
data.set(xf, h, zf, bd);
placed++;
}
return placed;
}
static BlockData fixFacesForHunk(BlockData b, Hunk<BlockData> hunk, int rX, int rZ,
int x, int y, int z, EngineMantle mantle) {
if (!B.isVineBlock(b)) {
return b;
}
MultipleFacing data = (MultipleFacing) b.clone();
data.getFaces().forEach(f -> data.setFace(f, false));
boolean found = false;
for (BlockFace f : BlockFace.values()) {
if (!f.isCartesian()) {
continue;
}
int yy = y + f.getModY();
BlockData r = null;
if (mantle != null) {
r = mantle.getMantle().get(x + f.getModX(), yy, z + f.getModZ(), BlockData.class);
}
if (r == null) {
r = EngineMantle.AIR;
}
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
found = true;
data.setFace(f, true);
continue;
}
int xx = rX + f.getModX();
int zz = rZ + f.getModZ();
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) {
continue;
}
r = hunk.get(xx, yy, zz);
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
found = true;
data.setFace(f, true);
}
}
if (!found) {
data.setFace(BlockFace.DOWN, true);
}
return data;
}
static boolean canGoOn(BlockData decorator, BlockData surface) {
return surface.isFaceSturdy(BlockFace.UP, BlockSupport.FULL);
}
private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
IrisData irisData, int max) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) {
stack = Math.min((int) Math.ceil((double) max * ((double) stack / 100)), decorator.getAbsoluteMaxStack());
} else {
stack = Math.min(max, stack);
}
return stack;
}
// Lazily populated on first dripstone decoration avoids Bukkit API at class-load time.
// Index: 0=TIP, 1=FRUSTUM, 2=BASE. Race on init is benign (only allocation cost, not correctness).
private static volatile BlockData[] dripstoneUp;
private static volatile BlockData[] dripstoneDown;
private static BlockData[] buildDripstoneArr(BlockFace direction) {
PointedDripstone.Thickness[] order = {
PointedDripstone.Thickness.TIP,
PointedDripstone.Thickness.FRUSTUM,
PointedDripstone.Thickness.BASE
};
BlockData[] arr = new BlockData[3];
for (int k = 0; k < 3; k++) {
BlockData bd = Material.POINTED_DRIPSTONE.createBlockData();
((PointedDripstone) bd).setThickness(order[k]);
((PointedDripstone) bd).setVerticalDirection(direction);
arr[k] = bd;
}
return arr;
}
private static BlockData dripstoneBlock(int stack, int i, BlockFace direction) {
int thIdx;
if (i == stack - 1) {
thIdx = 0;
} else if (i == stack - 2) {
thIdx = 1;
} else {
thIdx = 2;
}
if (direction == BlockFace.UP) {
if (dripstoneUp == null) {
dripstoneUp = buildDripstoneArr(BlockFace.UP);
}
return dripstoneUp[thIdx];
}
if (dripstoneDown == null) {
dripstoneDown = buildDripstoneArr(BlockFace.DOWN);
}
return dripstoneDown[thIdx];
}
}
@@ -1,55 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData;
/*
* Floating island decoration path. Bypasses all canGoOn, slope, whitelist, and blacklist
* gating from IrisSurfaceDecorator the island top IS the biome's designated surface by
* construction, so those material-compatibility checks are never meaningful here.
*/
public class FloatingDecorator {
public static int decorateColumn(Engine engine, IrisBiome target, IrisDecorationPart part,
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<BlockData> data, RNG rng,
Runnable candidatesNullCallback) {
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part));
IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ);
if (decorator == null) {
candidatesNullCallback.run();
return 0;
}
if (!decorator.isStacking()) {
DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData());
return max > 1 ? 1 : 0;
}
return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data, rng, engine.getData());
}
}
@@ -1,65 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData;
public class IrisCeilingDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisCeilingDecorator(Engine engine) {
super(engine, "Ceiling", IrisDecorationPart.CEILING);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.CEILING));
}
@BlockCoordinates
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (!decorator.isStacking()) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle());
return;
}
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
opts.caveSkipFluid = caveSkipFluid;
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, getEngine().getMinHeight(), data, rng, getData(), max, opts, getEngine().getMantle());
}
}
@@ -1,45 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
import lombok.Getter;
public abstract class IrisEngineDecorator extends EngineAssignedComponent implements EngineDecorator {
@Getter
private final IrisDecorationPart part;
public IrisEngineDecorator(Engine engine, String name, IrisDecorationPart part) {
super(engine, name + " Decorator");
this.part = part;
}
@BlockCoordinates
protected RNG getRNG(int x, int z) {
long seed = DecoratorCore.partSeed(getSeed(), part);
long modX = 29356788L ^ (part.ordinal() + 6);
long modZ = 10439677L ^ (part.ordinal() + 1);
return new RNG(x * modX + z * modZ + seed);
}
}
@@ -1,84 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData;
public class IrisSeaFloorDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisSeaFloorDecorator(Engine engine) {
super(engine, "Sea Floor", IrisDecorationPart.SEA_FLOOR);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_FLOOR));
}
@BlockCoordinates
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (!decorator.isStacking()) {
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
if (height >= 0 || height < getEngine().getHeight()) {
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
}
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
stack = Math.min(stack, max - height);
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
int engineHeight = getEngine().getHeight();
for (int i = 0; i < stack; i++) {
int h = height + i;
if (h > max || h > engineHeight) {
continue;
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h, z, threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
}
}
}
@@ -1,78 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData;
public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisSeaSurfaceDecorator(Engine engine) {
super(engine, "Sea Surface", IrisDecorationPart.SEA_SURFACE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SEA_SURFACE));
}
@BlockCoordinates
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (!decorator.isStacking()) {
if (height >= 0 || height < getEngine().getHeight()) {
data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
}
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
int engineHeight = getEngine().getHeight();
for (int i = 0; i < stack; i++) {
int h = height + i;
if (h >= max || h >= engineHeight) {
continue;
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h + 1, z, threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
}
}
}
@@ -1,93 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData;
public class IrisShoreLineDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisShoreLineDecorator(Engine engine) {
super(engine, "Shore Line", IrisDecorationPart.SHORE_LINE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.SHORE_LINE));
}
@BlockCoordinates
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
if (height != getDimension().getFluidHeight()) {
return;
}
double complexFluidHeight = getComplex().getFluidHeight();
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight
&& Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) {
return;
}
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null) {
return;
}
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
if (!decorator.isStacking()) {
data.set(x, height + 1, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
stack = Math.min(max - height, stack);
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
for (int i = 0; i < stack; i++) {
int h = height + i;
double threshold = ((double) i) / (stack - 1);
data.set(x, h + 1, z, threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
}
}
}
@@ -1,82 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.data.BlockData;
public class IrisSurfaceDecorator extends IrisEngineDecorator {
private final RNG partRNG;
public IrisSurfaceDecorator(Engine engine) {
super(engine, "Surface", IrisDecorationPart.NONE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE));
}
protected IrisSurfaceDecorator(Engine engine, String name) {
super(engine, name, IrisDecorationPart.NONE);
this.partRNG = new RNG(DecoratorCore.partSeed(getSeed(), IrisDecorationPart.NONE));
}
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) {
if (decorator.isForcePlace() || decorator.getSlopeCondition().isDefault()) {
return true;
}
return decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ));
}
@BlockCoordinates
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<BlockData> data, IrisBiome biome, int height, int max) {
int fluidHeight = getDimension().getFluidHeight();
if (biome.getInferredType().equals(InferredType.SHORE) && height < fluidHeight) {
return;
}
boolean underwater = height < fluidHeight && biome.getInferredType() != InferredType.CAVE;
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
if (decorator == null || !isSlopeValid(decorator, realX, realZ)) {
return;
}
if (decorator.isStacking()) {
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
opts.underwater = underwater;
opts.fluidHeight = fluidHeight;
opts.caveSkipFluid = caveSkipFluid;
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
return;
}
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
}
}
@@ -1,107 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.framework;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
public interface EngineMode extends Staged {
RollingSequence r = new RollingSequence(64);
RollingSequence r2 = new RollingSequence(256);
void close();
Engine getEngine();
default MultiBurst burst() {
return getEngine().burst();
}
default EngineStage burst(EngineStage... stages) {
return (x, z, blocks, biomes, multicore, ctx) -> {
BurstExecutor e = burst().burst(stages.length);
e.setMulticore(multicore);
for (EngineStage i : stages) {
e.queue(() -> i.generate(x, z, blocks, biomes, multicore, ctx));
}
e.complete();
};
}
default IrisComplex getComplex() {
return getEngine().getComplex();
}
default EngineMantle getMantle() {
return getEngine().getMantle();
}
default void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateMatter(x, z, multicore, context);
}
@BlockCoordinates
default void generate(int x, int z, Hunk<BlockData> blocks, Hunk<Biome> biomes, boolean multicore) {
IrisContext context = IrisContext.getOr(getEngine());
boolean cacheContext = true;
if (J.isFolia()) {
org.bukkit.World world = getEngine().getWorld().realWorld();
if (world != null && shouldDisableContextCacheForMaintenance(world)) {
cacheContext = false;
}
}
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
ChunkContext ctx = new ChunkContext(x, z, getComplex(), context.getGenerationSessionId(), cacheContext, prefillPlan, getEngine().getMetrics());
context.setChunkContext(ctx);
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
}
static boolean shouldDisableContextCacheForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
return maintenanceActive && !pregeneratorTargetsWorld;
}
private boolean shouldDisableContextCacheForMaintenance(org.bukkit.World world) {
boolean maintenanceActive = IrisToolbelt.isWorldMaintenanceActive(world);
if (!maintenanceActive) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
return shouldDisableContextCacheForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
@@ -1,18 +0,0 @@
package art.arcane.iris.engine.framework;
public class GenerationSessionException extends WrongEngineBroException {
private final boolean expectedTeardown;
public GenerationSessionException(String message) {
this(message, false);
}
public GenerationSessionException(String message, boolean expectedTeardown) {
super(message);
this.expectedTeardown = expectedTeardown;
}
public boolean isExpectedTeardown() {
return expectedTeardown;
}
}
@@ -1,35 +0,0 @@
package art.arcane.iris.engine.framework;
public final class GenerationSessionLease implements AutoCloseable {
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L);
private final GenerationSessionManager manager;
private final GenerationSessionManager.GenerationSessionState state;
private final long sessionId;
private boolean released;
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) {
this.manager = manager;
this.state = state;
this.sessionId = sessionId;
this.released = false;
}
public static GenerationSessionLease noop() {
return NOOP;
}
public long sessionId() {
return sessionId;
}
@Override
public void close() {
if (released || state == null) {
return;
}
released = true;
manager.releaseLease(state);
}
}
@@ -1,116 +0,0 @@
package art.arcane.iris.engine.framework;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public final class GenerationSessionManager {
private final AtomicLong sessionSequence;
private final AtomicReference<GenerationSessionState> current;
private final Object drainMonitor;
public GenerationSessionManager() {
this.sessionSequence = new AtomicLong(0L);
this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
this.drainMonitor = new Object();
}
public GenerationSessionLease acquire(String operation) throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || !state.accepting().get()) {
throw rejected(operation, state == null ? null : state);
}
state.activeLeases().incrementAndGet();
if (state != current.get()) {
state.activeLeases().decrementAndGet();
continue;
}
if (!state.accepting().get()) {
releaseLease(state);
throw rejected(operation, state);
}
return new GenerationSessionLease(this, state, state.sessionId());
}
}
public long currentSessionId() {
GenerationSessionState state = current.get();
return state == null ? 0L : state.sessionId();
}
public int activeLeases() {
GenerationSessionState state = current.get();
return state == null ? 0 : state.activeLeases().get();
}
public void sealAndAwait(String reason, long timeoutMs) throws GenerationSessionException {
sealAndAwait(reason, timeoutMs, false);
}
public void sealAndAwait(String reason, long timeoutMs, boolean teardown) throws GenerationSessionException {
GenerationSessionState state = current.get();
if (state == null) {
return;
}
state.accepting().set(false);
state.teardown().set(teardown);
state.sealReason().set(reason);
long deadline = System.currentTimeMillis() + Math.max(0L, timeoutMs);
synchronized (drainMonitor) {
while (state.activeLeases().get() > 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0L) {
break;
}
try {
drainMonitor.wait(remaining);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GenerationSessionException("Generation session " + state.sessionId() + " was interrupted while draining for " + reason + ".", teardown);
}
}
}
if (state.activeLeases().get() > 0) {
throw new GenerationSessionException("Generation session " + state.sessionId() + " failed to drain for " + reason + " after " + timeoutMs + "ms. Active leases=" + state.activeLeases().get() + ".", teardown);
}
}
public void activateNextSession() {
current.set(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
}
private long nextSessionId() {
return sessionSequence.incrementAndGet();
}
void releaseLease(GenerationSessionState state) {
int remaining = state.activeLeases().decrementAndGet();
if (remaining <= 0) {
synchronized (drainMonitor) {
drainMonitor.notifyAll();
}
}
}
private GenerationSessionException rejected(String operation, GenerationSessionState state) {
long sessionId = state == null ? currentSessionId() : state.sessionId();
boolean teardown = state != null && state.teardown().get();
String reason = state == null ? null : state.sealReason().get();
if (teardown && reason != null && !reason.isBlank()) {
return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + " during " + reason + ".", true);
}
return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + ".", teardown);
}
record GenerationSessionState(long sessionId, AtomicBoolean accepting, AtomicInteger activeLeases, AtomicBoolean teardown, AtomicReference<String> sealReason) {
}
}
@@ -1,282 +0,0 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.common.misc.RegenRuntime;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.block.data.BlockData;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
public interface MatterGenerator {
long REGEN_PASS_CACHE_TTL_MS = 600000L;
Executor DISPATCHER = MultiBurst.burst;
ConcurrentHashMap<String, Set<Long>> REGEN_GENERATED_CHUNKS_BY_PASS = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Set<Long>> REGEN_CLEARED_CHUNKS_BY_PASS = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Set<Long>> REGEN_PLANNED_CHUNKS_BY_PASS = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Long> REGEN_PASS_TOUCHED_MS = new ConcurrentHashMap<>();
Engine getEngine();
Mantle<Matter> getMantle();
int getRadius();
int getRealRadius();
List<Pair<List<MantleComponent>, Integer>> getComponents();
@ChunkCoordinates
default void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
if (!getEngine().getDimension().isUseMantle()) {
return;
}
boolean useMulticore = multicore || IrisSettings.get().getGenerator().isUseMulticoreMantle();
String threadName = Thread.currentThread().getName();
boolean regenThread = threadName.startsWith("Iris-Regen-");
boolean traceRegen = regenThread && IrisSettings.get().getGeneral().isDebug();
boolean forceRegen = regenThread;
String regenPassKey = forceRegen ? resolveRegenPassKey(threadName) : null;
boolean optimizedRegen = forceRegen && !IrisSettings.get().getGeneral().isDebug() && regenPassKey != null;
int writeRadius = optimizedRegen ? Math.min(getRadius(), getRealRadius()) : getRadius();
Set<Long> clearedChunks = optimizedRegen ? getRegenPassSet(REGEN_CLEARED_CHUNKS_BY_PASS, regenPassKey) : new HashSet<>();
Set<Long> partialChunks = forceRegen ? null : new HashSet<>();
Set<Long> plannedChunks = optimizedRegen ? getRegenPassSet(REGEN_PLANNED_CHUNKS_BY_PASS, regenPassKey) : null;
if (optimizedRegen) {
touchRegenPass(regenPassKey);
}
if (traceRegen) {
Iris.info("Regen matter start: center=" + x + "," + z
+ " radius=" + getRadius()
+ " realRadius=" + getRealRadius()
+ " writeRadius=" + writeRadius
+ " multicore=" + useMulticore
+ " components=" + getComponents().size()
+ " optimized=" + optimizedRegen
+ " passKey=" + (regenPassKey == null ? "none" : regenPassKey)
+ " thread=" + threadName);
}
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, useMulticore)) {
for (Pair<List<MantleComponent>, Integer> pair : getComponents()) {
int rawPassRadius = pair.getB();
int passRadius = optimizedRegen ? Math.min(rawPassRadius, writeRadius) : rawPassRadius;
String passFlags = pair.getA().stream().map(component -> component.getFlag().toString()).collect(Collectors.joining(","));
String passFlagKey = optimizedRegen ? regenPassKey + "|" + passFlags : null;
Set<Long> generatedChunks = passFlagKey == null ? null : getRegenPassSet(REGEN_GENERATED_CHUNKS_BY_PASS, passFlagKey);
int visitedChunks = 0;
int clearedCount = 0;
int plannedSkipped = 0;
int componentSkipped = 0;
int componentForcedReset = 0;
int launchedLayers = 0;
int dedupSkipped = 0;
List<CompletableFuture<Void>> launchedTasks = useMulticore ? new ArrayList<>() : null;
if (passFlagKey != null) {
touchRegenPass(passFlagKey);
}
if (traceRegen) {
Iris.info("Regen matter pass start: center=" + x + "," + z
+ " passRadius=" + passRadius
+ " rawPassRadius=" + rawPassRadius
+ " flags=[" + passFlags + "]");
}
for (int i = -passRadius; i <= passRadius; i++) {
for (int j = -passRadius; j <= passRadius; j++) {
int passX = x + i;
int passZ = z + j;
visitedChunks++;
long passKey = chunkKey(passX, passZ);
if (generatedChunks != null && !generatedChunks.add(passKey)) {
dedupSkipped++;
continue;
}
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
if (forceRegen) {
if (clearedChunks.add(passKey)) {
chunk.deleteSlices(BlockData.class);
chunk.deleteSlices(String.class);
chunk.deleteSlices(TileWrapper.class);
chunk.flag(MantleFlag.PLANNED, false);
clearedCount++;
}
}
if (!forceRegen && chunk.isFlagged(MantleFlag.PLANNED)) {
plannedSkipped++;
continue;
}
for (MantleComponent component : pair.getA()) {
if (!component.isEnabled()) {
continue;
}
boolean componentAlreadyGenerated = !forceRegen && chunk.isFlagged(component.getFlag());
if (componentAlreadyGenerated) {
componentSkipped++;
continue;
}
int componentRadius = component.getRadius();
if (componentRadius > 0) {
int componentPassRadius = Math.ceilDiv(componentRadius, 16);
if (Math.abs(i) > componentPassRadius || Math.abs(j) > componentPassRadius) {
partialChunks.add(passKey);
continue;
}
}
MantleFlag[] prerequisites = component.getPrerequisiteFlags();
if (prerequisites.length > 0) {
boolean prerequisitesMet = true;
for (MantleFlag prereq : prerequisites) {
if (!chunk.isFlagged(prereq)) {
prerequisitesMet = false;
break;
}
}
if (!prerequisitesMet) {
partialChunks.add(passKey);
continue;
}
}
if (forceRegen && chunk.isFlagged(component.getFlag())) {
chunk.flag(component.getFlag(), false);
componentForcedReset++;
}
launchedLayers++;
int finalPassX = passX;
int finalPassZ = passZ;
MantleChunk<Matter> finalChunk = chunk;
MantleComponent finalComponent = component;
Runnable task = () -> finalChunk.raiseFlagUnchecked(finalComponent.getFlag(),
() -> finalComponent.generateLayer(writer, finalPassX, finalPassZ, context));
if (useMulticore) {
launchedTasks.add(CompletableFuture.runAsync(task, DISPATCHER));
} else {
task.run();
}
}
}
}
if (useMulticore) {
for (CompletableFuture<Void> launchedTask : launchedTasks) {
launchedTask.join();
}
}
if (traceRegen) {
Iris.info("Regen matter pass done: center=" + x + "," + z
+ " passRadius=" + passRadius
+ " rawPassRadius=" + rawPassRadius
+ " visited=" + visitedChunks
+ " cleared=" + clearedCount
+ " dedupSkipped=" + dedupSkipped
+ " plannedSkipped=" + plannedSkipped
+ " componentSkipped=" + componentSkipped
+ " componentForcedReset=" + componentForcedReset
+ " launchedLayers=" + launchedLayers
+ " flags=[" + passFlags + "]");
}
}
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
for (int j = -getRealRadius(); j <= getRealRadius(); j++) {
int realX = x + i;
int realZ = z + j;
long realKey = chunkKey(realX, realZ);
if (plannedChunks != null && !plannedChunks.add(realKey)) {
continue;
}
if (partialChunks != null && partialChunks.contains(realKey)) {
continue;
}
writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true);
}
}
}
if (traceRegen) {
Iris.info("Regen matter done: center=" + x + "," + z
+ " markedRealRadius=" + getRealRadius()
+ " forceRegen=" + forceRegen);
}
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
private static Set<Long> getRegenPassSet(ConcurrentHashMap<String, Set<Long>> store, String passKey) {
return store.computeIfAbsent(passKey, key -> ConcurrentHashMap.newKeySet());
}
private static String resolveRegenPassKey(String threadName) {
String runtimeKey = RegenRuntime.getRunId();
if (runtimeKey != null && !runtimeKey.isBlank()) {
return runtimeKey;
}
if (!threadName.startsWith("Iris-Regen-")) {
return null;
}
String suffix = threadName.substring("Iris-Regen-".length());
int lastDash = suffix.lastIndexOf('-');
if (lastDash <= 0) {
return suffix;
}
return suffix.substring(0, lastDash);
}
private static void touchRegenPass(String passKey) {
long now = System.currentTimeMillis();
REGEN_PASS_TOUCHED_MS.put(passKey, now);
if (REGEN_PASS_TOUCHED_MS.size() <= 64) {
return;
}
Iterator<Map.Entry<String, Long>> iterator = REGEN_PASS_TOUCHED_MS.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = iterator.next();
if (now - entry.getValue() <= REGEN_PASS_CACHE_TTL_MS) {
continue;
}
String key = entry.getKey();
iterator.remove();
REGEN_GENERATED_CHUNKS_BY_PASS.remove(key);
REGEN_CLEARED_CHUNKS_BY_PASS.remove(key);
REGEN_PLANNED_CHUNKS_BY_PASS.remove(key);
}
}
}
@@ -1,260 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.util.common.data.B;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.Nullable;
public class IslandObjectPlacer implements IObjectPlacer {
private static final int OVERHANG_RADIUS = 2;
public enum AnchorFace { TOP, BOTTOM }
private final MantleWriter wrapped;
private final FloatingIslandSample[] samples;
private final boolean[] overhangAllowed;
private final int minX;
private final int minZ;
private final int chunkMaxIslandTopY;
private final int chunkMinIslandBottomY;
private final int anchorY;
private final AnchorFace face;
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorTopY) {
this(wrapped, samples, minX, minZ, anchorTopY, AnchorFace.TOP);
}
public IslandObjectPlacer(MantleWriter wrapped, FloatingIslandSample[] samples, int minX, int minZ, int anchorY, AnchorFace face) {
this.wrapped = wrapped;
this.samples = samples;
this.minX = minX;
this.minZ = minZ;
this.anchorY = anchorY;
this.face = face;
int maxTopY = -1;
int minBottomY = Integer.MAX_VALUE;
for (FloatingIslandSample s : samples) {
if (s != null) {
int ty = s.topY();
if (ty > maxTopY) {
maxTopY = ty;
}
int by = s.bottomY();
if (by >= 0 && by < minBottomY) {
minBottomY = by;
}
}
}
this.chunkMaxIslandTopY = maxTopY;
this.chunkMinIslandBottomY = (minBottomY == Integer.MAX_VALUE) ? -1 : minBottomY;
this.overhangAllowed = buildOverhangMask(samples);
}
private static boolean[] buildOverhangMask(FloatingIslandSample[] samples) {
boolean[] mask = new boolean[256];
for (int zf = 0; zf < 16; zf++) {
for (int xf = 0; xf < 16; xf++) {
int idx = (zf << 4) | xf;
if (samples[idx] != null) {
mask[idx] = true;
continue;
}
boolean touchedEdge = false;
boolean found = false;
for (int dz = -OVERHANG_RADIUS; dz <= OVERHANG_RADIUS && !found; dz++) {
int nzf = zf + dz;
for (int dx = -OVERHANG_RADIUS; dx <= OVERHANG_RADIUS; dx++) {
int nxf = xf + dx;
if (nxf < 0 || nxf >= 16 || nzf < 0 || nzf >= 16) {
touchedEdge = true;
continue;
}
if (samples[(nzf << 4) | nxf] != null) {
found = true;
break;
}
}
}
mask[idx] = found || touchedEdge;
}
}
return mask;
}
private boolean shouldSkipAirColumn(int x, int y, int z) {
int xf = x - minX;
int zf = z - minZ;
if (xf >= 0 && xf < 16 && zf >= 0 && zf < 16) {
int idx = (zf << 4) | xf;
if (samples[idx] != null) {
if (face == AnchorFace.TOP) {
return false;
}
if (y >= anchorY) {
return true;
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
return true;
}
if (!overhangAllowed[idx]) {
return true;
}
} else {
if (y >= anchorY) {
return true;
}
if (!overhangAllowed[idx]) {
return true;
}
}
return false;
}
if (face == AnchorFace.TOP) {
if (y <= anchorY) {
return true;
}
} else {
if (y >= anchorY) {
return true;
}
}
return true;
}
public boolean canWriteObjectBlock(int x, int y, int z) {
return !shouldSkipAirColumn(x, y, z);
}
private @Nullable FloatingIslandSample sampleAt(int x, int z) {
int xf = x - minX;
int zf = z - minZ;
if (xf < 0 || xf >= 16 || zf < 0 || zf >= 16) {
return null;
}
return samples[(zf << 4) | xf];
}
@Override
public int getHighest(int x, int z, IrisData data) {
FloatingIslandSample s = sampleAt(x, z);
if (face == AnchorFace.TOP) {
if (s != null) {
return s.topY();
}
return chunkMaxIslandTopY;
}
if (s != null) {
int by = s.bottomY();
return (by >= 0) ? by : chunkMinIslandBottomY;
}
return chunkMinIslandBottomY;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return getHighest(x, z, data);
}
@Override
public boolean isUnderwater(int x, int z) {
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
FloatingIslandSample s = sampleAt(x, z);
if (s != null) {
int idx = y - s.islandBaseY;
if (idx >= 0 && idx < s.solidMask.length) {
return s.solidMask[idx];
}
return false;
}
return wrapped.isSolid(x, y, z);
}
@Override
public boolean isCarved(int x, int y, int z) {
return wrapped.isCarved(x, y, z);
}
@Override
public void set(int x, int y, int z, BlockData d) {
if (shouldSkipAirColumn(x, y, z)) {
return;
}
wrapped.set(x, y, z, d);
}
@Override
public BlockData get(int x, int y, int z) {
return wrapped.get(x, y, z);
}
@Override
public boolean isPreventingDecay() {
return wrapped.isPreventingDecay();
}
@Override
public int getFluidHeight() {
return wrapped.getFluidHeight();
}
@Override
public boolean isDebugSmartBore() {
return wrapped.isDebugSmartBore();
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
if (shouldSkipAirColumn(xx, yy, zz)) {
return;
}
wrapped.setTile(xx, yy, zz, tile);
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
if (shouldSkipAirColumn(xx, yy, zz)) {
return;
}
wrapped.setData(xx, yy, zz, data);
}
@Override
public <T> @Nullable T getData(int xx, int yy, int zz, Class<T> t) {
return wrapped.getData(xx, yy, zz, t);
}
@Override
public Engine getEngine() {
return wrapped.getEngine();
}
}
@@ -1,637 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCaveProfile;
import art.arcane.iris.engine.object.IrisDimensionCarvingEntry;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@ComponentFlag(ReservedFlag.CARVED)
public class MantleCarvingComponent extends IrisMantleComponent {
private static final int CHUNK_SIZE = 16;
private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE;
private static final int BLEND_RADIUS = 3;
private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2);
private static final double MIN_WEIGHT = 0.08D;
private static final double THRESHOLD_PENALTY = 0.24D;
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1;
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
private static final int[] KERNEL_DZ = new int[KERNEL_SIZE];
private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE];
private static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
private final Map<IrisCaveProfile, IrisCaveCarver3D> profileCarvers = new IdentityHashMap<>();
static {
int kernelIndex = 0;
for (int offsetX = -BLEND_RADIUS; offsetX <= BLEND_RADIUS; offsetX++) {
for (int offsetZ = -BLEND_RADIUS; offsetZ <= BLEND_RADIUS; offsetZ++) {
KERNEL_DX[kernelIndex] = offsetX;
KERNEL_DZ[kernelIndex] = offsetZ;
int edgeDistance = Math.max(Math.abs(offsetX), Math.abs(offsetZ));
KERNEL_WEIGHT[kernelIndex] = (BLEND_RADIUS + 1D) - edgeDistance;
kernelIndex++;
}
}
}
public MantleCarvingComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.CARVED, 0);
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
BlendScratch blendScratch = BLEND_SCRATCH.get();
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
for (WeightedProfile weightedProfile : weightedProfiles) {
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
}
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
if (upperCtx != null && getDimension().isUpperDimensionCarving()) {
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights);
}
}
@ChunkCoordinates
private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, int[] chunkSurfaceHeights) {
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
}
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles, MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights) {
int chunkHeight = getEngineMantle().getEngine().getHeight();
int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight();
int gap = getDimension().getUpperDimensionGap();
int baseX = PowerOfTwoCoordinates.chunkToBlock(cx);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(cz);
int minUpperSurfaceY = chunkHeight;
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
int upperY = Math.max(rawUpper, lowerSurfaceHeights[columnIndex] + gap);
if (upperY < minUpperSurfaceY) {
minUpperSurfaceY = upperY;
}
}
}
if (minUpperSurfaceY >= chunkHeight - 2) {
return;
}
IrisRange upperYRange = new IrisRange(
minUpperSurfaceY + worldMinHeight,
chunkHeight - 1 + worldMinHeight
);
IrisRange fullVerticalRange = new IrisRange(0, chunkHeight);
int[] ceilingSurfaceHeights = new int[CHUNK_AREA];
Arrays.fill(ceilingSurfaceHeights, chunkHeight - 1);
for (WeightedProfile weightedProfile : normalProfiles) {
IrisRange constrainedRange;
if (weightedProfile.worldYRange != null) {
double min = Math.max(weightedProfile.worldYRange.getMin(), upperYRange.getMin());
double max = Math.min(weightedProfile.worldYRange.getMax(), upperYRange.getMax());
if (min >= max) {
continue;
}
constrainedRange = new IrisRange(min, max);
} else {
constrainedRange = upperYRange;
}
IrisCaveCarver3D carver = getCarver(weightedProfile.profile);
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY,
constrainedRange, ceilingSurfaceHeights, fullVerticalRange);
}
}
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState) {
BlendScratch blendScratch = BLEND_SCRATCH.get();
IrisCaveProfile[] profileField = blendScratch.profileField;
Map<IrisCaveProfile, double[]> columnProfileWeights = blendScratch.columnProfileWeights;
IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = blendScratch.activeProfiles;
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
activeProfiles.clear();
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int profileCount = 0;
int centerX = localX + BLEND_RADIUS;
int centerZ = localZ + BLEND_RADIUS;
double totalKernelWeight = 0D;
for (int kernelIndex = 0; kernelIndex < KERNEL_SIZE; kernelIndex++) {
int sampleX = centerX + KERNEL_DX[kernelIndex];
int sampleZ = centerZ + KERNEL_DZ[kernelIndex];
IrisCaveProfile profile = profileField[(sampleX * FIELD_SIZE) + sampleZ];
if (!isProfileEnabled(profile)) {
continue;
}
double kernelWeight = KERNEL_WEIGHT[kernelIndex];
int existingIndex = findProfileIndex(kernelProfiles, profileCount, profile);
if (existingIndex >= 0) {
kernelProfileWeights[existingIndex] += kernelWeight;
} else {
kernelProfiles[profileCount] = profile;
kernelProfileWeights[profileCount] = kernelWeight;
profileCount++;
}
totalKernelWeight += kernelWeight;
}
if (totalKernelWeight <= 0D || profileCount == 0) {
continue;
}
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
for (int profileIndex = 0; profileIndex < profileCount; profileIndex++) {
IrisCaveProfile profile = kernelProfiles[profileIndex];
double kernelWeight = kernelProfileWeights[profileIndex];
kernelProfiles[profileIndex] = null;
kernelProfileWeights[profileIndex] = 0D;
double columnWeight = clampWeight(kernelWeight / totalKernelWeight);
if (columnWeight < MIN_WEIGHT) {
continue;
}
double[] weights = columnProfileWeights.get(profile);
if (weights == null) {
weights = new double[CHUNK_AREA];
columnProfileWeights.put(profile, weights);
} else if (!activeProfiles.containsKey(profile)) {
Arrays.fill(weights, 0D);
}
activeProfiles.put(profile, Boolean.TRUE);
weights[columnIndex] = columnWeight;
}
}
}
List<WeightedProfile> columnWeightedProfiles = new ArrayList<>();
for (IrisCaveProfile profile : activeProfiles.keySet()) {
double[] weights = columnProfileWeights.get(profile);
if (weights == null) {
continue;
}
double totalWeight = 0D;
double maxWeight = 0D;
for (double weight : weights) {
totalWeight += weight;
if (weight > maxWeight) {
maxWeight = weight;
}
}
if (maxWeight < MIN_WEIGHT) {
continue;
}
double averageWeight = totalWeight / CHUNK_AREA;
columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null));
}
List<WeightedProfile> blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA);
List<WeightedProfile> resolvedProfiles = resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState, blendScratch);
resolvedProfiles.addAll(blendedProfiles);
return resolvedProfiles;
}
private List<WeightedProfile> resolveDimensionCarvingProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) {
List<WeightedProfile> weightedProfiles = new ArrayList<>();
List<IrisDimensionCarvingEntry> entries = getDimension().getCarving();
if (entries == null || entries.isEmpty()) {
return weightedProfiles;
}
Map<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = blendScratch.dimensionColumnPlans;
dimensionColumnPlans.clear();
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null || !entry.isEnabled()) {
continue;
}
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState);
if (rootBiome == null) {
continue;
}
IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]);
buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState);
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
IrisRange worldYRange = entry.getWorldYRange();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex];
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState);
if (resolvedBiome == null) {
continue;
}
IrisCaveProfile profile = resolvedBiome.getCaveProfile();
if (!isProfileEnabled(profile)) {
continue;
}
double[] columnWeights = rootProfileColumnWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]);
columnWeights[columnIndex] = 1D;
}
List<Map.Entry<IrisCaveProfile, double[]>> profileEntries = new ArrayList<>(rootProfileColumnWeights.entrySet());
profileEntries.sort((a, b) -> Integer.compare(a.getKey().hashCode(), b.getKey().hashCode()));
for (Map.Entry<IrisCaveProfile, double[]> profileEntry : profileEntries) {
weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), profileEntry.getValue(), -1D, worldYRange));
}
}
return weightedProfiles;
}
private void buildDimensionColumnPlan(IrisDimensionCarvingEntry[] columnPlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) {
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
columnPlan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState);
}
}
}
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) {
int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS;
int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS;
prefillProfileFieldSamples(startX, startZ, complex, blendScratch);
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int worldZ = startZ + fieldZ;
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
profileField[fieldIndex] = resolveColumnProfile(
worldX,
worldZ,
blendScratch.fieldSurfaceHeights[fieldIndex],
blendScratch.fieldRegions[fieldIndex],
blendScratch.fieldSurfaceBiomes[fieldIndex],
blendScratch.fieldCaveBiomes[fieldIndex],
resolverState
);
}
}
}
private int findProfileIndex(IrisCaveProfile[] profiles, int size, IrisCaveProfile profile) {
for (int index = 0; index < size; index++) {
if (profiles[index] == profile) {
return index;
}
}
return -1;
}
private IrisCaveProfile resolveColumnProfile(
int worldX,
int worldZ,
double surfaceHeight,
IrisRegion region,
IrisBiome surfaceBiome,
IrisBiome caveBiome,
IrisDimensionCarvingResolver.State resolverState
) {
IrisCaveProfile resolved = null;
IrisCaveProfile dimensionProfile = getDimension().getCaveProfile();
if (isProfileEnabled(dimensionProfile)) {
resolved = dimensionProfile;
}
if (region != null) {
IrisCaveProfile regionProfile = region.getCaveProfile();
if (isProfileEnabled(regionProfile)) {
resolved = regionProfile;
}
}
if (surfaceBiome != null) {
IrisCaveProfile surfaceProfile = surfaceBiome.getCaveProfile();
if (isProfileEnabled(surfaceProfile)) {
resolved = surfaceProfile;
}
}
int roundedSurfaceY = (int) Math.round(surfaceHeight);
int sampleY = Math.max(1, roundedSurfaceY - 56);
int worldY = sampleY + getEngineMantle().getEngine().getWorld().minHeight();
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(getEngineMantle().getEngine(), worldY, resolverState);
IrisBiome resolvedCarvingBiome = null;
if (rootCarvingEntry != null) {
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), rootCarvingEntry, worldX, worldZ, resolverState);
resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedCarvingEntry, resolverState);
if (resolvedCarvingBiome != null) {
caveBiome = resolvedCarvingBiome;
}
}
if (caveBiome != null) {
IrisBiome effectiveCaveBiome = caveBiome;
if (resolvedCarvingBiome == null && surfaceBiome != null) {
if (surfaceBiome != null) {
int truncatedSurfaceY = (int) surfaceHeight;
int depthBelowSurface = truncatedSurfaceY - sampleY;
if (depthBelowSurface <= 0) {
effectiveCaveBiome = surfaceBiome;
} else {
int minDepth = Math.max(0, caveBiome.getCaveMinDepthBelowSurface());
if (depthBelowSurface < minDepth) {
effectiveCaveBiome = surfaceBiome;
}
}
}
}
IrisCaveProfile caveProfile = effectiveCaveBiome.getCaveProfile();
if (isProfileEnabled(caveProfile)) {
resolved = caveProfile;
}
}
return resolved;
}
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
}
private <T> void fillFieldObjects(ProceduralStream<T> stream, int startX, int startZ, T[] target) {
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
target[(fieldX * FIELD_SIZE) + fieldZ] = stream.get(worldX, startZ + fieldZ);
}
}
}
private void fillFieldHeights(ProceduralStream<Double> stream, int startX, int startZ, double[] target) {
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
target[(fieldX * FIELD_SIZE) + fieldZ] = stream.getDouble(worldX, startZ + fieldZ);
}
}
}
private IrisCaveCarver3D getCarver(IrisCaveProfile profile) {
synchronized (profileCarvers) {
IrisCaveCarver3D carver = profileCarvers.get(profile);
if (carver != null) {
return carver;
}
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile);
profileCarvers.put(profile, createdCarver);
return createdCarver;
}
}
private boolean isProfileEnabled(IrisCaveProfile profile) {
return profile != null && profile.isEnabled();
}
protected int computeRadius() {
return 0;
}
private int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) {
int[] surfaceHeights = scratch;
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
boolean useContextHeight = context != null
&& context.getHeight() != null
&& context.getX() == baseX
&& context.getZ() == baseZ;
double[] cachedChunkHeights = null;
if (!useContextHeight && context != null) {
ProceduralStream<Double> heightStream = context.getComplex().getHeightStream();
if (heightStream instanceof ChunkFillableDoubleStream2D cachedHeightStream) {
cachedChunkHeights = BLEND_SCRATCH.get().chunkSurfaceHeightSamples;
cachedHeightStream.fillChunkDoubles(baseX, baseZ, cachedChunkHeights);
}
}
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
if (useContextHeight) {
surfaceHeights[columnIndex] = context.getRoundedHeight(localX, localZ);
continue;
}
if (cachedChunkHeights != null) {
surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[(localZ << 4) + localX]);
continue;
}
surfaceHeights[columnIndex] = getEngineMantle().getEngine().getHeight(worldX, worldZ);
}
}
return surfaceHeights;
}
private static List<WeightedProfile> limitAndMergeBlendedProfiles(List<WeightedProfile> blendedProfiles, int maxProfiles) {
return limitAndMergeBlendedProfiles(blendedProfiles, maxProfiles, CHUNK_AREA);
}
private static List<WeightedProfile> limitAndMergeBlendedProfiles(List<WeightedProfile> blendedProfiles, int maxProfiles, int areaSize) {
if (blendedProfiles == null || blendedProfiles.isEmpty()) {
return new ArrayList<>();
}
int clampedLimit = Math.max(1, maxProfiles);
List<WeightedProfile> rankedProfiles = new ArrayList<>(blendedProfiles);
rankedProfiles.sort(MantleCarvingComponent::compareBySelectionRank);
List<WeightedProfile> keptProfiles = new ArrayList<>();
int keptCount = Math.min(clampedLimit, rankedProfiles.size());
for (int index = 0; index < keptCount; index++) {
keptProfiles.add(rankedProfiles.get(index));
}
if (rankedProfiles.size() > keptCount) {
for (int columnIndex = 0; columnIndex < areaSize; columnIndex++) {
int dominantIndex = 0;
double dominantWeight = Double.NEGATIVE_INFINITY;
for (int keptIndex = 0; keptIndex < keptProfiles.size(); keptIndex++) {
double keptWeight = keptProfiles.get(keptIndex).columnWeights[columnIndex];
if (keptWeight > dominantWeight) {
dominantWeight = keptWeight;
dominantIndex = keptIndex;
}
}
double droppedWeight = 0D;
for (int droppedIndex = keptCount; droppedIndex < rankedProfiles.size(); droppedIndex++) {
droppedWeight += rankedProfiles.get(droppedIndex).columnWeights[columnIndex];
}
if (droppedWeight <= 0D) {
continue;
}
WeightedProfile dominantProfile = keptProfiles.get(dominantIndex);
double mergedWeight = dominantProfile.columnWeights[columnIndex] + droppedWeight;
dominantProfile.columnWeights[columnIndex] = clampWeight(mergedWeight);
}
}
List<WeightedProfile> mergedProfiles = new ArrayList<>();
for (WeightedProfile keptProfile : keptProfiles) {
double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize);
mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange));
}
mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder);
return mergedProfiles;
}
private static int compareBySelectionRank(WeightedProfile a, WeightedProfile b) {
int weightOrder = Double.compare(b.averageWeight, a.averageWeight);
if (weightOrder != 0) {
return weightOrder;
}
return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile));
}
private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) {
int weightOrder = Double.compare(a.averageWeight, b.averageWeight);
if (weightOrder != 0) {
return weightOrder;
}
return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile));
}
private static int profileSortKey(IrisCaveProfile profile) {
if (profile == null) {
return 0;
}
return profile.hashCode();
}
private static double computeAverageWeight(double[] weights) {
return computeAverageWeight(weights, CHUNK_AREA);
}
private static double computeAverageWeight(double[] weights, int areaSize) {
if (weights == null || weights.length == 0) {
return 0D;
}
double sum = 0D;
for (double weight : weights) {
sum += weight;
}
return sum / Math.max(1, areaSize);
}
private static double clampWeight(double value) {
if (Double.isNaN(value) || Double.isInfinite(value)) {
return 0D;
}
if (value <= 0D) {
return 0D;
}
if (value >= 1D) {
return 1D;
}
return value;
}
private static final class WeightedProfile {
private final IrisCaveProfile profile;
private final double[] columnWeights;
private final double averageWeight;
private final IrisRange worldYRange;
private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight, IrisRange worldYRange) {
this.profile = profile;
this.columnWeights = columnWeights;
this.averageWeight = averageWeight;
this.worldYRange = worldYRange;
}
private double averageWeight() {
return averageWeight;
}
}
private static final class BlendScratch {
private final IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE];
private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE];
private final double[] kernelProfileWeights = new double[KERNEL_SIZE];
private final IdentityHashMap<IrisCaveProfile, double[]> columnProfileWeights = new IdentityHashMap<>();
private final IdentityHashMap<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = new IdentityHashMap<>();
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
}
}
@@ -1,591 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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 art.arcane.iris.engine.mantle.components;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.FloatingObjectFootprint;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector;
import java.io.File;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
@ComponentFlag(ReservedFlag.FLOATING_OBJECT)
public class MantleFloatingObjectComponent extends IrisMantleComponent {
private static final int MIN_FOOTPRINT_CELLS_CHECKED = 3;
private static final int INVERTED_PICK_ATTEMPTS = 8;
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisData data = getData();
int chunkHeight = getEngineMantle().getEngine().getHeight();
int minX = x << 4;
int minZ = z << 4;
long baseSeed = getEngineMantle().getEngine().getSeedManager().getTerrain() ^ IrisFloatingChildBiomeModifier.FLOATING_BASE_SEED_SALT;
RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL);
FloatingIslandSample.clearChunkMemo();
FloatingIslandSample[] samples = new FloatingIslandSample[256];
for (int xf = 0; xf < 16; xf++) {
for (int zf = 0; zf < 16; zf++) {
int wx = minX + xf;
int wz = minZ + zf;
IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, getEngineMantle().getEngine());
if (sample != null) {
samples[(zf << 4) | xf] = sample;
}
}
}
IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> entryColumns = new IdentityHashMap<>();
IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> bottomEntryColumns = new IdentityHashMap<>();
for (int i = 0; i < 256; i++) {
FloatingIslandSample s = samples[i];
if (s == null || s.entry == null) {
continue;
}
entryColumns.computeIfAbsent(s.entry, e -> new KList<>()).add(i);
IrisFloatingChildBiomes bottomEntry = s.bottomEntry();
if (bottomEntry != null) {
bottomEntryColumns.computeIfAbsent(bottomEntry, e -> new KList<>()).add(i);
}
}
for (Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : entryColumns.entrySet()) {
IrisFloatingChildBiomes entry = ec.getKey();
KList<Integer> columns = ec.getValue();
if (columns.isEmpty()) {
continue;
}
IrisBiome parent = complex.getTrueBiomeStream().get(minX + (columns.get(0) & 15), minZ + (columns.get(0) >> 4));
IrisBiome target = entry.getRealBiome(parent, data);
KList<IrisObjectPlacement> floating = entry.getFloatingObjects();
if (floating != null && !floating.isEmpty()) {
for (IrisObjectPlacement placement : floating) {
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
}
}
KList<IrisObjectPlacement> surface = target != null ? entry.resolveTopObjects(target) : null;
KList<IrisObjectPlacement> extras = entry.getExtraObjects();
boolean hasSurface = surface != null && !surface.isEmpty();
boolean hasExtras = extras != null && !extras.isEmpty();
KList<Integer> interior = null;
if (hasSurface || hasExtras) {
interior = interiorColumns(samples, columns);
if (hasSurface) {
for (IrisObjectPlacement placement : surface) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
}
}
if (hasExtras) {
for (IrisObjectPlacement placement : extras) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
}
}
}
}
for (Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : bottomEntryColumns.entrySet()) {
IrisFloatingChildBiomes entry = ec.getKey();
KList<Integer> columns = ec.getValue();
if (columns.isEmpty()) {
continue;
}
IrisBiome parent = complex.getTrueBiomeStream().get(minX + (columns.get(0) & 15), minZ + (columns.get(0) >> 4));
IrisBiome target = entry.getRealBiome(parent, data);
KList<IrisObjectPlacement> bottom = target != null ? entry.resolveBottomObjects(target) : null;
if (bottom != null && !bottom.isEmpty()) {
KList<Integer> interior = interiorColumns(samples, columns);
for (IrisObjectPlacement placement : bottom) {
tryPlaceInvertedChunk(writer, complex, chunkRng, data, placement, samples, columns, interior, minX, minZ, entry);
}
}
}
}
@ChunkCoordinates
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns == null || columns.isEmpty()) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
continue;
}
}
final IrisObject obj = obj0;
int key = columns.get(rng.i(0, columns.size() - 1));
int xx = minX + (key & 15);
int zz = minZ + (key >> 4);
IrisObjectPlacement floatingPlacement = placement.toPlacement(obj.getLoadKey());
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null && shouldWritePlacementMarker(writer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
@ChunkCoordinates
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns.isEmpty()) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
continue;
}
}
final IrisObject obj = obj0;
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int pickedXf = pickedKey & 15;
int pickedZf = pickedKey >> 4;
FloatingIslandSample pickedSample = samples[(pickedZf << 4) | pickedXf];
if (pickedSample == null) {
continue;
}
int pickTopY = pickedSample.topY();
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 2)) {
if (!isFootprintFlat(fp, pickedXf, pickedZf, pickTopY, samples, 4)) {
continue;
}
}
int wx = minX + pickedXf - fp.getTallestKx();
int wz = minZ + pickedZf - fp.getTallestKz();
IrisObjectPlacement anchored = placement.toPlacement(obj.getLoadKey());
anchored.setMode(translateStiltModeForFloating(anchored.getMode()));
anchored.setTranslate(new IrisObjectTranslate());
anchored.setRotation(ROTATION_NONE);
anchored.setForcePlace(true);
anchored.setBottom(false);
int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickTopY);
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(wx, yv, wz, islandPlacer, anchored, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null
&& islandPlacer.canWriteObjectBlock(b.getX(), b.getY(), b.getZ())
&& shouldWritePlacementMarker(islandPlacer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
@ChunkCoordinates
private void tryPlaceInvertedChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, KList<Integer> interior, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns.isEmpty()) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
continue;
}
}
final IrisObject obj = obj0;
FloatingObjectFootprint fp = FloatingObjectFootprint.compute(obj);
int invertedYRotation = rng.i(0, 3) * 90;
IrisObjectRotation invertedRotation = IrisObjectRotation.xFlip180WithY(invertedYRotation);
KList<Integer> pool = interior.isEmpty() ? columns : interior;
int pickedXf = -1;
int pickedZf = -1;
int pickBottomY = -1;
boolean foundBottomAnchor = false;
for (int attempt = 0; attempt < INVERTED_PICK_ATTEMPTS; attempt++) {
int pickedKey = pool.get(rng.i(0, pool.size() - 1));
int candidateXf = pickedKey & 15;
int candidateZf = pickedKey >> 4;
FloatingIslandSample candidateSample = samples[(candidateZf << 4) | candidateXf];
if (candidateSample == null) {
continue;
}
int candidateBottomY = candidateSample.bottomY();
if (candidateBottomY < 0) {
continue;
}
if (!isFootprintFlatBottom(fp, invertedRotation, candidateXf, candidateZf, candidateBottomY, samples, 2)
&& !isFootprintFlatBottom(fp, invertedRotation, candidateXf, candidateZf, candidateBottomY, samples, 4)) {
continue;
}
pickedXf = candidateXf;
pickedZf = candidateZf;
pickBottomY = candidateBottomY;
foundBottomAnchor = true;
break;
}
if (!foundBottomAnchor) {
continue;
}
int wx = invertedBaseX(minX, pickedXf, fp, invertedRotation);
int wz = invertedBaseZ(minZ, pickedZf, fp, invertedRotation);
IrisObjectPlacement inverted = placement.toPlacement(obj.getLoadKey());
inverted.setMode(translateStiltModeForFloating(inverted.getMode()));
inverted.setTranslate(new IrisObjectTranslate());
inverted.setRotation(invertedRotation);
inverted.setForcePlace(true);
inverted.setBottom(false);
int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
IslandObjectPlacer islandPlacer = new IslandObjectPlacer(writer, samples, minX, minZ, pickBottomY, IslandObjectPlacer.AnchorFace.BOTTOM);
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(wx, yv, wz, islandPlacer, inverted, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null
&& islandPlacer.canWriteObjectBlock(b.getX(), b.getY(), b.getZ())
&& shouldWritePlacementMarker(islandPlacer, bd, b.getX(), b.getY(), b.getZ())) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
private static boolean isFootprintFlatBottom(FloatingObjectFootprint fp, IrisObjectRotation rotation, int pickedXf, int pickedZf, int pickBottomY, FloatingIslandSample[] samples, int tolerance) {
BlockVector anchor = invertedFootprintAnchor(fp, rotation);
int checked = 0;
boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ();
for (int i = 0, n = cells.length; i < n; i++) {
long encoded = cells[i];
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
BlockVector cell = rotation.rotate(new BlockVector(kx, 0, kz), 0, 0, 0);
int colXf = pickedXf + cell.getBlockX() - anchor.getBlockX();
int colZf = pickedZf + cell.getBlockZ() - anchor.getBlockZ();
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
}
FloatingIslandSample s = samples[(colZf << 4) | colXf];
if (s == null) {
return false;
}
int by = s.bottomY();
if (by < 0 || Math.abs(by - pickBottomY) > tolerance) {
return false;
}
checked++;
}
if (checked >= MIN_FOOTPRINT_CELLS_CHECKED) {
return true;
}
return touchedChunkEdge;
}
static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp) {
return invertedBaseX(minX, pickedXf, fp, IrisObjectRotation.xFlip180());
}
static int invertedBaseY(int pickBottomY, FloatingObjectFootprint fp) {
return invertedBaseY(pickBottomY, fp, IrisObjectRotation.xFlip180());
}
static int invertedBaseZ(int minZ, int pickedZf, FloatingObjectFootprint fp) {
return invertedBaseZ(minZ, pickedZf, fp, IrisObjectRotation.xFlip180());
}
static int invertedBaseX(int minX, int pickedXf, FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return minX + pickedXf - invertedFootprintAnchor(fp, rotation).getBlockX();
}
static int invertedBaseY(int pickBottomY, FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return pickBottomY - 1 - invertedSolidAnchor(fp, rotation).getBlockY();
}
static int invertedBaseZ(int minZ, int pickedZf, FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return minZ + pickedZf - invertedFootprintAnchor(fp, rotation).getBlockZ();
}
private static BlockVector invertedFootprintAnchor(FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return rotation.rotate(new BlockVector(fp.getTallestKx(), 0, fp.getTallestKz()), 0, 0, 0);
}
private static BlockVector invertedSolidAnchor(FloatingObjectFootprint fp, IrisObjectRotation rotation) {
return rotation.rotate(new BlockVector(fp.getTallestKx(), fp.getLowestSolidKeyY(), fp.getTallestKz()), 0, 0, 0);
}
private static boolean shouldWritePlacementMarker(IObjectPlacer placer, BlockData data, int x, int y, int z) {
if (data == null) {
return false;
}
BlockData existing = placer.get(x, y, z);
boolean wouldReplace = existing != null && B.isSolid(existing) && B.isVineBlock(data);
boolean placesBlock = !data.getMaterial().equals(Material.AIR) && !data.getMaterial().equals(Material.CAVE_AIR) && !wouldReplace;
return data instanceof IrisCustomData || placesBlock;
}
private static boolean isFootprintFlat(FloatingObjectFootprint fp, int pickedXf, int pickedZf, int pickTopY, FloatingIslandSample[] samples, int tolerance) {
int tallestKx = fp.getTallestKx();
int tallestKz = fp.getTallestKz();
int checked = 0;
boolean touchedChunkEdge = false;
long[] cells = fp.footprintXZ();
for (int i = 0, n = cells.length; i < n; i++) {
long encoded = cells[i];
int kx = (int) (encoded >> 32);
int kz = (int) (encoded & 0xFFFFFFFFL);
int colXf = pickedXf + (kx - tallestKx);
int colZf = pickedZf + (kz - tallestKz);
if (colXf < 0 || colXf >= 16 || colZf < 0 || colZf >= 16) {
touchedChunkEdge = true;
continue;
}
FloatingIslandSample s = samples[(colZf << 4) | colXf];
if (s == null || Math.abs(s.topY() - pickTopY) > tolerance) {
return false;
}
checked++;
}
if (checked >= MIN_FOOTPRINT_CELLS_CHECKED) {
return true;
}
return touchedChunkEdge;
}
private static KList<Integer> interiorColumns(FloatingIslandSample[] samples, KList<Integer> columns) {
KList<Integer> interior = new KList<>();
for (int key : columns) {
int xf = key & 15;
int zf = key >> 4;
if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) {
continue;
}
if (samples[(zf << 4) | (xf + 1)] == null) {
continue;
}
if (samples[(zf << 4) | (xf - 1)] == null) {
continue;
}
if (samples[((zf + 1) << 4) | xf] == null) {
continue;
}
if (samples[((zf - 1) << 4) | xf] == null) {
continue;
}
interior.add(key);
}
return interior;
}
private static String placementMarker(IrisObject object, int id) {
if (object == null) {
return null;
}
String key = object.getLoadKey();
if (key == null || key.isEmpty() || key.equals("null")) {
return null;
}
return key + "@" + id;
}
private static ObjectPlaceMode translateStiltModeForFloating(ObjectPlaceMode m) {
return switch (m) {
case STILT -> ObjectPlaceMode.MAX_HEIGHT;
case FAST_STILT -> ObjectPlaceMode.FAST_MAX_HEIGHT;
case MIN_STILT -> ObjectPlaceMode.MIN_HEIGHT;
case FAST_MIN_STILT -> ObjectPlaceMode.FAST_MIN_HEIGHT;
case CENTER_STILT -> ObjectPlaceMode.CENTER_HEIGHT;
case ERODE_STILT -> ObjectPlaceMode.MAX_HEIGHT;
case STRUCTURE_PIECE -> ObjectPlaceMode.CENTER_HEIGHT;
default -> m;
};
}
@Override
protected int computeRadius() {
int maxObjectExtent = 0;
Set<String> objectKeys = new HashSet<>();
try {
IrisData data = getData();
for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) {
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
if (entries == null || entries.isEmpty()) {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
collectPlacementKeys(entry.getFloatingObjects(), objectKeys);
collectPlacementKeys(entry.getExtraObjects(), objectKeys);
collectPlacementKeys(entry.getTopObjectOverrides(), objectKeys);
collectPlacementKeys(entry.getBottomObjectOverrides(), objectKeys);
try {
IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) {
collectPlacementKeys(entry.resolveTopObjects(target), objectKeys);
collectPlacementKeys(entry.resolveBottomObjects(target), objectKeys);
}
} catch (Throwable ignored) {
}
}
}
for (String key : objectKeys) {
try {
File f = data.getObjectLoader().findFile(key);
if (f == null) {
continue;
}
BlockVector sz = IrisObject.sampleSize(f);
int extent = Math.max(sz.getBlockX(), sz.getBlockZ());
if (extent > maxObjectExtent) {
maxObjectExtent = extent;
}
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return Math.max(16, maxObjectExtent);
}
private static void collectPlacementKeys(KList<IrisObjectPlacement> placements, Set<String> out) {
if (placements == null) {
return;
}
for (IrisObjectPlacement p : placements) {
if (p == null || p.getPlace() == null) {
continue;
}
out.addAll(p.getPlace());
}
}
}

Some files were not shown because too many files have changed in this diff Show More