Compare commits

..

115 Commits

Author SHA1 Message Date
Brian Neumann-Fopiano d9c7f35709 Revert "Update LICENSE.md"
This reverts commit 37893460f3.
2024-09-24 14:57:45 -04:00
Brian Fopiano 37893460f3 Update LICENSE.md 2024-09-09 16:59:14 -04:00
RePixelatedMC 898f815878 sync 2024-09-08 18:51:27 +02:00
RePixelatedMC 49ee36b089 renames! 2024-09-08 18:08:16 +02:00
RePixelatedMC 073be82dcc Sync! 2024-09-08 17:56:32 +02:00
RePixelatedMC 713c3a4762 Merge remote-tracking branch 'origin/iris4' into iris4 2024-09-08 17:45:44 +02:00
Julian Krings b736377aec woops 2024-09-08 15:01:37 +02:00
Julian Krings 62fff7a56e take server size into account when picking a server 2024-09-08 13:41:08 +02:00
RePixelatedMC 5efb71eb3e Merge remote-tracking branch 'origin/iris4' into iris4 2024-09-07 12:54:12 +02:00
Julian Krings d22f49492f implement done packet into master and add safety checks for remote server version 2024-09-07 12:32:09 +02:00
Julian Krings b65b112220 Merge branch 'refs/heads/v3.4.3' into iris4
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java
#	core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java
#	core/src/main/java/com/volmit/iris/core/service/WandSVC.java
#	core/src/main/java/com/volmit/iris/core/tools/IrisConverter.java
#	core/src/main/java/com/volmit/iris/engine/framework/Engine.java
2024-09-06 23:32:21 +02:00
Julian Krings c5456aa65c Merge remote-tracking branch 'origin/iris4' into iris4 2024-09-06 23:21:30 +02:00
Julian Krings 0a256eaa4c remote/cloud pregen! 2024-09-06 23:21:08 +02:00
RePixelatedMC 7d07ee4eb2 Merge remote-tracking branch 'origin/iris4' into iris4 2024-09-06 15:24:28 +02:00
Julian Krings c98ed48ee2 reconnect players on dimension type registry change 2024-09-06 14:58:09 +02:00
RePixelatedMC 9f392654d3 cleaner 2024-09-06 14:35:42 +02:00
RePixelatedMC 613575c0c5 e 2024-08-29 22:02:23 +02:00
RePixelatedMC b414b01ac4 Headless pregen! 2024-08-29 21:40:34 +02:00
RePixelatedMC 65dd039b07 ops 2024-08-29 15:57:03 +02:00
RePixelatedMC 747adb53d9 Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-29 15:46:24 +02:00
RePixelatedMC 7125b38fd5 Sync! 2024-08-29 15:46:16 +02:00
Julian Krings 82f71198e6 add saving headless regions after x seconds 2024-08-23 23:13:23 +02:00
Julian Krings b1e87afc93 more headless speed 2024-08-23 15:05:45 +02:00
RePixelatedMC 3bffe4cc7e - Removed VanillaHeight.
- Removed useless code
- okey I really need to sync now
2024-08-23 13:19:29 +02:00
Julian Krings 08ab82216d improve headless speed 2024-08-23 12:34:43 +02:00
Julian Krings c3ed7080dc improve headless chunk saving 2024-08-23 12:33:51 +02:00
RePixelatedMC ea8fb1bf86 - Iris doesnt kill engines on shutdown anymore! 2024-08-23 12:02:17 +02:00
Julian Krings 4434cf6475 add headless pregen to world creation 2024-08-22 20:31:24 +02:00
RePixelatedMC 26aae2b730 em 2024-08-22 20:24:59 +02:00
RePixelatedMC e5c818cf7b save 2024-08-22 20:23:58 +02:00
RePixelatedMC 6b4575e75d Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-22 17:16:54 +02:00
RePixelatedMC 773be08b24 SYNc 2024-08-22 17:16:46 +02:00
Julian Krings 386131ddf0 wait for all loaded chunks to be saved 2024-08-22 16:20:16 +02:00
RePixelatedMC 3dfdb9654a Sync! x2 2024-08-22 16:14:58 +02:00
RePixelatedMC 805523d069 Sync! 2024-08-22 16:07:02 +02:00
repixelatedmc 6cfa593eee em 2024-08-22 10:42:45 +02:00
repixelatedmc 00c2a5245a Bit more performance ig 2024-08-20 19:23:12 +02:00
repixelatedmc f6791b786e Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-20 19:22:48 +02:00
repixelatedmc 0fa9654824 20-30% performance increase 2024-08-20 19:22:36 +02:00
Julian Krings 2262e19cd1 fix world creation 2024-08-20 16:25:05 +02:00
repixelatedmc 055ddc7c9b wops 2024-08-20 13:40:39 +02:00
repixelatedmc 817d7a602b Fixed biome NMS calculations 2024-08-20 13:36:46 +02:00
Julian Krings 3af4a8f621 implement biome replacements in all bindings 2024-08-19 22:03:30 +02:00
Julian Krings 7b80eb1c06 woops 2024-08-19 20:45:06 +02:00
Julian Krings 19c6f4f2ba example 2024-08-19 20:43:30 +02:00
repixelatedmc 8a753b42f8 weeee 2024-08-19 20:33:26 +02:00
repixelatedmc 3f66634e5f Stash! 2024-08-19 19:35:16 +02:00
repixelatedmc c86815f47b Every Biome is custom 2024-08-18 21:39:24 +02:00
repixelatedmc f9d108dbb7 Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-18 17:15:06 +02:00
Julian Krings 302e02ddac Merge branch 'v3.4.3' into iris4 2024-08-18 17:06:06 +02:00
repixelatedmc f32f8744b2 Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-18 12:32:03 +02:00
Julian Krings dd98f6f07e Merge branch 'v3.4.3' into iris4 2024-08-17 19:06:31 +02:00
Julian Krings bbf42d1af0 fix headless chunk offset 2024-08-17 15:23:23 +02:00
RePixelatedMC 70aa607e5b Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-17 14:29:47 +02:00
Julian Krings 09635e12a9 woops 2024-08-17 14:04:30 +02:00
RePixelatedMC 7b283a56ee commit 2024-08-17 13:55:32 +02:00
Julian Krings 888ba34eee Merge branch 'v3.4.3' into iris4
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java
#	core/src/main/java/com/volmit/iris/core/service/DolphinSVC.java
#	core/src/main/java/com/volmit/iris/core/service/VillageSVC.java
#	core/src/main/java/com/volmit/iris/util/mantle/MantleFlag.java
#	nms/v1_19_R1/src/main/java/com/volmit/iris/core/nms/v1_19_R1/NMSBinding.java
#	nms/v1_19_R2/src/main/java/com/volmit/iris/core/nms/v1_19_R2/NMSBinding.java
#	nms/v1_19_R3/src/main/java/com/volmit/iris/core/nms/v1_19_R3/NMSBinding.java
#	nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
#	nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java
2024-08-17 13:25:48 +02:00
Julian Krings 62e98cc371 Merge branch 'master' into iris4
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/core/nms/INMS.java
#	core/src/main/java/com/volmit/iris/engine/IrisEngine.java
2024-08-17 01:03:17 +02:00
Julian Krings 8fc70f42fc Merge branch 'v3.4.3' into iris4
# Conflicts:
#	core/src/main/java/com/volmit/iris/engine/IrisEngine.java
2024-08-17 01:00:36 +02:00
repixelatedmc 13447b882c No deadlock? 2024-08-16 13:44:44 +02:00
Julian Krings 344c50154a Merge branch 'v3.4.2' into iris4
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	core/src/main/java/com/volmit/iris/core/safeguard/ModesSFG.java
#	core/src/main/java/com/volmit/iris/core/service/WandSVC.java
#	core/src/main/java/com/volmit/iris/engine/framework/Engine.java
#	core/src/main/java/com/volmit/iris/engine/object/TileBanner.java
#	core/src/main/java/com/volmit/iris/engine/object/TileData.java
#	core/src/main/java/com/volmit/iris/engine/object/TileSign.java
#	core/src/main/java/com/volmit/iris/engine/object/TileSpawner.java
#	core/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java
#	core/src/main/java/com/volmit/iris/util/matter/TileWrapper.java
#	nms/v1_19_R1/src/main/java/com/volmit/iris/core/nms/v1_19_R1/NMSBinding.java
#	nms/v1_19_R2/src/main/java/com/volmit/iris/core/nms/v1_19_R2/NMSBinding.java
#	nms/v1_19_R3/src/main/java/com/volmit/iris/core/nms/v1_19_R3/NMSBinding.java
#	nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
#	nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java
2024-08-14 21:53:13 +02:00
RePixelatedMC ef93bee0b9 Fixed double hotload and added a tone for hotload, 2024-08-12 22:28:27 +02:00
Brian Fopiano 9de0c5b96f Merge branch 'master' into iris4 2024-08-09 12:53:09 -04:00
Brian Neumann-Fopiano a0a7b8cb3e . 2024-08-07 20:40:56 -04:00
Brian Neumann-Fopiano ae2600227e Formatting 2024-08-07 20:08:11 -04:00
Brian Neumann-Fopiano ec1187923b Gradle Update 2024-08-07 20:01:32 -04:00
Brian Neumann-Fopiano ab04a686e9 The Great Copyright Number 3 2024-08-07 19:47:23 -04:00
repixelatedmc efbfad437a Decorators wont be placed above sea level when set to sea_floor 2024-08-07 16:03:21 +02:00
repixelatedmc 9bcb1845b8 magic 2024-08-07 15:29:42 +02:00
repixelatedmc 0f5364982d Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-07 15:01:43 +02:00
repixelatedmc 1b0411e23a Need help on this one. 2024-08-07 15:00:10 +02:00
Julian Krings 29199dc2d2 woops 2024-08-07 14:57:53 +02:00
Julian Krings 3cb5f612c6 use world name as thread id 2024-08-07 14:56:44 +02:00
Julian Krings 3b98b20f73 implement engine services and other improvements 2024-08-07 13:40:17 +02:00
repixelatedmc 90bab2b292 Reset Region cache option. 2024-08-07 11:46:52 +02:00
repixelatedmc 8ad3cdf820 - Decos dont float anymore
- Decos dont go over fluidheight anymore
2024-08-07 11:22:55 +02:00
repixelatedmc ca8933541a Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-07 09:19:24 +02:00
Brian Neumann-Fopiano 8572a444fa Making Logo Changes for V4
This is a simplified image print so my file is not bloated
2024-08-06 19:18:41 -04:00
repixelatedmc 8dd14c80f0 eh 2024-08-05 10:53:10 +02:00
Julian Krings 61410aea97 iris go brrrr 2024-08-04 16:58:36 +02:00
Julian Krings f892eb599c build headless biome cache at creation 2024-08-03 20:14:19 +02:00
Julian Krings 86f89bc718 Merge remote-tracking branch 'origin/master' into iris4
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
#	nms/v1_21_R1/src/main/java/com/volmit/iris/core/nms/v1_21_R1/NMSBinding.java
2024-08-03 18:50:38 +02:00
Julian Krings 5be19c7c3c fix nullpointer with headless 2024-08-03 18:43:21 +02:00
RePixelatedMC 9d8be5b382 did nextdoors work 2024-08-03 17:18:24 +02:00
Julian Krings ab30710e2a make PackBenchmark startup show errors 2024-08-03 16:56:17 +02:00
Julian Krings d2ecbc5727 fix datapack dump + change target location 2024-08-03 16:55:39 +02:00
RePixelatedMC 22f9306fa3 eghem 2024-08-03 15:52:24 +02:00
RePixelatedMC 6b4a19a525 Merge remote-tracking branch 'origin/iris4' into iris4 2024-08-03 15:25:55 +02:00
RePixelatedMC ad8ff2643b wee 2024-08-03 15:25:23 +02:00
Julian Krings e6f829db31 Merge remote-tracking branch 'origin/master' into iris4 2024-08-03 13:31:09 +02:00
RePixelatedMC b429448885 e 2024-08-03 10:11:55 +02:00
RePixelatedMC f00e037e26 bug fix + better eta 2024-08-03 10:07:03 +02:00
RePixelatedMC bad3cd27e1 bug fix 2024-08-03 09:09:02 +02:00
RePixelatedMC cad679a808 Fast pregen 2024-08-02 23:17:52 +02:00
Julian Krings 488b76d1d2 add dummy datapack + fix world creation 2024-08-02 18:14:41 +02:00
RePixelatedMC 1e22a65329 whopsie 2024-08-02 18:09:03 +02:00
RePixelatedMC 1477dc037c changes i guess 2024-08-02 17:29:39 +02:00
RePixelatedMC 6174ec04ab changes 2024-08-02 16:31:36 +02:00
RePixelatedMC 1cac86252f Refactors 2024-08-02 15:05:23 +02:00
RePixelatedMC 773065eb56 Advanced world loading! 2024-08-02 14:59:05 +02:00
RePixelatedMC a8524e43b9 dev shit 2024-08-01 13:26:13 +02:00
RePixelatedMC 0a62e222ee No gui errors anymore! 2024-07-30 15:24:26 +02:00
Julian Krings ec9a000bcf Merge remote-tracking branch 'origin/master' into iris4 2024-07-30 12:57:34 +02:00
RePixelatedMC 0445b6fe6e Merge branch 'iris4' of https://github.com/VolmitSoftware/Iris into mca 2024-07-29 20:41:25 +02:00
RePixelatedMC a0719117ad Iris wont fail loading the rest of the worlds if 1 fails 2024-07-29 20:40:01 +02:00
RePixelatedMC 7ae846af6f eghum 2024-07-29 20:16:15 +02:00
RePixelatedMC fe5bb67973 okey back to normal 2024-07-29 18:51:07 +02:00
Julian Krings 482fa9b11e add missing methods to 1.21 bindings 2024-07-29 18:29:28 +02:00
Julian Krings 295fe16f8f Merge remote-tracking branch 'refs/remotes/origin/v1_21' into iris4
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandDeveloper.java
#	core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java
#	nms/v1_19_R1/src/main/java/com/volmit/iris/core/nms/v1_19_R1/NMSBinding.java
#	nms/v1_19_R2/src/main/java/com/volmit/iris/core/nms/v1_19_R2/NMSBinding.java
#	nms/v1_19_R3/src/main/java/com/volmit/iris/core/nms/v1_19_R3/NMSBinding.java
#	nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
2024-07-29 18:11:11 +02:00
RePixelatedMC c2ab688590 Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into mca 2024-07-29 16:24:42 +02:00
RePixelatedMC 13c61501e6 Merge pull request #27 from CrazyDev05/mca
Revert "Revert "Merge branch 'mca' into jigsaw_dist""
2024-07-09 20:43:44 +02:00
Julian Krings 5ad848fc54 Revert "Revert "Merge branch 'mca' into jigsaw_dist""
This reverts commit 55017b9a
2024-07-09 20:24:21 +02:00
RePixelatedMC f6cf0682ed Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into mca 2024-07-06 13:50:31 +02:00
RePixelatedMC 3cb74ac922 Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into mca
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/safeguard/ServerBootSFG.java
2024-06-05 09:53:46 +02:00
RePixelatedMC dae3de8982 1.20.6 compat 2024-06-05 09:48:27 +02:00
1322 changed files with 105205 additions and 63570 deletions
+1 -3
View File
@@ -10,6 +10,4 @@ libs/
collection/
/core/src/main/java/art/arcane/iris/util/uniques/
DataPackExamples/
/core/src/main/java/com/volmit/iris/util/uniques/

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+6 -6
View File
@@ -7,7 +7,7 @@ The master branch is for the latest version of minecraft.
# Building
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
development.[README.md](README.md)
development.
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
@@ -15,17 +15,17 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
### Command Line Builds
1. Install [Java JDK 25](https://adoptium.net/temurin/releases/?version=25)
1. Install [Java JDK 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
View File
@@ -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)
-25
View File
@@ -1,25 +0,0 @@
import org.gradle.jvm.toolchain.JavaLanguageVersion
plugins {
id 'java'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
gradlePluginPortal()
maven {
url = uri('https://jitpack.io')
}
}
dependencies {
implementation('org.ow2.asm:asm:9.8')
implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.18')
}
-162
View File
@@ -1,162 +0,0 @@
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.publish.maven.MavenPublication;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.jvm.tasks.Jar;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
public class ApiGenerator implements Plugin<Project> {
@Override
public void apply(Project target) {
target.getPlugins().apply("maven-publish");
TaskProvider<GenerateApiTask> task = target.getTasks().register("irisApi", GenerateApiTask.class);
PublishingExtension publishing = target.getExtensions().findByType(PublishingExtension.class);
if (publishing == null) {
throw new GradleException("Publishing extension not found");
}
publishing.getRepositories().maven(repository -> {
repository.setName("deployDir");
repository.setUrl(targetDirectory(target).toURI());
});
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
publication.setGroupId(target.getName());
publication.setVersion(target.getVersion().toString());
publication.artifact(task);
});
}
public static File targetDirectory(Project project) {
String dir = System.getenv("DEPLOY_DIR");
if (dir == null) {
return project.getLayout().getBuildDirectory().dir("api").get().getAsFile();
}
return new File(dir);
}
}
abstract class GenerateApiTask extends DefaultTask {
private final File inputFile;
private final File outputFile;
public GenerateApiTask() {
setGroup("iris");
dependsOn("jar");
finalizedBy("publishMavenPublicationToDeployDirRepository");
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().getAbsolutePath()));
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
this.inputFile = jarTask.get().getArchiveFile().get().getAsFile();
this.outputFile = ApiGenerator.targetDirectory(getProject()).toPath().resolve(this.inputFile.getName()).toFile();
}
@InputFile
public File getInputFile() {
return inputFile;
}
@OutputFile
public File getOutputFile() {
return outputFile;
}
@TaskAction
public void generate() throws IOException {
File parent = outputFile.getParentFile();
if (parent != null) {
parent.mkdirs();
}
try (JarFile jar = new JarFile(inputFile);
JarOutputStream out = new JarOutputStream(new FileOutputStream(outputFile))) {
jar.stream()
.parallel()
.filter(entry -> !entry.isDirectory())
.filter(entry -> entry.getName().endsWith(".class"))
.forEach(entry -> writeStrippedClass(jar, out, entry));
}
}
private static void writeStrippedClass(JarFile jar, JarOutputStream out, JarEntry entry) {
byte[] bytes;
try (InputStream input = jar.getInputStream(entry)) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new MethodClearingVisitor(writer);
ClassReader reader = new ClassReader(input);
reader.accept(visitor, 0);
bytes = writer.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
synchronized (out) {
try {
JarEntry outputEntry = new JarEntry(entry.getName());
out.putNextEntry(outputEntry);
out.write(bytes);
out.closeEntry();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
class MethodClearingVisitor extends ClassVisitor {
public MethodClearingVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions));
}
}
class ExceptionThrowingMethodVisitor extends MethodVisitor {
public ExceptionThrowingMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
if (mv == null) {
return;
}
mv.visitCode();
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException");
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn("Only API");
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"java/lang/IllegalStateException",
"<init>",
"(Ljava/lang/String;)V",
false
);
mv.visitInsn(Opcodes.ATHROW);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
}
-5
View File
@@ -1,5 +0,0 @@
public class Config {
public int jvm = 25;
public NMSBinding.Type type = NMSBinding.Type.DIRECT;
public String version;
}
-282
View File
@@ -1,282 +0,0 @@
import com.volmit.nmstools.NMSToolsExtension;
import com.volmit.nmstools.NMSToolsPlugin;
import io.papermc.paperweight.userdev.PaperweightUser;
import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension;
import io.papermc.paperweight.userdev.PaperweightUserExtension;
import io.papermc.paperweight.userdev.attribute.Obfuscation;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Named;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Bundling;
import org.gradle.api.attributes.Category;
import org.gradle.api.attributes.LibraryElements;
import org.gradle.api.attributes.Usage;
import org.gradle.api.file.FileTree;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskAction;
import org.gradle.jvm.toolchain.JavaLanguageVersion;
import org.gradle.jvm.toolchain.JavaToolchainService;
import org.gradle.work.DisableCachingByDefault;
import javax.inject.Inject;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static io.papermc.paperweight.util.constants.ConstantsKt.REOBF_CONFIG;
public class NMSBinding implements Plugin<Project> {
private static final String NEW_LINE = System.lineSeparator();
private static final byte[] NEW_LINE_BYTES = NEW_LINE.getBytes(StandardCharsets.UTF_8);
@Override
public void apply(Project target) {
ExtraPropertiesExtension extra = target.getExtensions().getExtraProperties();
Object configValue = extra.has("nms") ? extra.get("nms") : null;
if (!(configValue instanceof Config)) {
throw new GradleException("No NMS binding configuration found");
}
Config config = (Config) configValue;
int jvm = config.jvm;
Type type = config.type;
if (type == Type.USER_DEV) {
target.getPlugins().apply(PaperweightUser.class);
PaperweightUserDependenciesExtension dependenciesExtension =
target.getDependencies().getExtensions().findByType(PaperweightUserDependenciesExtension.class);
if (dependenciesExtension != null) {
dependenciesExtension.paperDevBundle(config.version);
}
JavaPluginExtension java = target.getExtensions().findByType(JavaPluginExtension.class);
if (java == null) {
throw new GradleException("Java plugin not found");
}
java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.of(jvm));
JavaToolchainService javaToolchains = target.getExtensions().getByType(JavaToolchainService.class);
target.getExtensions().configure(PaperweightUserExtension.class,
extension -> extension.getJavaLauncher().set(javaToolchains.launcherFor(java.getToolchain())));
} else {
extra.set("nmsTools.useBuildTools", type == Type.BUILD_TOOLS);
target.getPlugins().apply(NMSToolsPlugin.class);
target.getExtensions().configure(NMSToolsExtension.class, extension -> {
extension.getJvm().set(jvm);
extension.getVersion().set(config.version);
});
}
String outgoingArtifactTask = type == Type.USER_DEV ? "jar" : "remap";
ObjectFactory objects = target.getObjects();
Configuration reobfConfiguration = target.getConfigurations().findByName(REOBF_CONFIG);
if (reobfConfiguration == null) {
reobfConfiguration = target.getConfigurations().create(REOBF_CONFIG);
}
target.getConfigurations().named(REOBF_CONFIG).configure(configuration -> {
configuration.setCanBeConsumed(true);
configuration.setCanBeResolved(false);
configuration.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, named(objects, Usage.class, Usage.JAVA_RUNTIME));
configuration.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, named(objects, Category.class, Category.LIBRARY));
configuration.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named(objects, LibraryElements.class, LibraryElements.JAR));
configuration.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, named(objects, Bundling.class, Bundling.EXTERNAL));
configuration.getAttributes().attribute(Obfuscation.Companion.getOBFUSCATION_ATTRIBUTE(), named(objects, Obfuscation.class, Obfuscation.OBFUSCATED));
configuration.getOutgoing().artifact(target.getTasks().named(outgoingArtifactTask));
});
int[] version = parseVersion(config.version);
int major = version[0];
int minor = version[1];
if (major <= 20 && minor <= 4) {
return;
}
target.getTasks().register("convert", ConversionTask.class, type);
target.getTasks().named("compileJava").configure(task -> task.dependsOn("convert"));
target.getRootProject().getTasks()
.matching(task -> task.getName().equals("prepareKotlinBuildScriptModel"))
.configureEach(task -> task.dependsOn(target.getPath() + ":convert"));
}
public static void nmsBinding(Project project, Action<Config> action) {
Config config = new Config();
action.execute(config);
project.getExtensions().getExtraProperties().set("nms", config);
project.getPlugins().apply(NMSBinding.class);
}
private static int[] parseVersion(String version) {
String trimmed = version;
int suffix = trimmed.indexOf('-');
if (suffix >= 0) {
trimmed = trimmed.substring(0, suffix);
}
String[] parts = trimmed.split("\\.");
return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])};
}
private static <T extends Named> T named(ObjectFactory objects, Class<T> type, String name) {
return objects.named(type, name);
}
@DisableCachingByDefault
public abstract static class ConversionTask extends DefaultTask {
private final Pattern pattern;
private final String replacement;
@Inject
public ConversionTask(Type type) {
setGroup("nms");
getInputs().property("type", type);
JavaPluginExtension java = getProject().getExtensions().findByType(JavaPluginExtension.class);
if (java == null) {
throw new GradleException("Java plugin not found");
}
Provider<FileTree> source = java.getSourceSets().named(SourceSet.MAIN_SOURCE_SET_NAME).map(SourceSet::getAllJava);
getInputs().files(source);
getOutputs().files(source);
if (type == Type.USER_DEV) {
this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\." + getProject().getName());
this.replacement = "org.bukkit.craftbukkit";
} else {
this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\.(?!" + getProject().getName() + ")");
this.replacement = "org.bukkit.craftbukkit." + getProject().getName() + ".";
}
}
@TaskAction
public void process() {
ExecutorService executor = Executors.newFixedThreadPool(16);
try {
Set<File> files = getInputs().getFiles().getFiles();
List<Future<?>> futures = new ArrayList<>(files.size());
for (File file : files) {
if (!file.getName().endsWith(".java")) {
continue;
}
futures.add(executor.submit(() -> processFile(file)));
}
for (Future<?> future : futures) {
future.get();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
} finally {
executor.shutdown();
}
}
private void processFile(File file) {
List<String> output = new ArrayList<>();
boolean changed = false;
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("package") || line.isBlank()) {
output.add(line);
continue;
}
if (!line.startsWith("import")) {
if (!changed) {
return;
}
output.add(line);
continue;
}
Matcher matcher = pattern.matcher(line);
if (!matcher.find()) {
output.add(line);
continue;
}
output.add(matcher.replaceAll(replacement));
changed = true;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!changed) {
return;
}
try {
if (hasTrailingNewLine(file)) {
output.add("");
}
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
for (int i = 0; i < output.size(); i++) {
writer.append(output.get(i));
if (i + 1 < output.size()) {
writer.append(NEW_LINE);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean hasTrailingNewLine(File file) throws IOException {
if (NEW_LINE_BYTES.length == 0) {
return false;
}
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
if (raf.length() < NEW_LINE_BYTES.length) {
return false;
}
byte[] bytes = new byte[NEW_LINE_BYTES.length];
raf.seek(raf.length() - bytes.length);
raf.readFully(bytes);
return Arrays.equals(bytes, NEW_LINE_BYTES);
}
}
}
public enum Type {
USER_DEV,
BUILD_TOOLS,
DIRECT
}
}
-14
View File
@@ -1,14 +0,0 @@
import org.gradle.jvm.tasks.Jar
plugins {
id 'java'
}
tasks.named('jar', Jar).configure {
manifest.attributes(
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
'Premain-Class': 'art.arcane.iris.util.project.agent.Installer',
'Can-Redefine-Classes': true,
'Can-Retransform-Classes': true
)
}
@@ -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
View File
@@ -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 })
}
}
}
}
-1
View File
@@ -1 +0,0 @@
1435163759
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
package art.arcane.iris.core;
public enum IrisPaperLikeBackendMode {
AUTO,
TICKET,
SERVICE
}
@@ -1,73 +0,0 @@
package art.arcane.iris.core;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import java.util.Locale;
public enum IrisRuntimeSchedulerMode {
AUTO,
PAPER_LIKE,
FOLIA;
public static IrisRuntimeSchedulerMode resolve(IrisSettings.IrisSettingsPregen pregen) {
Server server = Bukkit.getServer();
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
if (regionizedRuntime) {
return FOLIA;
}
IrisRuntimeSchedulerMode configuredMode = pregen == null ? null : pregen.getRuntimeSchedulerMode();
if (configuredMode != null && configuredMode != AUTO) {
if (configuredMode == FOLIA) {
return PAPER_LIKE;
}
return configuredMode;
}
String bukkitName = Bukkit.getName();
String bukkitVersion = Bukkit.getVersion();
String serverClassName = server == null ? "" : server.getClass().getName();
if (containsIgnoreCase(bukkitName, "folia")
|| containsIgnoreCase(bukkitVersion, "folia")
|| containsIgnoreCase(serverClassName, "folia")) {
return FOLIA;
}
if (containsIgnoreCase(bukkitName, "purpur")
|| containsIgnoreCase(bukkitVersion, "purpur")
|| containsIgnoreCase(serverClassName, "purpur")
|| containsIgnoreCase(bukkitName, "canvas")
|| containsIgnoreCase(bukkitVersion, "canvas")
|| containsIgnoreCase(serverClassName, "canvas")
|| containsIgnoreCase(bukkitName, "paper")
|| containsIgnoreCase(bukkitVersion, "paper")
|| containsIgnoreCase(serverClassName, "paper")
|| containsIgnoreCase(bukkitName, "pufferfish")
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|| containsIgnoreCase(serverClassName, "pufferfish")
|| containsIgnoreCase(bukkitName, "spigot")
|| containsIgnoreCase(bukkitVersion, "spigot")
|| containsIgnoreCase(serverClassName, "spigot")
|| containsIgnoreCase(bukkitName, "craftbukkit")
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|| containsIgnoreCase(serverClassName, "craftbukkit")) {
return PAPER_LIKE;
}
if (regionizedRuntime) {
return FOLIA;
}
return PAPER_LIKE;
}
private static boolean containsIgnoreCase(String value, String contains) {
if (value == null || contains == null || contains.isEmpty()) {
return false;
}
return value.toLowerCase(Locale.ROOT).contains(contains.toLowerCase(Locale.ROOT));
}
}
@@ -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) {}
};
}
@@ -1,376 +0,0 @@
package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.data.Varint;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.documentation.RegionCoordinates;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import net.jpountz.lz4.LZ4BlockInputStream;
import net.jpountz.lz4.LZ4BlockOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PregenCacheImpl implements PregenCache {
private static final ExecutorService DISPATCHER = Executors.newFixedThreadPool(4);
private static final short SIZE = 1024;
private final File directory;
private final int maxSize;
private final Long2ObjectLinkedOpenHashMap<Plate> cache = new Long2ObjectLinkedOpenHashMap<>();
public PregenCacheImpl(File directory, int maxSize) {
this.directory = directory;
this.maxSize = maxSize;
}
@Override
@ChunkCoordinates
public boolean isChunkCached(int x, int z) {
return getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).isCached(
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
region -> region.isCached(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS)
)
);
}
@Override
@RegionCoordinates
public boolean isRegionCached(int x, int z) {
return getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).isCached(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
Region::isCached
);
}
@Override
@ChunkCoordinates
public void cacheChunk(int x, int z) {
getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).cache(
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
region -> region.cache(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS)
)
);
}
@Override
@RegionCoordinates
public void cacheRegion(int x, int z) {
getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).cache(
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
Region::cache
);
}
@Override
public void write() {
if (cache.isEmpty()) {
return;
}
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size());
for (Plate plate : cache.values()) {
if (!plate.dirty) {
continue;
}
futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER));
}
for (CompletableFuture<Void> future : futures) {
future.join();
}
}
@Override
public void trim(long unloadDuration) {
if (cache.isEmpty()) {
return;
}
long threshold = System.currentTimeMillis() - unloadDuration;
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size());
Iterator<Plate> iterator = cache.values().iterator();
while (iterator.hasNext()) {
Plate plate = iterator.next();
if (plate.lastAccess < threshold) {
iterator.remove();
}
futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER));
}
for (CompletableFuture<Void> future : futures) {
future.join();
}
}
private Plate getPlate(int x, int z) {
long key = key(x, z);
Plate plate = cache.getAndMoveToFirst(key);
if (plate != null) {
return plate;
}
Plate loaded = readPlate(x, z);
cache.putAndMoveToFirst(key, loaded);
if (cache.size() > maxSize) {
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size() - maxSize);
while (cache.size() > maxSize) {
Plate evicted = cache.removeLast();
futures.add(CompletableFuture.runAsync(() -> writePlate(evicted), DISPATCHER));
}
for (CompletableFuture<Void> future : futures) {
future.join();
}
}
return loaded;
}
private Plate readPlate(int x, int z) {
File file = fileForPlate(x, z);
if (!file.exists()) {
return new Plate(x, z);
}
try (DataInputStream input = new DataInputStream(new LZ4BlockInputStream(new FileInputStream(file)))) {
return readPlate(x, z, input);
} catch (IOException e) {
Iris.error("Failed to read pregen cache " + file);
e.printStackTrace();
Iris.reportError(e);
}
return new Plate(x, z);
}
private void writePlate(Plate plate) {
if (!plate.dirty) {
return;
}
File file = fileForPlate(plate.x, plate.z);
try {
IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write);
plate.dirty = false;
} catch (IOException e) {
Iris.error("Failed to write preen cache " + file);
e.printStackTrace();
Iris.reportError(e);
}
}
private File fileForPlate(int x, int z) {
if (!directory.exists() && !directory.mkdirs()) {
throw new IllegalStateException("Cannot create directory: " + directory.getAbsolutePath());
}
return new File(directory, "c." + x + "." + z + ".lz4b");
}
private static long key(int x, int z) {
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
private interface RegionPredicate {
boolean test(Region region);
}
private static class Plate {
private final int x;
private final int z;
private short count;
private Region[] regions;
private boolean dirty;
private long lastAccess;
private Plate(int x, int z) {
this(x, z, (short) 0, new Region[1024]);
}
private Plate(int x, int z, short count, Region[] regions) {
this.x = x;
this.z = z;
this.count = count;
this.regions = regions;
this.lastAccess = System.currentTimeMillis();
}
private boolean cache(int x, int z, RegionPredicate predicate) {
lastAccess = System.currentTimeMillis();
if (count == SIZE) {
return false;
}
int index = PowerOfTwoCoordinates.packLocal32(x, z);
Region region = regions[index];
if (region == null) {
region = new Region();
regions[index] = region;
}
if (!predicate.test(region)) {
return false;
}
count++;
if (count == SIZE) {
regions = null;
}
dirty = true;
return true;
}
private boolean isCached(int x, int z, RegionPredicate predicate) {
lastAccess = System.currentTimeMillis();
if (count == SIZE) {
return true;
}
Region region = regions[PowerOfTwoCoordinates.packLocal32(x, z)];
if (region == null) {
return false;
}
return predicate.test(region);
}
private void write(DataOutput output) throws IOException {
Varint.writeSignedVarInt(count, output);
if (regions == null) {
return;
}
for (Region region : regions) {
output.writeBoolean(region == null);
if (region != null) {
region.write(output);
}
}
}
}
private static class Region {
private short count;
private long[] words;
private Region() {
this((short) 0, new long[64]);
}
private Region(short count, long[] words) {
this.count = count;
this.words = words;
}
private boolean cache() {
if (count == SIZE) {
return false;
}
count = SIZE;
words = null;
return true;
}
private boolean cache(int x, int z) {
if (count == SIZE) {
return false;
}
long[] value = words;
if (value == null) {
return false;
}
int index = PowerOfTwoCoordinates.packLocal32(x, z);
int wordIndex = index >> 6;
long bit = 1L << (index & 63);
boolean current = (value[wordIndex] & bit) != 0L;
if (current) {
return false;
}
count++;
if (count == SIZE) {
words = null;
return true;
}
value[wordIndex] = value[wordIndex] | bit;
return false;
}
private boolean isCached() {
return count == SIZE;
}
private boolean isCached(int x, int z) {
int index = PowerOfTwoCoordinates.packLocal32(x, z);
if (count == SIZE) {
return true;
}
return (words[index >> 6] & (1L << (index & 63))) != 0L;
}
private void write(DataOutput output) throws IOException {
Varint.writeSignedVarInt(count, output);
if (words == null) {
return;
}
for (long word : words) {
Varint.writeUnsignedVarLong(word, output);
}
}
}
private static Plate readPlate(int x, int z, DataInput input) throws IOException {
int count = Varint.readSignedVarInt(input);
if (count == 1024) {
return new Plate(x, z, SIZE, null);
}
Region[] regions = new Region[1024];
for (int i = 0; i < regions.length; i++) {
boolean isNull = input.readBoolean();
if (!isNull) {
regions[i] = readRegion(input);
}
}
return new Plate(x, z, (short) count, regions);
}
private static Region readRegion(DataInput input) throws IOException {
int count = Varint.readSignedVarInt(input);
if (count == 1024) {
return new Region(SIZE, null);
}
long[] words = new long[64];
for (int i = 0; i < words.length; i++) {
words[i] = Varint.readUnsignedVarLong(input);
}
return new Region((short) count, words);
}
}
@@ -1,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();
}
}
@@ -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