From 40a91ca05ea17505156665459e44a01b75fa9c56 Mon Sep 17 00:00:00 2001
From: Luna <90072930+LunaWasFlaggedAgain@users.noreply.github.com>
Date: Tue, 22 Aug 2023 21:15:48 -0300
Subject: [PATCH] Slime (#136)
* Slime
* Add proper credits. Thanks ASP!
* Cleanup
---
.gitmodules | 3 +
README.md | 4 +-
aswm-api/build.gradle.kts | 17 +
aswm-api/src | 1 +
aswm-core/build.gradle.kts | 7 +
aswm-core/src | 1 +
build.gradle.kts | 1 +
.../0001-AdvancedSlimePaper-API-Changes.patch | 33 +
...ch => 0002-Add-MasterBlockFireEvent.patch} | 2 +-
...> 0003-Add-spectator-teleport-event.patch} | 2 +-
...004-Add-Scissors-configuration-file.patch} | 0
...Add-command-block-player-edit-event.patch} | 0
...01-AdvancedSlimePaper-Server-Changes.patch | 2128 +++++++++++++++++
...changes.patch => 0002-Build-changes.patch} | 16 +-
...ation.patch => 0003-UUID-validation.patch} | 0
...=> 0004-ResourceLocation-validation.patch} | 2 +-
...-Fixes-the-Blank-SkullOwner-exploit.patch} | 0
...used-by-invalid-entities-in-beehive.patch} | 0
...emoves-useless-spammy-error-logging.patch} | 0
...own-when-trying-to-remove-minecart-.patch} | 0
...if-items-are-air-before-calling-set.patch} | 0
...ooks-causing-log-spam-when-invalid-.patch} | 0
...te-BlockState-and-SoundEvent-values.patch} | 0
...d-items-in-HoverEvent-and-ItemFrame.patch} | 0
... 0013-Change-version-fetcher-to-AMG.patch} | 2 +-
...handling-of-invalid-JSON-components.patch} | 0
...=> 0015-Block-server-side-chunkbans.patch} | 0
...-oversized-components-from-updating.patch} | 0
...Scissors-configuration-file-command.patch} | 0
...s-with-invalid-namespaces-from-bein.patch} | 0
...ry-player-data-in-the-nbt-component.patch} | 0
...tring-tag-visitors-to-1024-elements.patch} | 0
...lling-potion-effects-and-certain-po.patch} | 0
...ch => 0022-Fix-negative-death-times.patch} | 0
...ehicle-collision-checks-to-3-and-di.patch} | 0
...Add-custom-classes-used-by-Scissors.patch} | 0
...tags.patch => 0025-Reset-large-tags.patch} | 0
...-Don-t-log-invalid-teams-to-console.patch} | 0
...-bounds-HangingEntity-crash-exploit.patch} | 0
...ch => 0028-Add-MasterBlockFireEvent.patch} | 0
...> 0029-Add-spectator-teleport-event.patch} | 0
...30-Prevent-invalid-container-events.patch} | 0
...unning-commands-in-books-by-default.patch} | 0
...k-entity-entity-tag-query-positions.patch} | 0
...ents-on-Signs-bypassing-permissions.patch} | 2 +-
...-legacy-messages-over-1k-characters.patch} | 0
...tch => 0035-Prevent-velocity-freeze.patch} | 0
...n-option-to-disable-chat-signatures.patch} | 0
...ch-invalid-entity-rotation-log-spam.patch} | 0
...Patch-large-selector-distance-crash.patch} | 0
...mit-sculk-catalyst-cursor-positions.patch} | 0
...patch => 0040-Limit-map-decorations.patch} | 2 +-
...layer-banning-using-duplicate-UUIDs.patch} | 4 +-
...on-t-warn-on-duplicate-entity-UUIDs.patch} | 2 +-
...component-extra-empty-array-exploit.patch} | 0
...pth-limit-to-Component-deserializer.patch} | 0
...0045-Implement-command-block-events.patch} | 0
...tch => 0046-Add-depth-limit-to-SNBT.patch} | 0
...ch => 0047-Limit-beacon-effectRange.patch} | 0
...ove-validation-of-ResourceLocations.patch} | 0
...n-t-log-on-too-many-chained-updates.patch} | 0
...050-Fix-packet-related-lag-exploits.patch} | 0
...-Limit-save-data-for-Bees-and-Vexes.patch} | 0
...tch => 0052-Mute-invalid-attributes.patch} | 0
...053-Mute-invalid-Enderdragon-phases.patch} | 0
...l-Components-in-the-Component-codec.patch} | 0
settings.gradle.kts | 2 +-
submodules/AdvancedSlimePaper | 1 +
68 files changed, 2213 insertions(+), 19 deletions(-)
create mode 100644 .gitmodules
create mode 100644 aswm-api/build.gradle.kts
create mode 120000 aswm-api/src
create mode 100644 aswm-core/build.gradle.kts
create mode 120000 aswm-core/src
create mode 100644 patches/api/0001-AdvancedSlimePaper-API-Changes.patch
rename patches/api/{0001-Add-MasterBlockFireEvent.patch => 0002-Add-MasterBlockFireEvent.patch} (94%)
rename patches/api/{0002-Add-spectator-teleport-event.patch => 0003-Add-spectator-teleport-event.patch} (95%)
rename patches/api/{0003-Add-Scissors-configuration-file.patch => 0004-Add-Scissors-configuration-file.patch} (100%)
rename patches/api/{0004-Add-command-block-player-edit-event.patch => 0005-Add-command-block-player-edit-event.patch} (100%)
create mode 100644 patches/server/0001-AdvancedSlimePaper-Server-Changes.patch
rename patches/server/{0001-Build-changes.patch => 0002-Build-changes.patch} (92%)
rename patches/server/{0002-UUID-validation.patch => 0003-UUID-validation.patch} (100%)
rename patches/server/{0003-ResourceLocation-validation.patch => 0004-ResourceLocation-validation.patch} (99%)
rename patches/server/{0004-Fixes-the-Blank-SkullOwner-exploit.patch => 0005-Fixes-the-Blank-SkullOwner-exploit.patch} (100%)
rename patches/server/{0005-Fixes-log-spam-caused-by-invalid-entities-in-beehive.patch => 0006-Fixes-log-spam-caused-by-invalid-entities-in-beehive.patch} (100%)
rename patches/server/{0006-Removes-useless-spammy-error-logging.patch => 0007-Removes-useless-spammy-error-logging.patch} (100%)
rename patches/server/{0007-Ignore-errors-thrown-when-trying-to-remove-minecart-.patch => 0008-Ignore-errors-thrown-when-trying-to-remove-minecart-.patch} (100%)
rename patches/server/{0008-ItemEntity-Check-if-items-are-air-before-calling-set.patch => 0009-ItemEntity-Check-if-items-are-air-before-calling-set.patch} (100%)
rename patches/server/{0009-Fixes-Knowledge-Books-causing-log-spam-when-invalid-.patch => 0010-Fixes-Knowledge-Books-causing-log-spam-when-invalid-.patch} (100%)
rename patches/server/{0010-Validate-BlockState-and-SoundEvent-values.patch => 0011-Validate-BlockState-and-SoundEvent-values.patch} (100%)
rename patches/server/{0011-Do-not-log-invalid-items-in-HoverEvent-and-ItemFrame.patch => 0012-Do-not-log-invalid-items-in-HoverEvent-and-ItemFrame.patch} (100%)
rename patches/server/{0012-Change-version-fetcher-to-AMG.patch => 0013-Change-version-fetcher-to-AMG.patch} (98%)
rename patches/server/{0013-Better-handling-of-invalid-JSON-components.patch => 0014-Better-handling-of-invalid-JSON-components.patch} (100%)
rename patches/server/{0014-Block-server-side-chunkbans.patch => 0015-Block-server-side-chunkbans.patch} (100%)
rename patches/server/{0015-Reject-oversized-components-from-updating.patch => 0016-Reject-oversized-components-from-updating.patch} (100%)
rename patches/server/{0016-Add-Scissors-configuration-file-command.patch => 0017-Add-Scissors-configuration-file-command.patch} (100%)
rename patches/server/{0017-Prevent-attributes-with-invalid-namespaces-from-bein.patch => 0018-Prevent-attributes-with-invalid-namespaces-from-bein.patch} (100%)
rename patches/server/{0018-Don-t-query-player-data-in-the-nbt-component.patch => 0019-Don-t-query-player-data-in-the-nbt-component.patch} (100%)
rename patches/server/{0019-Limit-string-tag-visitors-to-1024-elements.patch => 0020-Limit-string-tag-visitors-to-1024-elements.patch} (100%)
rename patches/server/{0020-Fixes-creative-killing-potion-effects-and-certain-po.patch => 0021-Fixes-creative-killing-potion-effects-and-certain-po.patch} (100%)
rename patches/server/{0021-Fix-negative-death-times.patch => 0022-Fix-negative-death-times.patch} (100%)
rename patches/server/{0022-Limit-amount-of-vehicle-collision-checks-to-3-and-di.patch => 0023-Limit-amount-of-vehicle-collision-checks-to-3-and-di.patch} (100%)
rename patches/server/{0023-Add-custom-classes-used-by-Scissors.patch => 0024-Add-custom-classes-used-by-Scissors.patch} (100%)
rename patches/server/{0024-Reset-large-tags.patch => 0025-Reset-large-tags.patch} (100%)
rename patches/server/{0025-Don-t-log-invalid-teams-to-console.patch => 0026-Don-t-log-invalid-teams-to-console.patch} (100%)
rename patches/server/{0026-Fixes-out-of-bounds-HangingEntity-crash-exploit.patch => 0027-Fixes-out-of-bounds-HangingEntity-crash-exploit.patch} (100%)
rename patches/server/{0027-Add-MasterBlockFireEvent.patch => 0028-Add-MasterBlockFireEvent.patch} (100%)
rename patches/server/{0028-Add-spectator-teleport-event.patch => 0029-Add-spectator-teleport-event.patch} (100%)
rename patches/server/{0029-Prevent-invalid-container-events.patch => 0030-Prevent-invalid-container-events.patch} (100%)
rename patches/server/{0030-Disable-running-commands-in-books-by-default.patch => 0031-Disable-running-commands-in-books-by-default.patch} (100%)
rename patches/server/{0031-Validate-block-entity-entity-tag-query-positions.patch => 0032-Validate-block-entity-entity-tag-query-positions.patch} (100%)
rename patches/server/{0032-Fix-ClickEvents-on-Signs-bypassing-permissions.patch => 0033-Fix-ClickEvents-on-Signs-bypassing-permissions.patch} (97%)
rename patches/server/{0033-Refuse-to-convert-legacy-messages-over-1k-characters.patch => 0034-Refuse-to-convert-legacy-messages-over-1k-characters.patch} (100%)
rename patches/server/{0034-Prevent-velocity-freeze.patch => 0035-Prevent-velocity-freeze.patch} (100%)
rename patches/server/{0035-Add-configuration-option-to-disable-chat-signatures.patch => 0036-Add-configuration-option-to-disable-chat-signatures.patch} (100%)
rename patches/server/{0036-Patch-invalid-entity-rotation-log-spam.patch => 0037-Patch-invalid-entity-rotation-log-spam.patch} (100%)
rename patches/server/{0037-Patch-large-selector-distance-crash.patch => 0038-Patch-large-selector-distance-crash.patch} (100%)
rename patches/server/{0038-Limit-sculk-catalyst-cursor-positions.patch => 0039-Limit-sculk-catalyst-cursor-positions.patch} (100%)
rename patches/server/{0039-Limit-map-decorations.patch => 0040-Limit-map-decorations.patch} (95%)
rename patches/server/{0040-Prevent-player-banning-using-duplicate-UUIDs.patch => 0041-Prevent-player-banning-using-duplicate-UUIDs.patch} (88%)
rename patches/server/{0041-Don-t-warn-on-duplicate-entity-UUIDs.patch => 0042-Don-t-warn-on-duplicate-entity-UUIDs.patch} (91%)
rename patches/server/{0042-Fix-component-extra-empty-array-exploit.patch => 0043-Fix-component-extra-empty-array-exploit.patch} (100%)
rename patches/server/{0043-Add-depth-limit-to-Component-deserializer.patch => 0044-Add-depth-limit-to-Component-deserializer.patch} (100%)
rename patches/server/{0044-Implement-command-block-events.patch => 0045-Implement-command-block-events.patch} (100%)
rename patches/server/{0045-Add-depth-limit-to-SNBT.patch => 0046-Add-depth-limit-to-SNBT.patch} (100%)
rename patches/server/{0046-Limit-beacon-effectRange.patch => 0047-Limit-beacon-effectRange.patch} (100%)
rename patches/server/{0047-Improve-validation-of-ResourceLocations.patch => 0048-Improve-validation-of-ResourceLocations.patch} (100%)
rename patches/server/{0048-Don-t-log-on-too-many-chained-updates.patch => 0049-Don-t-log-on-too-many-chained-updates.patch} (100%)
rename patches/server/{0049-Fix-packet-related-lag-exploits.patch => 0050-Fix-packet-related-lag-exploits.patch} (100%)
rename patches/server/{0050-Limit-save-data-for-Bees-and-Vexes.patch => 0051-Limit-save-data-for-Bees-and-Vexes.patch} (100%)
rename patches/server/{0051-Mute-invalid-attributes.patch => 0052-Mute-invalid-attributes.patch} (100%)
rename patches/server/{0052-Mute-invalid-Enderdragon-phases.patch => 0053-Mute-invalid-Enderdragon-phases.patch} (100%)
rename patches/server/{0053-Don-t-return-null-Components-in-the-Component-codec.patch => 0054-Don-t-return-null-Components-in-the-Component-codec.patch} (100%)
create mode 160000 submodules/AdvancedSlimePaper
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e9b11ec
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "submodules/AdvancedSlimePaper"]
+ path = submodules/AdvancedSlimePaper
+ url = git@github.com:InfernalSuite/AdvancedSlimePaper.git
diff --git a/README.md b/README.md
index c640311..adab081 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
# Scissors [![Build Status](https://ci.scissors.gg/job/Scissors/job/1.20.1/badge/icon)](https://ci.scissors.gg/job/Scissors/job/1.20.1/)
Scissors is a fork of Paper that aims to fix exploits possible in Creative Mode. Many of these exploits are ones that
-Paper's own team has either refused to fix or would have.
+Paper's own team has either refused to fix or would have.
+
+All SWM patches/SWM API belongs to [AdvancedSlimePaper and InfernalSuite](https://github.com/InfernalSuite/AdvancedSlimePaper)
## Links
### [Scissors Download](https://ci.plex.us.org/job/Scissors)
diff --git a/aswm-api/build.gradle.kts b/aswm-api/build.gradle.kts
new file mode 100644
index 0000000..ba0eb6f
--- /dev/null
+++ b/aswm-api/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ `java-library`
+ `maven-publish`
+ signing
+}
+
+dependencies {
+ api("com.flowpowered:flow-nbt:2.0.2")
+ api("org.jetbrains:annotations:23.0.0")
+
+ compileOnly("io.papermc.paper:paper-api:1.20-R0.1-SNAPSHOT")
+}
+
+java {
+ withSourcesJar()
+ withJavadocJar()
+}
diff --git a/aswm-api/src b/aswm-api/src
new file mode 120000
index 0000000..7f4b0eb
--- /dev/null
+++ b/aswm-api/src
@@ -0,0 +1 @@
+../submodules/AdvancedSlimePaper/api/src
\ No newline at end of file
diff --git a/aswm-core/build.gradle.kts b/aswm-core/build.gradle.kts
new file mode 100644
index 0000000..40bd17b
--- /dev/null
+++ b/aswm-core/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+}
+
+dependencies {
+ compileOnly(project(":aswm-api"))
+ implementation("com.github.luben:zstd-jni:1.5.2-2")
+}
\ No newline at end of file
diff --git a/aswm-core/src b/aswm-core/src
new file mode 120000
index 0000000..b08d836
--- /dev/null
+++ b/aswm-core/src
@@ -0,0 +1 @@
+../submodules/AdvancedSlimePaper/core/src
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index db662da..1545e9c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -49,6 +49,7 @@ subprojects {
repositories {
mavenCentral()
maven(paperMavenPublicUrl)
+ maven("https://repo.rapture.pw/repository/maven-releases/")
}
}
diff --git a/patches/api/0001-AdvancedSlimePaper-API-Changes.patch b/patches/api/0001-AdvancedSlimePaper-API-Changes.patch
new file mode 100644
index 0000000..4163d76
--- /dev/null
+++ b/patches/api/0001-AdvancedSlimePaper-API-Changes.patch
@@ -0,0 +1,33 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
+Date: Mon, 26 Dec 2022 12:08:15 -0500
+Subject: [PATCH] AdvancedSlimePaper API Changes
+
+AdvancedSlimePaper
+Copyright (C) 2023 InfernalSuite
+
+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 .
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 8045f92ffdfb4164bcbef99c41359590c45f9006..51e49260927f6840c3640275ff26e7398ec72b89 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -27,6 +27,7 @@ configurations.api {
+
+ dependencies {
+ // api dependencies are listed transitively to API consumers
++ api(project(":aswm-api")) // ASWM
+ api("com.google.guava:guava:31.1-jre")
+ api("com.google.code.gson:gson:2.10")
+ api("net.md-5:bungeecord-chat:$bungeeCordChatVersion-deprecated+build.14") // Paper
diff --git a/patches/api/0001-Add-MasterBlockFireEvent.patch b/patches/api/0002-Add-MasterBlockFireEvent.patch
similarity index 94%
rename from patches/api/0001-Add-MasterBlockFireEvent.patch
rename to patches/api/0002-Add-MasterBlockFireEvent.patch
index 12f29e3..92967a5 100644
--- a/patches/api/0001-Add-MasterBlockFireEvent.patch
+++ b/patches/api/0002-Add-MasterBlockFireEvent.patch
@@ -6,7 +6,7 @@ Subject: [PATCH] Add MasterBlockFireEvent
diff --git a/src/main/java/me/totalfreedom/scissors/event/block/MasterBlockFireEvent.java b/src/main/java/me/totalfreedom/scissors/event/block/MasterBlockFireEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a24cb52a5af62012c5d5acc29e4c3558e92ae572
+index 0000000000000000000000000000000000000000..812e6ae9f1c8eb9558e5109c522d3ce3a7deb35c
--- /dev/null
+++ b/src/main/java/me/totalfreedom/scissors/event/block/MasterBlockFireEvent.java
@@ -0,0 +1,51 @@
diff --git a/patches/api/0002-Add-spectator-teleport-event.patch b/patches/api/0003-Add-spectator-teleport-event.patch
similarity index 95%
rename from patches/api/0002-Add-spectator-teleport-event.patch
rename to patches/api/0003-Add-spectator-teleport-event.patch
index 4862b37..c5e85f0 100644
--- a/patches/api/0002-Add-spectator-teleport-event.patch
+++ b/patches/api/0003-Add-spectator-teleport-event.patch
@@ -6,7 +6,7 @@ Subject: [PATCH] Add spectator teleport event
diff --git a/src/main/java/me/totalfreedom/scissors/event/player/SpectatorTeleportEvent.java b/src/main/java/me/totalfreedom/scissors/event/player/SpectatorTeleportEvent.java
new file mode 100644
-index 0000000000000000000000000000000000000000..d7efa63c316ed99c3eccfeadc1b0873b2ccb5d8a
+index 0000000000000000000000000000000000000000..e4c9256c78f8b395aea86e9ea1a112f8e7426c1f
--- /dev/null
+++ b/src/main/java/me/totalfreedom/scissors/event/player/SpectatorTeleportEvent.java
@@ -0,0 +1,60 @@
diff --git a/patches/api/0003-Add-Scissors-configuration-file.patch b/patches/api/0004-Add-Scissors-configuration-file.patch
similarity index 100%
rename from patches/api/0003-Add-Scissors-configuration-file.patch
rename to patches/api/0004-Add-Scissors-configuration-file.patch
diff --git a/patches/api/0004-Add-command-block-player-edit-event.patch b/patches/api/0005-Add-command-block-player-edit-event.patch
similarity index 100%
rename from patches/api/0004-Add-command-block-player-edit-event.patch
rename to patches/api/0005-Add-command-block-player-edit-event.patch
diff --git a/patches/server/0001-AdvancedSlimePaper-Server-Changes.patch b/patches/server/0001-AdvancedSlimePaper-Server-Changes.patch
new file mode 100644
index 0000000..e63dae3
--- /dev/null
+++ b/patches/server/0001-AdvancedSlimePaper-Server-Changes.patch
@@ -0,0 +1,2128 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
+Date: Mon, 26 Dec 2022 11:25:35 -0500
+Subject: [PATCH] AdvancedSlimePaper Server Changes
+
+AdvancedSlimePaper
+Copyright (C) 2023 InfernalSuite
+
+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 .
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index fb98936bb8a5488db75d676c5bcb4060597fbbf8..2143180a92ec6d0c0eba5559dd5497291348fdfa 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -13,6 +13,7 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) {
+ val alsoShade: Configuration by configurations.creating
+
+ dependencies {
++ implementation(project(":aswm-core"))
+ implementation(project(":paper-api"))
+ implementation(project(":paper-mojangapi"))
+ // Paper start
+diff --git a/src/main/java/com/infernalsuite/aswm/Converter.java b/src/main/java/com/infernalsuite/aswm/Converter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7d1753c0b7e89bbf0c245a0231b62773eca2779e
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/Converter.java
+@@ -0,0 +1,118 @@
++package com.infernalsuite.aswm;
++
++import com.flowpowered.nbt.CompoundMap;
++import com.flowpowered.nbt.TagType;
++import com.infernalsuite.aswm.api.utils.NibbleArray;
++import net.minecraft.nbt.*;
++import net.minecraft.world.level.chunk.DataLayer;
++import org.apache.logging.log4j.LogManager;
++import org.apache.logging.log4j.Logger;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class Converter {
++
++ private static final Logger LOGGER = LogManager.getLogger("SWM Converter");
++
++ static DataLayer convertArray(NibbleArray array) {
++ return new DataLayer(array.getBacking());
++ }
++
++ public static NibbleArray convertArray(DataLayer array) {
++ if(array == null) {
++ return null;
++ }
++
++ return new NibbleArray(array.getData());
++ }
++
++ public static Tag convertTag(com.flowpowered.nbt.Tag tag) {
++ try {
++ switch(tag.getType()) {
++ case TAG_BYTE:
++ return ByteTag.valueOf(((com.flowpowered.nbt.ByteTag) tag).getValue());
++ case TAG_SHORT:
++ return ShortTag.valueOf(((com.flowpowered.nbt.ShortTag) tag).getValue());
++ case TAG_INT:
++ return IntTag.valueOf(((com.flowpowered.nbt.IntTag) tag).getValue());
++ case TAG_LONG:
++ return LongTag.valueOf(((com.flowpowered.nbt.LongTag) tag).getValue());
++ case TAG_FLOAT:
++ return FloatTag.valueOf(((com.flowpowered.nbt.FloatTag) tag).getValue());
++ case TAG_DOUBLE:
++ return DoubleTag.valueOf(((com.flowpowered.nbt.DoubleTag) tag).getValue());
++ case TAG_BYTE_ARRAY:
++ return new ByteArrayTag(((com.flowpowered.nbt.ByteArrayTag) tag).getValue());
++ case TAG_STRING:
++ return StringTag.valueOf(((com.flowpowered.nbt.StringTag) tag).getValue());
++ case TAG_LIST:
++ ListTag list = new ListTag();
++ ((com.flowpowered.nbt.ListTag>) tag).getValue().stream().map(Converter::convertTag).forEach(list::add);
++
++ return list;
++ case TAG_COMPOUND:
++ CompoundTag compound = new CompoundTag();
++
++ ((com.flowpowered.nbt.CompoundTag) tag).getValue().forEach((key, value) -> compound.put(key, convertTag(value)));
++ return compound;
++ case TAG_INT_ARRAY:
++ return new IntArrayTag(((com.flowpowered.nbt.IntArrayTag) tag).getValue());
++ case TAG_LONG_ARRAY:
++ return new LongArrayTag(((com.flowpowered.nbt.LongArrayTag) tag).getValue());
++ default:
++ throw new IllegalArgumentException("Invalid tag type " + tag.getType().name());
++ }
++ } catch(Exception ex) {
++ LOGGER.error("Failed to convert NBT object:");
++ LOGGER.error(tag.toString());
++
++ throw ex;
++ }
++ }
++
++ public static com.flowpowered.nbt.Tag convertTag(String name, Tag base) {
++ switch(base.getId()) {
++ case Tag.TAG_BYTE:
++ return new com.flowpowered.nbt.ByteTag(name, ((ByteTag) base).getAsByte());
++ case Tag.TAG_SHORT:
++ return new com.flowpowered.nbt.ShortTag(name, ((ShortTag) base).getAsShort());
++ case Tag.TAG_INT:
++ return new com.flowpowered.nbt.IntTag(name, ((IntTag) base).getAsInt());
++ case Tag.TAG_LONG:
++ return new com.flowpowered.nbt.LongTag(name, ((LongTag) base).getAsLong());
++ case Tag.TAG_FLOAT:
++ return new com.flowpowered.nbt.FloatTag(name, ((FloatTag) base).getAsFloat());
++ case Tag.TAG_DOUBLE:
++ return new com.flowpowered.nbt.DoubleTag(name, ((DoubleTag) base).getAsDouble());
++ case Tag.TAG_BYTE_ARRAY:
++ return new com.flowpowered.nbt.ByteArrayTag(name, ((ByteArrayTag) base).getAsByteArray());
++ case Tag.TAG_STRING:
++ return new com.flowpowered.nbt.StringTag(name, ((StringTag) base).getAsString());
++ case Tag.TAG_LIST:
++ List list = new ArrayList<>();
++ ListTag originalList = ((ListTag) base);
++
++ for(Tag entry : originalList) {
++ list.add(convertTag("", entry));
++ }
++
++ return new com.flowpowered.nbt.ListTag(name, TagType.getById(originalList.getElementType()), list);
++ case Tag.TAG_COMPOUND:
++ CompoundTag originalCompound = ((CompoundTag) base);
++ com.flowpowered.nbt.CompoundTag compound = new com.flowpowered.nbt.CompoundTag(name, new CompoundMap());
++
++ for(String key : originalCompound.getAllKeys()) {
++ compound.getValue().put(key, convertTag(key, originalCompound.get(key)));
++ }
++
++ return compound;
++ case Tag.TAG_INT_ARRAY:
++ return new com.flowpowered.nbt.IntArrayTag(name, ((IntArrayTag) base).getAsIntArray());
++ case Tag.TAG_LONG_ARRAY:
++ return new com.flowpowered.nbt.LongArrayTag(name, ((LongArrayTag) base).getAsLongArray());
++ default:
++ throw new IllegalArgumentException("Invalid tag type " + base.getId());
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/InternalPlugin.java b/src/main/java/com/infernalsuite/aswm/InternalPlugin.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..61518ab2b68e7a41500f3c8c8a5ec1230597f0e5
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/InternalPlugin.java
+@@ -0,0 +1,28 @@
++package com.infernalsuite.aswm;
++
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.Server;
++import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
++import org.bukkit.plugin.PluginLogger;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.logging.LogRecord;
++
++public class InternalPlugin extends MinecraftInternalPlugin {
++
++ @Override
++ public @NotNull Server getServer() {
++ return MinecraftServer.getServer().server;
++ }
++
++ @Override
++ public @NotNull PluginLogger getLogger() {
++ return new PluginLogger(new InternalPlugin()) {
++ @Override
++ public void log(@NotNull LogRecord logRecord) {
++ MinecraftServer.LOGGER.info(logRecord.getMessage());
++ }
++ };
++ }
++
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/SimpleDataFixerConverter.java b/src/main/java/com/infernalsuite/aswm/SimpleDataFixerConverter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1affe4c94b490a05184deccc9eb80530f67fd5ea
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/SimpleDataFixerConverter.java
+@@ -0,0 +1,101 @@
++package com.infernalsuite.aswm;
++
++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils;
++import ca.spottedleaf.dataconverter.types.nbt.NBTMapType;
++import com.flowpowered.nbt.CompoundTag;
++import com.infernalsuite.aswm.serialization.SlimeWorldReader;
++import com.infernalsuite.aswm.skeleton.SkeletonSlimeWorld;
++import com.infernalsuite.aswm.skeleton.SlimeChunkSectionSkeleton;
++import com.infernalsuite.aswm.skeleton.SlimeChunkSkeleton;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeChunkSection;
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++import net.minecraft.SharedConstants;
++
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.function.Consumer;
++
++class SimpleDataFixerConverter implements SlimeWorldReader {
++
++ @Override
++ public SlimeWorld readFromData(SlimeWorld data) {
++ int newVersion = SharedConstants.getCurrentVersion().getDataVersion().getVersion();
++ int currentVersion = data.getDataVersion();
++ // Already fixed
++ if (currentVersion == newVersion) {
++ return data;
++ }
++
++ Map chunks = new HashMap<>();
++ for (SlimeChunk chunk : data.getChunkStorage()) {
++ List entities = new ArrayList<>();
++ List blockEntities = new ArrayList<>();
++ for (CompoundTag upgradeEntity : chunk.getTileEntities()) {
++ blockEntities.add(
++ convertAndBack(upgradeEntity, (tag) -> MCTypeRegistry.TILE_ENTITY.convert(new NBTMapType(tag), currentVersion, newVersion))
++ );
++ }
++ for (CompoundTag upgradeEntity : chunk.getEntities()) {
++ entities.add(
++ convertAndBack(upgradeEntity, (tag) -> MCTypeRegistry.ENTITY.convert(new NBTMapType(tag), currentVersion, newVersion))
++ );
++ }
++
++ SlimeChunkSection[] sections = new SlimeChunkSection[chunk.getSections().length];
++ for (int i = 0; i < sections.length; i++) {
++ SlimeChunkSection dataSection = chunk.getSections()[i];
++
++ com.flowpowered.nbt.CompoundTag blockStateTag = blockStateTag = convertAndBack(dataSection.getBlockStatesTag(), (tag) -> {
++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, new NBTMapType(tag), "palette", currentVersion, newVersion);
++ });
++
++ com.flowpowered.nbt.CompoundTag biomeTag = convertAndBack(dataSection.getBiomeTag(), (tag) -> {
++ WalkerUtils.convertList(MCTypeRegistry.BIOME, new NBTMapType(tag), "palette", currentVersion, newVersion);
++ });
++
++ sections[i] = new SlimeChunkSectionSkeleton(
++ blockStateTag,
++ biomeTag,
++ dataSection.getBlockLight(),
++ dataSection.getSkyLight()
++ );
++
++ chunks.put(new ChunkPos(chunk.getX(), chunk.getZ()), new SlimeChunkSkeleton(
++ chunk.getX(),
++ chunk.getZ(),
++ sections,
++ chunk.getHeightMaps(),
++ blockEntities,
++ entities
++ ));
++ }
++
++ }
++
++ return new SkeletonSlimeWorld(
++ data.getName(),
++ data.getLoader(),
++ data.isReadOnly(),
++ chunks,
++ data.getExtraData(),
++ data.getPropertyMap(),
++ newVersion
++ );
++ }
++
++
++ private static com.flowpowered.nbt.CompoundTag convertAndBack(com.flowpowered.nbt.CompoundTag value, Consumer acceptor) {
++ if (value == null) {
++ return null;
++ }
++
++ net.minecraft.nbt.CompoundTag converted = (net.minecraft.nbt.CompoundTag) Converter.convertTag(value);
++ acceptor.accept(converted);
++
++ return (com.flowpowered.nbt.CompoundTag) Converter.convertTag(value.getName(), converted);
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/SlimeNMSBridgeImpl.java b/src/main/java/com/infernalsuite/aswm/SlimeNMSBridgeImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..20f16757ba8b8da525ff17d51d4f7eb660c4d22b
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/SlimeNMSBridgeImpl.java
+@@ -0,0 +1,206 @@
++package com.infernalsuite.aswm;
++
++import com.infernalsuite.aswm.api.SlimeNMSBridge;
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++import com.infernalsuite.aswm.api.world.SlimeWorldInstance;
++import com.infernalsuite.aswm.api.world.properties.SlimeProperties;
++import com.infernalsuite.aswm.level.SlimeBootstrap;
++import com.infernalsuite.aswm.level.SlimeInMemoryWorld;
++import com.infernalsuite.aswm.level.SlimeLevelInstance;
++import com.mojang.serialization.Lifecycle;
++import net.kyori.adventure.util.Services;
++import net.minecraft.SharedConstants;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.dedicated.DedicatedServer;
++import net.minecraft.server.dedicated.DedicatedServerProperties;
++import net.minecraft.world.level.GameRules;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.LevelSettings;
++import net.minecraft.world.level.dimension.LevelStem;
++import net.minecraft.world.level.levelgen.WorldOptions;
++import net.minecraft.world.level.storage.CommandStorage;
++import net.minecraft.world.level.storage.DimensionDataStorage;
++import net.minecraft.world.level.storage.PrimaryLevelData;
++import org.apache.logging.log4j.LogManager;
++import org.apache.logging.log4j.Logger;
++import org.bukkit.Bukkit;
++import org.bukkit.World;
++import org.bukkit.craftbukkit.CraftWorld;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.IOException;
++import java.util.Locale;
++
++public class SlimeNMSBridgeImpl implements SlimeNMSBridge {
++
++ private static final SimpleDataFixerConverter DATA_FIXER_CONVERTER = new SimpleDataFixerConverter();
++
++ private static final Logger LOGGER = LogManager.getLogger("SWM");
++
++ private SlimeWorld defaultWorld;
++ private SlimeWorld defaultNetherWorld;
++ private SlimeWorld defaultEndWorld;
++
++ public static SlimeNMSBridgeImpl instance() {
++ return (SlimeNMSBridgeImpl) SlimeNMSBridge.instance();
++ }
++
++ @Override
++ public boolean loadOverworldOverride() {
++ if (defaultWorld == null) {
++ return false;
++ }
++
++ // See MinecraftServer loading logic
++ // Some stuff is needed when loading overworld world
++ SlimeLevelInstance instance = ((SlimeInMemoryWorld) this.loadInstance(defaultWorld, Level.OVERWORLD)).getInstance();
++ DimensionDataStorage worldpersistentdata = instance.getDataStorage();
++ instance.getCraftServer().scoreboardManager = new org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager(instance.getServer(), instance.getScoreboard());
++ instance.getServer().commandStorage = new CommandStorage(worldpersistentdata);
++
++ return true;
++ }
++
++ @Override
++ public boolean loadNetherOverride() {
++ if (defaultNetherWorld == null) {
++ return false;
++ }
++
++ this.loadInstance(defaultNetherWorld, Level.NETHER);
++
++ return true;
++ }
++
++ @Override
++ public boolean loadEndOverride() {
++ if (defaultEndWorld == null) {
++ return false;
++ }
++
++ this.loadInstance(defaultEndWorld, Level.END);
++
++ return true;
++ }
++
++ @Override
++ public void setDefaultWorlds(SlimeWorld normalWorld, SlimeWorld netherWorld, SlimeWorld endWorld) {
++ if (normalWorld != null) {
++ normalWorld.getPropertyMap().setValue(SlimeProperties.ENVIRONMENT, World.Environment.NORMAL.toString().toLowerCase());
++ defaultWorld = normalWorld;
++ }
++
++ if (netherWorld != null) {
++ netherWorld.getPropertyMap().setValue(SlimeProperties.ENVIRONMENT, World.Environment.NETHER.toString().toLowerCase());
++ defaultNetherWorld = netherWorld;
++ }
++
++ if (endWorld != null) {
++ endWorld.getPropertyMap().setValue(SlimeProperties.ENVIRONMENT, World.Environment.THE_END.toString().toLowerCase());
++ defaultEndWorld = endWorld;
++ }
++
++ }
++
++ @Override
++ public SlimeWorldInstance loadInstance(SlimeWorld slimeWorld) {
++ return this.loadInstance(slimeWorld, null);
++ }
++
++ public SlimeWorldInstance loadInstance(SlimeWorld slimeWorld, @Nullable ResourceKey dimensionOverride) {
++ String worldName = slimeWorld.getName();
++
++ if (Bukkit.getWorld(worldName) != null) {
++ throw new IllegalArgumentException("World " + worldName + " already exists! Maybe it's an outdated SlimeWorld object?");
++ }
++
++ SlimeLevelInstance server = createCustomWorld(slimeWorld, dimensionOverride);
++ registerWorld(server);
++ return server.getSlimeInstance();
++ }
++
++ @Override
++ public SlimeWorldInstance getInstance(World world) {
++ CraftWorld craftWorld = (CraftWorld) world;
++
++ if (!(craftWorld.getHandle() instanceof SlimeLevelInstance worldServer)) {
++ return null;
++ }
++
++ return worldServer.getSlimeInstance();
++ }
++
++ @Override
++ public SlimeWorld applyDataFixers(SlimeWorld world) {
++ return DATA_FIXER_CONVERTER.readFromData(world);
++ }
++
++
++ @Override
++ public int getCurrentVersion() {
++ return SharedConstants.getCurrentVersion().getDataVersion().getVersion();
++ }
++
++ public void registerWorld(SlimeLevelInstance server) {
++ MinecraftServer mcServer = MinecraftServer.getServer();
++ mcServer.initWorld(server, server.serverLevelData, mcServer.getWorldData(), server.serverLevelData.worldGenOptions());
++
++ mcServer.addLevel(server);
++ }
++
++ private SlimeLevelInstance createCustomWorld(SlimeWorld world, @Nullable ResourceKey dimensionOverride) {
++ SlimeBootstrap bootstrap = new SlimeBootstrap(world);
++ String worldName = world.getName();
++
++ PrimaryLevelData worldDataServer = createWorldData(world);
++ World.Environment environment = getEnvironment(world);
++ ResourceKey dimension = switch (environment) {
++ case NORMAL -> LevelStem.OVERWORLD;
++ case NETHER -> LevelStem.NETHER;
++ case THE_END -> LevelStem.END;
++ default -> throw new IllegalArgumentException("Unknown dimension supplied");
++ };
++
++ ResourceKey worldKey = dimensionOverride == null ? ResourceKey.create(Registries.DIMENSION, new ResourceLocation(worldName.toLowerCase(Locale.ENGLISH))) : dimensionOverride;
++ LevelStem stem = MinecraftServer.getServer().registries().compositeAccess().registryOrThrow(Registries.LEVEL_STEM).get(dimension);
++
++ SlimeLevelInstance level;
++
++ try {
++ level = new SlimeLevelInstance(bootstrap, worldDataServer, worldKey, dimension, stem, environment);
++ } catch (IOException ex) {
++ throw new RuntimeException(ex); // TODO do something better with this?
++ }
++
++ // level.setReady(true);
++ level.setSpawnSettings(world.getPropertyMap().getValue(SlimeProperties.ALLOW_MONSTERS), world.getPropertyMap().getValue(SlimeProperties.ALLOW_ANIMALS));
++
++ return level;
++ }
++
++ private World.Environment getEnvironment(SlimeWorld world) {
++ return World.Environment.valueOf(world.getPropertyMap().getValue(SlimeProperties.ENVIRONMENT).toUpperCase());
++ }
++
++ private PrimaryLevelData createWorldData(SlimeWorld world) {
++ MinecraftServer mcServer = MinecraftServer.getServer();
++ DedicatedServerProperties serverProps = ((DedicatedServer) mcServer).getProperties();
++ String worldName = world.getName();
++
++ LevelSettings worldsettings = new LevelSettings(worldName, serverProps.gamemode, false, serverProps.difficulty,
++ true, new GameRules(), mcServer.worldLoader.dataConfiguration());
++
++ WorldOptions worldoptions = new WorldOptions(0, false, false);
++
++ PrimaryLevelData data = new PrimaryLevelData(worldsettings, worldoptions, PrimaryLevelData.SpecialWorldProperty.FLAT, Lifecycle.stable());
++ data.checkName(worldName);
++ data.setModdedInfo(mcServer.getServerModName(), mcServer.getModdedStatus().shouldReportAsModified());
++ data.setInitialized(true);
++
++ return data;
++ }
++
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/ChunkDataLoadTask.java b/src/main/java/com/infernalsuite/aswm/level/ChunkDataLoadTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..41e652b568598926e838e81fdc338e51f8e97ef8
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/ChunkDataLoadTask.java
+@@ -0,0 +1,121 @@
++package com.infernalsuite.aswm.level;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.chunk.system.poi.PoiChunk;
++import io.papermc.paper.chunk.system.scheduling.ChunkLoadTask;
++import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler;
++import io.papermc.paper.chunk.system.scheduling.GenericDataLoadTask;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.ImposterProtoChunk;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.UpgradeData;
++import net.minecraft.world.level.material.Fluid;
++import net.minecraft.world.ticks.LevelChunkTicks;
++import org.slf4j.Logger;
++
++import java.util.function.Consumer;
++
++public final class ChunkDataLoadTask implements CommonLoadTask {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private final ChunkTaskScheduler scheduler;
++ private final ServerLevel world;
++ private final int chunkX;
++ private final int chunkZ;
++ private Consumer> onRun;
++
++ private PrioritisedExecutor.PrioritisedTask task;
++
++ private final ChunkLoadTask chunkLoadTask;
++
++ protected ChunkDataLoadTask(ChunkLoadTask chunkLoadTask, final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
++ final int chunkZ, final PrioritisedExecutor.Priority priority, final Consumer> onRun) {
++ this.chunkLoadTask = chunkLoadTask;
++ this.scheduler = scheduler;
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.onRun = onRun;
++
++ this.task = this.scheduler.createChunkTask(this.chunkX, this.chunkZ, () -> {
++ try {
++ SlimeChunk chunk = ((SlimeLevelInstance) this.world).slimeInstance.getChunk(this.chunkX, this.chunkZ);
++ this.onRun.accept(new GenericDataLoadTask.TaskResult<>(runOnMain(chunk), null));
++ } catch (Throwable e) {
++ LOGGER.error("ERROR", e);
++ this.onRun.accept(new GenericDataLoadTask.TaskResult<>(null, e));
++ }
++ }, priority);
++ }
++
++ private ChunkAccess getEmptyChunk() {
++ LevelChunkTicks blockLevelChunkTicks = new LevelChunkTicks<>();
++ LevelChunkTicks fluidLevelChunkTicks = new LevelChunkTicks<>();
++
++ return new ImposterProtoChunk(new LevelChunk(this.world, new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, blockLevelChunkTicks, fluidLevelChunkTicks,
++ 0L, null, null, null), true);
++ }
++
++ protected ChunkAccess runOnMain(final SlimeChunk data) {
++ final PoiChunk poiChunk = this.chunkLoadTask.chunkHolder.getPoiChunk();
++ if (poiChunk == null) {
++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString());
++ } else {
++ poiChunk.load();
++ }
++
++ // have tasks to run (at this point, it's just the POI consistency checking)
++ try {
++ // if (data.tasks != null) {
++ // for (int i = 0, len = data.tasks.size(); i < len; i) {
++ // data.tasks.poll().run();
++ // }
++ // }
++
++ LevelChunk chunk = this.world.slimeInstance.promote(chunkX, chunkZ, data);
++
++ return new ImposterProtoChunk(chunk, false);
++ } catch (final ThreadDeath death) {
++ throw death;
++ } catch (final Throwable thr2) {
++ LOGGER.error("Failed to parse main tasks for task " + this.toString() + ", chunk data will be lost", thr2);
++ return this.getEmptyChunk();
++ }
++ }
++
++ @Override
++ public PrioritisedExecutor.Priority getPriority() {
++ return this.task.getPriority();
++ }
++
++ @Override
++ public void setPriority(PrioritisedExecutor.Priority priority) {
++ this.task.setPriority(priority);
++ }
++
++ @Override
++ public void raisePriority(PrioritisedExecutor.Priority priority) {
++ this.task.raisePriority(priority);
++ }
++
++ @Override
++ public void lowerPriority(PrioritisedExecutor.Priority priority) {
++ this.task.lowerPriority(priority);
++ }
++
++ @Override
++ public boolean cancel() {
++ return this.task.cancel();
++ }
++
++ public boolean schedule(boolean schedule) {
++ this.scheduler.scheduleChunkTask(chunkX, chunkZ, this.task::execute);
++ return true;
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/CommonLoadTask.java b/src/main/java/com/infernalsuite/aswm/level/CommonLoadTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fc6e46972bcc77134ed718c8c157ec3893d4bcdf
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/CommonLoadTask.java
+@@ -0,0 +1,18 @@
++package com.infernalsuite.aswm.level;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++
++public interface CommonLoadTask {
++
++ boolean schedule(boolean schedule);
++
++ PrioritisedExecutor.Priority getPriority();
++
++ boolean cancel();
++
++ void lowerPriority(PrioritisedExecutor.Priority priority);
++
++ void raisePriority(PrioritisedExecutor.Priority priority);
++
++ void setPriority(PrioritisedExecutor.Priority priority);
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/FastChunkPruner.java b/src/main/java/com/infernalsuite/aswm/level/FastChunkPruner.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c0e47f25e9be33da374dc737c96d8d3c2bb1cd0f
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/FastChunkPruner.java
+@@ -0,0 +1,59 @@
++package com.infernalsuite.aswm.level;
++
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++import com.infernalsuite.aswm.api.world.properties.SlimeProperties;
++import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap;
++import io.papermc.paper.world.ChunkEntitySlices;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++
++public class FastChunkPruner {
++
++ public static boolean canBePruned(SlimeWorld world, LevelChunk chunk) {
++ // Kenox
++ // It's not safe to assume that the chunk can be pruned
++ // if there isn't a loaded chunk there
++ if (chunk == null || chunk.getChunkHolder() == null) {
++ return false;
++ }
++
++ SlimePropertyMap propertyMap = world.getPropertyMap();
++ if (propertyMap.getValue(SlimeProperties.SHOULD_LIMIT_SAVE)) {
++ int minX = propertyMap.getValue(SlimeProperties.SAVE_MIN_X);
++ int maxX = propertyMap.getValue(SlimeProperties.SAVE_MAX_X);
++
++ int minZ = propertyMap.getValue(SlimeProperties.SAVE_MIN_Z);
++ int maxZ = propertyMap.getValue(SlimeProperties.SAVE_MAX_Z);
++
++ int chunkX = chunk.locX;
++ int chunkZ = chunk.locZ;
++
++ if (chunkX < minX || chunkX > maxX) {
++ return true;
++ }
++
++ if (chunkZ < minZ || chunkZ > maxZ) {
++ return true;
++ }
++ }
++
++ String pruningSetting = world.getPropertyMap().getValue(SlimeProperties.CHUNK_PRUNING);
++ if (pruningSetting.equals("aggressive")) {
++ ChunkEntitySlices slices = chunk.getChunkHolder().getEntityChunk();
++
++ return chunk.blockEntities.isEmpty() && (slices == null || slices.isEmpty()) && areSectionsEmpty(chunk);
++ }
++
++ return false;
++ }
++
++ private static boolean areSectionsEmpty(LevelChunk chunk) {
++ for (LevelChunkSection section : chunk.getSections()) {
++ if (!section.hasOnlyAir()) {
++ return false;
++ }
++ }
++
++ return true;
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/NMSSlimeChunk.java b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeChunk.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f1db2fe121bb3aabfad727a8133b645524b8f19a
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeChunk.java
+@@ -0,0 +1,203 @@
++package com.infernalsuite.aswm.level;
++
++import com.flowpowered.nbt.CompoundMap;
++import com.flowpowered.nbt.CompoundTag;
++import com.flowpowered.nbt.LongArrayTag;
++import com.google.common.collect.Lists;
++import com.infernalsuite.aswm.Converter;
++import com.infernalsuite.aswm.skeleton.SlimeChunkSectionSkeleton;
++import com.infernalsuite.aswm.api.utils.NibbleArray;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeChunkSection;
++import com.mojang.logging.LogUtils;
++import com.mojang.serialization.Codec;
++import io.papermc.paper.world.ChunkEntitySlices;
++import net.minecraft.core.Holder;
++import net.minecraft.core.Registry;
++import net.minecraft.core.SectionPos;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.nbt.Tag;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.LightLayer;
++import net.minecraft.world.level.biome.Biome;
++import net.minecraft.world.level.biome.Biomes;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.entity.BlockEntity;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.PalettedContainer;
++import net.minecraft.world.level.chunk.PalettedContainerRO;
++import net.minecraft.world.level.chunk.storage.ChunkSerializer;
++import net.minecraft.world.level.levelgen.Heightmap;
++import net.minecraft.world.level.lighting.LevelLightEngine;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++
++public class NMSSlimeChunk implements SlimeChunk {
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ private static final CompoundTag EMPTY_BLOCK_STATE_PALETTE;
++ private static final CompoundTag EMPTY_BIOME_PALETTE;
++
++ // Optimized empty section serialization
++ static {
++ {
++ PalettedContainer empty = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null);
++ Tag tag = ChunkSerializer.BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, empty).getOrThrow(false, (error) -> {
++ throw new AssertionError(error);
++ });
++
++ EMPTY_BLOCK_STATE_PALETTE = (CompoundTag) Converter.convertTag("", tag);
++ }
++ {
++ Registry biomes = net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME);
++ PalettedContainer> empty = new PalettedContainer<>(biomes.asHolderIdMap(), biomes.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null);
++ Tag tag = ChunkSerializer.makeBiomeCodec(biomes).encodeStart(NbtOps.INSTANCE, empty).getOrThrow(false, (error) -> {
++ throw new AssertionError(error);
++ });
++
++ EMPTY_BIOME_PALETTE = (CompoundTag) Converter.convertTag("", tag);
++ }
++ }
++
++ private LevelChunk chunk;
++
++ public NMSSlimeChunk(LevelChunk chunk) {
++ this.chunk = chunk;
++ }
++
++ @Override
++ public int getX() {
++ return chunk.getPos().x;
++ }
++
++ @Override
++ public int getZ() {
++ return chunk.getPos().z;
++ }
++
++ @Override
++ public SlimeChunkSection[] getSections() {
++ SlimeChunkSection[] sections = new SlimeChunkSection[this.chunk.getSectionsCount()];
++ LevelLightEngine lightEngine = chunk.getLevel().getChunkSource().getLightEngine();
++
++ Registry biomeRegistry = chunk.getLevel().registryAccess().registryOrThrow(Registries.BIOME);
++
++ // Ignore deprecation, spigot only method
++ Codec>> codec = PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
++
++ for (int sectionId = 0; sectionId < chunk.getSections().length; sectionId++) {
++ LevelChunkSection section = chunk.getSections()[sectionId];
++ // Sections CANNOT be null in 1.18
++
++ // Block Light Nibble Array
++ NibbleArray blockLightArray = Converter.convertArray(lightEngine.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunk.getPos(), sectionId)));
++
++ // Sky light Nibble Array
++ NibbleArray skyLightArray = Converter.convertArray(lightEngine.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunk.getPos(), sectionId)));
++
++ // Tile/Entity Data
++
++ // Block Data
++ CompoundTag blockStateTag;
++ if (section.hasOnlyAir()) {
++ blockStateTag = EMPTY_BLOCK_STATE_PALETTE;
++ } else {
++ Tag data = ChunkSerializer.BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, section.getStates()).getOrThrow(false, System.err::println); // todo error handling
++ blockStateTag = (CompoundTag) Converter.convertTag("", data);
++ }
++
++
++ CompoundTag biomeTag;
++ PalettedContainer> biomes = (PalettedContainer>) section.getBiomes();
++ if (biomes.data.palette().getSize() == 1 && biomes.data.palette().maybeHas((h) -> h.is(Biomes.PLAINS))) {
++ biomeTag = EMPTY_BIOME_PALETTE;
++ } else {
++ Tag biomeData = codec.encodeStart(NbtOps.INSTANCE, section.getBiomes()).getOrThrow(false, System.err::println); // todo error handling
++ biomeTag = (CompoundTag) Converter.convertTag("", biomeData);
++ }
++
++ sections[sectionId] = new SlimeChunkSectionSkeleton(blockStateTag, biomeTag, blockLightArray, skyLightArray);
++ }
++
++ return sections;
++ }
++
++ @Override
++ public CompoundTag getHeightMaps() {
++ // HeightMap
++ CompoundMap heightMaps = new CompoundMap();
++
++ for (Map.Entry entry : chunk.heightmaps.entrySet()) {
++ if (!entry.getKey().keepAfterWorldgen()) {
++ continue;
++ }
++
++ Heightmap.Types type = entry.getKey();
++ Heightmap map = entry.getValue();
++
++ heightMaps.put(type.name(), new LongArrayTag(type.name(), map.getRawData()));
++ }
++
++ return new CompoundTag("", heightMaps);
++ }
++
++ @Override
++ public List getTileEntities() {
++ List tileEntities = new ArrayList<>();
++
++ for (BlockEntity entity : chunk.blockEntities.values()) {
++ net.minecraft.nbt.CompoundTag entityNbt = entity.saveWithFullMetadata();
++ tileEntities.add(entityNbt);
++ }
++
++ return Lists.transform(tileEntities, (compound) -> {
++ return (CompoundTag) Converter.convertTag("", compound);
++ });
++ }
++
++ @Override
++ public List getEntities() {
++ List entities = new ArrayList<>();
++
++ if(this.chunk == null || this.chunk.getChunkHolder() == null) {
++ return new ArrayList<>();
++ }
++
++ ChunkEntitySlices slices = this.chunk.getChunkHolder().getEntityChunk();
++ if (slices == null) {
++ return new ArrayList<>();
++ }
++
++ // Work by
++ for (Entity entity : slices.entities) {
++ net.minecraft.nbt.CompoundTag entityNbt = new net.minecraft.nbt.CompoundTag();
++ try {
++ if (entity.save(entityNbt)) {
++ entities.add(entityNbt);
++ }
++ } catch (Exception e) {
++ LOGGER.error("Could not save the entity = {}, exception = {}", entity, e);
++ }
++ }
++
++ return Lists.transform(entities, (compound) -> {
++ return (CompoundTag) Converter.convertTag("", compound);
++ });
++ }
++
++ public LevelChunk getChunk() {
++ return chunk;
++ }
++
++ public void setChunk(LevelChunk chunk) {
++ this.chunk = chunk;
++ }
++
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/NMSSlimeWorld.java b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeWorld.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9a27369c00345bbb94aa19f77687269dc94c0b0a
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/NMSSlimeWorld.java
+@@ -0,0 +1,91 @@
++package com.infernalsuite.aswm.level;
++
++import com.flowpowered.nbt.CompoundTag;
++import com.infernalsuite.aswm.api.exceptions.WorldAlreadyExistsException;
++import com.infernalsuite.aswm.api.loaders.SlimeLoader;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap;
++import net.minecraft.SharedConstants;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.world.level.chunk.LevelChunk;
++
++import java.io.IOException;
++import java.util.Collection;
++import java.util.List;
++import java.util.Objects;
++import java.util.stream.Collectors;
++
++public class NMSSlimeWorld implements SlimeWorld {
++
++ private final SlimeInMemoryWorld memoryWorld;
++ private final SlimeLevelInstance instance;
++
++ public NMSSlimeWorld(SlimeInMemoryWorld memoryWorld) {
++ this.instance = memoryWorld.getInstance();
++ this.memoryWorld = memoryWorld;
++ }
++
++ @Override
++ public String getName() {
++ return this.instance.getMinecraftWorld().serverLevelData.getLevelName();
++ }
++
++ @Override
++ public SlimeLoader getLoader() {
++ return this.instance.slimeInstance.getSaveStrategy();
++ }
++
++ @Override
++ public SlimeChunk getChunk(int x, int z) {
++ LevelChunk chunk = this.instance.getChunkIfLoaded(x, z);
++ if (chunk == null) {
++ return null;
++ }
++
++ return new NMSSlimeChunk(chunk);
++ }
++
++ @Override
++ public Collection getChunkStorage() {
++ List chunks = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.instance); // Paper
++ return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull)
++ .map(NMSSlimeChunk::new)
++ .collect(Collectors.toList());
++ }
++
++ @Override
++ public CompoundTag getExtraData() {
++ return this.instance.slimeInstance.getExtraData();
++ }
++
++ @Override
++ public Collection getWorldMaps() {
++ return List.of();
++ }
++
++ @Override
++ public SlimePropertyMap getPropertyMap() {
++ return this.instance.slimeInstance.getPropertyMap();
++ }
++
++ @Override
++ public boolean isReadOnly() {
++ return this.getLoader() == null;
++ }
++
++ @Override
++ public SlimeWorld clone(String worldName) {
++ return this.memoryWorld.clone(worldName);
++ }
++
++ @Override
++ public SlimeWorld clone(String worldName, SlimeLoader loader) throws WorldAlreadyExistsException, IOException {
++ return this.memoryWorld.clone(worldName, loader);
++ }
++
++ @Override
++ public int getDataVersion() {
++ return SharedConstants.getCurrentVersion().getDataVersion().getVersion();
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SafeNmsChunkWrapper.java b/src/main/java/com/infernalsuite/aswm/level/SafeNmsChunkWrapper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b20a037679182e3c4a8bf31f084078f6d7e4ff46
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SafeNmsChunkWrapper.java
+@@ -0,0 +1,83 @@
++package com.infernalsuite.aswm.level;
++
++import com.flowpowered.nbt.CompoundTag;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeChunkSection;
++
++import java.util.List;
++
++public class SafeNmsChunkWrapper implements SlimeChunk {
++
++ private final NMSSlimeChunk wrapper;
++ private final SlimeChunk safety;
++
++ public SafeNmsChunkWrapper(NMSSlimeChunk wrapper, SlimeChunk safety) {
++ this.wrapper = wrapper;
++ this.safety = safety;
++ }
++
++ @Override
++ public int getX() {
++ return this.wrapper.getX();
++ }
++
++ @Override
++ public int getZ() {
++ return this.wrapper.getZ();
++ }
++
++ @Override
++ public SlimeChunkSection[] getSections() {
++ if (shouldDefaultBackToSlimeChunk()) {
++ return this.safety.getSections();
++ }
++
++ return this.wrapper.getSections();
++ }
++
++ @Override
++ public CompoundTag getHeightMaps() {
++ if (shouldDefaultBackToSlimeChunk()) {
++ return this.safety.getHeightMaps();
++ }
++
++ return this.wrapper.getHeightMaps();
++ }
++
++ @Override
++ public List getTileEntities() {
++ if (shouldDefaultBackToSlimeChunk()) {
++ return this.safety.getTileEntities();
++ }
++
++ return this.wrapper.getTileEntities();
++ }
++
++ @Override
++ public List getEntities() {
++ if (shouldDefaultBackToSlimeChunk()) {
++ return this.safety.getEntities();
++ }
++
++ return this.wrapper.getEntities();
++ }
++
++ /*
++Slime chunks can still be requested but not actually loaded, this caused
++some things to not properly save because they are not "loaded" into the chunk.
++See ChunkMap#protoChunkToFullChunk
++anything in the if statement will not be loaded and is stuck inside the runnable.
++Inorder to possibly not corrupt the state, simply refer back to the slime saved object.
++*/
++ public boolean shouldDefaultBackToSlimeChunk() {
++ return this.safety != null && !this.wrapper.getChunk().loaded;
++ }
++
++ public NMSSlimeChunk getWrapper() {
++ return wrapper;
++ }
++
++ public SlimeChunk getSafety() {
++ return safety;
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeBootstrap.java b/src/main/java/com/infernalsuite/aswm/level/SlimeBootstrap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8853088c5c6306511716bbffac9bf73c633b61bb
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SlimeBootstrap.java
+@@ -0,0 +1,8 @@
++package com.infernalsuite.aswm.level;
++
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++
++public record SlimeBootstrap(
++ SlimeWorld initial
++) {
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeChunkConverter.java b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkConverter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..91a7f41db47c7df3ecc301e0827a1d07305f604e
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkConverter.java
+@@ -0,0 +1,164 @@
++package com.infernalsuite.aswm.level;
++
++import ca.spottedleaf.starlight.common.light.SWMRNibbleArray;
++import com.flowpowered.nbt.CompoundMap;
++import com.flowpowered.nbt.CompoundTag;
++import com.flowpowered.nbt.LongArrayTag;
++import com.infernalsuite.aswm.Converter;
++import com.infernalsuite.aswm.api.utils.NibbleArray;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeChunkSection;
++import com.mojang.serialization.Codec;
++import com.mojang.serialization.DataResult;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Holder;
++import net.minecraft.core.Registry;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.nbt.NbtOps;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.biome.Biome;
++import net.minecraft.world.level.biome.Biomes;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.entity.BlockEntity;
++import net.minecraft.world.level.block.state.BlockState;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.PalettedContainer;
++import net.minecraft.world.level.chunk.UpgradeData;
++import net.minecraft.world.level.chunk.storage.ChunkSerializer;
++import net.minecraft.world.level.levelgen.Heightmap;
++import net.minecraft.world.level.material.Fluid;
++import net.minecraft.world.ticks.LevelChunkTicks;
++
++import java.util.EnumSet;
++import java.util.List;
++import java.util.Optional;
++
++public class SlimeChunkConverter {
++
++ static SlimeChunkLevel deserializeSlimeChunk(SlimeLevelInstance instance, SlimeChunk chunk) {
++ int x = chunk.getX();
++ int z = chunk.getZ();
++
++ ChunkPos pos = new ChunkPos(x, z);
++
++ // Chunk sections
++ LevelChunkSection[] sections = new LevelChunkSection[instance.getSectionsCount()];
++
++ SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(instance);
++ SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(instance);
++ instance.getServer().scheduleOnMain(() -> {
++ instance.getLightEngine().retainData(pos, true);
++ });
++
++ Registry biomeRegistry = instance.registryAccess().registryOrThrow(Registries.BIOME);
++ // Ignore deprecated method
++
++ Codec>> codec = PalettedContainer.codecRW(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS), null);
++
++ for (int sectionId = 0; sectionId < chunk.getSections().length; sectionId++) {
++ SlimeChunkSection slimeSection = chunk.getSections()[sectionId];
++
++ if (slimeSection != null) {
++ NibbleArray blockLight = slimeSection.getBlockLight();
++ if (blockLight != null) {
++ blockNibbles[sectionId] = new SWMRNibbleArray(blockLight.getBacking());
++ }
++
++ NibbleArray skyLight = slimeSection.getSkyLight();
++ if (skyLight != null) {
++ skyNibbles[sectionId] = new SWMRNibbleArray(skyLight.getBacking());
++ }
++
++ PalettedContainer blockPalette;
++ if (slimeSection.getBlockStatesTag() != null) {
++ DataResult> dataresult = ChunkSerializer.BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, Converter.convertTag(slimeSection.getBlockStatesTag())).promotePartial((s) -> {
++ System.out.println("Recoverable error when parsing section " + x + "," + z + ": " + s); // todo proper logging
++ });
++ blockPalette = dataresult.getOrThrow(false, System.err::println); // todo proper logging
++ } else {
++ blockPalette = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null);
++ }
++
++ PalettedContainer> biomePalette;
++
++ if (slimeSection.getBiomeTag() != null) {
++ DataResult>> dataresult = codec.parse(NbtOps.INSTANCE, Converter.convertTag(slimeSection.getBiomeTag())).promotePartial((s) -> {
++ System.out.println("Recoverable error when parsing section " + x + "," + z + ": " + s); // todo proper logging
++ });
++ biomePalette = dataresult.getOrThrow(false, System.err::println); // todo proper logging
++ } else {
++ biomePalette = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null);
++ }
++
++ if (sectionId < sections.length) {
++ LevelChunkSection section = new LevelChunkSection(blockPalette, biomePalette);
++ sections[sectionId] = section;
++ }
++ }
++ }
++
++ // Keep the chunk loaded at level 33 to avoid light glitches
++ // Such a high level will let the server not tick the chunk,
++ // but at the same time it won't be completely unloaded from memory
++ // getChunkProvider().addTicket(SWM_TICKET, pos, 33, Unit.INSTANCE);
++
++
++ LevelChunk.PostLoadProcessor loadEntities = (nmsChunk) -> {
++
++ // TODO
++ // Load tile entities
++ List tileEntities = chunk.getTileEntities();
++
++ if (tileEntities != null) {
++ for (CompoundTag tag : tileEntities) {
++ Optional type = tag.getStringValue("id");
++
++ // Sometimes null tile entities are saved
++ if (type.isPresent()) {
++ BlockPos blockPosition = new BlockPos(tag.getIntValue("x").get(), tag.getIntValue("y").get(), tag.getIntValue("z").get());
++ BlockState blockData = nmsChunk.getBlockState(blockPosition);
++ BlockEntity entity = BlockEntity.loadStatic(blockPosition, blockData, (net.minecraft.nbt.CompoundTag) Converter.convertTag(tag));
++
++ if (entity != null) {
++ nmsChunk.setBlockEntity(entity);
++ }
++ }
++ }
++ }
++ };
++
++ LevelChunkTicks blockLevelChunkTicks = new LevelChunkTicks<>();
++ LevelChunkTicks fluidLevelChunkTicks = new LevelChunkTicks<>();
++ SlimeChunkLevel nmsChunk = new SlimeChunkLevel(instance, pos, UpgradeData.EMPTY, blockLevelChunkTicks, fluidLevelChunkTicks, 0L, sections, loadEntities, null);
++
++ // Height Maps
++ EnumSet heightMapTypes = nmsChunk.getStatus().heightmapsAfter();
++ CompoundMap heightMaps = chunk.getHeightMaps().getValue();
++ EnumSet unsetHeightMaps = EnumSet.noneOf(Heightmap.Types.class);
++
++ // Light
++ nmsChunk.setBlockNibbles(blockNibbles);
++ nmsChunk.setSkyNibbles(skyNibbles);
++
++ for (Heightmap.Types type : heightMapTypes) {
++ String name = type.getSerializedName();
++
++ if (heightMaps.containsKey(name)) {
++ LongArrayTag heightMap = (LongArrayTag) heightMaps.get(name);
++ nmsChunk.setHeightmap(type, heightMap.getValue());
++ } else {
++ unsetHeightMaps.add(type);
++ }
++ }
++
++ // Don't try to populate heightmaps if there are none.
++ // Does a crazy amount of block lookups
++ if (!unsetHeightMaps.isEmpty()) {
++ Heightmap.primeHeightmaps(nmsChunk, unsetHeightMaps);
++ }
++
++ return nmsChunk;
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeChunkLevel.java b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkLevel.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b159fc8751e9840b311cc1eda01e496e2dbc5f2e
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SlimeChunkLevel.java
+@@ -0,0 +1,27 @@
++package com.infernalsuite.aswm.level;
++
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.LevelChunkSection;
++import net.minecraft.world.level.chunk.UpgradeData;
++import net.minecraft.world.level.levelgen.blending.BlendingData;
++import net.minecraft.world.level.material.Fluid;
++import net.minecraft.world.ticks.LevelChunkTicks;
++import org.jetbrains.annotations.Nullable;
++
++public class SlimeChunkLevel extends LevelChunk {
++
++ private final SlimeInMemoryWorld inMemoryWorld;
++
++ public SlimeChunkLevel(SlimeLevelInstance world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks blockTickScheduler, LevelChunkTicks fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) {
++ super(world, pos, upgradeData, blockTickScheduler, fluidTickScheduler, inhabitedTime, sectionArrayInitializer, entityLoader, blendingData);
++ this.inMemoryWorld = world.slimeInstance;
++ }
++
++ @Override
++ public void unloadCallback() {
++ super.unloadCallback();
++ this.inMemoryWorld.unload(this);
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeInMemoryWorld.java b/src/main/java/com/infernalsuite/aswm/level/SlimeInMemoryWorld.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fd4cfb9cceb4f23265cb3cce7f1f251051bfba92
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SlimeInMemoryWorld.java
+@@ -0,0 +1,251 @@
++package com.infernalsuite.aswm.level;
++
++import com.flowpowered.nbt.CompoundTag;
++import com.infernalsuite.aswm.ChunkPos;
++import com.infernalsuite.aswm.api.exceptions.WorldAlreadyExistsException;
++import com.infernalsuite.aswm.api.loaders.SlimeLoader;
++import com.infernalsuite.aswm.serialization.slime.SlimeSerializer;
++import com.infernalsuite.aswm.skeleton.SkeletonCloning;
++import com.infernalsuite.aswm.skeleton.SkeletonSlimeWorld;
++import com.infernalsuite.aswm.skeleton.SlimeChunkSkeleton;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++import com.infernalsuite.aswm.api.world.SlimeWorldInstance;
++import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.chunk.UpgradeData;
++import net.minecraft.world.level.material.Fluid;
++import net.minecraft.world.ticks.LevelChunkTicks;
++import org.bukkit.World;
++
++import java.io.IOException;
++import java.util.ArrayList;
++import java.util.Collection;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++
++/*
++The concept of this is a bit flawed, since ideally this should be a 1:1 representation of the MC world.
++However, due to the complexity of the chunk system we essentially need to wrap around it.
++This stores slime chunks, and when unloaded, will properly convert it to a normal slime chunk for storage.
++ */
++public class SlimeInMemoryWorld implements SlimeWorld, SlimeWorldInstance {
++
++ private final SlimeLevelInstance instance;
++ private final SlimeWorld liveWorld;
++
++ private final CompoundTag extra;
++ private final SlimePropertyMap propertyMap;
++ private final SlimeLoader loader;
++
++ private final Map chunkStorage = new HashMap<>();
++ private boolean readOnly;
++ // private final Map> entityStorage = new HashMap<>();
++
++ public SlimeInMemoryWorld(SlimeBootstrap bootstrap, SlimeLevelInstance instance) {
++ this.instance = instance;
++ this.extra = bootstrap.initial().getExtraData();
++ this.propertyMap = bootstrap.initial().getPropertyMap();
++ this.loader = bootstrap.initial().getLoader();
++ this.readOnly = bootstrap.initial().isReadOnly();
++
++ for (SlimeChunk initial : bootstrap.initial().getChunkStorage()) {
++ ChunkPos pos = new ChunkPos(initial.getX(), initial.getZ());
++ List tags = new ArrayList<>(initial.getEntities());
++
++ // this.entityStorage.put(pos, tags);
++ this.chunkStorage.put(pos, initial);
++ }
++
++ this.liveWorld = new NMSSlimeWorld(this);
++ }
++
++ @Override
++ public String getName() {
++ return this.instance.getMinecraftWorld().serverLevelData.getLevelName();
++ }
++
++ @Override
++ public SlimeLoader getLoader() {
++ return this.loader;
++ }
++
++ public LevelChunk promote(int x, int z, SlimeChunk chunk) {
++ SlimeChunkLevel levelChunk;
++ if (chunk == null) {
++ net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(x, z);
++ LevelChunkTicks blockLevelChunkTicks = new LevelChunkTicks<>();
++ LevelChunkTicks fluidLevelChunkTicks = new LevelChunkTicks<>();
++
++ levelChunk = new SlimeChunkLevel(this.instance, pos, UpgradeData.EMPTY, blockLevelChunkTicks, fluidLevelChunkTicks,
++ 0L, null, null, null);
++
++ chunk = new NMSSlimeChunk(levelChunk);
++
++ } else {
++ levelChunk = SlimeChunkConverter.deserializeSlimeChunk(this.instance, chunk);
++ chunk = new SafeNmsChunkWrapper(new NMSSlimeChunk(levelChunk), chunk);
++ }
++ this.chunkStorage.put(new ChunkPos(x, z), chunk);
++
++ return levelChunk;
++ }
++
++ // Authored by: Kenox
++ // Don't use the NMS live chunk in the chunk map
++ public void unload(LevelChunk providedChunk) {
++ final int x = providedChunk.locX;
++ final int z = providedChunk.locZ;
++
++ SlimeChunk chunk = new NMSSlimeChunk(providedChunk);
++
++ if (FastChunkPruner.canBePruned(this.liveWorld, providedChunk)) {
++ this.chunkStorage.remove(new ChunkPos(x, z));
++ return;
++ }
++
++ this.chunkStorage.put(new ChunkPos(x, z),
++ new SlimeChunkSkeleton(chunk.getX(), chunk.getZ(), chunk.getSections(),
++ chunk.getHeightMaps(), chunk.getTileEntities(), chunk.getEntities()));
++ }
++
++ @Override
++ public SlimeChunk getChunk(int x, int z) {
++ return this.chunkStorage.get(new ChunkPos(x, z));
++ }
++
++ @Override
++ public Collection getChunkStorage() {
++ return this.chunkStorage.values();
++ }
++
++ @Override
++ public World getBukkitWorld() {
++ return this.instance.getWorld();
++ }
++
++ @Override
++ public SlimeWorld getSlimeWorldMirror() {
++ return this.liveWorld;
++ }
++
++ @Override
++ public SlimePropertyMap getPropertyMap() {
++ return this.propertyMap;
++ }
++
++ @Override
++ public boolean isReadOnly() {
++ return this.getSaveStrategy() == null || this.readOnly;
++ }
++
++ @Override
++ public SlimeWorld clone(String worldName) {
++ try {
++ return clone(worldName, null);
++ } catch (WorldAlreadyExistsException | IOException ignored) {
++ return null; // Never going to happen
++ }
++ }
++
++ @Override
++ public SlimeWorld clone(String worldName, SlimeLoader loader) throws WorldAlreadyExistsException, IOException {
++ if (this.getName().equals(worldName)) {
++ throw new IllegalArgumentException("The clone world cannot have the same name as the original world!");
++ }
++
++ if (worldName == null) {
++ throw new IllegalArgumentException("The world name cannot be null!");
++ }
++ if (loader != null) {
++ if (loader.worldExists(worldName)) {
++ throw new WorldAlreadyExistsException(worldName);
++ }
++ }
++
++ SlimeWorld cloned = SkeletonCloning.fullClone(worldName, this, loader);
++ if (loader != null) {
++ loader.saveWorld(worldName, SlimeSerializer.serialize(cloned));
++ }
++
++ return cloned;
++ }
++
++ @Override
++ public int getDataVersion() {
++ return this.liveWorld.getDataVersion();
++ }
++
++ @Override
++ public SlimeLoader getSaveStrategy() {
++ return this.loader;
++ }
++
++ @Override
++ public CompoundTag getExtraData() {
++ return this.extra;
++ }
++
++ @Override
++ public Collection getWorldMaps() {
++ return List.of();
++ }
++
++ // public Map> getEntityStorage() {
++ // return entityStorage;
++ // }
++
++ public SlimeWorld getForSerialization() {
++ SlimeWorld world = SkeletonCloning.weakCopy(this);
++
++ Map cloned = new HashMap<>();
++ for (Map.Entry entry : this.chunkStorage.entrySet()) {
++ SlimeChunk clonedChunk = entry.getValue();
++ // NMS "live" chunks need to be converted
++ {
++ LevelChunk chunk = null;
++ if (clonedChunk instanceof SafeNmsChunkWrapper safeNmsChunkWrapper) {
++ if (safeNmsChunkWrapper.shouldDefaultBackToSlimeChunk()) {
++ clonedChunk = safeNmsChunkWrapper.getSafety();
++ } else {
++ chunk = safeNmsChunkWrapper.getWrapper().getChunk();
++ }
++ } else if (clonedChunk instanceof NMSSlimeChunk nmsSlimeChunk) {
++ chunk = nmsSlimeChunk.getChunk();
++ }
++
++ if (chunk != null) {
++ if (FastChunkPruner.canBePruned(world, chunk)) {
++ continue;
++ }
++
++ clonedChunk = new SlimeChunkSkeleton(
++ clonedChunk.getX(),
++ clonedChunk.getZ(),
++ clonedChunk.getSections(),
++ clonedChunk.getHeightMaps(),
++ clonedChunk.getTileEntities(),
++ clonedChunk.getEntities()
++ );
++ }
++ }
++
++ cloned.put(entry.getKey(), clonedChunk);
++ }
++
++ return new SkeletonSlimeWorld(world.getName(),
++ world.getLoader(),
++ world.isReadOnly(),
++ cloned,
++ world.getExtraData(),
++ world.getPropertyMap(),
++ world.getDataVersion()
++ );
++ }
++
++ public SlimeLevelInstance getInstance() {
++ return instance;
++ }
++}
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeLevelGenerator.java b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelGenerator.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4f48b7a1a41aabc78cc9276fbf9f372cb117003f
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelGenerator.java
+@@ -0,0 +1,39 @@
++package com.infernalsuite.aswm.level;
++
++import com.mojang.serialization.Codec;
++import net.minecraft.core.Holder;
++import net.minecraft.world.level.biome.Biome;
++import net.minecraft.world.level.biome.BiomeSource;
++import net.minecraft.world.level.biome.Climate;
++import net.minecraft.world.level.levelgen.FlatLevelSource;
++import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
++
++import java.util.List;
++import java.util.Optional;
++import java.util.stream.Stream;
++
++public class SlimeLevelGenerator extends FlatLevelSource {
++
++ public SlimeLevelGenerator(Holder biome) {
++ super(new FlatLevelGeneratorSettings(Optional.empty(), biome, List.of()), getSource(biome));
++ }
++
++ private static BiomeSource getSource(Holder biome) {
++ return new BiomeSource() {
++ @Override
++ protected Codec extends BiomeSource> codec() {
++ return null;
++ }
++
++ @Override
++ protected Stream> collectPossibleBiomes() {
++ return Stream.of(biome);
++ }
++
++ @Override
++ public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler noise) {
++ return biome;
++ }
++ };
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/level/SlimeLevelInstance.java b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelInstance.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2d18d76829b6dc590913d974d50dbaafcb79e175
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/level/SlimeLevelInstance.java
+@@ -0,0 +1,195 @@
++package com.infernalsuite.aswm.level;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import com.google.common.util.concurrent.ThreadFactoryBuilder;
++import com.infernalsuite.aswm.Converter;
++import com.infernalsuite.aswm.serialization.slime.SlimeSerializer;
++import com.infernalsuite.aswm.api.world.SlimeChunk;
++import com.infernalsuite.aswm.api.world.SlimeWorld;
++import com.infernalsuite.aswm.api.world.SlimeWorldInstance;
++import com.infernalsuite.aswm.api.world.properties.SlimeProperties;
++import com.infernalsuite.aswm.api.world.properties.SlimePropertyMap;
++import io.papermc.paper.chunk.system.scheduling.ChunkLoadTask;
++import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler;
++import io.papermc.paper.chunk.system.scheduling.GenericDataLoadTask;
++import net.minecraft.core.BlockPos;
++import net.minecraft.core.Holder;
++import net.minecraft.core.registries.Registries;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.util.ProgressListener;
++import net.minecraft.util.Unit;
++import net.minecraft.util.datafix.DataFixers;
++import net.minecraft.world.Difficulty;
++import net.minecraft.world.entity.EntityType;
++import net.minecraft.world.level.biome.Biome;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.ChunkGenerator;
++import net.minecraft.world.level.dimension.LevelStem;
++import net.minecraft.world.level.storage.LevelStorageSource;
++import net.minecraft.world.level.storage.PrimaryLevelData;
++import net.minecraft.world.level.validation.DirectoryValidator;
++import net.minecraft.world.level.validation.PathAllowList;
++import org.apache.commons.io.FileUtils;
++import org.bukkit.Bukkit;
++import org.bukkit.event.world.WorldSaveEvent;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.IOException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.Collections;
++import java.util.UUID;
++import java.util.concurrent.ExecutorService;
++import java.util.concurrent.Executors;
++import java.util.concurrent.Future;
++import java.util.function.Consumer;
++import java.util.logging.Level;
++import java.util.stream.Collectors;
++
++public class SlimeLevelInstance extends ServerLevel {
++
++
++ public static LevelStorageSource CUSTOM_LEVEL_STORAGE;
++
++ static {
++ try {
++ Path path = Files.createTempDirectory("swm-" + UUID.randomUUID().toString().substring(0, 5)).toAbsolutePath();
++ DirectoryValidator directoryvalidator = LevelStorageSource.parseValidator(path.resolve("allowed_symlinks.txt"));
++ CUSTOM_LEVEL_STORAGE = new LevelStorageSource(path, path, directoryvalidator, DataFixers.getDataFixer());
++
++ FileUtils.forceDeleteOnExit(path.toFile());
++
++ } catch (IOException ex) {
++ throw new IllegalStateException("Couldn't create dummy file directory.", ex);
++ }
++ }
++
++ private static final ExecutorService WORLD_SAVER_SERVICE = Executors.newFixedThreadPool(4, new ThreadFactoryBuilder()
++ .setNameFormat("SWM Pool Thread #%1$d").build());
++ private static final TicketType SWM_TICKET = TicketType.create("swm-chunk", (a, b) -> 0);
++
++ private final Object saveLock = new Object();
++
++ private boolean ready = false;
++
++ public SlimeLevelInstance(SlimeBootstrap slimeBootstrap, PrimaryLevelData primaryLevelData,
++ ResourceKey worldKey,
++ ResourceKey dimensionKey, LevelStem worldDimension,
++ org.bukkit.World.Environment environment) throws IOException {
++
++ super(slimeBootstrap, MinecraftServer.getServer(), MinecraftServer.getServer().executor,
++ CUSTOM_LEVEL_STORAGE.createAccess(slimeBootstrap.initial().getName() + UUID.randomUUID(), dimensionKey),
++ primaryLevelData, worldKey, worldDimension,
++ MinecraftServer.getServer().progressListenerFactory.create(11), false, 0,
++ Collections.emptyList(), true, environment, null, null);
++ this.slimeInstance = new SlimeInMemoryWorld(slimeBootstrap, this);
++
++
++ SlimePropertyMap propertyMap = slimeBootstrap.initial().getPropertyMap();
++
++ this.serverLevelData.setDifficulty(Difficulty.valueOf(propertyMap.getValue(SlimeProperties.DIFFICULTY).toUpperCase()));
++ this.serverLevelData.setSpawn(new BlockPos(propertyMap.getValue(SlimeProperties.SPAWN_X), propertyMap.getValue(SlimeProperties.SPAWN_Y), propertyMap.getValue(SlimeProperties.SPAWN_Z)), 0);
++ super.setSpawnSettings(propertyMap.getValue(SlimeProperties.ALLOW_MONSTERS), propertyMap.getValue(SlimeProperties.ALLOW_ANIMALS));
++
++ this.pvpMode = propertyMap.getValue(SlimeProperties.PVP);
++
++ this.keepSpawnInMemory = false;
++ }
++
++ @Override
++ public ChunkGenerator getGenerator(SlimeBootstrap slimeBootstrap) {
++ String biomeStr = slimeBootstrap.initial().getPropertyMap().getValue(SlimeProperties.DEFAULT_BIOME);
++ ResourceKey biomeKey = ResourceKey.create(Registries.BIOME, new ResourceLocation(biomeStr));
++ Holder defaultBiome = MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME).getHolder(biomeKey).orElseThrow();
++ return new SlimeLevelGenerator(defaultBiome);
++ }
++
++ @Override
++ public void save(@Nullable ProgressListener progressUpdate, boolean forceSave, boolean savingDisabled, boolean close) {
++ try {
++ if (!this.slimeInstance.isReadOnly() && !savingDisabled) {
++ Bukkit.getPluginManager().callEvent(new WorldSaveEvent(getWorld()));
++
++ //this.getChunkSource().save(forceSave);
++ this.serverLevelData.setWorldBorder(this.getWorldBorder().createSettings());
++ this.serverLevelData.setCustomBossEvents(MinecraftServer.getServer().getCustomBossEvents().save());
++
++ // Update level data
++ net.minecraft.nbt.CompoundTag compound = new net.minecraft.nbt.CompoundTag();
++ net.minecraft.nbt.CompoundTag nbtTagCompound = this.serverLevelData.createTag(MinecraftServer.getServer().registryAccess(), compound);
++
++ if (MinecraftServer.getServer().isStopped()) { // Make sure the world gets saved before stopping the server by running it from the main thread
++ save().get(); // Async wait for it to finish
++ this.slimeInstance.getLoader().unlockWorld(this.slimeInstance.getName()); // Unlock
++ } else {
++ this.save();
++ //WORLD_SAVER_SERVICE.execute(this::save);
++ }
++ }
++ } catch (Throwable e) {
++ e.printStackTrace();
++ }
++ }
++
++ @Override
++ public void saveIncrementally(boolean doFull) {
++ if (doFull) {
++ this.save(null, false, false);
++ }
++ }
++
++ private Future> save() {
++ synchronized (saveLock) { // Don't want to save the SlimeWorld from multiple threads simultaneously
++ SlimeWorldInstance slimeWorld = this.slimeInstance;
++ Bukkit.getLogger().log(Level.INFO, "Saving world " + this.slimeInstance.getName() + "...");
++ long start = System.currentTimeMillis();
++
++ Bukkit.getLogger().log(Level.INFO, "CONVERTING NMS -> SKELETON");
++ SlimeWorld world = this.slimeInstance.getForSerialization();
++ Bukkit.getLogger().log(Level.INFO, "CONVERTED TO SKELETON, PUSHING OFF-THREAD");
++ return WORLD_SAVER_SERVICE.submit(() -> {
++ try {
++ byte[] serializedWorld = SlimeSerializer.serialize(world);
++ long saveStart = System.currentTimeMillis();
++ slimeWorld.getSaveStrategy().saveWorld(slimeWorld.getName(), serializedWorld);
++ Bukkit.getLogger().log(Level.INFO, "World " + slimeWorld.getName() + " serialized in " + (saveStart - start) + "ms and saved in " + (System.currentTimeMillis() - saveStart) + "ms.");
++ } catch (IOException | IllegalStateException ex) {
++ ex.printStackTrace();
++ }
++ });
++
++ }
++ }
++
++ public SlimeWorldInstance getSlimeInstance() {
++ return this.slimeInstance;
++ }
++
++ public ChunkDataLoadTask getLoadTask(ChunkLoadTask task, ChunkTaskScheduler scheduler, ServerLevel world, int chunkX, int chunkZ, PrioritisedExecutor.Priority priority, Consumer> onRun) {
++ return new ChunkDataLoadTask(task, scheduler, world, chunkX, chunkZ, priority, onRun);
++ }
++
++ public void loadEntities(int chunkX, int chunkZ) {
++ SlimeChunk slimeChunk = this.slimeInstance.getChunk(chunkX, chunkZ);
++ if (slimeChunk != null) {
++ this.getEntityLookup().addLegacyChunkEntities(new ArrayList<>(
++ EntityType.loadEntitiesRecursive(slimeChunk.getEntities()
++ .stream()
++ .map((tag) -> (net.minecraft.nbt.CompoundTag) Converter.convertTag(tag))
++ .collect(Collectors.toList()), this)
++ .toList()
++ ));
++ }
++ }
++
++ // @Override
++ // public void unload(LevelChunk chunk) {
++ // this.slimeInstance.unload(chunk);
++ // super.unload(chunk);
++ // }
++}
+\ No newline at end of file
+diff --git a/src/main/java/com/infernalsuite/aswm/util/NmsUtil.java b/src/main/java/com/infernalsuite/aswm/util/NmsUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3500005bb09dc484bc333f1e0799613d097a37d3
+--- /dev/null
++++ b/src/main/java/com/infernalsuite/aswm/util/NmsUtil.java
+@@ -0,0 +1,9 @@
++package com.infernalsuite.aswm.util;
++
++public class NmsUtil {
++
++ public static long asLong(int chunkX, int chunkZ) {
++ return (((long) chunkZ) * Integer.MAX_VALUE + ((long) chunkX));
++ //return (long)chunkX & 4294967295L | ((long)chunkZ & 4294967295L) << 32;
++ }
++}
+\ No newline at end of file
+diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
+index abd0217cf0bff183c8e262edc173a53403797c1a..ab450a0ffbfd914c33323d740b3c382a96cb0e8f 100644
+--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java
+@@ -165,7 +165,8 @@ public final class ChunkHolderManager {
+ return this.chunkHolders.size();
+ }
+
+- public void close(final boolean save, final boolean halt) {
++ public void close(boolean save, final boolean halt) { // ASWM
++ if (this.world instanceof com.infernalsuite.aswm.level.SlimeLevelInstance) save = false; // ASWM
+ TickThread.ensureTickThread("Closing world off-main");
+ if (halt) {
+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'");
+diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java
+index e7fb084ddb88ab62f1d493a999cc82b9258d275e..7e5b9c4033e81d5c348ff9eb110b8c9cd1b3aeeb 100644
+--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java
++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java
+@@ -6,6 +6,7 @@ import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+ import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+ import ca.spottedleaf.dataconverter.minecraft.MCDataConverter;
+ import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry;
++import com.infernalsuite.aswm.level.CommonLoadTask; // ASWM
+ import com.mojang.logging.LogUtils;
+ import io.papermc.paper.chunk.system.io.RegionFileIOThread;
+ import io.papermc.paper.chunk.system.poi.PoiChunk;
+@@ -32,8 +33,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+- private final NewChunkHolder chunkHolder;
+- private final ChunkDataLoadTask loadTask;
++ public final NewChunkHolder chunkHolder; // ASWM - public -> private
++ private final CommonLoadTask loadTask; // ASWM
+
+ private volatile boolean cancelled;
+ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask;
+@@ -45,11 +46,20 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
+ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+- this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
+- this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> {
+- ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement
+- ChunkLoadTask.this.tryCompleteLoad();
+- });
++ // ASWM start
++ if (world instanceof com.infernalsuite.aswm.level.SlimeLevelInstance levelInstance) {
++
++ this.loadTask = levelInstance.getLoadTask(this, scheduler, world, chunkX, chunkZ, priority, result -> {
++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
++ });
++ } else {
++ ChunkDataLoadTask task = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
++ task.addCallback((final GenericDataLoadTask.TaskResult result) -> {
++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right());
++ });
++ this.loadTask = task;
++ }
++ // ASWM end
+ }
+
+ private void tryCompleteLoad() {
+@@ -274,7 +284,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
+ }
+ }
+
+- public static final class ChunkDataLoadTask extends CallbackDataLoadTask {
++ public static final class ChunkDataLoadTask extends CallbackDataLoadTask implements CommonLoadTask { // ASWM
+ protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority);
+diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
+index 51304c5cf4b0ac7646693ef97ef4a3847d3342b5..56280c18be01deabc53932c1580dce7b3d83e06f 100644
+--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java
+@@ -112,7 +112,10 @@ public final class NewChunkHolder {
+ }
+
+ if (!transientChunk) {
+- if (entityChunk != null) {
++ // ASWM start
++ if (this.world instanceof com.infernalsuite.aswm.level.SlimeLevelInstance world) {
++ world.loadEntities(this.chunkX, this.chunkZ);
++ } else if (entityChunk != null) { // ASWM end
+ final List entities = EntityStorage.readEntities(this.world, entityChunk);
+
+ this.world.getEntityLookup().addEntityChunkEntities(entities);
+diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
+index 7e8dc9e8f381abfdcce2746edc93122d623622d1..148db7995f8bb8bef8f1eeeadc338dffab891d57 100644
+--- a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
++++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
+@@ -36,7 +36,7 @@ public final class ChunkEntitySlices {
+ protected final EntityCollectionBySection allEntities;
+ protected final EntityCollectionBySection hardCollidingEntities;
+ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass;
+- protected final EntityList entities = new EntityList();
++ public final EntityList entities = new EntityList(); // ASWM - protected -> public
+
+ public FullChunkStatus status;
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 435f5ee3388f5da93df938c43ea2578f7d586407..9d636e893885a411cde5fb72f9a79da0d2f7cbf9 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -272,7 +272,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public
+ private final CustomBossEvents customBossEvents;
+ private final ServerFunctionManager functionManager;
+ private final FrameTimer frameTimer;
+@@ -462,18 +462,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
+ // Paper end
+
+- public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) {
++ // ASWM start
++ public final com.infernalsuite.aswm.level.SlimeBootstrap bootstrap;
++ public ServerChunkCache(com.infernalsuite.aswm.level.SlimeBootstrap bootstrap, ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { // ASWM
++ this.bootstrap = bootstrap;
++ // ASWM end
+ this.level = world;
+ this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world);
+ this.mainThread = Thread.currentThread();
+diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
+index 18aac3da3c88f33b1a71a5920a8daa27e9723913..20099425babf5325005d11bf25faa783e3bc2715 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -643,6 +643,14 @@ public class ServerLevel extends Level implements WorldGenLevel {
+
+ // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
+ public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
++ // ASWM start
++ this(null, minecraftserver, executor, convertable_conversionsession, iworlddataserver, resourcekey, worlddimension, worldloadlistener, flag, i, list, flag1, env, gen, biomeProvider);
++ }
++
++ public com.infernalsuite.aswm.level.SlimeInMemoryWorld slimeInstance;
++
++ public ServerLevel(com.infernalsuite.aswm.level.SlimeBootstrap bootstrap, MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
++ // ASWM end
+ // IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error
+ // Holder holder = worlddimension.type(); // CraftBukkit - decompile error
+
+@@ -681,6 +689,10 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ chunkgenerator = new org.bukkit.craftbukkit.generator.CustomChunkGenerator(this, chunkgenerator, gen);
+ }
+ // CraftBukkit end
++ // ASWM start
++ ChunkGenerator result = this.getGenerator(bootstrap);
++ if (result != null) chunkgenerator = result;
++ // ASWM end
+ boolean flag2 = minecraftserver.forceSynchronousWrites();
+ DataFixer datafixer = minecraftserver.getFixerUpper();
+ this.entityStorage = new EntityRegionFileStorage(convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system //EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver);
+@@ -692,7 +704,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system
+
+ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system
+- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system
++ this.chunkSource = new ServerChunkCache(bootstrap, this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // ASWM // Paper - rewrite chunk system
+ return minecraftserver.overworld().getDataStorage();
+ });
+ this.chunkSource.getGeneratorState().ensureStructuresGenerated();
+@@ -721,7 +733,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
+
+ this.sleepStatus = new SleepStatus();
+ this.gameEventDispatcher = new GameEventDispatcher(this);
+- this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(randomsequences, () -> {
++ this.randomSequences = (RandomSequences) Objects.requireNonNullElseGet(getRandomSequences(), () -> { // ASWM
+ return (RandomSequences) this.getDataStorage().computeIfAbsent((nbttagcompound) -> {
+ return RandomSequences.load(l, nbttagcompound);
+ }, () -> {
+@@ -748,6 +760,12 @@ public class ServerLevel extends Level implements WorldGenLevel {
+ this.dragonFight = enderDragonFight;
+ }
+
++ // ASWM start
++ public ChunkGenerator getGenerator(com.infernalsuite.aswm.level.SlimeBootstrap bootstrap) {
++ return null;
++ }
++ // ASWM end
++
+ public void setWeatherParameters(int clearDuration, int rainDuration, boolean raining, boolean thundering) {
+ this.serverLevelData.setClearWeatherTime(clearDuration);
+ this.serverLevelData.setRainTime(rainDuration);
+diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+index 7f5547dc31aa53b2863f4c09f598fa88e7fe2afd..7b28346e8256013d071b265d9e382738bb4b33c8 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+@@ -30,7 +30,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer
+ };
+ public final IdMap registry;
+ private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values
+- private volatile PalettedContainer.Data data;
++ public volatile PalettedContainer.Data data; // ASWM - private -> public
+ private final PalettedContainer.Strategy strategy;
+ // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
+
+@@ -398,7 +398,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer
+ void accept(T object, int count);
+ }
+
+- static record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) {
++ public static record Data(PalettedContainer.Configuration configuration, BitStorage storage, Palette palette) { // ASWM - private -> public
+ public void copyFrom(Palette palette, BitStorage storage) {
+ for(int i = 0; i < storage.getSize(); ++i) {
+ T object = palette.valueFor(storage.get(i));
+diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+index 9c6a2884c34a9f6e775103da42480cd6b8c693b3..8f70e99dc48bd8b0e6ebe7a955b8e3e1a826aabf 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
+@@ -434,7 +434,7 @@ public class ChunkSerializer {
+ ChunkSerializer.LOGGER.error("Recoverable errors when loading section [" + chunkPos.x + ", " + y + ", " + chunkPos.z + "]: " + message);
+ }
+
+- private static Codec>> makeBiomeCodec(Registry biomeRegistry) {
++ public static Codec>> makeBiomeCodec(Registry biomeRegistry) { // ASWM - private -> public
+ return PalettedContainer.codecRO(biomeRegistry.asHolderIdMap(), biomeRegistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, biomeRegistry.getHolderOrThrow(Biomes.PLAINS));
+ }
+
+diff --git a/src/main/resources/META-INF/services/com.infernalsuite.aswm.api.SlimeNMSBridge b/src/main/resources/META-INF/services/com.infernalsuite.aswm.api.SlimeNMSBridge
+new file mode 100644
+index 0000000000000000000000000000000000000000..916b4d2edba2f2a8a0fc1fdb6ab6a57e2a16f938
+--- /dev/null
++++ b/src/main/resources/META-INF/services/com.infernalsuite.aswm.api.SlimeNMSBridge
+@@ -0,0 +1 @@
++com.infernalsuite.aswm.SlimeNMSBridgeImpl
diff --git a/patches/server/0001-Build-changes.patch b/patches/server/0002-Build-changes.patch
similarity index 92%
rename from patches/server/0001-Build-changes.patch
rename to patches/server/0002-Build-changes.patch
index 14b15fc..60faacb 100644
--- a/patches/server/0001-Build-changes.patch
+++ b/patches/server/0002-Build-changes.patch
@@ -5,13 +5,13 @@ Subject: [PATCH] Build changes
diff --git a/build.gradle.kts b/build.gradle.kts
-index fb98936bb8a5488db75d676c5bcb4060597fbbf8..904bf48288e0865db9bbe8d2ca183b8385f73407 100644
+index 2143180a92ec6d0c0eba5559dd5497291348fdfa..91c9a0239b8a4d821dd83ea32d971a0a63e5929a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
-@@ -13,8 +13,12 @@ configurations.named(log4jPlugins.compileClasspathConfigurationName) {
- val alsoShade: Configuration by configurations.creating
+@@ -14,8 +14,12 @@ val alsoShade: Configuration by configurations.creating
dependencies {
+ implementation(project(":aswm-core"))
- implementation(project(":paper-api"))
- implementation(project(":paper-mojangapi"))
+ // Scissors start
@@ -23,7 +23,7 @@ index fb98936bb8a5488db75d676c5bcb4060597fbbf8..904bf48288e0865db9bbe8d2ca183b83
// Paper start
implementation("org.jline:jline-terminal-jansi:3.21.0")
implementation("net.minecrell:terminalconsoleappender:1.3.0")
-@@ -68,11 +72,19 @@ tasks.jar {
+@@ -69,11 +73,19 @@ tasks.jar {
val gitHash = git("rev-parse", "--short=7", "HEAD").getText().trim()
val implementationVersion = System.getenv("BUILD_NUMBER") ?: "\"$gitHash\""
val date = git("show", "-s", "--format=%ci", gitHash).getText().trim() // Paper
@@ -45,7 +45,7 @@ index fb98936bb8a5488db75d676c5bcb4060597fbbf8..904bf48288e0865db9bbe8d2ca183b83
"Implementation-Vendor" to date, // Paper
"Specification-Title" to "Bukkit",
"Specification-Version" to project.version,
-@@ -149,7 +161,7 @@ fun TaskContainer.registerRunTask(
+@@ -150,7 +162,7 @@ fun TaskContainer.registerRunTask(
name: String,
block: JavaExec.() -> Unit
): TaskProvider = register(name) {
@@ -95,10 +95,10 @@ index c5d5648f4ca603ef2b1df723b58f9caf4dd3c722..21ded7c14c56a40feaa7741131be5166
.completer(new ConsoleCommandCompleter(this.server))
.option(LineReader.Option.COMPLETE_IN_WORD, true);
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 3238cbcba567b1242c77e41f6b6f19a8d157fb4e..dd4dc6b29e37a4db459779fa8d4adc231e44d144 100644
+index 9d636e893885a411cde5fb72f9a79da0d2f7cbf9..c69729fb08819860f366fbe578213a8327b82569 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -1693,7 +1693,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {
diff --git a/patches/server/0042-Fix-component-extra-empty-array-exploit.patch b/patches/server/0043-Fix-component-extra-empty-array-exploit.patch
similarity index 100%
rename from patches/server/0042-Fix-component-extra-empty-array-exploit.patch
rename to patches/server/0043-Fix-component-extra-empty-array-exploit.patch
diff --git a/patches/server/0043-Add-depth-limit-to-Component-deserializer.patch b/patches/server/0044-Add-depth-limit-to-Component-deserializer.patch
similarity index 100%
rename from patches/server/0043-Add-depth-limit-to-Component-deserializer.patch
rename to patches/server/0044-Add-depth-limit-to-Component-deserializer.patch
diff --git a/patches/server/0044-Implement-command-block-events.patch b/patches/server/0045-Implement-command-block-events.patch
similarity index 100%
rename from patches/server/0044-Implement-command-block-events.patch
rename to patches/server/0045-Implement-command-block-events.patch
diff --git a/patches/server/0045-Add-depth-limit-to-SNBT.patch b/patches/server/0046-Add-depth-limit-to-SNBT.patch
similarity index 100%
rename from patches/server/0045-Add-depth-limit-to-SNBT.patch
rename to patches/server/0046-Add-depth-limit-to-SNBT.patch
diff --git a/patches/server/0046-Limit-beacon-effectRange.patch b/patches/server/0047-Limit-beacon-effectRange.patch
similarity index 100%
rename from patches/server/0046-Limit-beacon-effectRange.patch
rename to patches/server/0047-Limit-beacon-effectRange.patch
diff --git a/patches/server/0047-Improve-validation-of-ResourceLocations.patch b/patches/server/0048-Improve-validation-of-ResourceLocations.patch
similarity index 100%
rename from patches/server/0047-Improve-validation-of-ResourceLocations.patch
rename to patches/server/0048-Improve-validation-of-ResourceLocations.patch
diff --git a/patches/server/0048-Don-t-log-on-too-many-chained-updates.patch b/patches/server/0049-Don-t-log-on-too-many-chained-updates.patch
similarity index 100%
rename from patches/server/0048-Don-t-log-on-too-many-chained-updates.patch
rename to patches/server/0049-Don-t-log-on-too-many-chained-updates.patch
diff --git a/patches/server/0049-Fix-packet-related-lag-exploits.patch b/patches/server/0050-Fix-packet-related-lag-exploits.patch
similarity index 100%
rename from patches/server/0049-Fix-packet-related-lag-exploits.patch
rename to patches/server/0050-Fix-packet-related-lag-exploits.patch
diff --git a/patches/server/0050-Limit-save-data-for-Bees-and-Vexes.patch b/patches/server/0051-Limit-save-data-for-Bees-and-Vexes.patch
similarity index 100%
rename from patches/server/0050-Limit-save-data-for-Bees-and-Vexes.patch
rename to patches/server/0051-Limit-save-data-for-Bees-and-Vexes.patch
diff --git a/patches/server/0051-Mute-invalid-attributes.patch b/patches/server/0052-Mute-invalid-attributes.patch
similarity index 100%
rename from patches/server/0051-Mute-invalid-attributes.patch
rename to patches/server/0052-Mute-invalid-attributes.patch
diff --git a/patches/server/0052-Mute-invalid-Enderdragon-phases.patch b/patches/server/0053-Mute-invalid-Enderdragon-phases.patch
similarity index 100%
rename from patches/server/0052-Mute-invalid-Enderdragon-phases.patch
rename to patches/server/0053-Mute-invalid-Enderdragon-phases.patch
diff --git a/patches/server/0053-Don-t-return-null-Components-in-the-Component-codec.patch b/patches/server/0054-Don-t-return-null-Components-in-the-Component-codec.patch
similarity index 100%
rename from patches/server/0053-Don-t-return-null-Components-in-the-Component-codec.patch
rename to patches/server/0054-Don-t-return-null-Components-in-the-Component-codec.patch
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6d38822..14941b4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -7,4 +7,4 @@ pluginManagement {
rootProject.name = "Scissors"
-include("Scissors-API", "Scissors-Server")
+include("aswm-api", "aswm-core", "Scissors-API", "Scissors-Server")
diff --git a/submodules/AdvancedSlimePaper b/submodules/AdvancedSlimePaper
new file mode 160000
index 0000000..3934c52
--- /dev/null
+++ b/submodules/AdvancedSlimePaper
@@ -0,0 +1 @@
+Subproject commit 3934c520664e7e8f61f3a21463017ab2efac64b6