mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-05-20 00:20:24 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d3edff459 | |||
| e06724fcf6 | |||
| b8219fac1b | |||
| bddc061f46 | |||
| bf6af9a58d | |||
| aaf2f2f8a6 | |||
| dc8cf0ad38 | |||
| bd07f5d325 | |||
| bd722fdacb | |||
| d5ec6a18a4 | |||
| 2f16c0cfb7 | |||
| f7ac827692 | |||
| bddc62f385 | |||
| 68a214edb5 | |||
| 49d2392c80 | |||
| fcbbd2135b | |||
| c3442ab2ce | |||
| fd3971018b | |||
| b440d0257d | |||
| 42a26a1de2 | |||
| c8eab22427 | |||
| fa3e35f702 | |||
| cf0bc81778 | |||
| bef99f18c3 | |||
| 96a384c09c | |||
| d61b2205c0 | |||
| ebdfb94392 | |||
| 1c5fe016cb | |||
| 0957b9baf2 | |||
| 7570064b1a | |||
| e461c1e199 | |||
| 35b879f0df | |||
| ba6fac5422 | |||
| 2577344ac0 |
+1
-3
@@ -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 |
@@ -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
@@ -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' }
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "2.0.20"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.ow2.asm:asm:9.8")
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
public class Config {
|
||||
public int jvm = 25;
|
||||
public NMSBinding.Type type = NMSBinding.Type.DIRECT;
|
||||
public String version;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.util.project.agent;
|
||||
package com.volmit.iris.util.agent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
Vendored
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
-376
@@ -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);
|
||||
}
|
||||
}
|
||||
-74
@@ -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();
|
||||
}
|
||||
}
|
||||
-144
@@ -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;
|
||||
}
|
||||
}
|
||||
-77
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
-637
@@ -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];
|
||||
}
|
||||
}
|
||||
-591
@@ -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
Reference in New Issue
Block a user