mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-05-20 08:30:37 +00:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9c7f35709 | |||
| 37893460f3 | |||
| 898f815878 | |||
| 49ee36b089 | |||
| 073be82dcc | |||
| 713c3a4762 | |||
| b736377aec | |||
| 62fff7a56e | |||
| 5efb71eb3e | |||
| d22f49492f | |||
| b65b112220 | |||
| c5456aa65c | |||
| 0a256eaa4c | |||
| 7d07ee4eb2 | |||
| c98ed48ee2 | |||
| 9f392654d3 | |||
| 613575c0c5 | |||
| b414b01ac4 | |||
| 65dd039b07 | |||
| 747adb53d9 | |||
| 7125b38fd5 | |||
| 82f71198e6 | |||
| b1e87afc93 | |||
| 3bffe4cc7e | |||
| 08ab82216d | |||
| c3ed7080dc | |||
| ea8fb1bf86 | |||
| 4434cf6475 | |||
| 26aae2b730 | |||
| e5c818cf7b | |||
| 6b4575e75d | |||
| 773be08b24 | |||
| 386131ddf0 | |||
| 3dfdb9654a | |||
| 805523d069 | |||
| 6cfa593eee | |||
| 00c2a5245a | |||
| f6791b786e | |||
| 0fa9654824 | |||
| 2262e19cd1 | |||
| 055ddc7c9b | |||
| 817d7a602b | |||
| 3af4a8f621 | |||
| 7b80eb1c06 | |||
| 19c6f4f2ba | |||
| 8a753b42f8 | |||
| 3f66634e5f | |||
| c86815f47b | |||
| f9d108dbb7 | |||
| 302e02ddac | |||
| f32f8744b2 | |||
| dd98f6f07e | |||
| bbf42d1af0 | |||
| 70aa607e5b | |||
| 09635e12a9 | |||
| 7b283a56ee | |||
| 888ba34eee | |||
| 62e98cc371 | |||
| 8fc70f42fc | |||
| 13447b882c | |||
| 344c50154a | |||
| ef93bee0b9 | |||
| 9de0c5b96f | |||
| a0a7b8cb3e | |||
| ae2600227e | |||
| ec1187923b | |||
| ab04a686e9 | |||
| efbfad437a | |||
| 9bcb1845b8 | |||
| 0f5364982d | |||
| 1b0411e23a | |||
| 29199dc2d2 | |||
| 3cb5f612c6 | |||
| 3b98b20f73 | |||
| 90bab2b292 | |||
| 8ad3cdf820 | |||
| ca8933541a | |||
| 8572a444fa | |||
| 8dd14c80f0 | |||
| 61410aea97 | |||
| f892eb599c | |||
| 86f89bc718 | |||
| 5be19c7c3c | |||
| 9d8be5b382 | |||
| ab30710e2a | |||
| d2ecbc5727 | |||
| 22f9306fa3 | |||
| 6b4a19a525 | |||
| ad8ff2643b | |||
| e6f829db31 | |||
| b429448885 | |||
| f00e037e26 | |||
| bad3cd27e1 | |||
| cad679a808 | |||
| 488b76d1d2 | |||
| 1e22a65329 | |||
| 1477dc037c | |||
| 6174ec04ab | |||
| 1cac86252f | |||
| 773065eb56 | |||
| a8524e43b9 | |||
| 0a62e222ee | |||
| ec9a000bcf | |||
| 0445b6fe6e | |||
| a0719117ad | |||
| 7ae846af6f | |||
| fe5bb67973 | |||
| 482fa9b11e | |||
| 295fe16f8f | |||
| c2ab688590 | |||
| 13c61501e6 | |||
| 5ad848fc54 | |||
| f6cf0682ed | |||
| 3cb74ac922 | |||
| dae3de8982 |
+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 17](https://www.oracle.com/java/technologies/javase/jdk17-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-17.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 17
|
||||
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`
|
||||
@@ -35,7 +35,7 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
|
||||
|
||||
### IDE Builds (for development)
|
||||
|
||||
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
|
||||
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||
prefer.
|
||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||
@@ -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();
|
||||
|
||||
+234
-270
@@ -1,270 +1,234 @@
|
||||
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' }
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2024 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
buildscript() {
|
||||
repositories {
|
||||
maven { url 'https://jitpack.io'}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.github.VolmitSoftware:NMSTools:1.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
id "io.github.goooler.shadow" version "8.1.7"
|
||||
id "de.undercouch.download" version "5.0.1"
|
||||
}
|
||||
|
||||
version '4.0-1.19.2-1.21.1'
|
||||
|
||||
// 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('Pixel', 'D://Iris Dimension Engine/1.20.4 - Development/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')
|
||||
// ==============================================================
|
||||
|
||||
def NMS_BINDINGS = Map.of(
|
||||
"v1_21_R1", "1.21-R0.1-SNAPSHOT",
|
||||
"v1_20_R4", "1.20.6-R0.1-SNAPSHOT",
|
||||
"v1_20_R3", "1.20.4-R0.1-SNAPSHOT",
|
||||
"v1_20_R2", "1.20.2-R0.1-SNAPSHOT",
|
||||
"v1_20_R1", "1.20.1-R0.1-SNAPSHOT",
|
||||
"v1_19_R3", "1.19.4-R0.1-SNAPSHOT",
|
||||
"v1_19_R2", "1.19.3-R0.1-SNAPSHOT",
|
||||
"v1_19_R1", "1.19.2-R0.1-SNAPSHOT"
|
||||
)
|
||||
def JVM_VERSION = Map.of(
|
||||
"v1_21_R1", 21,
|
||||
"v1_20_R4", 21,
|
||||
)
|
||||
def entryPoint = 'com.volmit.iris.server.EntryPoint'
|
||||
NMS_BINDINGS.each { nms ->
|
||||
project(":nms:${nms.key}") {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'com.volmit.nmstools'
|
||||
|
||||
nmsTools {
|
||||
it.jvm = JVM_VERSION.getOrDefault(nms.key, 17)
|
||||
it.version = nms.value
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
NMS_BINDINGS.each {
|
||||
dependsOn(":nms:${it.key}:remap")
|
||||
from("${project(":nms:${it.key}").layout.buildDirectory.asFile.get()}/libs/${it.key}-mapped.jar")
|
||||
}
|
||||
//dependsOn(':com.volmit.gui:build')
|
||||
|
||||
//minimize()
|
||||
append("plugin.yml")
|
||||
relocate 'com.dfsek.paralithic', 'com.volmit.iris.util.paralithic'
|
||||
relocate 'io.papermc.lib', 'com.volmit.iris.util.paper'
|
||||
relocate 'net.kyori', 'com.volmit.iris.util.kyori'
|
||||
archiveFileName.set("Iris-${project.version}.jar")
|
||||
|
||||
manifest {
|
||||
attributes 'Main-Class': entryPoint
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
resolutionStrategy.cacheChangingModulesFor 60, 'minutes'
|
||||
resolutionStrategy.cacheDynamicVersionsFor 60, 'minutes'
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.papermc.io/repository/maven-public/"}
|
||||
maven { url "https://repo.codemc.org/repository/maven-public" }
|
||||
maven { url "https://mvn.lumine.io/repository/maven-public/" }
|
||||
maven { url "https://jitpack.io"}
|
||||
|
||||
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
|
||||
maven { url "https://mvn.lumine.io/repository/maven/" }
|
||||
maven { url "https://repo.triumphteam.dev/snapshots" }
|
||||
maven { url "https://repo.mineinabyss.com/releases" }
|
||||
maven { url 'https://hub.jeff-media.com/nexus/repository/jeff-media-public/' }
|
||||
maven { url "https://repo.oraxen.com/releases" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Provided or Classpath
|
||||
compileOnly 'org.projectlombok:lombok:1.18.34'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.34'
|
||||
|
||||
// Shaded
|
||||
implementation 'com.dfsek:Paralithic:0.4.0'
|
||||
implementation 'io.papermc:paperlib:1.0.5'
|
||||
implementation "net.kyori:adventure-text-minimessage:4.17.0"
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
|
||||
implementation 'net.kyori:adventure-api:4.17.0'
|
||||
//implementation 'org.bytedeco:javacpp:1.5.10'
|
||||
//implementation 'org.bytedeco:cuda-platform:12.3-8.9-1.5.10'
|
||||
//implementation "org.deeplearning4j:deeplearning4j-core:1.0.0-M2.1"
|
||||
compileOnly 'io.lumine:Mythic-Dist:5.2.1'
|
||||
|
||||
// Dynamically Loaded
|
||||
compileOnly 'io.timeandspace:smoothie-map:2.0.2'
|
||||
compileOnly 'it.unimi.dsi:fastutil:8.5.8'
|
||||
compileOnly 'com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2'
|
||||
compileOnly 'org.zeroturnaround:zt-zip:1.14'
|
||||
compileOnly 'com.google.code.gson:gson:2.10.1'
|
||||
compileOnly 'org.ow2.asm:asm:9.2'
|
||||
compileOnly 'com.google.guava:guava:33.0.0-jre'
|
||||
compileOnly 'bsf:bsf:2.4.0'
|
||||
compileOnly 'rhino:js:1.7R2'
|
||||
compileOnly 'com.github.ben-manes.caffeine:caffeine:3.0.6'
|
||||
compileOnly 'org.apache.commons:commons-lang3:3.12.0'
|
||||
compileOnly 'net.bytebuddy:byte-buddy:1.14.14'
|
||||
compileOnly 'net.bytebuddy:byte-buddy-agent:1.12.8'
|
||||
compileOnly 'org.bytedeco:javacpp:1.5.10'
|
||||
compileOnly 'org.bytedeco:cuda-platform:12.3-8.9-1.5.10'
|
||||
compileOnly 'io.netty:netty-all:4.1.112.Final'
|
||||
}
|
||||
|
||||
/**
|
||||
* We need parameter meta for the decree command system
|
||||
*/
|
||||
compileJava {
|
||||
options.compilerArgs << '-parameters'
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
}
|
||||
|
||||
if (JavaVersion.current().toString() != "17") {
|
||||
System.err.println()
|
||||
System.err.println("=========================================================================================================")
|
||||
System.err.println("You must run gradle on Java 17. You are using " + JavaVersion.current())
|
||||
System.err.println()
|
||||
System.err.println("=== For IDEs ===")
|
||||
System.err.println("1. Configure the project for Java 17")
|
||||
System.err.println("2. Configure the bundled gradle to use Java 17 in settings")
|
||||
System.err.println()
|
||||
System.err.println("=== For Command Line (gradlew) ===")
|
||||
System.err.println("1. Install JDK 17 from https://www.oracle.com/java/technologies/javase/jdk17-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-17.0.1")
|
||||
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);
|
||||
}
|
||||
|
||||
task iris(type: Copy) {
|
||||
group "iris"
|
||||
from new File(layout.buildDirectory.asFile.get(), "libs/Iris-${version}.jar")
|
||||
into layout.buildDirectory.asFile.get()
|
||||
dependsOn(build)
|
||||
}
|
||||
|
||||
def registerCustomOutputTask(name, path) {
|
||||
if (!System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
return;
|
||||
}
|
||||
|
||||
tasks.register('build' + name, Copy) {
|
||||
group('development')
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn(iris)
|
||||
from(new File(buildDir, "Iris-" + version + ".jar"))
|
||||
into(file(path))
|
||||
rename { String fileName ->
|
||||
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def registerCustomOutputTaskUnix(name, path) {
|
||||
if (System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
return;
|
||||
}
|
||||
|
||||
tasks.register('build' + name, Copy) {
|
||||
group('development')
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn(iris)
|
||||
from(new File(buildDir, "Iris-" + version + ".jar"))
|
||||
into(file(path))
|
||||
rename { String fileName ->
|
||||
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.build.dependsOn(shadowJar)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package art.arcane.iris.util.project.agent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
public class Installer {
|
||||
private static volatile Instrumentation instrumentation;
|
||||
|
||||
public static Instrumentation getInstrumentation() {
|
||||
Instrumentation instrumentation = Installer.instrumentation;
|
||||
if (instrumentation == null) {
|
||||
throw new IllegalStateException("The agent is not loaded or this method is not called via the system class loader");
|
||||
}
|
||||
return instrumentation;
|
||||
}
|
||||
|
||||
public static void premain(String arguments, Instrumentation instrumentation) {
|
||||
doMain(instrumentation);
|
||||
}
|
||||
|
||||
public static void agentmain(String arguments, Instrumentation instrumentation) {
|
||||
doMain(instrumentation);
|
||||
}
|
||||
|
||||
private static synchronized void doMain(Instrumentation instrumentation) {
|
||||
if (Installer.instrumentation != null)
|
||||
return;
|
||||
Installer.instrumentation = instrumentation;
|
||||
}
|
||||
}
|
||||
+48
-254
@@ -1,18 +1,6 @@
|
||||
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)
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2024 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -31,22 +19,24 @@ import java.net.URI
|
||||
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)
|
||||
id "io.freefair.lombok" version "8.6"
|
||||
}
|
||||
|
||||
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()
|
||||
def apiVersion = '1.19'
|
||||
def main = 'com.volmit.iris.Iris'
|
||||
|
||||
/**
|
||||
* We need parameter meta for the decree command system
|
||||
*/
|
||||
compileJava {
|
||||
options.compilerArgs << '-parameters'
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url 'https://nexus.phoenixdevt.fr/repository/maven-public/' }
|
||||
maven { url 'https://repo.auxilor.io/repository/maven-public/' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependencies.
|
||||
@@ -62,237 +52,41 @@ boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank
|
||||
*/
|
||||
dependencies {
|
||||
// Provided or Classpath
|
||||
compileOnly(libs.spigot)
|
||||
compileOnly(libs.log4j.api)
|
||||
compileOnly(libs.log4j.core)
|
||||
compileOnly 'org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.apache.logging.log4j:log4j-api:2.19.0'
|
||||
compileOnly 'org.apache.logging.log4j:log4j-core:2.19.0'
|
||||
compileOnly 'commons-io:commons-io:2.13.0'
|
||||
compileOnly 'commons-lang:commons-lang:2.6'
|
||||
compileOnly 'com.github.oshi:oshi-core:5.8.5'
|
||||
compileOnly 'org.lz4:lz4-java:1.8.0'
|
||||
|
||||
// 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)
|
||||
compileOnly 'com.ticxo.playeranimator:PlayerAnimator:R1.2.7'
|
||||
compileOnly 'io.th0rgal:oraxen:1.173.0'
|
||||
compileOnly 'com.github.LoneDev6:api-itemsadder:3.4.1-r4'
|
||||
compileOnly 'com.github.PlaceholderAPI:placeholderapi:2.11.3'
|
||||
compileOnly 'com.github.Ssomar-Developement:SCore:4.23.10.8'
|
||||
compileOnly 'net.Indyuce:MMOItems-API:6.9.5-SNAPSHOT'
|
||||
compileOnly 'com.willfp:EcoItems:5.44.0'
|
||||
//implementation files('libs/CustomItems.jar')
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget('25'))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Gradle is weird sometimes, we need to delete the plugin yml from the build folder to actually filter properly.
|
||||
*/
|
||||
file(jar.archiveFile.get().getAsFile().getParentFile().getParentFile().getParentFile().getAbsolutePath() + '/build/resources/main/plugin.yml').delete()
|
||||
|
||||
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)
|
||||
/**
|
||||
* Expand properties into plugin yml
|
||||
*/
|
||||
processResources {
|
||||
filesMatching('**/plugin.yml') {
|
||||
expand(pluginProperties)
|
||||
expand(
|
||||
'name': rootProject.name.toString(),
|
||||
'version': rootProject.version.toString(),
|
||||
'main': main.toString(),
|
||||
'apiversion': apiVersion.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class IrisWorlds {
|
||||
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType();
|
||||
private final KMap<String, String> worlds;
|
||||
private volatile boolean dirty = false;
|
||||
|
||||
private IrisWorlds(KMap<String, String> worlds) {
|
||||
this.worlds = worlds;
|
||||
readBukkitWorlds().forEach(this::put0);
|
||||
save();
|
||||
}
|
||||
|
||||
public static IrisWorlds get() {
|
||||
return cache.aquire(() -> {
|
||||
File file = Iris.instance.getDataFile("worlds.json");
|
||||
if (!file.exists()) {
|
||||
return new IrisWorlds(new KMap<>());
|
||||
}
|
||||
|
||||
try {
|
||||
String json = IO.readAll(file);
|
||||
KMap<String, String> worlds = GSON.fromJson(json, TYPE);
|
||||
return new IrisWorlds(Objects.requireNonNullElseGet(worlds, KMap::new));
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to load worlds.json!");
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return new IrisWorlds(new KMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
public void put(String name, String type) {
|
||||
put0(name, type);
|
||||
save();
|
||||
}
|
||||
|
||||
private void put0(String name, String type) {
|
||||
String old = worlds.put(name, type);
|
||||
if (!type.equals(old))
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
public KMap<String, String> getWorlds() {
|
||||
clean();
|
||||
return readBukkitWorlds().put(worlds);
|
||||
}
|
||||
|
||||
public Stream<IrisData> getPacks() {
|
||||
return getDimensions()
|
||||
.map(IrisDimension::getLoader)
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public Stream<IrisDimension> getDimensions() {
|
||||
return getWorlds()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> Iris.loadDimension(entry.getKey(), entry.getValue()))
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public void clean() {
|
||||
dirty = worlds.entrySet().removeIf(entry -> !new File(Bukkit.getWorldContainer(), entry.getKey() + "/iris/pack/dimensions/" + entry.getValue() + ".json").exists());
|
||||
}
|
||||
|
||||
public synchronized void save() {
|
||||
clean();
|
||||
if (!dirty) return;
|
||||
try {
|
||||
IO.write(Iris.instance.getDataFile("worlds.json"), OutputStreamWriter::new, writer -> GSON.toJson(worlds, TYPE, writer));
|
||||
dirty = false;
|
||||
} catch (IOException e) {
|
||||
Iris.error("Failed to save worlds.json!");
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static KMap<String, String> readBukkitWorlds() {
|
||||
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
var worlds = bukkit.getConfigurationSection("worlds");
|
||||
if (worlds == null) return new KMap<>();
|
||||
|
||||
var result = new KMap<String, String>();
|
||||
for (String world : worlds.getKeys(false)) {
|
||||
var gen = worlds.getString(world + ".generator");
|
||||
if (gen == null) continue;
|
||||
|
||||
String loadKey;
|
||||
if (gen.equalsIgnoreCase("iris")) {
|
||||
loadKey = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
} else if (gen.startsWith("Iris:")) {
|
||||
loadKey = gen.substring(5);
|
||||
} else continue;
|
||||
|
||||
result.put(world, loadKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,454 +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;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
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 lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ServerConfigurator {
|
||||
private static volatile boolean deferredInstallPending = false;
|
||||
|
||||
public static void configure() {
|
||||
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
||||
if (s.isConfigureSpigotTimeoutTime()) {
|
||||
J.attempt(ServerConfigurator::increaseKeepAliveSpigot);
|
||||
}
|
||||
|
||||
if (s.isConfigurePaperWatchdogDelay()) {
|
||||
J.attempt(ServerConfigurator::increasePaperWatchdog);
|
||||
}
|
||||
|
||||
if (shouldDeferInstallUntilWorldsReady()) {
|
||||
deferredInstallPending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
deferredInstallPending = false;
|
||||
installDataPacks(true);
|
||||
}
|
||||
|
||||
public static void configureIfDeferred() {
|
||||
if (!deferredInstallPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
configure();
|
||||
if (deferredInstallPending) {
|
||||
J.a(ServerConfigurator::configureIfDeferred, 20);
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException {
|
||||
File spigotConfig = new File("spigot.yml");
|
||||
FileConfiguration f = new YamlConfiguration();
|
||||
f.load(spigotConfig);
|
||||
long tt = f.getLong("settings.timeout-time");
|
||||
|
||||
long spigotTimeout = TimeUnit.MINUTES.toSeconds(5);
|
||||
|
||||
if (tt < spigotTimeout) {
|
||||
Iris.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)");
|
||||
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||
f.set("settings.timeout-time", spigotTimeout);
|
||||
f.save(spigotConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
|
||||
File spigotConfig = new File("config/paper-global.yml");
|
||||
FileConfiguration f = new YamlConfiguration();
|
||||
f.load(spigotConfig);
|
||||
long tt = f.getLong("watchdog.early-warning-delay");
|
||||
|
||||
long watchdog = TimeUnit.MINUTES.toMillis(3);
|
||||
if (tt < watchdog) {
|
||||
Iris.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)");
|
||||
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||
f.set("watchdog.early-warning-delay", watchdog);
|
||||
f.save(spigotConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private static KList<File> getDatapacksFolder() {
|
||||
if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) {
|
||||
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
|
||||
}
|
||||
KList<File> worlds = new KList<>();
|
||||
Bukkit.getServer().getWorlds().forEach(w -> {
|
||||
File folder = resolveDatapacksFolder(w.getWorldFolder());
|
||||
if (!worlds.contains(folder)) {
|
||||
worlds.add(folder);
|
||||
}
|
||||
});
|
||||
if (worlds.isEmpty()) {
|
||||
worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
|
||||
}
|
||||
return worlds;
|
||||
}
|
||||
|
||||
public static boolean installDataPacks(boolean fullInstall) {
|
||||
IDataFixer fixer = DataVersion.getDefault();
|
||||
if (fixer == null) {
|
||||
DataVersion fallback = DataVersion.getLatest();
|
||||
Iris.warn("Primary datapack fixer was null, forcing latest fixer: " + fallback.getVersion());
|
||||
fixer = fallback.get();
|
||||
}
|
||||
return installDataPacks(fixer, fullInstall);
|
||||
}
|
||||
|
||||
public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) {
|
||||
if (fixer == null) {
|
||||
Iris.error("Unable to install datapacks, fixer is null!");
|
||||
return false;
|
||||
}
|
||||
if (fullInstall) {
|
||||
Iris.info("Checking Data Packs...");
|
||||
} else {
|
||||
Iris.verbose("Checking Data Packs...");
|
||||
}
|
||||
DimensionHeight height = new DimensionHeight(fixer);
|
||||
KList<File> folders = getDatapacksFolder();
|
||||
java.util.concurrent.ConcurrentMap<String, KSet<String>> biomes = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
try (Stream<IrisData> stream = allPacks()) {
|
||||
stream.flatMap(height::merge)
|
||||
.parallel()
|
||||
.forEach(dim -> {
|
||||
Iris.verbose(" Checking Dimension " + dim.getLoadFile().getPath());
|
||||
dim.installBiomes(fixer, dim::getLoader, folders, biomes.computeIfAbsent(dim.getLoadKey(), k -> new KSet<>()));
|
||||
dim.installDimensionType(fixer, folders);
|
||||
});
|
||||
}
|
||||
IrisDimension.writeShared(folders, height);
|
||||
if (fullInstall) {
|
||||
Iris.info("Data Packs Setup!");
|
||||
} else {
|
||||
Iris.verbose("Data Packs Setup!");
|
||||
}
|
||||
|
||||
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
|
||||
}
|
||||
|
||||
public static boolean installDataPacksIfChanged(boolean fullInstall) {
|
||||
File packsDir = Iris.instance.getDataFolder("packs");
|
||||
String current = computePackFingerprint(packsDir);
|
||||
File cacheFile = new File(Iris.instance.getDataFolder("cache"), "datapack-fingerprint");
|
||||
String cached = "";
|
||||
if (cacheFile.exists()) {
|
||||
try {
|
||||
cached = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8).trim();
|
||||
} catch (IOException e) {
|
||||
cached = "";
|
||||
}
|
||||
}
|
||||
if (!current.isEmpty() && current.equals(cached)) {
|
||||
Iris.verbose("Data packs unchanged, skipping install.");
|
||||
return false;
|
||||
}
|
||||
boolean result = installDataPacks(fullInstall);
|
||||
try {
|
||||
cacheFile.getParentFile().mkdirs();
|
||||
Files.writeString(cacheFile.toPath(), current, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
Iris.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String computePackFingerprint(File packsDir) {
|
||||
if (packsDir == null || !packsDir.isDirectory()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
List<String> entries = new ArrayList<>();
|
||||
collectFingerprintEntries(packsDir, packsDir.getAbsolutePath(), entries);
|
||||
Collections.sort(entries);
|
||||
for (String entry : entries) {
|
||||
digest.update(entry.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
byte[] hash = digest.digest();
|
||||
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectFingerprintEntries(File dir, String rootPath, List<String> entries) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
collectFingerprintEntries(file, rootPath, entries);
|
||||
} else {
|
||||
String relative = file.getAbsolutePath().substring(rootPath.length());
|
||||
entries.add(relative + "|" + file.length() + "|" + file.lastModified());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldDeferInstallUntilWorldsReady() {
|
||||
String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld;
|
||||
if (forcedMainWorld != null && !forcedMainWorld.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Bukkit.getServer().getWorlds().isEmpty();
|
||||
}
|
||||
|
||||
public static File resolveDatapacksFolder(File worldFolder) {
|
||||
File rootFolder = resolveWorldRootFolder(worldFolder);
|
||||
return new File(rootFolder, "datapacks");
|
||||
}
|
||||
|
||||
static File resolveWorldRootFolder(File worldFolder) {
|
||||
if (worldFolder == null) {
|
||||
return new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME);
|
||||
}
|
||||
|
||||
File current = worldFolder.getAbsoluteFile();
|
||||
while (current != null) {
|
||||
if ("dimensions".equals(current.getName())) {
|
||||
File parent = current.getParentFile();
|
||||
if (parent != null) {
|
||||
return parent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.getParentFile();
|
||||
}
|
||||
|
||||
return worldFolder.getAbsoluteFile();
|
||||
}
|
||||
|
||||
private static boolean verifyDataPacksPost(boolean allowRestarting) {
|
||||
try (Stream<IrisData> stream = allPacks()) {
|
||||
boolean bad = stream
|
||||
.map(data -> {
|
||||
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
|
||||
var loader = data.getDimensionLoader();
|
||||
return loader.loadAll(loader.getPossibleKeys())
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ServerConfigurator::verifyDataPackInstalled)
|
||||
.toList()
|
||||
.contains(false);
|
||||
})
|
||||
.toList()
|
||||
.contains(true);
|
||||
if (!bad) return false;
|
||||
}
|
||||
|
||||
|
||||
if (allowRestarting) {
|
||||
restart();
|
||||
} else if (INMS.get().supportsDataPacks()) {
|
||||
Iris.error("============================================================================");
|
||||
Iris.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
|
||||
Iris.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
|
||||
Iris.error("----------------------------------------------------------------------------");
|
||||
Iris.error(C.UNDERLINE + "IT IS HIGHLY RECOMMENDED YOU RESTART THE SERVER BEFORE GENERATING!");
|
||||
Iris.error("============================================================================");
|
||||
|
||||
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||
if (i.isOp() || i.hasPermission("iris.all")) {
|
||||
VolmitSender sender = new VolmitSender(i, Iris.instance.getTag("WARNING"));
|
||||
sender.sendMessage("There are some Iris Packs that have custom biomes in them");
|
||||
sender.sendMessage("You need to restart your server to use these packs.");
|
||||
}
|
||||
}
|
||||
|
||||
J.sleep(3000);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void restart() {
|
||||
J.s(() -> {
|
||||
Iris.warn("New data pack entries have been installed in Iris! Restarting server!");
|
||||
Iris.warn("This will only happen when your pack changes (updates/first time setup)");
|
||||
Iris.warn("(You can disable this auto restart in iris settings)");
|
||||
J.s(() -> {
|
||||
Iris.warn("Looks like the restart command didn't work. Stopping the server instead!");
|
||||
Bukkit.shutdown();
|
||||
}, 100);
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
||||
KSet<String> keys = new KSet<>();
|
||||
boolean warn = false;
|
||||
|
||||
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
keys.add(dimension.getLoadKey() + ":" + j.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
String key = getWorld(dimension.getLoader());
|
||||
if (key == null) key = dimension.getLoadKey();
|
||||
else key += "/" + dimension.getLoadKey();
|
||||
|
||||
if (!INMS.get().supportsDataPacks()) {
|
||||
if (!keys.isEmpty()) {
|
||||
Iris.warn("===================================================================================");
|
||||
Iris.warn("Pack " + key + " has " + keys.size() + " custom biome(s). ");
|
||||
Iris.warn("Your server version does not yet support datapacks for iris.");
|
||||
Iris.warn("The world will generate these biomes as backup biomes.");
|
||||
Iris.warn("====================================================================================");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String i : keys) {
|
||||
Object o = INMS.get().getCustomBiomeBaseFor(i);
|
||||
|
||||
if (o == null) {
|
||||
Iris.warn("The Biome " + i + " is not registered on the server.");
|
||||
warn = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
|
||||
Iris.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
|
||||
warn = true;
|
||||
}
|
||||
|
||||
if (warn) {
|
||||
Iris.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
|
||||
Iris.error("If not done automatically, restart your server before generating with this pack!");
|
||||
}
|
||||
|
||||
return !warn;
|
||||
}
|
||||
|
||||
public static Stream<IrisData> allPacks() {
|
||||
File[] packs = Iris.instance.getDataFolder("packs").listFiles(File::isDirectory);
|
||||
Stream<File> locals = packs == null ? Stream.empty() : Arrays.stream(packs);
|
||||
return Stream.concat(locals
|
||||
.filter( base -> {
|
||||
var content = new File(base, "dimensions").listFiles();
|
||||
return content != null && content.length > 0;
|
||||
})
|
||||
.map(IrisData::get), IrisWorlds.get().getPacks());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getWorld(@NonNull IrisData data) {
|
||||
String worldContainer = Bukkit.getWorldContainer().getAbsolutePath();
|
||||
if (!worldContainer.endsWith(File.separator)) worldContainer += File.separator;
|
||||
|
||||
String path = data.getDataFolder().getAbsolutePath();
|
||||
if (!path.startsWith(worldContainer)) return null;
|
||||
int l = path.endsWith(File.separator) ? 11 : 10;
|
||||
return path.substring(worldContainer.length(), path.length() - l);
|
||||
}
|
||||
|
||||
public static class DimensionHeight {
|
||||
private final IDataFixer fixer;
|
||||
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
||||
|
||||
public DimensionHeight(IDataFixer fixer) {
|
||||
this.fixer = fixer;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
dimensions[i] = new AtomicIntegerArray(new int[]{
|
||||
Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<IrisDimension> merge(IrisData data) {
|
||||
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
|
||||
var loader = data.getDimensionLoader();
|
||||
return loader.loadAll(loader.getPossibleKeys())
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.peek(this::merge);
|
||||
}
|
||||
|
||||
public void merge(IrisDimension dimension) {
|
||||
AtomicIntegerArray array = dimensions[dimension.getBaseDimension().ordinal()];
|
||||
array.updateAndGet(0, min -> Math.min(min, dimension.getMinHeight()));
|
||||
array.updateAndGet(1, max -> Math.max(max, dimension.getMaxHeight()));
|
||||
array.updateAndGet(2, logical -> Math.max(logical, dimension.getLogicalHeight()));
|
||||
}
|
||||
|
||||
public String[] jsonStrings() {
|
||||
var dims = IDataFixer.Dimension.values();
|
||||
var arr = new String[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
arr[i] = jsonString(dims[i]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
public String jsonString(IDataFixer.Dimension dimension) {
|
||||
var data = dimensions[dimension.ordinal()];
|
||||
int minY = data.get(0);
|
||||
int maxY = data.get(1);
|
||||
int logicalHeight = data.get(2);
|
||||
if (minY == Integer.MAX_VALUE || maxY == Integer.MIN_VALUE || Integer.MIN_VALUE == logicalHeight)
|
||||
return null;
|
||||
return fixer.createDimension(dimension, minY, maxY - minY, logicalHeight, null).toString(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
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,92 +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.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
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 art.arcane.volmlib.util.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Pregenerate a world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
int radius,
|
||||
@Param(description = "The world to pregen", contextual = true)
|
||||
World world,
|
||||
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||
Vector center,
|
||||
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
|
||||
boolean gui
|
||||
) {
|
||||
try {
|
||||
if (sender().isPlayer() && access() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
||||
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
||||
}
|
||||
radius = Math.max(radius, 1024);
|
||||
IrisToolbelt.pregenerate(PregenTask
|
||||
.builder()
|
||||
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||
.gui(gui)
|
||||
.radiusX(radius)
|
||||
.radiusZ(radius)
|
||||
.build(), world);
|
||||
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||
sender().sendMessage(msg);
|
||||
Iris.info(msg);
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Epic fail. See console.");
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Stop the active pregeneration task", aliases = "x")
|
||||
public void stop() {
|
||||
if (PregeneratorJob.shutdownInstance()) {
|
||||
Iris.info( C.BLUE + "Finishing up mca region...");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||
public void pause() {
|
||||
if (PregeneratorJob.pauseResume()) {
|
||||
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,852 +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.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
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.*;
|
||||
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.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
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 art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.parallel.SyncExecutor;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.O;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import art.arcane.iris.util.common.scheduling.jobs.ParallelRadiusJob;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.util.BlockVector;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
public static String hrf(Duration duration) {
|
||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||
}
|
||||
|
||||
//TODO fix pack trimming
|
||||
@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 = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
||||
//boolean trim,
|
||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||
boolean overwrite
|
||||
) {
|
||||
new CommandIris().download(pack, branch, overwrite);
|
||||
}
|
||||
|
||||
@Director(description = "Open a new studio world", aliases = "o", sync = true)
|
||||
public void open(
|
||||
@Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||
long seed) {
|
||||
sender().sendMessage(C.GREEN + "Opening studio for the \"" + dimension.getName() + "\" pack (seed: " + seed + ")");
|
||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
|
||||
public void vscode(
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "Opening VSCode for the \"" + dimension.getName() + "\" pack");
|
||||
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Close an open studio project", aliases = {"x", "c"}, sync = true)
|
||||
public void close() {
|
||||
VolmitSender commandSender = sender();
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
commandSender.sendMessage(C.RED + "No open studio projects.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(C.YELLOW + "Closing studio...");
|
||||
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||
if (throwable != null) {
|
||||
commandSender.sendMessage(C.RED + "Studio close failed: " + throwable.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.failureCause() != null) {
|
||||
commandSender.sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.startupCleanupQueued()) {
|
||||
commandSender.sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(C.GREEN + "Studio closed.");
|
||||
}));
|
||||
}
|
||||
|
||||
@Director(description = "Create a new studio project", aliases = "+", sync = true)
|
||||
public void create(
|
||||
@Param(description = "The name of this new Iris Project.")
|
||||
String name,
|
||||
@Param(
|
||||
description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.",
|
||||
contextual = true,
|
||||
customHandler = NullableDimensionHandler.class
|
||||
)
|
||||
IrisDimension template) {
|
||||
if (template != null) {
|
||||
Iris.service(StudioSVC.class).create(sender(), name, template.getLoadKey());
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).create(sender(), name);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Get the version of a pack")
|
||||
public void version(
|
||||
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
|
||||
}
|
||||
|
||||
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n", "generator", "gen"})
|
||||
public void noise(
|
||||
@Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true)
|
||||
IrisGenerator generator,
|
||||
@Param(description = "The seed to preview the generator with", defaultValue = "12345")
|
||||
long seed
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
||||
|
||||
if (generator == null) {
|
||||
NoiseExplorerGUI.launch();
|
||||
return;
|
||||
}
|
||||
|
||||
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
|
||||
NoiseExplorerGUI.launch(supplier, "Custom Generator");
|
||||
}
|
||||
|
||||
@Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void loot(
|
||||
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false")
|
||||
boolean fast,
|
||||
@Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", defaultValue = "true")
|
||||
boolean add
|
||||
) {
|
||||
if (noStudio()) return;
|
||||
|
||||
KList<IrisLootTable> tables = engine().getLootTables(RNG.r, player().getLocation().getBlock());
|
||||
Inventory inv = Bukkit.createInventory(null, 27 * 2);
|
||||
|
||||
try {
|
||||
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
O<Integer> ta = new O<>();
|
||||
ta.set(-1);
|
||||
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
var engine = engine();
|
||||
|
||||
ta.set(J.sr(() ->
|
||||
{
|
||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||
J.csr(ta.get());
|
||||
sender.sendMessage(C.GREEN + "Opened inventory!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!add) {
|
||||
inv.clear();
|
||||
}
|
||||
|
||||
engine.addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
|
||||
}, fast ? 5 : 35));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Opening inventory now!");
|
||||
player().openInventory(inv);
|
||||
}
|
||||
|
||||
@Director(description = "Calculate the chance for each region to generate", origin = DirectorOrigin.PLAYER)
|
||||
public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) {
|
||||
var engine = engine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "Only works in an Iris world!");
|
||||
return;
|
||||
}
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
Thread.ofVirtual()
|
||||
.start(() -> {
|
||||
int d = radius * 2;
|
||||
KMap<String, AtomicInteger> data = new KMap<>();
|
||||
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
|
||||
var multiBurst = new MultiBurst("Region Sampler");
|
||||
var executor = multiBurst.burst(radius * radius);
|
||||
sender.sendMessage(C.GRAY + "Generating data...");
|
||||
var loc = player.getLocation();
|
||||
int totalTasks = d * d;
|
||||
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||
int c = J.ar(() -> sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding regions"), 0);
|
||||
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
|
||||
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
|
||||
data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0))
|
||||
.incrementAndGet();
|
||||
completedTasks.incrementAndGet();
|
||||
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||
executor.complete();
|
||||
multiBurst.close();
|
||||
J.car(c);
|
||||
|
||||
sender.sendMessage(C.GREEN + "Done!");
|
||||
var loader = engine.getData().getRegionLoader();
|
||||
data.forEach((k, v) -> sender.sendMessage(C.GREEN + k + ": " + loader.load(k).getRarity() + " / " + Form.f((double) v.get() / totalTasks * 100, 2) + "%"));
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DirectorOrigin.PLAYER)
|
||||
public void distances(@Param(description = "The radius in chunks") int radius) {
|
||||
sender().sendMessage(C.YELLOW + "Structure distance sampling for legacy structure data has been removed.");
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Render a world map (External GUI)", aliases = "render")
|
||||
public void map(
|
||||
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
||||
World world
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You need to be in or specify an Iris-generated world!");
|
||||
return;
|
||||
}
|
||||
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
|
||||
sender().sendMessage(C.GREEN + "Opening map!");
|
||||
}
|
||||
|
||||
@Director(description = "Package a dimension into a compressed format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
// Todo: Make this more accurate
|
||||
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
||||
File report = Iris.instance.getDataFile("profile.txt");
|
||||
IrisProject project = new IrisProject(pack);
|
||||
IrisData data = IrisData.get(pack);
|
||||
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
||||
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
||||
|
||||
if (activeEngine != null) {
|
||||
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
||||
} else {
|
||||
IrisToolbelt.applyPregenPerformanceProfile();
|
||||
}
|
||||
|
||||
KList<String> fileText = new KList<>();
|
||||
|
||||
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
|
||||
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
|
||||
KMap<String, Double> generatorTimings = new KMap<>();
|
||||
KMap<String, Double> biomeTimings = new KMap<>();
|
||||
KMap<String, Double> regionTimings = new KMap<>();
|
||||
|
||||
sender().sendMessage("Calculating Performance Metrics for Noise generators");
|
||||
|
||||
for (NoiseStyle i : NoiseStyle.values()) {
|
||||
CNG c = i.create(new RNG(i.hashCode()));
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
styleTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Style Performance Impacts: ");
|
||||
|
||||
for (NoiseStyle i : styleTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + styleTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage("Calculating Interpolator Timings...");
|
||||
|
||||
for (InterpolationMethod i : InterpolationMethod.values()) {
|
||||
IrisInterpolator in = new IrisInterpolator();
|
||||
in.setFunction(i);
|
||||
in.setHorizontalScale(8);
|
||||
|
||||
NoiseProvider np = (x, z) -> Math.random();
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
in.interpolate(j, -j, np);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
in.interpolate(j + 10000, -j - 100000, np);
|
||||
}
|
||||
|
||||
interpolatorTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Interpolator Performance Impacts: ");
|
||||
|
||||
for (InterpolationMethod i : interpolatorTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + interpolatorTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage("Processing Generator Scores: ");
|
||||
|
||||
KMap<String, KList<String>> btx = new KMap<>();
|
||||
|
||||
for (String i : data.getGeneratorLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisGenerator g = data.getGeneratorLoader().load(i);
|
||||
KList<IrisNoiseGenerator> composites = g.getAllComposites();
|
||||
double score = 0;
|
||||
int m = 0;
|
||||
for (IrisNoiseGenerator j : composites) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += interpolatorTimings.get(g.getInterpolator().getFunction());
|
||||
vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction()));
|
||||
generatorTimings.put(i, score);
|
||||
btx.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Generator Performance Impacts: ");
|
||||
|
||||
for (String i : generatorTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + generatorTimings.get(i));
|
||||
|
||||
btx.get(i).forEach((ii) -> fileText.add(" " + ii));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
KMap<String, KList<String>> bt = new KMap<>();
|
||||
|
||||
for (String i : data.getBiomeLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisBiome b = data.getBiomeLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
int m = 0;
|
||||
for (IrisBiomePaletteLayer j : b.getLayers()) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += styleTimings.get(b.getBiomeStyle().getStyle());
|
||||
vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle()));
|
||||
score += styleTimings.get(b.getChildStyle().getStyle());
|
||||
vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle()));
|
||||
biomeTimings.put(i, score);
|
||||
bt.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Biome Performance Impacts: ");
|
||||
|
||||
for (String i : biomeTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + biomeTimings.get(i));
|
||||
|
||||
bt.get(i).forEach((ff) -> fileText.add(" " + ff));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
for (String i : data.getRegionLoader().getPossibleKeys()) {
|
||||
IrisRegion b = data.getRegionLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
score += styleTimings.get(b.getLakeStyle().getStyle());
|
||||
score += styleTimings.get(b.getRiverStyle().getStyle());
|
||||
regionTimings.put(i, score);
|
||||
}
|
||||
|
||||
fileText.add("Project Region Performance Impacts: ");
|
||||
|
||||
for (String i : regionTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + regionTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
double m = 0;
|
||||
for (double i : biomeTimings.v()) {
|
||||
m += i;
|
||||
}
|
||||
m /= biomeTimings.size();
|
||||
double mm = 0;
|
||||
for (double i : generatorTimings.v()) {
|
||||
mm += i;
|
||||
}
|
||||
mm /= generatorTimings.size();
|
||||
m += mm;
|
||||
double mmm = 0;
|
||||
for (double i : regionTimings.v()) {
|
||||
mmm += i;
|
||||
}
|
||||
mmm /= regionTimings.size();
|
||||
m += mmm;
|
||||
|
||||
fileText.add("Average Score: " + m);
|
||||
sender().sendMessage("Score: " + Form.duration(m, 0));
|
||||
|
||||
try {
|
||||
IO.writeAll(report, fileText.toString("\n"));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
|
||||
}
|
||||
|
||||
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (studioService != null && studioService.isProjectOpen()) {
|
||||
IrisProject activeProject = studioService.getActiveProject();
|
||||
if (activeProject != null) {
|
||||
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||
if (isGeneratorDimension(activeProvider, dimension)) {
|
||||
return activeProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sender().isPlayer()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Player player = sender().player();
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
|
||||
if (isGeneratorDimension(worldAccess, dimension)) {
|
||||
return worldAccess;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
|
||||
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IrisDimension engineDimension = generator.getEngine().getDimension();
|
||||
if (engineDimension == null || engineDimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
|
||||
public void spawn(
|
||||
@Param(description = "The entity to spawn")
|
||||
IrisEntity entity,
|
||||
@Param(description = "The location to spawn the entity at", contextual = true)
|
||||
Vector location
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You have to be in an Iris world to spawn entities properly. Trying to spawn the best we can do.");
|
||||
}
|
||||
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to the active studio world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void tpstudio() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You are already in a studio world!");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||
var player = player();
|
||||
PaperLib.teleportAsync(player(), Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld()
|
||||
.spawnLocation()
|
||||
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
}
|
||||
|
||||
@Director(description = "Update your dimension projects VSCode workspace")
|
||||
public void update(
|
||||
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GOLD + "Updating Code Workspace for " + dimension.getName() + "...");
|
||||
if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) {
|
||||
sender().sendMessage(C.GREEN + "Updated Code Workspace for " + dimension.getName());
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "Invalid project: " + dimension.getName() + ". Try deleting the code-workspace file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(aliases = "find-objects", description = "Get information about nearby structures")
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage("You must be in an iris world.");
|
||||
return;
|
||||
}
|
||||
KList<Chunk> chunks = new KList<>();
|
||||
int bx = player().getLocation().getChunk().getX();
|
||||
int bz = player().getLocation().getChunk().getZ();
|
||||
|
||||
try {
|
||||
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
|
||||
sender().sendMessage("Capturing IGenData from " + chunks.size() + " nearby chunks.");
|
||||
try {
|
||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||
PrintWriter pw = new PrintWriter(ff);
|
||||
pw.println("=== Iris Chunk Report ===");
|
||||
pw.println("== General Info ==");
|
||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
||||
pw.println("MC Version: " + Bukkit.getVersion());
|
||||
pw.println("PaperSpigot: " + (PaperLib.isPaper() ? "Yup!" : "Nope!"));
|
||||
pw.println("Report Captured At: " + new Date());
|
||||
pw.println("Chunks: (" + chunks.size() + "): ");
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
pw.println("- [" + i.getX() + ", " + i.getZ() + "]");
|
||||
}
|
||||
|
||||
int regions = 0;
|
||||
long size = 0;
|
||||
String age = "No idea...";
|
||||
|
||||
try {
|
||||
for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) {
|
||||
if (i.isFile()) {
|
||||
size += i.length();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime");
|
||||
age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
KList<String> biomes = new KList<>();
|
||||
KList<String> caveBiomes = new KList<>();
|
||||
KMap<String, KMap<String, KList<String>>> objects = new KMap<>();
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
for (int j = 0; j < 16; j += 3) {
|
||||
|
||||
for (int k = 0; k < 16; k += 3) {
|
||||
|
||||
assert engine() != null;
|
||||
IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")");
|
||||
caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")");
|
||||
exportObjects(bb, pw, engine(), objects);
|
||||
exportObjects(bxf, pw, engine(), objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regions = Objects.requireNonNull(new File(world.getWorldFolder().getPath() + "/region").list()).length;
|
||||
|
||||
pw.println();
|
||||
pw.println("== World Info ==");
|
||||
pw.println("World Name: " + world.getName());
|
||||
pw.println("Age: " + age);
|
||||
pw.println("Folder: " + world.getWorldFolder().getPath());
|
||||
pw.println("Regions: " + Form.f(regions));
|
||||
pw.println("Chunks: max. " + Form.f(regions * 32 * 32));
|
||||
pw.println("World Size: min. " + Form.fileSize(size));
|
||||
pw.println();
|
||||
pw.println("== Biome Info ==");
|
||||
pw.println("Found " + biomes.size() + " Biome(s): ");
|
||||
|
||||
for (String i : biomes) {
|
||||
pw.println("- " + i);
|
||||
}
|
||||
pw.println();
|
||||
|
||||
pw.println("== Object Info ==");
|
||||
|
||||
for (String i : objects.k()) {
|
||||
pw.println("- " + i);
|
||||
|
||||
for (String j : objects.get(i).k()) {
|
||||
pw.println(" @ " + j);
|
||||
|
||||
for (String k : objects.get(i).get(j)) {
|
||||
pw.println(" * " + k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pw.println();
|
||||
pw.close();
|
||||
|
||||
sender().sendMessage("Reported to: " + ff.getPath());
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap<String, KMap<String, KList<String>>> objects) {
|
||||
String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")";
|
||||
int m = 0;
|
||||
KSet<String> stop = new KSet<>();
|
||||
for (IrisObjectPlacement f : bb.getObjects()) {
|
||||
m++;
|
||||
String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)";
|
||||
|
||||
for (String i : f.getPlace()) {
|
||||
String nn3 = i + ": [ERROR] Failed to find object!";
|
||||
|
||||
try {
|
||||
if (stop.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File ff = g.getData().getObjectLoader().findFile(i);
|
||||
BlockVector sz = IrisObject.sampleSize(ff);
|
||||
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
|
||||
stop.add(i);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
String n3 = nn3;
|
||||
objects.computeIfAbsent(n1, (k1) -> new KMap<>())
|
||||
.computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if server GUIs are not enabled
|
||||
*/
|
||||
private boolean noGUI() {
|
||||
if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if no studio is open or the player is not in one
|
||||
*/
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public void files(File clean, KList<File> files) {
|
||||
if (clean.isDirectory()) {
|
||||
for (File i : clean.listFiles()) {
|
||||
files(i, files);
|
||||
}
|
||||
} else if (clean.getName().endsWith(".json")) {
|
||||
try {
|
||||
files.add(clean);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONObject obj) {
|
||||
for (String i : obj.keySet()) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
|
||||
obj.put(i, "minecraft:" + o);
|
||||
}
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONArray obj) {
|
||||
for (int i = 0; i < obj.length(); i++) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +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.edit;
|
||||
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.SR;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.FallingBlock;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public class BlockSignal {
|
||||
public static final AtomicInteger active = new AtomicInteger(0);
|
||||
|
||||
public BlockSignal(Block block, int ticks) {
|
||||
active.incrementAndGet();
|
||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5);
|
||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg, block.getBlockData());
|
||||
e.setGravity(false);
|
||||
e.setInvulnerable(true);
|
||||
e.setGlowing(true);
|
||||
e.setDropItem(false);
|
||||
e.setHurtEntities(false);
|
||||
e.setSilent(true);
|
||||
e.setTicksLived(1);
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
Location blockLocation = block.getLocation();
|
||||
Runnable removeTask = () -> {
|
||||
if (!J.runEntity(e, e::remove) && !e.isDead()) {
|
||||
e.remove();
|
||||
}
|
||||
active.decrementAndGet();
|
||||
sendBlockRefresh(block);
|
||||
};
|
||||
if (!J.runAt(blockLocation, removeTask, ticks)) {
|
||||
if (!J.isFolia()) {
|
||||
J.s(removeTask, ticks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void of(Block block, int ticks) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
of(block.getWorld(), block.getX(), block.getY(), block.getZ(), ticks);
|
||||
}
|
||||
|
||||
public static void of(Block block) {
|
||||
of(block, 100);
|
||||
}
|
||||
|
||||
public static void of(World world, int x, int y, int z, int ticks) {
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = new Location(world, x, y, z);
|
||||
Runnable createTask = () -> new BlockSignal(world.getBlockAt(x, y, z), ticks);
|
||||
if (!J.runAt(location, createTask)) {
|
||||
if (!J.isFolia()) {
|
||||
J.s(createTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void of(World world, int x, int y, int z) {
|
||||
of(world, x, y, z, 100);
|
||||
}
|
||||
|
||||
public static Runnable forever(Block block) {
|
||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5).clone();
|
||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg.clone(), block.getBlockData());
|
||||
e.setGravity(false);
|
||||
e.setInvulnerable(true);
|
||||
e.setGlowing(true);
|
||||
e.teleport(tg.clone());
|
||||
e.setDropItem(false);
|
||||
e.setHurtEntities(false);
|
||||
e.setSilent(true);
|
||||
e.setTicksLived(1);
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
|
||||
new SR(20) {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!J.runEntity(e, () -> {
|
||||
if (e.isDead()) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
e.setTicksLived(1);
|
||||
e.teleport(tg.clone());
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
})) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () -> {
|
||||
if (!J.runEntity(e, e::remove) && !e.isDead()) {
|
||||
e.remove();
|
||||
}
|
||||
Location blockLocation = block.getLocation();
|
||||
Runnable refreshTask = () -> sendBlockRefresh(block);
|
||||
if (!J.runAt(blockLocation, refreshTask)) {
|
||||
refreshTask.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void sendBlockRefresh(Block block) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = block.getLocation();
|
||||
BlockData blockData = block.getBlockData();
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (!player.getWorld().equals(location.getWorld())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
J.runEntity(player, () -> player.sendBlockChange(location, blockData));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package art.arcane.iris.core.events;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.world.LootGenerateEvent;
|
||||
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.loot.LootContext;
|
||||
import org.bukkit.loot.LootTable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
public class IrisLootEvent extends Event {
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
private static final LootTable EMPTY = new LootTable() {
|
||||
@NotNull
|
||||
@Override
|
||||
public NamespacedKey getKey() {
|
||||
return new NamespacedKey(Iris.instance, "empty");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<ItemStack> populateLoot(@Nullable Random random, @NotNull LootContext context) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillInventory(@NotNull Inventory inventory, @Nullable Random random, @NotNull LootContext context) {
|
||||
}
|
||||
};
|
||||
|
||||
private final Engine engine;
|
||||
private final Block block;
|
||||
private final InventorySlotType slot;
|
||||
private final KList<IrisLootTable> tables;
|
||||
|
||||
/**
|
||||
* Constructor for IrisLootEvent with mode selection.
|
||||
*
|
||||
* @param engine The engine instance.
|
||||
* @param block The block associated with the event.
|
||||
* @param slot The inventory slot type.
|
||||
* @param tables The list of IrisLootTables. (mutable*)
|
||||
*/
|
||||
public IrisLootEvent(Engine engine, Block block, InventorySlotType slot, KList<IrisLootTable> tables) {
|
||||
this.engine = engine;
|
||||
this.block = block;
|
||||
this.slot = slot;
|
||||
this.tables = tables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required method to get the HandlerList for this event.
|
||||
*
|
||||
* @return The HandlerList.
|
||||
*/
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the corresponding Bukkit loot event.
|
||||
* This method integrates your custom IrisLootTables with Bukkit's LootGenerateEvent,
|
||||
* allowing other plugins to modify or cancel the loot generation.
|
||||
*
|
||||
* @return true when the event was canceled
|
||||
*/
|
||||
public static boolean callLootEvent(KList<ItemStack> loot, Inventory inv, World world, int x, int y, int z) {
|
||||
InventoryHolder holder = inv.getHolder();
|
||||
Location loc = new Location(world, x, y, z);
|
||||
if (holder == null) {
|
||||
holder = new InventoryHolder() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inv;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
LootContext context = new LootContext.Builder(loc).build();
|
||||
LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true);
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
|
||||
Thread.dumpStack();
|
||||
J.sfut(() -> {
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("LootGenerateEvent dispatch failed at "
|
||||
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}).join();
|
||||
} else {
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("LootGenerateEvent dispatch failed at "
|
||||
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return event.isCancelled();
|
||||
}
|
||||
}
|
||||
@@ -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,170 +0,0 @@
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import art.arcane.iris.core.link.data.DataType;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ExternalDataProvider implements Listener {
|
||||
|
||||
@NonNull
|
||||
private final String pluginId;
|
||||
|
||||
@Nullable
|
||||
public Plugin getPlugin() {
|
||||
return Bukkit.getPluginManager().getPlugin(pluginId);
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return getPlugin() != null && getPlugin().isEnabled();
|
||||
}
|
||||
|
||||
public abstract void init();
|
||||
|
||||
/**
|
||||
* @see ExternalDataProvider#getBlockData(Identifier, KMap)
|
||||
*/
|
||||
@NotNull
|
||||
public BlockData getBlockData(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
return getBlockData(blockId, new KMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a {@link BlockData} corresponding to the blockID
|
||||
* it is used in any place Iris accepts {@link BlockData}
|
||||
*
|
||||
* @param blockId The id of the block to get
|
||||
* @param state The state of the block to get
|
||||
* @return Corresponding {@link BlockData} to the blockId
|
||||
* may return {@link IrisCustomData} for blocks that need a world for placement
|
||||
* @throws MissingResourceException when the blockId is invalid
|
||||
*/
|
||||
@NotNull
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of all {@link BlockProperty} objects associated with the specified block identifier.
|
||||
*
|
||||
* @param blockId The identifier of the block whose properties are to be retrieved. Must not be null.
|
||||
* @return A list of {@link BlockProperty} objects representing the properties of the block.
|
||||
* @throws MissingResourceException If the specified block identifier is invalid or cannot be found.
|
||||
*/
|
||||
@NotNull
|
||||
public List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ExternalDataProvider#getItemStack(Identifier)
|
||||
*/
|
||||
@NotNull
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId) throws MissingResourceException {
|
||||
return getItemStack(itemId, new KMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a {@link ItemStack} corresponding to the itemID
|
||||
* it is used in loot tables
|
||||
*
|
||||
* @param itemId The id of the item to get
|
||||
* @param customNbt Custom nbt to apply to the item
|
||||
* @return Corresponding {@link ItemStack} to the itemId
|
||||
* @throws MissingResourceException when the itemId is invalid
|
||||
*/
|
||||
@NotNull
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used for placing blocks that need to use the plugins api
|
||||
* it will only be called when the {@link ExternalDataProvider#getBlockData(Identifier, KMap)} returned a {@link IrisCustomData}
|
||||
*
|
||||
* @param engine The engine of the world the block is being placed in
|
||||
* @param block The block where the block should be placed
|
||||
* @param blockId The blockId to place
|
||||
*/
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {}
|
||||
|
||||
/**
|
||||
* Spawns a mob in the specified location using the given engine and entity identifier.
|
||||
*
|
||||
* @param location The location in the world where the mob should spawn. Must not be null.
|
||||
* @param entityId The identifier of the mob entity to spawn. Must not be null.
|
||||
* @return The spawned {@link Entity} if successful, or null if the mob could not be spawned.
|
||||
*/
|
||||
@Nullable
|
||||
public Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException {
|
||||
throw new MissingResourceException("Failed to find Entity!", entityId.namespace(), entityId.key());
|
||||
}
|
||||
|
||||
public abstract @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType);
|
||||
|
||||
public abstract boolean isValidProvider(@NotNull Identifier id, DataType dataType);
|
||||
|
||||
protected static Pair<Float, BlockFace> parseYawAndFace(@NotNull Engine engine, @NotNull Block block, @NotNull KMap<@NotNull String, @NotNull String> state) {
|
||||
float yaw = 0;
|
||||
BlockFace face = BlockFace.NORTH;
|
||||
|
||||
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
|
||||
RNG rng = new RNG(seed);
|
||||
if ("true".equals(state.get("randomYaw"))) {
|
||||
yaw = rng.f(0, 360);
|
||||
} else if (state.containsKey("yaw")) {
|
||||
yaw = Float.parseFloat(state.get("yaw"));
|
||||
}
|
||||
if ("true".equals(state.get("randomFace"))) {
|
||||
BlockFace[] faces = BlockFace.values();
|
||||
face = faces[rng.i(0, faces.length - 1)];
|
||||
} else if (state.containsKey("face")) {
|
||||
face = BlockFace.valueOf(state.get("face").toUpperCase());
|
||||
}
|
||||
if (face == BlockFace.SELF) {
|
||||
face = BlockFace.NORTH;
|
||||
}
|
||||
|
||||
return new Pair<>(yaw, face);
|
||||
}
|
||||
|
||||
protected static List<BlockProperty> YAW_FACE_BIOME_PROPERTIES = List.of(
|
||||
BlockProperty.ofEnum(BiomeColor.class, "matchBiome", null),
|
||||
BlockProperty.ofBoolean("randomYaw", false),
|
||||
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
||||
BlockProperty.ofBoolean("randomFace", true),
|
||||
new BlockProperty(
|
||||
"face",
|
||||
BlockFace.class,
|
||||
BlockFace.NORTH,
|
||||
Arrays.asList(BlockFace.values()).subList(0, BlockFace.values().length - 1),
|
||||
BlockFace::name
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,77 +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.link;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.mvplugins.multiverse.core.MultiverseCoreApi;
|
||||
import org.mvplugins.multiverse.core.world.MultiverseWorld;
|
||||
import org.mvplugins.multiverse.core.world.WorldManager;
|
||||
import org.mvplugins.multiverse.core.world.options.ImportWorldOptions;
|
||||
|
||||
public class MultiverseCoreLink {
|
||||
private final boolean active;
|
||||
|
||||
public MultiverseCoreLink() {
|
||||
active = Bukkit.getPluginManager().getPlugin("Multiverse-Core") != null;
|
||||
}
|
||||
|
||||
public void removeFromConfig(World world) {
|
||||
removeFromConfig(world.getName());
|
||||
}
|
||||
|
||||
public void removeFromConfig(String world) {
|
||||
if (!active) return;
|
||||
var manager = worldManager();
|
||||
manager.removeWorld(world).onSuccess(manager::saveWorldsConfig);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void updateWorld(World bukkitWorld, String pack) {
|
||||
if (!active) return;
|
||||
var generator = "Iris:" + pack;
|
||||
var manager = worldManager();
|
||||
var world = manager.getWorld(bukkitWorld).getOrElse(() -> {
|
||||
var options = ImportWorldOptions.worldName(bukkitWorld.getName())
|
||||
.generator(generator)
|
||||
.environment(bukkitWorld.getEnvironment())
|
||||
.useSpawnAdjust(false);
|
||||
return manager.importWorld(options).get();
|
||||
});
|
||||
|
||||
world.setAutoLoad(false);
|
||||
if (!generator.equals(world.getGenerator())) {
|
||||
var field = MultiverseWorld.class.getDeclaredField("worldConfig");
|
||||
field.setAccessible(true);
|
||||
|
||||
var config = field.get(world);
|
||||
config.getClass()
|
||||
.getDeclaredMethod("setGenerator", String.class)
|
||||
.invoke(config, generator);
|
||||
}
|
||||
|
||||
manager.saveWorldsConfig();
|
||||
}
|
||||
|
||||
private WorldManager worldManager() {
|
||||
var api = MultiverseCoreApi.get();
|
||||
return api.getWorldManager();
|
||||
}
|
||||
}
|
||||
@@ -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,33 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public enum DataType implements BiPredicate<ExternalDataProvider, Identifier> {
|
||||
ITEM,
|
||||
BLOCK,
|
||||
ENTITY;
|
||||
|
||||
@Override
|
||||
public boolean test(ExternalDataProvider dataProvider, Identifier identifier) {
|
||||
if (!dataProvider.isValidProvider(identifier, this)) return false;
|
||||
try {
|
||||
switch (this) {
|
||||
case ITEM -> dataProvider.getItemStack(identifier);
|
||||
case BLOCK -> dataProvider.getBlockData(identifier);
|
||||
case ENTITY -> {}
|
||||
}
|
||||
return true;
|
||||
} catch (MissingResourceException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Predicate<Identifier> asPredicate(ExternalDataProvider dataProvider) {
|
||||
return i -> test(dataProvider, i);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import com.willfp.ecoitems.items.EcoItem;
|
||||
import com.willfp.ecoitems.items.EcoItems;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class EcoItemsDataProvider extends ExternalDataProvider {
|
||||
private WrappedField<EcoItem, ItemStack> itemStack;
|
||||
private WrappedField<EcoItem, NamespacedKey> id;
|
||||
|
||||
public EcoItemsDataProvider() {
|
||||
super("EcoItems");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up EcoItems Link...");
|
||||
itemStack = new WrappedField<>(EcoItem.class, "_itemStack");
|
||||
if (this.itemStack.hasFailed()) {
|
||||
Iris.error("Failed to set up EcoItems Link: Unable to fetch ItemStack field!");
|
||||
}
|
||||
id = new WrappedField<>(EcoItem.class, "id");
|
||||
if (this.id.hasFailed()) {
|
||||
Iris.error("Failed to set up EcoItems Link: Unable to fetch id field!");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
EcoItem item = EcoItems.INSTANCE.getByID(itemId.key());
|
||||
if (item == null) throw new MissingResourceException("Failed to find Item!", itemId.namespace(), itemId.key());
|
||||
return itemStack.get(item).clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ITEM) return List.of();
|
||||
return EcoItems.INSTANCE.values()
|
||||
.stream()
|
||||
.map(x -> Identifier.fromNamespacedKey(id.get(x)))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
return id.namespace().equalsIgnoreCase("ecoitems") && dataType == DataType.ITEM;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import com.ssomar.score.api.executableitems.ExecutableItemsAPI;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ExecutableItemsDataProvider extends ExternalDataProvider {
|
||||
public ExecutableItemsDataProvider() {
|
||||
super("ExecutableItems");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up ExecutableItems Link...");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
return ExecutableItemsAPI.getExecutableItemsManager().getExecutableItem(itemId.key())
|
||||
.map(item -> item.buildItem(1, Optional.empty()))
|
||||
.orElseThrow(() -> new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ITEM) return List.of();
|
||||
return ExecutableItemsAPI.getExecutableItemsManager()
|
||||
.getExecutableItemIdsList()
|
||||
.stream()
|
||||
.map(name -> new Identifier("executable_items", name))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier key, DataType dataType) {
|
||||
return key.namespace().equalsIgnoreCase("executable_items") && dataType == DataType.ITEM;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class HMCLeavesDataProvider extends ExternalDataProvider {
|
||||
private Object apiInstance;
|
||||
private WrappedReturningMethod<Object, Material> worldBlockType;
|
||||
private WrappedReturningMethod<Object, Boolean> setCustomBlock;
|
||||
private Map<String, Object> blockDataMap = Map.of();
|
||||
private Map<String, Supplier<ItemStack>> itemDataField = Map.of();
|
||||
|
||||
public HMCLeavesDataProvider() {
|
||||
super("HMCLeaves");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginId() {
|
||||
return "HMCLeaves";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
try {
|
||||
worldBlockType = new WrappedReturningMethod<>((Class<Object>) Class.forName("io.github.fisher2911.hmcleaves.data.BlockData"), "worldBlockType");
|
||||
apiInstance = getApiInstance(Class.forName("io.github.fisher2911.hmcleaves.api.HMCLeavesAPI"));
|
||||
setCustomBlock = new WrappedReturningMethod<>((Class<Object>) apiInstance.getClass(), "setCustomBlock", Location.class, String.class, boolean.class);
|
||||
Object config = getLeavesConfig(apiInstance.getClass());
|
||||
blockDataMap = getMap(config, "blockDataMap");
|
||||
itemDataField = getMap(config, "itemSupplierMap");
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to initialize HMCLeavesDataProvider: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
Object o = blockDataMap.get(blockId.key());
|
||||
if (o == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
Material material = worldBlockType.invoke(o, new Object[0]);
|
||||
if (material == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
BlockData blockData = Bukkit.createBlockData(material);
|
||||
if (IrisSettings.get().getGenerator().preventLeafDecay && blockData instanceof Leaves leaves)
|
||||
leaves.setPersistent(true);
|
||||
return IrisCustomData.of(blockData, ExternalDataSVC.buildState(blockId, state));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
if (!itemDataField.containsKey(itemId.key()))
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return itemDataField.get(itemId.key()).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var pair = ExternalDataSVC.parseState(blockId);
|
||||
blockId = pair.getA();
|
||||
Boolean result = setCustomBlock.invoke(apiInstance, new Object[]{block.getLocation(), blockId.key(), false});
|
||||
if (result == null || !result)
|
||||
Iris.warn("Failed to set custom block! " + blockId.key() + " " + block.getX() + " " + block.getY() + " " + block.getZ());
|
||||
else if (IrisSettings.get().getGenerator().preventLeafDecay) {
|
||||
BlockData blockData = block.getBlockData();
|
||||
if (blockData instanceof Leaves leaves)
|
||||
leaves.setPersistent(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return (dataType == DataType.BLOCK ? blockDataMap.keySet() : itemDataField.keySet())
|
||||
.stream()
|
||||
.map(x -> new Identifier("hmcleaves", x))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return (dataType == DataType.ITEM ? itemDataField.keySet() : blockDataMap.keySet()).contains(id.key());
|
||||
}
|
||||
|
||||
private <C, T> Map<String, T> getMap(C config, String name) {
|
||||
WrappedField<C, Map<String, T>> field = new WrappedField<>((Class<C>) config.getClass(), name);
|
||||
return field.get(config);
|
||||
}
|
||||
|
||||
private <A> A getApiInstance(Class<A> apiClass) {
|
||||
WrappedReturningMethod<A, A> instance = new WrappedReturningMethod<>(apiClass, "getInstance");
|
||||
return instance.invoke();
|
||||
}
|
||||
|
||||
private <A, C> C getLeavesConfig(Class<A> apiClass) {
|
||||
WrappedReturningMethod<A, A> instance = new WrappedReturningMethod<>(apiClass, "getInstance");
|
||||
WrappedField<A, C> config = new WrappedField<>(apiClass, "config");
|
||||
return config.get(instance.invoke());
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import dev.lone.itemsadder.api.CustomBlock;
|
||||
import dev.lone.itemsadder.api.CustomStack;
|
||||
import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ItemAdderDataProvider extends ExternalDataProvider {
|
||||
|
||||
private volatile Set<String> itemNamespaces = Set.of();
|
||||
private volatile Set<String> blockNamespaces = Set.of();
|
||||
|
||||
public ItemAdderDataProvider() {
|
||||
super("ItemsAdder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
updateNamespaces();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLoadData(ItemsAdderLoadDataEvent event) {
|
||||
updateNamespaces();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
CustomBlock block = CustomBlock.getInstance(blockId.toString());
|
||||
if (block == null) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
return IrisCustomData.of(block.getBaseBlockData(), blockId);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
CustomStack stack = CustomStack.getInstance(itemId.toString());
|
||||
if (stack == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
return stack.getItemStack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
CustomBlock custom;
|
||||
if ((custom = CustomBlock.place(blockId.toString(), block.getLocation())) == null)
|
||||
return;
|
||||
block.setBlockData(custom.getBaseBlockData(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return switch (dataType) {
|
||||
case ENTITY -> List.of();
|
||||
case ITEM -> CustomStack.getNamespacedIdsInRegistry()
|
||||
.stream()
|
||||
.map(Identifier::fromString)
|
||||
.toList();
|
||||
case BLOCK -> CustomBlock.getNamespacedIdsInRegistry()
|
||||
.stream()
|
||||
.map(Identifier::fromString)
|
||||
.toList();
|
||||
};
|
||||
}
|
||||
|
||||
private void updateNamespaces() {
|
||||
try {
|
||||
updateNamespaces(DataType.ITEM);
|
||||
updateNamespaces(DataType.BLOCK);
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Failed to update ItemAdder namespaces: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNamespaces(DataType dataType) {
|
||||
var namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet());
|
||||
if (dataType == DataType.ITEM) itemNamespaces = namespaces;
|
||||
else blockNamespaces = namespaces;
|
||||
Iris.debug("Updated ItemAdder namespaces: " + dataType + " - " + namespaces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return dataType == DataType.ITEM ? itemNamespaces.contains(id.namespace()) : blockNamespaces.contains(id.namespace());
|
||||
}
|
||||
}
|
||||
@@ -1,74 +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.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import me.kryniowesegryderiusz.kgenerators.Main;
|
||||
import me.kryniowesegryderiusz.kgenerators.api.KGeneratorsAPI;
|
||||
import me.kryniowesegryderiusz.kgenerators.generators.locations.objects.GeneratorLocation;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class KGeneratorsDataProvider extends ExternalDataProvider {
|
||||
public KGeneratorsDataProvider() {
|
||||
super("KGenerators");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
if (Main.getGenerators().get(blockId.key()) == null) throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
return IrisCustomData.of(Material.STRUCTURE_VOID.createBlockData(), ExternalDataSVC.buildState(blockId, state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
var gen = Main.getGenerators().get(itemId.key());
|
||||
if (gen == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return gen.getGeneratorItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
if (block.getType() != Material.STRUCTURE_VOID) return;
|
||||
var existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation());
|
||||
if (existing != null) return;
|
||||
block.setBlockData(B.getAir(), false);
|
||||
var gen = Main.getGenerators().get(blockId.key());
|
||||
if (gen == null) return;
|
||||
var loc = new GeneratorLocation(-1, gen, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null);
|
||||
Main.getDatabases().getDb().saveGenerator(loc);
|
||||
Main.getPlacedGenerators().addLoaded(loc);
|
||||
Main.getSchedules().schedule(loc, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return Main.getGenerators().getAll().stream()
|
||||
.map(gen -> new Identifier("kgenerators", gen.getId()))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return "kgenerators".equalsIgnoreCase(id.namespace());
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import net.Indyuce.mmoitems.MMOItems;
|
||||
import net.Indyuce.mmoitems.api.ItemTier;
|
||||
import net.Indyuce.mmoitems.api.block.CustomBlock;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class MMOItemsDataProvider extends ExternalDataProvider {
|
||||
|
||||
public MMOItemsDataProvider() {
|
||||
super("MMOItems");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up MMOItems Link...");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
int id = -1;
|
||||
try {
|
||||
id = Integer.parseInt(blockId.key());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
CustomBlock block = api().getCustomBlocks().getBlock(id);
|
||||
if (block == null) throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
return block.getState().getBlockData();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
String[] parts = itemId.namespace().split("_", 2);
|
||||
if (parts.length != 2)
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
CompletableFuture<ItemStack> future = new CompletableFuture<>();
|
||||
Runnable run = () -> {
|
||||
try {
|
||||
var type = api().getTypes().get(parts[1]);
|
||||
int level = -1;
|
||||
ItemTier tier = null;
|
||||
|
||||
if (customNbt != null) {
|
||||
level = (int) customNbt.getOrDefault("level", -1);
|
||||
tier = api().getTiers().get(String.valueOf(customNbt.get("tier")));
|
||||
}
|
||||
|
||||
ItemStack itemStack;
|
||||
if (type == null) {
|
||||
future.complete(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level != -1 && tier != null) {
|
||||
itemStack = api().getItem(type, itemId.key(), level, tier);
|
||||
} else {
|
||||
itemStack = api().getItem(type, itemId.key());
|
||||
}
|
||||
future.complete(itemStack);
|
||||
} catch (Throwable e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
};
|
||||
if (Bukkit.isPrimaryThread()) run.run();
|
||||
else J.s(run);
|
||||
ItemStack item = null;
|
||||
try {
|
||||
item = future.get();
|
||||
} catch (InterruptedException | ExecutionException ignored) {}
|
||||
if (item == null)
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return switch (dataType) {
|
||||
case ENTITY -> List.of();
|
||||
case BLOCK -> api().getCustomBlocks().getBlockIds().stream().map(id -> new Identifier("mmoitems", String.valueOf(id)))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
case ITEM -> {
|
||||
Supplier<Collection<Identifier>> supplier = () -> api().getTypes()
|
||||
.getAll()
|
||||
.stream()
|
||||
.flatMap(type -> api()
|
||||
.getTemplates()
|
||||
.getTemplateNames(type)
|
||||
.stream()
|
||||
.map(name -> new Identifier("mmoitems_" + type.getId(), name)))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
|
||||
if (Bukkit.isPrimaryThread()) yield supplier.get();
|
||||
else yield J.sfut(supplier).join();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return dataType == DataType.ITEM ? id.namespace().split("_", 2).length == 2 : id.namespace().equals("mmoitems");
|
||||
}
|
||||
|
||||
private MMOItems api() {
|
||||
return MMOItems.plugin;
|
||||
}
|
||||
}
|
||||
@@ -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.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import io.lumine.mythic.bukkit.BukkitAdapter;
|
||||
import io.lumine.mythic.bukkit.utils.serialize.Chroma;
|
||||
import io.lumine.mythiccrucible.MythicCrucible;
|
||||
import io.lumine.mythiccrucible.items.CrucibleItem;
|
||||
import io.lumine.mythiccrucible.items.ItemManager;
|
||||
import io.lumine.mythiccrucible.items.blocks.CustomBlockItemContext;
|
||||
import io.lumine.mythiccrucible.items.furniture.FurnitureItemContext;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
|
||||
private ItemManager itemManager;
|
||||
|
||||
public MythicCrucibleDataProvider() {
|
||||
super("MythicCrucible");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up MythicCrucible Link...");
|
||||
try {
|
||||
this.itemManager = MythicCrucible.inst().getItemManager();
|
||||
} catch (Exception e) {
|
||||
Iris.error("Failed to set up MythicCrucible Link: Unable to fetch MythicCrucible instance!");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
CrucibleItem crucibleItem = this.itemManager.getItem(blockId.key())
|
||||
.orElseThrow(() -> new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()));
|
||||
CustomBlockItemContext blockItemContext = crucibleItem.getBlockData();
|
||||
FurnitureItemContext furnitureItemContext = crucibleItem.getFurnitureData();
|
||||
if (furnitureItemContext != null) {
|
||||
return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state));
|
||||
} else if (blockItemContext != null) {
|
||||
return blockItemContext.getBlockData();
|
||||
}
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
CrucibleItem crucibleItem = this.itemManager.getItem(blockId.key())
|
||||
.orElseThrow(() -> new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()));
|
||||
|
||||
if (crucibleItem.getFurnitureData() != null) {
|
||||
return YAW_FACE_BIOME_PROPERTIES;
|
||||
} else if (crucibleItem.getBlockData() != null) {
|
||||
return List.of();
|
||||
}
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
Optional<CrucibleItem> opt = this.itemManager.getItem(itemId.key());
|
||||
return BukkitAdapter.adapt(opt.orElseThrow(() ->
|
||||
new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()))
|
||||
.getMythicItem()
|
||||
.generateItemStack(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return itemManager.getItems()
|
||||
.stream()
|
||||
.map(i -> new Identifier("crucible", i.getInternalName()))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var parsedState = ExternalDataSVC.parseState(blockId);
|
||||
var state = parsedState.getB();
|
||||
blockId = parsedState.getA();
|
||||
|
||||
Optional<CrucibleItem> item = itemManager.getItem(blockId.key());
|
||||
if (item.isEmpty()) return;
|
||||
FurnitureItemContext furniture = item.get().getFurnitureData();
|
||||
if (furniture == null) return;
|
||||
|
||||
var pair = parseYawAndFace(engine, block, state);
|
||||
BiomeColor type = null;
|
||||
Chroma color = null;
|
||||
try {
|
||||
type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase());
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
if (type != null) {
|
||||
var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
color = Chroma.of(biomeColor.getRGB());
|
||||
}
|
||||
furniture.place(block, pair.getB(), pair.getA(), color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier key, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return key.namespace().equalsIgnoreCase("crucible");
|
||||
}
|
||||
}
|
||||
@@ -1,130 +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.tools.IrisToolbelt;
|
||||
import io.lumine.mythic.api.adapters.AbstractLocation;
|
||||
import io.lumine.mythic.api.config.MythicLineConfig;
|
||||
import io.lumine.mythic.api.mobs.entities.SpawnReason;
|
||||
import io.lumine.mythic.api.skills.conditions.ILocationCondition;
|
||||
import io.lumine.mythic.bukkit.BukkitAdapter;
|
||||
import io.lumine.mythic.bukkit.MythicBukkit;
|
||||
import io.lumine.mythic.bukkit.adapters.BukkitWorld;
|
||||
import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent;
|
||||
import io.lumine.mythic.core.mobs.ActiveMob;
|
||||
import io.lumine.mythic.core.mobs.MobStack;
|
||||
import io.lumine.mythic.core.skills.SkillCondition;
|
||||
import io.lumine.mythic.core.utils.annotations.MythicCondition;
|
||||
import io.lumine.mythic.core.utils.annotations.MythicField;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class MythicMobsDataProvider extends ExternalDataProvider {
|
||||
public MythicMobsDataProvider() {
|
||||
super("MythicMobs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException {
|
||||
var mm = spawnMob(BukkitAdapter.adapt(location), entityId);
|
||||
return mm == null ? null : mm.getEntity().getBukkitEntity();
|
||||
}
|
||||
|
||||
private ActiveMob spawnMob(AbstractLocation location, Identifier entityId) throws MissingResourceException {
|
||||
var manager = MythicBukkit.inst().getMobManager();
|
||||
var mm = manager.getMythicMob(entityId.key()).orElse(null);
|
||||
if (mm == null) {
|
||||
var stack = manager.getMythicMobStack(entityId.key());
|
||||
if (stack == null) throw new MissingResourceException("Failed to find Mob!", entityId.namespace(), entityId.key());
|
||||
return stack.spawn(location, 1d, SpawnReason.OTHER, null);
|
||||
}
|
||||
return mm.spawn(location, 1d, SpawnReason.OTHER, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ENTITY) return List.of();
|
||||
var manager = MythicBukkit.inst().getMobManager();
|
||||
return Stream.concat(manager.getMobNames().stream(),
|
||||
manager.getMobStacks()
|
||||
.stream()
|
||||
.map(MobStack::getName)
|
||||
)
|
||||
.distinct()
|
||||
.map(name -> new Identifier("mythicmobs", name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
return id.namespace().equalsIgnoreCase("mythicmobs") && dataType == DataType.ENTITY;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(MythicConditionLoadEvent event) {
|
||||
switch (event.getConditionName()) {
|
||||
case "irisbiome" -> event.register(new IrisBiomeCondition(event.getConditionName(), event.getConfig()));
|
||||
case "irisregion" -> event.register(new IrisRegionCondition(event.getConditionName(), event.getConfig()));
|
||||
}
|
||||
}
|
||||
|
||||
@MythicCondition(author = "CrazyDev22", name = "irisbiome", description = "Tests if the target is within the given list of biomes")
|
||||
public static class IrisBiomeCondition extends SkillCondition implements ILocationCondition {
|
||||
@MythicField(name = "biome", aliases = {"b"}, description = "A list of biomes to check")
|
||||
private Set<String> biomes = ConcurrentHashMap.newKeySet();
|
||||
@MythicField(name = "surface", aliases = {"s"}, description = "If the biome check should only be performed on the surface")
|
||||
private boolean surface;
|
||||
|
||||
public IrisBiomeCondition(String line, MythicLineConfig mlc) {
|
||||
super(line);
|
||||
String b = mlc.getString(new String[]{"biome", "b"}, "");
|
||||
biomes.addAll(Arrays.asList(b.split(",")));
|
||||
surface = mlc.getBoolean(new String[]{"surface", "s"}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(AbstractLocation target) {
|
||||
var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
if (access == null) return false;
|
||||
var engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
var biome = surface ?
|
||||
engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) :
|
||||
engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
|
||||
return biomes.contains(biome.getLoadKey());
|
||||
}
|
||||
}
|
||||
|
||||
@MythicCondition(author = "CrazyDev22", name = "irisregion", description = "Tests if the target is within the given list of biomes")
|
||||
public static class IrisRegionCondition extends SkillCondition implements ILocationCondition {
|
||||
@MythicField(name = "region", aliases = {"r"}, description = "A list of regions to check")
|
||||
private Set<String> regions = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public IrisRegionCondition(String line, MythicLineConfig mlc) {
|
||||
super(line);
|
||||
String b = mlc.getString(new String[]{"region", "r"}, "");
|
||||
regions.addAll(Arrays.asList(b.split(",")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(AbstractLocation target) {
|
||||
var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
if (access == null) return false;
|
||||
var engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
var region = engine.getRegion(target.getBlockX(), target.getBlockZ());
|
||||
return regions.contains(region.getLoadKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import com.nexomc.nexo.api.NexoBlocks;
|
||||
import com.nexomc.nexo.api.NexoFurniture;
|
||||
import com.nexomc.nexo.api.NexoItems;
|
||||
import com.nexomc.nexo.items.ItemBuilder;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.ItemDisplay;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class NexoDataProvider extends ExternalDataProvider {
|
||||
public NexoDataProvider() {
|
||||
super("Nexo");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
if (!NexoItems.exists(blockId.key())) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
Identifier blockState = ExternalDataSVC.buildState(blockId, state);
|
||||
if (NexoBlocks.isCustomBlock(blockId.key())) {
|
||||
BlockData data = NexoBlocks.blockData(blockId.key());
|
||||
if (data == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
return IrisCustomData.of(data, blockState);
|
||||
} else if (NexoFurniture.isFurniture(blockId.key())) {
|
||||
return IrisCustomData.of(B.getAir(), blockState);
|
||||
}
|
||||
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
if (!NexoItems.exists(blockId.key())) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
return NexoFurniture.isFurniture(blockId.key()) ? YAW_FACE_BIOME_PROPERTIES : List.of();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
ItemBuilder builder = NexoItems.itemFromId(itemId.key());
|
||||
if (builder == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
try {
|
||||
return builder.build();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var statePair = ExternalDataSVC.parseState(blockId);
|
||||
var state = statePair.getB();
|
||||
blockId = statePair.getA();
|
||||
|
||||
if (NexoBlocks.isCustomBlock(blockId.key())) {
|
||||
NexoBlocks.place(blockId.key(), block.getLocation());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NexoFurniture.isFurniture(blockId.key()))
|
||||
return;
|
||||
|
||||
var pair = parseYawAndFace(engine, block, state);
|
||||
ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB());
|
||||
if (display == null) return;
|
||||
ItemStack itemStack = display.getItemStack();
|
||||
if (itemStack == null) return;
|
||||
|
||||
BiomeColor type = null;
|
||||
try {
|
||||
type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase());
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
|
||||
if (type != null) {
|
||||
var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
var potionColor = Color.fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue());
|
||||
var meta = itemStack.getItemMeta();
|
||||
switch (meta) {
|
||||
case LeatherArmorMeta armor -> armor.setColor(potionColor);
|
||||
case PotionMeta potion -> potion.setColor(potionColor);
|
||||
case MapMeta map -> map.setColor(potionColor);
|
||||
case null, default -> {}
|
||||
}
|
||||
itemStack.setItemMeta(meta);
|
||||
}
|
||||
display.setItemStack(itemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return NexoItems.itemNames()
|
||||
.stream()
|
||||
.map(i -> new Identifier("nexo", i))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return "nexo".equalsIgnoreCase(id.namespace());
|
||||
}
|
||||
}
|
||||
@@ -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,39 +0,0 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.function.NastyRunnable;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class AutoClosing implements AutoCloseable {
|
||||
private static final KMap<Thread, AutoClosing> CONTEXTS = new KMap<>();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
private final NastyRunnable action;
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed.getAndSet(true)) return;
|
||||
try {
|
||||
removeContext();
|
||||
action.run();
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void storeContext() {
|
||||
CONTEXTS.put(Thread.currentThread(), this);
|
||||
}
|
||||
|
||||
public void removeContext() {
|
||||
CONTEXTS.values().removeIf(c -> c == this);
|
||||
}
|
||||
|
||||
public static void closeContext() {
|
||||
AutoClosing closing = CONTEXTS.remove(Thread.currentThread());
|
||||
if (closing == null) return;
|
||||
closing.close();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
public enum BiomeColor {
|
||||
FOG,
|
||||
WATER,
|
||||
WATER_FOG,
|
||||
SKY,
|
||||
FOLIAGE,
|
||||
GRASS
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BlockPos {
|
||||
private int x;
|
||||
private int y;
|
||||
private int z;
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class BlockProperty {
|
||||
private static final Set<Class<?>> NATIVES = Set.of(Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, String.class);
|
||||
private final String name;
|
||||
private final Class<?> type;
|
||||
|
||||
private final Object defaultValue;
|
||||
private final Set<Object> values;
|
||||
private final Function<Object, String> nameFunction;
|
||||
private final Function<Object, Object> jsonFunction;
|
||||
|
||||
public <T extends Comparable<T>> BlockProperty(
|
||||
String name,
|
||||
Class<T> type,
|
||||
T defaultValue,
|
||||
Collection<T> values,
|
||||
Function<T, String> nameFunction
|
||||
) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.defaultValue = defaultValue;
|
||||
this.values = Collections.unmodifiableSet(new TreeSet<>(values));
|
||||
this.nameFunction = (Function<Object, String>) (Object) nameFunction;
|
||||
jsonFunction = NATIVES.contains(type) ? Function.identity() : this.nameFunction::apply;
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> BlockProperty ofEnum(Class<T> type, String name, T defaultValue) {
|
||||
return new BlockProperty(
|
||||
name,
|
||||
type,
|
||||
defaultValue,
|
||||
Arrays.asList(type.getEnumConstants()),
|
||||
val -> val == null ? "null" : val.name()
|
||||
);
|
||||
}
|
||||
|
||||
public static BlockProperty ofDouble(String name, float defaultValue, float min, float max, boolean exclusiveMin, boolean exclusiveMax) {
|
||||
return new BoundedDouble(
|
||||
name,
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
exclusiveMin,
|
||||
exclusiveMax,
|
||||
(f) -> String.format("%.2f", f)
|
||||
);
|
||||
}
|
||||
|
||||
public static BlockProperty ofLong(String name, long defaultValue, long min, long max, boolean exclusiveMin, boolean exclusiveMax) {
|
||||
return new BoundedLong(
|
||||
name,
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
exclusiveMin,
|
||||
exclusiveMax,
|
||||
value -> Long.toString(value)
|
||||
);
|
||||
}
|
||||
|
||||
public static BlockProperty ofBoolean(String name, boolean defaultValue) {
|
||||
return new BlockProperty(
|
||||
name,
|
||||
Boolean.class,
|
||||
defaultValue,
|
||||
List.of(true, false),
|
||||
(b) -> b ? "true" : "false"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return name + "=" + nameFunction.apply(defaultValue) + " [" + String.join(",", names()) + "]";
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String defaultValue() {
|
||||
return nameFunction.apply(defaultValue);
|
||||
}
|
||||
|
||||
public List<String> names() {
|
||||
return values.stream().map(nameFunction).toList();
|
||||
}
|
||||
|
||||
public Object defaultValueAsJson() {
|
||||
return jsonFunction.apply(defaultValue);
|
||||
}
|
||||
|
||||
public JSONArray valuesAsJson() {
|
||||
return new JSONArray(values.stream().map(jsonFunction).toList());
|
||||
}
|
||||
|
||||
public JSONObject buildJson() {
|
||||
var json = new JSONObject();
|
||||
json.put("type", jsonType());
|
||||
json.put("default", defaultValueAsJson());
|
||||
if (!values.isEmpty()) json.put("enum", valuesAsJson());
|
||||
return json;
|
||||
}
|
||||
|
||||
public String jsonType() {
|
||||
if (type == Boolean.class)
|
||||
return "boolean";
|
||||
if (type == Byte.class || type == Short.class || type == Integer.class || type == Long.class)
|
||||
return "integer";
|
||||
if (type == Float.class || type == Double.class)
|
||||
return "number";
|
||||
return "string";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (BlockProperty) obj;
|
||||
return Objects.equals(this.name, that.name) &&
|
||||
Objects.equals(this.values, that.values) &&
|
||||
Objects.equals(this.type, that.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, values, type);
|
||||
}
|
||||
|
||||
private static class BoundedLong extends BlockProperty {
|
||||
private final long min;
|
||||
private final long max;
|
||||
private final boolean exclusiveMin;
|
||||
private final boolean exclusiveMax;
|
||||
|
||||
public BoundedLong(
|
||||
String name,
|
||||
long defaultValue,
|
||||
long min,
|
||||
long max,
|
||||
boolean exclusiveMin,
|
||||
boolean exclusiveMax,
|
||||
Function<Long, String> nameFunction
|
||||
) {
|
||||
super(name, Long.class, defaultValue, List.of(), nameFunction);
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.exclusiveMin = exclusiveMin;
|
||||
this.exclusiveMax = exclusiveMax;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject buildJson() {
|
||||
return super.buildJson()
|
||||
.put("minimum", min)
|
||||
.put("maximum", max)
|
||||
.put("exclusiveMinimum", exclusiveMin)
|
||||
.put("exclusiveMaximum", exclusiveMax);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BoundedDouble extends BlockProperty {
|
||||
private final double min, max;
|
||||
private final boolean exclusiveMin, exclusiveMax;
|
||||
|
||||
public BoundedDouble(
|
||||
String name,
|
||||
double defaultValue,
|
||||
double min,
|
||||
double max,
|
||||
boolean exclusiveMin,
|
||||
boolean exclusiveMax,
|
||||
Function<Double, String> nameFunction
|
||||
) {
|
||||
super(name, Double.class, defaultValue, List.of(), nameFunction);
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.exclusiveMin = exclusiveMin;
|
||||
this.exclusiveMax = exclusiveMax;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject buildJson() {
|
||||
return super.buildJson()
|
||||
.put("minimum", min)
|
||||
.put("maximum", max)
|
||||
.put("exclusiveMinimum", exclusiveMin)
|
||||
.put("exclusiveMaximum", exclusiveMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Pair<A, B> {
|
||||
private A a;
|
||||
private B b;
|
||||
}
|
||||
@@ -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,31 +0,0 @@
|
||||
package art.arcane.iris.core.nms.datapack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IDataFixer {
|
||||
default JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
return json;
|
||||
}
|
||||
|
||||
JSONObject resolve(Dimension dimension, @Nullable IrisDimensionTypeOptions options);
|
||||
|
||||
void fixDimension(Dimension dimension, JSONObject json);
|
||||
|
||||
default JSONObject createDimension(Dimension base, int minY, int height, int logicalHeight, @Nullable IrisDimensionTypeOptions options) {
|
||||
JSONObject obj = resolve(base, options);
|
||||
obj.put("min_y", minY);
|
||||
obj.put("height", height);
|
||||
obj.put("logical_height", logicalHeight);
|
||||
fixDimension(base, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
enum Dimension {
|
||||
OVERWORLD,
|
||||
NETHER,
|
||||
END
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1192;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.*;
|
||||
|
||||
public class DataFixerV1192 implements IDataFixer {
|
||||
private static final Map<Dimension, IrisDimensionTypeOptions> OPTIONS = Map.of(
|
||||
Dimension.OVERWORLD, new IrisDimensionTypeOptions(
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
1d,
|
||||
0f,
|
||||
null,
|
||||
192,
|
||||
0),
|
||||
Dimension.NETHER, new IrisDimensionTypeOptions(
|
||||
TRUE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
8d,
|
||||
0.1f,
|
||||
18000L,
|
||||
null,
|
||||
15),
|
||||
Dimension.END, new IrisDimensionTypeOptions(
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
1d,
|
||||
0f,
|
||||
6000L,
|
||||
null,
|
||||
0)
|
||||
);
|
||||
|
||||
private static final Map<Dimension, String> DIMENSIONS = Map.of(
|
||||
Dimension.OVERWORLD, """
|
||||
{
|
||||
"effects": "minecraft:overworld",
|
||||
"infiniburn": "#minecraft:infiniburn_overworld",
|
||||
"monster_spawn_light_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"value": {
|
||||
"max_inclusive": 7,
|
||||
"min_inclusive": 0
|
||||
}
|
||||
}
|
||||
}""",
|
||||
Dimension.NETHER, """
|
||||
{
|
||||
"effects": "minecraft:the_nether",
|
||||
"infiniburn": "#minecraft:infiniburn_nether",
|
||||
"monster_spawn_light_level": 7,
|
||||
}""",
|
||||
Dimension.END, """
|
||||
{
|
||||
"effects": "minecraft:the_end",
|
||||
"infiniburn": "#minecraft:infiniburn_end",
|
||||
"monster_spawn_light_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"value": {
|
||||
"max_inclusive": 7,
|
||||
"min_inclusive": 0
|
||||
}
|
||||
}
|
||||
}"""
|
||||
);
|
||||
|
||||
@Override
|
||||
public JSONObject resolve(Dimension dimension, @Nullable IrisDimensionTypeOptions options) {
|
||||
return options == null ? OPTIONS.get(dimension).toJson() : options.resolve(OPTIONS.get(dimension)).toJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fixDimension(Dimension dimension, JSONObject json) {
|
||||
var missing = new JSONObject(DIMENSIONS.get(dimension));
|
||||
for (String key : missing.keySet()) {
|
||||
if (json.has(key)) continue;
|
||||
json.put(key, missing.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1206;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class DataFixerV1206 extends DataFixerV1192 {
|
||||
@Override
|
||||
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
int spawnRarity = biome.getSpawnRarity();
|
||||
if (spawnRarity > 0) {
|
||||
json.put("creature_spawn_probability", Math.min(spawnRarity/20d, 0.9999999));
|
||||
} else {
|
||||
json.remove("creature_spawn_probability");
|
||||
}
|
||||
|
||||
var spawns = biome.getSpawns();
|
||||
if (spawns != null && spawns.isNotEmpty()) {
|
||||
JSONObject spawners = new JSONObject();
|
||||
KMap<IrisBiomeCustomSpawnType, JSONArray> groups = new KMap<>();
|
||||
|
||||
for (IrisBiomeCustomSpawn i : spawns) {
|
||||
if (i == null) {
|
||||
continue;
|
||||
}
|
||||
EntityType type = i.getType();
|
||||
if (type == null) {
|
||||
Iris.warn("Skipping custom biome spawn with null entity type in biome " + biome.getId());
|
||||
continue;
|
||||
}
|
||||
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
|
||||
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
|
||||
JSONObject o = new JSONObject();
|
||||
NamespacedKey key = type.getKey();
|
||||
if (key == null) {
|
||||
Iris.warn("Skipping custom biome spawn with unresolved entity key in biome " + biome.getId());
|
||||
continue;
|
||||
}
|
||||
o.put("type", key.toString());
|
||||
o.put("weight", i.getWeight());
|
||||
o.put("minCount", i.getMinCount());
|
||||
o.put("maxCount", i.getMaxCount());
|
||||
g.put(o);
|
||||
}
|
||||
|
||||
for (IrisBiomeCustomSpawnType i : groups.k()) {
|
||||
spawners.put(i.name().toLowerCase(Locale.ROOT), groups.get(i));
|
||||
}
|
||||
|
||||
json.put("spawners", spawners);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fixDimension(Dimension dimension, JSONObject json) {
|
||||
super.fixDimension(dimension, json);
|
||||
if (!(json.get("monster_spawn_light_level") instanceof JSONObject lightLevel))
|
||||
return;
|
||||
var value = (JSONObject) lightLevel.remove("value");
|
||||
lightLevel.put("max_inclusive", value.get("max_inclusive"));
|
||||
lightLevel.put("min_inclusive", value.get("min_inclusive"));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1213;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.v1206.DataFixerV1206;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
public class DataFixerV1213 extends DataFixerV1206 {
|
||||
|
||||
@Override
|
||||
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
json = super.fixCustomBiome(biome, json);
|
||||
json.put("carvers", new JSONArray());
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -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,268 +0,0 @@
|
||||
package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.RollingSequence;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class DeepSearchPregenerator extends Thread implements Listener {
|
||||
@Getter
|
||||
private static DeepSearchPregenerator instance;
|
||||
private final DeepSearchJob job;
|
||||
private final File destination;
|
||||
private final int maxPosition;
|
||||
private World world;
|
||||
private final ChronoLatch latch;
|
||||
private static AtomicInteger foundChunks;
|
||||
private final AtomicInteger foundLast;
|
||||
private final AtomicInteger foundTotalChunks;
|
||||
private final AtomicLong startTime;
|
||||
private final RollingSequence chunksPerSecond;
|
||||
private final RollingSequence chunksPerMinute;
|
||||
private final AtomicInteger chunkCachePos;
|
||||
private final AtomicInteger chunkCacheSize;
|
||||
private int pos;
|
||||
private final AtomicInteger foundCacheLast;
|
||||
private final AtomicInteger foundCache;
|
||||
private LinkedHashMap<Integer, Position2> chunkCache;
|
||||
private KList<Position2> chunkQueue;
|
||||
private final ReentrantLock cacheLock;
|
||||
|
||||
private static final Map<String, DeepSearchJob> jobs = new HashMap<>();
|
||||
|
||||
public DeepSearchPregenerator(DeepSearchJob job, File destination) {
|
||||
this.job = job;
|
||||
this.chunkCacheSize = new AtomicInteger(); // todo
|
||||
this.chunkCachePos = new AtomicInteger(1000);
|
||||
this.foundCacheLast = new AtomicInteger();
|
||||
this.foundCache = new AtomicInteger();
|
||||
this.cacheLock = new ReentrantLock();
|
||||
this.destination = destination;
|
||||
this.chunkCache = new LinkedHashMap<>();
|
||||
this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
|
||||
}).count();
|
||||
this.world = Bukkit.getWorld(job.getWorld().getUID());
|
||||
this.chunkQueue = new KList<>();
|
||||
this.latch = new ChronoLatch(3000);
|
||||
this.startTime = new AtomicLong(M.ms());
|
||||
this.chunksPerSecond = new RollingSequence(10);
|
||||
this.chunksPerMinute = new RollingSequence(10);
|
||||
foundChunks = new AtomicInteger(0);
|
||||
this.foundLast = new AtomicInteger(0);
|
||||
this.foundTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2)));
|
||||
|
||||
this.pos = 0;
|
||||
jobs.put(job.getWorld().getName(), job);
|
||||
DeepSearchPregenerator.instance = this;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(WorldUnloadEvent e) {
|
||||
if (e.getWorld().equals(world)) {
|
||||
interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
while (!interrupted()) {
|
||||
tick();
|
||||
}
|
||||
try {
|
||||
saveNow();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
DeepSearchJob job = jobs.get(world.getName());
|
||||
// chunkCache(); //todo finish this
|
||||
if (latch.flip() && !job.paused) {
|
||||
if (cacheLock.isLocked()) {
|
||||
Iris.info("DeepFinder: Caching: " + chunkCachePos.get() + " Of " + chunkCacheSize.get());
|
||||
} else {
|
||||
long eta = computeETA();
|
||||
save();
|
||||
int secondGenerated = foundChunks.get() - foundLast.get();
|
||||
foundLast.set(foundChunks.get());
|
||||
secondGenerated = secondGenerated / 3;
|
||||
chunksPerSecond.put(secondGenerated);
|
||||
chunksPerMinute.put(secondGenerated * 60);
|
||||
Iris.info("DeepFinder: " + C.IRIS + world.getName() + C.RESET + " Searching: " + Form.f(foundChunks.get()) + " of " + Form.f(foundTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2));
|
||||
}
|
||||
|
||||
}
|
||||
if (foundChunks.get() >= foundTotalChunks.get()) {
|
||||
Iris.info("Completed DeepSearch!");
|
||||
interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private long computeETA() {
|
||||
return (long) ((foundTotalChunks.get() - foundChunks.get()) / chunksPerSecond.getAverage()) * 1000;
|
||||
// todo broken
|
||||
}
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
|
||||
private void queueSystem(Position2 chunk) {
|
||||
if (chunkQueue.isEmpty()) {
|
||||
for (int limit = 512; limit != 0; limit--) {
|
||||
pos = job.getPosition() + 1;
|
||||
chunkQueue.add(getChunk(pos));
|
||||
}
|
||||
} else {
|
||||
//MCAUtil.read();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void findInChunk(World world, int x, int z) throws IOException {
|
||||
int xx = x * 16;
|
||||
int zz = z * 16;
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
for (int j = 0; j < 16; j++) {
|
||||
int height = engine.getHeight(xx + i, zz + j);
|
||||
if (height > 300) {
|
||||
File found = new File("plugins", "iris" + File.separator + "found.txt");
|
||||
found.getParentFile().mkdirs();
|
||||
IrisBiome biome = engine.getBiome(xx, engine.getHeight(), zz);
|
||||
Iris.info("Found at! " + xx + ", " + zz + " Biome ID: " + biome.getName());
|
||||
try (FileWriter writer = new FileWriter(found, true)) {
|
||||
writer.write("Biome at: X: " + xx + " Z: " + zz + " Biome ID: " + biome.getName() + "\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Position2 getChunk(int position) {
|
||||
int p = -1;
|
||||
AtomicInteger xx = new AtomicInteger();
|
||||
AtomicInteger zz = new AtomicInteger();
|
||||
Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
|
||||
xx.set(x);
|
||||
zz.set(z);
|
||||
});
|
||||
|
||||
while (s.hasNext() && p++ < position) {
|
||||
s.next();
|
||||
}
|
||||
|
||||
return new Position2(xx.get(), zz.get());
|
||||
}
|
||||
|
||||
public void save() {
|
||||
J.a(() -> {
|
||||
try {
|
||||
saveNow();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setPausedDeep(World world) {
|
||||
DeepSearchJob job = jobs.get(world.getName());
|
||||
if (isPausedDeep(world)){
|
||||
job.paused = false;
|
||||
} else {
|
||||
job.paused = true;
|
||||
}
|
||||
|
||||
if ( job.paused) {
|
||||
Iris.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Paused");
|
||||
} else {
|
||||
Iris.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Resumed");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPausedDeep(World world) {
|
||||
DeepSearchJob job = jobs.get(world.getName());
|
||||
return job != null && job.isPaused();
|
||||
}
|
||||
|
||||
public void shutdownInstance(World world) throws IOException {
|
||||
Iris.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Shutting down..");
|
||||
DeepSearchJob job = jobs.get(world.getName());
|
||||
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
|
||||
File deepFile = new File(worldDirectory, "DeepSearch.json");
|
||||
|
||||
if (job == null) {
|
||||
Iris.error("No DeepSearch job found for world: " + world.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!job.isPaused()) {
|
||||
job.setPaused(true);
|
||||
}
|
||||
save();
|
||||
jobs.remove(world.getName());
|
||||
J.a(() -> {
|
||||
while (deepFile.exists()) {
|
||||
deepFile.delete();
|
||||
J.sleep(1000);
|
||||
}
|
||||
Iris.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed.");
|
||||
}, 20);
|
||||
} catch (Exception e) {
|
||||
Iris.error("Failed to shutdown DeepSearch for " + world.getName());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
saveNow();
|
||||
interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void saveNow() throws IOException {
|
||||
IO.writeAll(this.destination, new Gson().toJson(job));
|
||||
}
|
||||
|
||||
@Data
|
||||
@lombok.Builder
|
||||
public static class DeepSearchJob {
|
||||
private World world;
|
||||
@lombok.Builder.Default
|
||||
private int radiusBlocks = 5000;
|
||||
@lombok.Builder.Default
|
||||
private int position = 0;
|
||||
@lombok.Builder.Default
|
||||
boolean paused = false;
|
||||
}
|
||||
}
|
||||
@@ -1,367 +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.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.tools.IrisPackBenchmarking;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
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.volmlib.util.math.Position2;
|
||||
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.Looper;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
||||
public class IrisPregenerator {
|
||||
private static final double INVALID = 9223372036854775807d;
|
||||
private final PregenTask task;
|
||||
private final PregeneratorMethod generator;
|
||||
private final PregenListener listener;
|
||||
private final Looper ticker;
|
||||
private final AtomicBoolean paused;
|
||||
private final AtomicBoolean shutdown;
|
||||
private final RollingSequence cachedPerSecond;
|
||||
private final RollingSequence chunksPerSecond;
|
||||
private final RollingSequence chunksPerMinute;
|
||||
private final RollingSequence regionsPerMinute;
|
||||
private final KList<Integer> chunksPerSecondHistory;
|
||||
private final AtomicLong generated;
|
||||
private final AtomicLong generatedLast;
|
||||
private final AtomicLong generatedLastMinute;
|
||||
private final AtomicLong cached;
|
||||
private final AtomicLong cachedLast;
|
||||
private final AtomicLong cachedLastMinute;
|
||||
private final AtomicLong totalChunks;
|
||||
private final AtomicLong startTime;
|
||||
private final ChronoLatch minuteLatch;
|
||||
private final AtomicReference<String> currentGeneratorMethod;
|
||||
private final KSet<Position2> generatedRegions;
|
||||
private final KSet<Position2> retry;
|
||||
private final KSet<Position2> net;
|
||||
private final ChronoLatch cl;
|
||||
private final ChronoLatch saveLatch;
|
||||
private final IrisPackBenchmarking benchmarking;
|
||||
|
||||
public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) {
|
||||
benchmarking = IrisPackBenchmarking.getInstance();
|
||||
this.listener = listenify(listener);
|
||||
cl = new ChronoLatch(5000);
|
||||
saveLatch = new ChronoLatch(IrisSettings.get().getPregen().getSaveIntervalMs());
|
||||
generatedRegions = new KSet<>();
|
||||
this.shutdown = new AtomicBoolean(false);
|
||||
this.paused = new AtomicBoolean(false);
|
||||
this.task = task;
|
||||
this.generator = generator;
|
||||
retry = new KSet<>();
|
||||
net = new KSet<>();
|
||||
currentGeneratorMethod = new AtomicReference<>("Void");
|
||||
minuteLatch = new ChronoLatch(60000, false);
|
||||
cachedPerSecond = new RollingSequence(5);
|
||||
chunksPerSecond = new RollingSequence(10);
|
||||
chunksPerMinute = new RollingSequence(10);
|
||||
regionsPerMinute = new RollingSequence(10);
|
||||
chunksPerSecondHistory = new KList<>();
|
||||
generated = new AtomicLong(0);
|
||||
generatedLast = new AtomicLong(0);
|
||||
generatedLastMinute = new AtomicLong(0);
|
||||
cached = new AtomicLong();
|
||||
cachedLast = new AtomicLong(0);
|
||||
cachedLastMinute = new AtomicLong(0);
|
||||
totalChunks = new AtomicLong(0);
|
||||
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
|
||||
startTime = new AtomicLong(M.ms());
|
||||
ticker = new Looper() {
|
||||
@Override
|
||||
protected long loop() {
|
||||
long eta = computeETA();
|
||||
|
||||
long secondCached = cached.get() - cachedLast.get();
|
||||
cachedLast.set(cached.get());
|
||||
cachedPerSecond.put(secondCached);
|
||||
|
||||
long secondGenerated = generated.get() - generatedLast.get() - secondCached;
|
||||
generatedLast.set(generated.get());
|
||||
if (secondCached == 0 || secondGenerated != 0) {
|
||||
chunksPerSecond.put(secondGenerated);
|
||||
chunksPerSecondHistory.add((int) secondGenerated);
|
||||
}
|
||||
|
||||
if (minuteLatch.flip()) {
|
||||
long minuteCached = cached.get() - cachedLastMinute.get();
|
||||
cachedLastMinute.set(cached.get());
|
||||
|
||||
long minuteGenerated = generated.get() - generatedLastMinute.get() - minuteCached;
|
||||
generatedLastMinute.set(generated.get());
|
||||
if (minuteCached == 0 || minuteGenerated != 0) {
|
||||
chunksPerMinute.put(minuteGenerated);
|
||||
regionsPerMinute.put((double) minuteGenerated / 1024D);
|
||||
}
|
||||
}
|
||||
boolean cached = cachedPerSecond.getAverage() != 0;
|
||||
|
||||
listener.onTick(
|
||||
cached ? cachedPerSecond.getAverage() : chunksPerSecond.getAverage(),
|
||||
chunksPerMinute.getAverage(),
|
||||
regionsPerMinute.getAverage(),
|
||||
(double) generated.get() / (double) totalChunks.get(), generated.get(),
|
||||
totalChunks.get(),
|
||||
totalChunks.get() - generated.get(), eta, M.ms() - startTime.get(), currentGeneratorMethod.get(),
|
||||
cached);
|
||||
|
||||
if (cl.flip()) {
|
||||
double percentage = ((double) generated.get() / (double) totalChunks.get()) * 100;
|
||||
|
||||
Iris.info("%s: %s of %s (%.0f%%), %s/s ETA: %s",
|
||||
benchmarking != null ? "Benchmarking" : "Pregen",
|
||||
Form.f(generated.get()),
|
||||
Form.f(totalChunks.get()),
|
||||
percentage,
|
||||
cached ?
|
||||
"Cached " + Form.f((int) cachedPerSecond.getAverage()) :
|
||||
Form.f((int) chunksPerSecond.getAverage()),
|
||||
Form.duration(eta, 2)
|
||||
);
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private long computeETA() {
|
||||
long gen = generated.get();
|
||||
long total = totalChunks.get();
|
||||
long remaining = total - gen;
|
||||
double d;
|
||||
if (gen > 1024) {
|
||||
d = remaining * ((double) (M.ms() - startTime.get()) / (double) gen);
|
||||
} else {
|
||||
double cps = chunksPerSecond.getAverage();
|
||||
d = cps > 0 ? (remaining / cps) * 1000 : 0;
|
||||
}
|
||||
return Double.isFinite(d) && d != INVALID ? (long) d : 0;
|
||||
}
|
||||
|
||||
|
||||
public void close() {
|
||||
shutdown.set(true);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
init();
|
||||
ticker.start();
|
||||
checkRegions();
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
task.iterateRegions((x, z) -> visitRegion(x, z, true));
|
||||
task.iterateRegions((x, z) -> visitRegion(x, z, false));
|
||||
Iris.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
|
||||
shutdown();
|
||||
if (benchmarking == null) {
|
||||
Iris.info(C.IRIS + "Pregen stopped.");
|
||||
} else {
|
||||
benchmarking.finishedBenchmark(chunksPerSecondHistory);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRegions() {
|
||||
task.iterateRegions(this::checkRegion);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
generator.init();
|
||||
}
|
||||
|
||||
private void shutdown() {
|
||||
listener.onSaving();
|
||||
generator.close();
|
||||
ticker.interrupt();
|
||||
listener.onClose();
|
||||
Mantle mantle = getMantle();
|
||||
if (mantle != null) {
|
||||
mantle.trim(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void visitRegion(int x, int z, boolean regions) {
|
||||
while (paused.get() && !shutdown.get()) {
|
||||
J.sleep(50);
|
||||
}
|
||||
|
||||
if (shutdown.get()) {
|
||||
listener.onRegionSkipped(x, z);
|
||||
return;
|
||||
}
|
||||
|
||||
Position2 pos = new Position2(x, z);
|
||||
|
||||
if (generatedRegions.contains(pos)) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentGeneratorMethod.set(generator.getMethod(x, z));
|
||||
boolean hit = false;
|
||||
if (generator.supportsRegions(x, z, listener) && regions) {
|
||||
hit = true;
|
||||
listener.onRegionGenerating(x, z);
|
||||
generator.generateRegion(x, z, listener);
|
||||
} else if (!regions) {
|
||||
hit = true;
|
||||
listener.onRegionGenerating(x, z);
|
||||
task.iterateChunks(x, z, (xx, zz) -> {
|
||||
while (paused.get() && !shutdown.get()) {
|
||||
J.sleep(50);
|
||||
}
|
||||
|
||||
generator.generateChunk(xx, zz, listener);
|
||||
});
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
listener.onRegionGenerated(x, z);
|
||||
|
||||
if (saveLatch.flip()) {
|
||||
listener.onSaving();
|
||||
generator.save();
|
||||
Mantle mantle = getMantle();
|
||||
if (mantle != null) {
|
||||
mantle.trim(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
generatedRegions.add(pos);
|
||||
checkRegions();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRegion(int x, int z) {
|
||||
if (generatedRegions.contains(new Position2(x, z))) {
|
||||
return;
|
||||
}
|
||||
|
||||
generator.supportsRegions(x, z, listener);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
paused.set(true);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused.set(false);
|
||||
}
|
||||
|
||||
private PregenListener listenify(PregenListener listener) {
|
||||
return new PregenListener() {
|
||||
@Override
|
||||
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
|
||||
listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerating(int x, int z) {
|
||||
listener.onChunkGenerating(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerated(int x, int z, boolean c) {
|
||||
listener.onChunkGenerated(x, z, c);
|
||||
generated.addAndGet(1);
|
||||
if (c) cached.addAndGet(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegionGenerated(int x, int z) {
|
||||
listener.onRegionGenerated(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegionGenerating(int x, int z) {
|
||||
listener.onRegionGenerating(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkCleaned(int x, int z) {
|
||||
listener.onChunkCleaned(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegionSkipped(int x, int z) {
|
||||
listener.onRegionSkipped(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkStarted(int x, int z) {
|
||||
net.add(new Position2(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkFailed(int x, int z) {
|
||||
retry.add(new Position2(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkReclaim(int revert) {
|
||||
generated.addAndGet(-revert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkGeneratedChunk(int x, int z) {
|
||||
generated.addAndGet(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNetworkDownloaded(int x, int z) {
|
||||
net.remove(new Position2(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
listener.onClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaving() {
|
||||
listener.onSaving();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkExistsInRegionGen(int x, int z) {
|
||||
listener.onChunkExistsInRegionGen(x, z);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public boolean paused() {
|
||||
return paused.get();
|
||||
}
|
||||
|
||||
public Mantle getMantle() {
|
||||
return generator.getMantle();
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.math.Spiraled;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class PregenTask {
|
||||
private static final KMap<Long, int[]> ORDERS = new KMap<>();
|
||||
|
||||
@Builder.Default
|
||||
private final boolean gui = false;
|
||||
@Builder.Default
|
||||
private final Position2 center = new Position2(0, 0);
|
||||
@Builder.Default
|
||||
private final int radiusX = 1;
|
||||
@Builder.Default
|
||||
private final int radiusZ = 1;
|
||||
|
||||
private final Bounds bounds = new Bounds();
|
||||
|
||||
protected PregenTask(boolean gui, Position2 center, int radiusX, int radiusZ) {
|
||||
this.gui = gui;
|
||||
this.center = new ProxiedPos(center);
|
||||
this.radiusX = radiusX;
|
||||
this.radiusZ = radiusZ;
|
||||
bounds.update();
|
||||
}
|
||||
|
||||
public static void iterateRegion(int xr, int zr, Spiraled s, Position2 pull) {
|
||||
iterateRegion(xr, zr, s, pull.getX(), pull.getZ());
|
||||
}
|
||||
|
||||
public static void iterateRegion(int xr, int zr, Spiraled s, int pullX, int pullZ) {
|
||||
for (int packed : orderForPull(pullX, pullZ)) {
|
||||
s.on(PowerOfTwoCoordinates.unpackLocal32X(packed) + PowerOfTwoCoordinates.regionToChunk(xr), PowerOfTwoCoordinates.unpackLocal32Z(packed) + PowerOfTwoCoordinates.regionToChunk(zr));
|
||||
}
|
||||
}
|
||||
|
||||
public static void iterateRegion(int xr, int zr, Spiraled s) {
|
||||
iterateRegion(xr, zr, s, -PowerOfTwoCoordinates.regionToChunk(xr), -PowerOfTwoCoordinates.regionToChunk(zr));
|
||||
}
|
||||
|
||||
private static int[] orderForPull(int pullX, int pullZ) {
|
||||
long key = orderKey(pullX, pullZ);
|
||||
return ORDERS.computeIfAbsent(key, PregenTask::computeOrder);
|
||||
}
|
||||
|
||||
private static int[] computeOrder(long key) {
|
||||
int pullX = (int) (key >> 32);
|
||||
int pullZ = (int) key;
|
||||
Position2 pull = new Position2(pullX, pullZ);
|
||||
KList<Position2> p = new KList<>();
|
||||
new Spiraler(33, 33, (x, z) -> {
|
||||
int xx = (x + 15);
|
||||
int zz = (z + 15);
|
||||
if (xx < 0 || xx > 31 || zz < 0 || zz > 31) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.add(new Position2(xx, zz));
|
||||
}).drain();
|
||||
p.sort(Comparator.comparing((i) -> i.distance(pull)));
|
||||
|
||||
int[] packed = new int[p.size()];
|
||||
for (int index = 0; index < p.size(); index++) {
|
||||
Position2 position = p.get(index);
|
||||
packed[index] = PowerOfTwoCoordinates.packLocal32(position.getX(), position.getZ());
|
||||
}
|
||||
|
||||
return packed;
|
||||
}
|
||||
|
||||
private static long orderKey(int pullX, int pullZ) {
|
||||
long high = (long) pullX << 32;
|
||||
long low = pullZ & 0xFFFFFFFFL;
|
||||
return high | low;
|
||||
}
|
||||
|
||||
public void iterateRegions(Spiraled s) {
|
||||
Bound bound = bounds.region();
|
||||
new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> {
|
||||
if (bound.check(x, z)) s.on(x, z);
|
||||
})).setOffset(PowerOfTwoCoordinates.blockToRegionFloor(center.getX()), PowerOfTwoCoordinates.blockToRegionFloor(center.getZ())).drain();
|
||||
}
|
||||
|
||||
public void iterateChunks(int rX, int rZ, Spiraled s) {
|
||||
Bound bound = bounds.chunk();
|
||||
iterateRegion(rX, rZ, ((x, z) -> {
|
||||
if (bound.check(x, z)) s.on(x, z);
|
||||
}));
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface InterleavedChunkConsumer {
|
||||
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
|
||||
}
|
||||
|
||||
public void iterateAllChunks(Spiraled s) {
|
||||
iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s)));
|
||||
}
|
||||
|
||||
public void iterateAllChunksInterleaved(InterleavedChunkConsumer consumer) {
|
||||
List<int[]> regions = new ArrayList<>();
|
||||
iterateRegions((rX, rZ) -> regions.add(new int[]{rX, rZ}));
|
||||
|
||||
List<List<int[]>> regionChunks = new ArrayList<>();
|
||||
for (int[] region : regions) {
|
||||
List<int[]> chunks = new ArrayList<>();
|
||||
iterateChunks(region[0], region[1], (cx, cz) -> chunks.add(new int[]{region[0], region[1], cx, cz}));
|
||||
if (!chunks.isEmpty()) {
|
||||
regionChunks.add(chunks);
|
||||
}
|
||||
}
|
||||
|
||||
int[] indices = new int[regionChunks.size()];
|
||||
boolean anyRemaining = true;
|
||||
while (anyRemaining) {
|
||||
anyRemaining = false;
|
||||
for (int r = 0; r < regionChunks.size(); r++) {
|
||||
List<int[]> chunks = regionChunks.get(r);
|
||||
int idx = indices[r];
|
||||
if (idx >= chunks.size()) {
|
||||
continue;
|
||||
}
|
||||
anyRemaining = true;
|
||||
int[] entry = chunks.get(idx);
|
||||
boolean first = idx == 0;
|
||||
boolean last = idx == chunks.size() - 1;
|
||||
indices[r]++;
|
||||
if (!consumer.on(entry[0], entry[1], entry[2], entry[3], first, last)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Bounds {
|
||||
private Bound chunk = null;
|
||||
private Bound region = null;
|
||||
|
||||
public void update() {
|
||||
int maxX = center.getX() + radiusX;
|
||||
int maxZ = center.getZ() + radiusZ;
|
||||
int minX = center.getX() - radiusX;
|
||||
int minZ = center.getZ() - radiusZ;
|
||||
|
||||
chunk = new Bound(
|
||||
PowerOfTwoCoordinates.blockToChunkFloor(minX),
|
||||
PowerOfTwoCoordinates.blockToChunkFloor(minZ),
|
||||
PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.CHUNK_BITS),
|
||||
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.CHUNK_BITS)
|
||||
);
|
||||
region = new Bound(
|
||||
PowerOfTwoCoordinates.blockToRegionFloor(minX),
|
||||
PowerOfTwoCoordinates.blockToRegionFloor(minZ),
|
||||
PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.REGION_BITS),
|
||||
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.REGION_BITS)
|
||||
);
|
||||
}
|
||||
|
||||
public Bound chunk() {
|
||||
if (chunk == null) update();
|
||||
return chunk;
|
||||
}
|
||||
|
||||
public Bound region() {
|
||||
if (region == null) update();
|
||||
return region;
|
||||
}
|
||||
}
|
||||
|
||||
private record Bound(int minX, int minZ, int maxX, int maxZ, int sizeX, int sizeZ) {
|
||||
private Bound(int minX, int minZ, int maxX, int maxZ) {
|
||||
this(minX, minZ, maxX, maxZ, maxX - minX + 1, maxZ - minZ + 1);
|
||||
}
|
||||
|
||||
boolean check(int x, int z) {
|
||||
return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProxiedPos extends Position2 {
|
||||
public ProxiedPos(Position2 p) {
|
||||
super(p.getX(), p.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setX(int x) {
|
||||
throw new IllegalStateException("This Position2 may not be modified");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZ(int z) {
|
||||
throw new IllegalStateException("This Position2 may not be modified");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package art.arcane.iris.core.pregenerator.cache;
|
||||
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.documentation.RegionCoordinates;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface PregenCache {
|
||||
default boolean isThreadSafe() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
boolean isChunkCached(int x, int z);
|
||||
|
||||
@RegionCoordinates
|
||||
boolean isRegionCached(int x, int z);
|
||||
|
||||
@ChunkCoordinates
|
||||
void cacheChunk(int x, int z);
|
||||
|
||||
@RegionCoordinates
|
||||
void cacheRegion(int x, int z);
|
||||
|
||||
void write();
|
||||
|
||||
void trim(long unloadDuration);
|
||||
|
||||
static PregenCache create(File directory) {
|
||||
if (directory == null) return EMPTY;
|
||||
return new PregenCacheImpl(directory, 16);
|
||||
}
|
||||
|
||||
default PregenCache sync() {
|
||||
if (isThreadSafe()) return this;
|
||||
return new SynchronizedCache(this);
|
||||
}
|
||||
|
||||
PregenCache EMPTY = new PregenCache() {
|
||||
@Override
|
||||
public boolean isThreadSafe() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkCached(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegionCached(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheChunk(int x, int z) {}
|
||||
|
||||
@Override
|
||||
public void cacheRegion(int x, int z) {}
|
||||
|
||||
@Override
|
||||
public void write() {}
|
||||
|
||||
@Override
|
||||
public void trim(long unloadDuration) {}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
-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);
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package art.arcane.iris.core.pregenerator.cache;
|
||||
|
||||
record SynchronizedCache(PregenCache cache) implements PregenCache {
|
||||
@Override
|
||||
public boolean isThreadSafe() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkCached(int x, int z) {
|
||||
synchronized (cache) {
|
||||
return cache.isChunkCached(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegionCached(int x, int z) {
|
||||
synchronized (cache) {
|
||||
return cache.isRegionCached(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheChunk(int x, int z) {
|
||||
synchronized (cache) {
|
||||
cache.cacheChunk(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheRegion(int x, int z) {
|
||||
synchronized (cache) {
|
||||
cache.cacheRegion(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write() {
|
||||
synchronized (cache) {
|
||||
cache.write();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trim(long unloadDuration) {
|
||||
synchronized (cache) {
|
||||
cache.trim(unloadDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,87 +0,0 @@
|
||||
package art.arcane.iris.core.pregenerator.methods;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
||||
import art.arcane.iris.core.service.GlobalCacheSVC;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class CachedPregenMethod implements PregeneratorMethod {
|
||||
private final PregeneratorMethod method;
|
||||
private final PregenCache cache;
|
||||
|
||||
public CachedPregenMethod(PregeneratorMethod method, String worldName) {
|
||||
this.method = method;
|
||||
var cache = Iris.service(GlobalCacheSVC.class).get(worldName);
|
||||
if (cache == null) {
|
||||
Iris.debug("Could not find existing cache for " + worldName + " creating fallback");
|
||||
cache = GlobalCacheSVC.createDefault(worldName);
|
||||
}
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
method.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
method.close();
|
||||
cache.write();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
method.save();
|
||||
cache.write();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRegions(int x, int z, PregenListener listener) {
|
||||
return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod(int x, int z) {
|
||||
return method.getMethod(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateRegion(int x, int z, PregenListener listener) {
|
||||
if (cache.isRegionCached(x, z)) {
|
||||
listener.onRegionGenerated(x, z);
|
||||
|
||||
int rX = x << 5, rZ = z << 5;
|
||||
for (int cX = 0; cX < 32; cX++) {
|
||||
for (int cZ = 0; cZ < 32; cZ++) {
|
||||
listener.onChunkGenerated(rX + cX, rZ + cZ, true);
|
||||
listener.onChunkCleaned(rX + cX, rZ + cZ);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
method.generateRegion(x, z, listener);
|
||||
cache.cacheRegion(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
if (cache.isChunkCached(x, z)) {
|
||||
listener.onChunkGenerated(x, z, true);
|
||||
listener.onChunkCleaned(x, z);
|
||||
return;
|
||||
}
|
||||
method.generateChunk(x, z, listener);
|
||||
cache.cacheChunk(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
return method.getMantle();
|
||||
}
|
||||
}
|
||||
-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,243 +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.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.board.Board;
|
||||
import art.arcane.volmlib.util.board.BoardProvider;
|
||||
import art.arcane.volmlib.util.board.BoardSettings;
|
||||
import art.arcane.volmlib.util.board.ScoreDirection;
|
||||
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.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BoardSVC implements IrisService, BoardProvider {
|
||||
private final KMap<Player, PlayerBoard> boards = new KMap<>();
|
||||
private BoardSettings settings;
|
||||
private boolean boardEnabled;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
boardEnabled = true;
|
||||
settings = BoardSettings.builder()
|
||||
.boardProvider(this)
|
||||
.scoreDirection(ScoreDirection.DOWN)
|
||||
.build();
|
||||
|
||||
for (Player player : Iris.instance.getServer().getOnlinePlayers()) {
|
||||
J.runEntity(player, () -> updatePlayer(player));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
boardEnabled = false;
|
||||
for (PlayerBoard board : new ArrayList<>(boards.values())) {
|
||||
board.cancel();
|
||||
}
|
||||
boards.clear();
|
||||
settings = null;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerChangedWorldEvent e) {
|
||||
J.runEntity(e.getPlayer(), () -> updatePlayer(e.getPlayer()));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerJoinEvent e) {
|
||||
J.runEntity(e.getPlayer(), () -> updatePlayer(e.getPlayer()));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerQuitEvent e) {
|
||||
remove(e.getPlayer());
|
||||
}
|
||||
|
||||
public void updatePlayer(Player p) {
|
||||
if (!boardEnabled || settings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.isOwnedByCurrentRegion(p)) {
|
||||
J.runEntity(p, () -> updatePlayer(p));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEligibleWorld(p)) {
|
||||
boards.computeIfAbsent(p, PlayerBoard::new);
|
||||
return;
|
||||
}
|
||||
|
||||
remove(p);
|
||||
}
|
||||
|
||||
private void remove(Player player) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.isOwnedByCurrentRegion(player)) {
|
||||
J.runEntity(player, () -> remove(player));
|
||||
return;
|
||||
}
|
||||
|
||||
var board = boards.remove(player);
|
||||
if (board != null) {
|
||||
board.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle(Player player) {
|
||||
return C.GREEN + "Iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLines(Player player) {
|
||||
PlayerBoard board = boards.get(player);
|
||||
if (board == null) {
|
||||
return List.of();
|
||||
}
|
||||
return board.lines;
|
||||
}
|
||||
|
||||
private boolean isEligibleWorld(Player player) {
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
World world = player.getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
return access != null && access.getEngine() != null;
|
||||
}
|
||||
|
||||
@Data
|
||||
public class PlayerBoard {
|
||||
private final Player player;
|
||||
private final Board board;
|
||||
private volatile List<String> lines;
|
||||
private volatile boolean cancelled;
|
||||
|
||||
public PlayerBoard(Player player) {
|
||||
this.player = player;
|
||||
this.board = new Board(player, settings);
|
||||
this.lines = new ArrayList<>();
|
||||
this.cancelled = false;
|
||||
schedule(0);
|
||||
}
|
||||
|
||||
private void schedule(int delayTicks) {
|
||||
if (cancelled || !boardEnabled || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
J.runEntity(player, this::tick, delayTicks);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!boardEnabled || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
board.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEligibleWorld(player)) {
|
||||
boards.remove(player);
|
||||
cancelled = true;
|
||||
board.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
update();
|
||||
board.update();
|
||||
schedule(20);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
cancelled = true;
|
||||
if (J.isOwnedByCurrentRegion(player) && player.isOnline()) {
|
||||
board.remove();
|
||||
} else {
|
||||
J.runEntity(player, board::remove);
|
||||
}
|
||||
}
|
||||
|
||||
public void update() {
|
||||
final World world = player.getWorld();
|
||||
final Location loc = player.getLocation();
|
||||
|
||||
final var access = IrisToolbelt.access(world);
|
||||
if (access == null) return;
|
||||
|
||||
final var engine = access.getEngine();
|
||||
if (engine == null) return;
|
||||
|
||||
int x = loc.getBlockX();
|
||||
int y = loc.getBlockY() - world.getMinHeight();
|
||||
int z = loc.getBlockZ();
|
||||
|
||||
List<String> lines = new ArrayList<>(this.lines.size());
|
||||
lines.add("&7&m ");
|
||||
lines.add(C.GREEN + "Speed" + C.GRAY + ": " + Form.f(engine.getGeneratedPerSecond(), 0) + "/s " + Form.duration(1000D / engine.getGeneratedPerSecond(), 0));
|
||||
lines.add(C.AQUA + "Cache" + C.GRAY + ": " + Form.f(IrisData.cacheSize()));
|
||||
lines.add(C.AQUA + "Mantle" + C.GRAY + ": " + engine.getMantle().getLoadedRegionCount());
|
||||
|
||||
if (IrisSettings.get().getGeneral().debug) {
|
||||
lines.add(C.LIGHT_PURPLE + "Carving" + C.GRAY + ": " + (engine.getMantle().getMantle().get(x, y, z, MatterCavern.class) != null));
|
||||
}
|
||||
|
||||
lines.add("&7&m ");
|
||||
lines.add(C.AQUA + "Region" + C.GRAY + ": " + engine.getRegion(x, z).getName());
|
||||
lines.add(C.AQUA + "Biome" + C.GRAY + ": " + engine.getBiomeOrMantle(x, y, z).getName());
|
||||
lines.add(C.AQUA + "Height" + C.GRAY + ": " + Math.round(engine.getHeight(x, z)));
|
||||
lines.add(C.AQUA + "Slope" + C.GRAY + ": " + Form.f(engine.getComplex().getSlopeStream().get(x, z), 2));
|
||||
lines.add(C.AQUA + "BUD/s" + C.GRAY + ": " + Form.f(engine.getBlockUpdatesPerSecond()));
|
||||
lines.add("&7&m ");
|
||||
this.lines = lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,140 +0,0 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
import lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.WorldInitEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class GlobalCacheSVC implements IrisService {
|
||||
private static final KMap<String, Reference<PregenCache>> REFERENCE_CACHE = new KMap<>();
|
||||
private final KMap<String, PregenCache> globalCache = new KMap<>();
|
||||
private transient boolean lastState;
|
||||
private static boolean disabled = true;
|
||||
private Looper trimmer;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
disabled = false;
|
||||
trimmer = new Looper() {
|
||||
@Override
|
||||
protected long loop() {
|
||||
var it = REFERENCE_CACHE.values().iterator();
|
||||
while (it.hasNext()) {
|
||||
var cache = it.next().get();
|
||||
if (cache == null) it.remove();
|
||||
else cache.trim(10_000);
|
||||
}
|
||||
return disabled ? -1 : 2_000;
|
||||
}
|
||||
};
|
||||
trimmer.start();
|
||||
lastState = !IrisSettings.get().getWorld().isGlobalPregenCache();
|
||||
if (lastState) return;
|
||||
Bukkit.getWorlds().forEach(this::createCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
disabled = true;
|
||||
Looper activeTrimmer = trimmer;
|
||||
if (activeTrimmer != null) {
|
||||
try {
|
||||
activeTrimmer.join();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
globalCache.qclear((world, cache) -> cache.write());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PregenCache get(@NonNull World world) {
|
||||
return globalCache.get(world.getName());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PregenCache get(@NonNull String world) {
|
||||
return globalCache.get(world);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void on(WorldInitEvent event) {
|
||||
if (isDisabled()) return;
|
||||
createCache(event.getWorld());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void on(WorldUnloadEvent event) {
|
||||
var cache = globalCache.remove(event.getWorld().getName());
|
||||
if (cache == null) return;
|
||||
cache.write();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void on(ChunkLoadEvent event) {
|
||||
var cache = get(event.getWorld());
|
||||
if (cache == null) return;
|
||||
cache.cacheChunk(event.getChunk().getX(), event.getChunk().getZ());
|
||||
}
|
||||
|
||||
private void createCache(World world) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) return;
|
||||
globalCache.computeIfAbsent(world.getName(), GlobalCacheSVC::createDefault);
|
||||
}
|
||||
|
||||
private boolean isDisabled() {
|
||||
boolean conf = IrisSettings.get().getWorld().isGlobalPregenCache();
|
||||
if (lastState != conf)
|
||||
return lastState;
|
||||
|
||||
if (conf) {
|
||||
Bukkit.getWorlds().forEach(this::createCache);
|
||||
} else {
|
||||
globalCache.values().removeIf(cache -> {
|
||||
cache.write();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return lastState = !conf;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
public static PregenCache createCache(@NonNull String worldName, @NonNull Function<String, PregenCache> provider) {
|
||||
PregenCache[] holder = new PregenCache[1];
|
||||
REFERENCE_CACHE.compute(worldName, (name, ref) -> {
|
||||
if (ref != null) {
|
||||
if ((holder[0] = ref.get()) != null)
|
||||
return ref;
|
||||
}
|
||||
return new WeakReference<>(holder[0] = provider.apply(worldName));
|
||||
});
|
||||
return holder[0];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static PregenCache createDefault(@NonNull String worldName) {
|
||||
return createCache(worldName, GlobalCacheSVC::createDefault0);
|
||||
}
|
||||
|
||||
private static PregenCache createDefault0(String worldName) {
|
||||
if (disabled) return PregenCache.EMPTY;
|
||||
return PregenCache.create(new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pregen"))).sync();
|
||||
}
|
||||
}
|
||||
@@ -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,631 +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 com.google.gson.JsonSyntaxException;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
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.pack.IrisPack;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONException;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
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 org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
import org.zeroturnaround.zip.commons.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StudioSVC implements IrisService {
|
||||
public static final String LISTING = "https://raw.githubusercontent.com/IrisDimensions/_listing/main/listing-v2.json";
|
||||
public static final String WORKSPACE_NAME = "packs";
|
||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||
private final KMap<String, String> cacheListing = null;
|
||||
private IrisProject activeProject;
|
||||
private CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> activeClose;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
J.a(() -> {
|
||||
String pack = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
File f = IrisPack.packsPack(pack);
|
||||
|
||||
if (!f.exists()) {
|
||||
if (pack.equals("overworld")) {
|
||||
Iris.info("Downloading Default Pack " + pack);
|
||||
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
|
||||
Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false);
|
||||
} else {
|
||||
Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Iris.debug("Studio Mode Active: Closing Projects");
|
||||
boolean stopping = IrisToolbelt.isServerStopping();
|
||||
LinkedHashSet<String> worldNamesToDelete = new LinkedHashSet<>(TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer()));
|
||||
|
||||
if (activeProject != null) {
|
||||
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||
if (activeProvider != null) {
|
||||
String activeWorldName = activeProvider.getTarget().getWorld().name();
|
||||
if (activeWorldName != null && !activeWorldName.isBlank()) {
|
||||
worldNamesToDelete.add(activeWorldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (World i : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(i) || !IrisToolbelt.isStudio(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
worldNamesToDelete.add(i.getName());
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(i);
|
||||
if (!stopping) {
|
||||
destroyStudioWorld(i, generator);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (generator != null) {
|
||||
try {
|
||||
generator.close();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to close studio generator for \"" + i.getName() + "\" during shutdown.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeProject = null;
|
||||
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
||||
}
|
||||
|
||||
public IrisDimension installIntoWorld(VolmitSender sender, String type, File folder) {
|
||||
return installInto(sender, type, new File(folder, "iris/pack"));
|
||||
}
|
||||
|
||||
public IrisDimension installInto(VolmitSender sender, String type, File folder) {
|
||||
sender.sendMessage("Looking for Package: " + type);
|
||||
IrisDimension dim = IrisData.loadAnyDimension(type, null);
|
||||
|
||||
if (dim == null) {
|
||||
File[] workspaceFiles = getWorkspaceFolder().listFiles();
|
||||
if (workspaceFiles != null) {
|
||||
for (File i : workspaceFiles) {
|
||||
if (i.isFile() && i.getName().equals(type + ".iris")) {
|
||||
sender.sendMessage("Found " + type + ".iris in " + WORKSPACE_NAME + " folder");
|
||||
ZipUtil.unpack(i, folder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage("Found " + type + " dimension in " + WORKSPACE_NAME + " folder. Repackaging");
|
||||
File f = new IrisProject(new File(getWorkspaceFolder(), type)).getPath();
|
||||
|
||||
try {
|
||||
FileUtils.copyDirectory(f, folder);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
File dimensionFile = new File(folder, "dimensions/" + type + ".json");
|
||||
|
||||
if (!dimensionFile.exists() || !dimensionFile.isFile()) {
|
||||
downloadSearch(sender, type, false);
|
||||
File downloaded = getWorkspaceFolder(type);
|
||||
File[] files = downloaded.listFiles();
|
||||
|
||||
if (files != null) {
|
||||
for (File i : files) {
|
||||
if (i.isFile()) {
|
||||
try {
|
||||
FileUtils.copyFile(i, new File(folder, i.getName()));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
FileUtils.copyDirectory(i, new File(folder, i.getName()));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IO.delete(downloaded);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dimensionFile.exists() || !dimensionFile.isFile()) {
|
||||
sender.sendMessage("Can't find the " + dimensionFile.getName() + " in the dimensions folder of this pack! Failed!");
|
||||
return null;
|
||||
}
|
||||
|
||||
IrisData dm = IrisData.get(folder);
|
||||
dm.hotloaded();
|
||||
dim = dm.getDimensionLoader().load(type);
|
||||
|
||||
if (dim == null) {
|
||||
sender.sendMessage("Can't load the dimension! Failed!");
|
||||
return null;
|
||||
}
|
||||
|
||||
sender.sendMessage(folder.getName() + " type installed. ");
|
||||
return dim;
|
||||
}
|
||||
|
||||
public void downloadSearch(VolmitSender sender, String key) {
|
||||
downloadSearch(sender, key, false);
|
||||
}
|
||||
|
||||
public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) {
|
||||
try {
|
||||
String url = getListing(false).get(key);
|
||||
|
||||
if (url == null) {
|
||||
sender.sendMessage("Pack '" + key + "' was not found in the pack listing.");
|
||||
sender.sendMessage("Use /iris download <user/repo> <branch> to download manually.");
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.info("Resolved pack '" + key + "' to " + url);
|
||||
String[] nodes = url.split("\\Q/\\E");
|
||||
String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1];
|
||||
String branch = nodes.length > 2 ? nodes[2] : "stable";
|
||||
download(sender, repo, branch, forceOverwrite, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
sender.sendMessage("Failed to download '" + key + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
public void downloadRelease(VolmitSender sender, String url, boolean forceOverwrite) {
|
||||
try {
|
||||
download(sender, "IrisDimensions", url, forceOverwrite, true);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
sender.sendMessage("Failed to download 'IrisDimensions/overworld' from " + url + ".");
|
||||
}
|
||||
}
|
||||
|
||||
public void download(VolmitSender sender, String repo, String branch) throws JsonSyntaxException, IOException {
|
||||
download(sender, repo, branch, false, false);
|
||||
}
|
||||
|
||||
public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException {
|
||||
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
|
||||
sender.sendMessage("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
|
||||
File zip = Iris.getNonCachedFile("pack-" + repo, url);
|
||||
File temp = Iris.getTemp();
|
||||
File work = new File(temp, "dl-" + UUID.randomUUID());
|
||||
File packs = getWorkspaceFolder();
|
||||
|
||||
if (zip == null || !zip.exists()) {
|
||||
sender.sendMessage("Failed to find pack at " + url);
|
||||
sender.sendMessage("Make sure you specified the correct repo and branch!");
|
||||
sender.sendMessage("For example: /iris download IrisDimensions/overworld branch=stable");
|
||||
return;
|
||||
}
|
||||
sender.sendMessage("Unpacking " + repo);
|
||||
try {
|
||||
ZipUtil.unpack(zip, work);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
sender.sendMessage(
|
||||
"""
|
||||
Issue when unpacking. Please check/do the following:
|
||||
1. Do you have a functioning internet connection?
|
||||
2. Did the download corrupt?
|
||||
3. Try deleting the */plugins/iris/packs folder and re-download.
|
||||
4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld
|
||||
5. Contact support (if all other options do not help)"""
|
||||
);
|
||||
}
|
||||
File dir = null;
|
||||
File[] zipFiles = work.listFiles();
|
||||
|
||||
if (zipFiles == null) {
|
||||
sender.sendMessage("No files were extracted from the zip file.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dir = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null;
|
||||
} catch (NullPointerException e) {
|
||||
Iris.reportError(e);
|
||||
sender.sendMessage("Error when finding home directory. Are there any non-text characters in the file name?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dir == null) {
|
||||
sender.sendMessage("Invalid Format. Missing root folder or too many folders!");
|
||||
return;
|
||||
}
|
||||
|
||||
IrisData data = IrisData.get(dir);
|
||||
String[] dimensions = data.getDimensionLoader().getPossibleKeys();
|
||||
|
||||
if (dimensions == null || dimensions.length == 0) {
|
||||
sender.sendMessage("No dimension file found in the extracted zip file.");
|
||||
sender.sendMessage("Check it is there on GitHub and report this to staff!");
|
||||
} else if (dimensions.length != 1) {
|
||||
sender.sendMessage("Dimensions folder must have 1 file in it");
|
||||
return;
|
||||
}
|
||||
|
||||
IrisDimension d = data.getDimensionLoader().load(dimensions[0]);
|
||||
data.close();
|
||||
|
||||
if (d == null) {
|
||||
sender.sendMessage("Invalid dimension (folder) in dimensions folder");
|
||||
return;
|
||||
}
|
||||
|
||||
String key = d.getLoadKey();
|
||||
sender.sendMessage("Importing " + d.getName() + " (" + key + ")");
|
||||
File packEntry = new File(packs, key);
|
||||
|
||||
if (forceOverwrite) {
|
||||
IO.delete(packEntry);
|
||||
}
|
||||
|
||||
if (IrisData.loadAnyDimension(key, null) != null) {
|
||||
sender.sendMessage("Another dimension in the packs folder is already using the key " + key + " IMPORT FAILED!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (packEntry.exists() && packEntry.listFiles().length > 0) {
|
||||
sender.sendMessage("Another pack is using the key " + key + ". IMPORT FAILED!");
|
||||
return;
|
||||
}
|
||||
|
||||
FileUtils.copyDirectory(dir, packEntry);
|
||||
|
||||
IrisData.getLoaded(packEntry)
|
||||
.ifPresent(IrisData::hotloaded);
|
||||
|
||||
sender.sendMessage("Successfully Aquired " + d.getName());
|
||||
ServerConfigurator.installDataPacks(true);
|
||||
}
|
||||
|
||||
public KMap<String, String> getListing(boolean cached) {
|
||||
JSONObject a;
|
||||
|
||||
if (cached) {
|
||||
a = new JSONObject(Iris.getCached("cachedlisting", LISTING));
|
||||
} else {
|
||||
a = new JSONObject(Iris.getNonCached(true + "listing", LISTING));
|
||||
}
|
||||
|
||||
KMap<String, String> l = new KMap<>();
|
||||
|
||||
for (String i : a.keySet()) {
|
||||
if (a.get(i) instanceof String)
|
||||
l.put(i, a.getString(i));
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
public boolean isProjectOpen() {
|
||||
return activeProject != null && activeProject.isOpen();
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, String dimm) {
|
||||
open(sender, 1337, dimm);
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, long seed, String dimm) {
|
||||
try {
|
||||
open(sender, seed, dimm, (w) -> {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Iris.reportError("Failed to open studio world \"" + dimm + "\".", e);
|
||||
sender.sendMessage("Failed to open studio world: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean blockIfPackBroken(VolmitSender sender, String dimm) {
|
||||
PackValidationResult validation = PackValidationRegistry.get(dimm);
|
||||
if (validation == null || validation.isLoadable()) {
|
||||
return false;
|
||||
}
|
||||
sender.sendMessage("Cannot open studio '" + dimm + "' - pack has blocking errors:");
|
||||
for (String reason : validation.getBlockingErrors()) {
|
||||
sender.sendMessage(" - " + reason);
|
||||
}
|
||||
sender.sendMessage("Fix the pack and run /iris pack validate " + dimm + " to revalidate.");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
|
||||
if (blockIfPackBroken(sender, dimm)) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> pendingClose = close();
|
||||
pendingClose.whenComplete((closeResult, closeThrowable) -> {
|
||||
if (closeThrowable != null) {
|
||||
Iris.reportError("Failed while closing an existing studio project before opening \"" + dimm + "\".", closeThrowable);
|
||||
J.s(() -> sender.sendMessage("Failed to close the existing studio project: " + closeThrowable.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeResult != null && closeResult.failureCause() != null) {
|
||||
Throwable failure = closeResult.failureCause();
|
||||
Iris.reportError("Failed while closing an existing studio project before opening \"" + dimm + "\".", failure);
|
||||
J.s(() -> sender.sendMessage("Failed to close the existing studio project: " + failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimm));
|
||||
activeProject = project;
|
||||
try {
|
||||
project.open(sender, seed, onDone).whenComplete((result, throwable) -> {
|
||||
if (throwable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeProject == project && !project.isOpen()) {
|
||||
activeProject = null;
|
||||
}
|
||||
});
|
||||
} catch (IrisException e) {
|
||||
if (activeProject == project) {
|
||||
activeProject = null;
|
||||
}
|
||||
J.s(() -> sender.sendMessage("Failed to open studio world: " + e.getMessage()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void openVSCode(VolmitSender sender, String dim) {
|
||||
new IrisProject(new File(getWorkspaceFolder(), dim)).openVSCode(sender);
|
||||
}
|
||||
|
||||
public File getWorkspaceFolder(String... sub) {
|
||||
return Iris.instance.getDataFolderList(WORKSPACE_NAME, sub);
|
||||
}
|
||||
|
||||
public File getWorkspaceFile(String... sub) {
|
||||
return Iris.instance.getDataFileList(WORKSPACE_NAME, sub);
|
||||
}
|
||||
|
||||
public CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> close() {
|
||||
if (activeClose != null && !activeClose.isDone()) {
|
||||
return activeClose;
|
||||
}
|
||||
|
||||
if (activeProject == null) {
|
||||
return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null));
|
||||
}
|
||||
|
||||
Iris.debug("Closing Active Project");
|
||||
IrisProject project = activeProject;
|
||||
activeProject = null;
|
||||
activeClose = project.close();
|
||||
activeClose.whenComplete((result, throwable) -> activeClose = null);
|
||||
return activeClose;
|
||||
}
|
||||
|
||||
private void destroyStudioWorld(World world, PlatformChunkGenerator generator) {
|
||||
try {
|
||||
IrisToolbelt.evacuate(world);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to evacuate studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||
}
|
||||
|
||||
if (generator != null) {
|
||||
try {
|
||||
generator.close();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to close studio generator for \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
WorldLifecycleService.get().unload(world, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to unload studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||
}
|
||||
|
||||
deleteTransientStudioFolders(world.getName());
|
||||
}
|
||||
|
||||
private void deleteTransientStudioFolders(String worldName) {
|
||||
if (worldName == null || worldName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File container = Bukkit.getWorldContainer();
|
||||
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||
File folder = new File(container, familyWorldName);
|
||||
if (!folder.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IO.delete(folder);
|
||||
}
|
||||
}
|
||||
|
||||
private void queueStudioWorldDeletionOnStartup(LinkedHashSet<String> worldNamesToDelete) {
|
||||
if (worldNamesToDelete.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LinkedHashSet<String> normalizedNames = new LinkedHashSet<>();
|
||||
for (String worldName : worldNamesToDelete) {
|
||||
String baseWorldName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName);
|
||||
if (baseWorldName != null) {
|
||||
normalizedNames.add(baseWorldName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (worldName != null && !worldName.isBlank()) {
|
||||
normalizedNames.add(worldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedNames.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Iris.queueWorldDeletionOnStartup(List.copyOf(normalizedNames));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError("Failed to queue studio world deletion on startup.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public File compilePackage(VolmitSender sender, String d, boolean obfuscate, boolean minify) {
|
||||
return new IrisProject(new File(getWorkspaceFolder(), d)).compilePackage(sender, obfuscate, minify);
|
||||
}
|
||||
|
||||
public void createFrom(String existingPack, String newName) {
|
||||
File importPack = getWorkspaceFolder(existingPack);
|
||||
File newPack = getWorkspaceFolder(newName);
|
||||
|
||||
if (importPack.listFiles().length == 0) {
|
||||
Iris.warn("Couldn't find the pack to create a new dimension from.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.copyDirectory(importPack, newPack, pathname -> !pathname.getAbsolutePath().contains(".git"), false);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
new File(importPack, existingPack + ".code-workspace").delete();
|
||||
File dimFile = new File(importPack, "dimensions/" + existingPack + ".json");
|
||||
File newDimFile = new File(newPack, "dimensions/" + newName + ".json");
|
||||
|
||||
try {
|
||||
FileUtils.copyFile(dimFile, newDimFile);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
new File(newPack, "dimensions/" + existingPack + ".json").delete();
|
||||
|
||||
try {
|
||||
JSONObject json = new JSONObject(IO.readAll(newDimFile));
|
||||
|
||||
if (json.has("name")) {
|
||||
json.put("name", Form.capitalizeWords(newName.replaceAll("\\Q-\\E", " ")));
|
||||
IO.writeAll(newDimFile, json.toString(4));
|
||||
}
|
||||
} catch (JSONException | IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
IrisProject p = new IrisProject(getWorkspaceFolder(newName));
|
||||
JSONObject ws = p.createCodeWorkspaceConfig();
|
||||
IO.writeAll(getWorkspaceFile(newName, newName + ".code-workspace"), ws.toString(0));
|
||||
} catch (JSONException | IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void create(VolmitSender sender, String s, String downloadable) {
|
||||
boolean shouldDelete = false;
|
||||
File importPack = getWorkspaceFolder(downloadable);
|
||||
File[] packFiles = importPack.listFiles();
|
||||
|
||||
if (packFiles == null || packFiles.length == 0) {
|
||||
downloadSearch(sender, downloadable, false);
|
||||
packFiles = importPack.listFiles();
|
||||
|
||||
if (packFiles != null && packFiles.length > 0) {
|
||||
shouldDelete = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (packFiles == null || packFiles.length == 0) {
|
||||
sender.sendMessage("Couldn't find the pack to create a new dimension from.");
|
||||
return;
|
||||
}
|
||||
|
||||
File importDimensionFile = new File(importPack, "dimensions/" + downloadable + ".json");
|
||||
|
||||
if (!importDimensionFile.exists()) {
|
||||
sender.sendMessage("Missing Imported Dimension File");
|
||||
return;
|
||||
}
|
||||
|
||||
sender.sendMessage("Importing " + downloadable + " into new Project " + s);
|
||||
createFrom(downloadable, s);
|
||||
if (shouldDelete) {
|
||||
importPack.delete();
|
||||
}
|
||||
open(sender, s);
|
||||
}
|
||||
|
||||
public void create(VolmitSender sender, String s) {
|
||||
create(sender, s, "example");
|
||||
}
|
||||
|
||||
public IrisProject getActiveProject() {
|
||||
return activeProject;
|
||||
}
|
||||
|
||||
public void updateWorkspace() {
|
||||
if (isProjectOpen()) {
|
||||
activeProject.updateWorkspace();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user