diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 658cffb..9a6e251 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,8 +3,8 @@ cloud-build-logic = "0.0.3"
ktlint = "0.50.0"
checkstyle = "10.12.5"
kotlin = "1.9.20"
-shadow = "8.1.1"
-paperweight = "1.7.1"
+shadow = "9.0.0-beta4"
+paperweight = "1.7.7"
run-paper = "2.3.0"
pluginyml = "0.6.0"
@@ -54,6 +54,6 @@ paperweight-userdev = { id = "io.papermc.paperweight.userdev", version.ref = "pa
cloud-buildLogic-spotless = { id = "org.incendo.cloud-build-logic.spotless", version.ref = "cloud-build-logic" }
cloud-buildLogic-rootProject-publishing = { id = "org.incendo.cloud-build-logic.publishing.root-project", version.ref = "cloud-build-logic" }
cloud-buildLogic-rootProject-spotless = { id = "org.incendo.cloud-build-logic.spotless.root-project", version.ref = "cloud-build-logic" }
-shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
+shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
run-paper = { id = "xyz.jpenilla.run-paper", version.ref = "run-paper" }
pluginyml = { id = "net.minecrell.plugin-yml.bukkit", version.ref = "pluginyml" }
diff --git a/hyperverse-core/build.gradle.kts b/hyperverse-core/build.gradle.kts
index 10cbea7..271db6d 100644
--- a/hyperverse-core/build.gradle.kts
+++ b/hyperverse-core/build.gradle.kts
@@ -44,22 +44,25 @@ dependencies {
implementation("net.kyori:adventure-text-minimessage:4.14.0")
implementation(projects.hyperverseNmsUnsupported)
- runtimeOnly(project(":hyperverse-nms-1-17")) {
+ runtimeOnly(projects.hyperverseNms117) {
targetConfiguration = "reobf"
}
- runtimeOnly(project(":hyperverse-nms-1-18")) {
+ runtimeOnly(projects.hyperverseNms118) {
targetConfiguration = "reobf"
}
- runtimeOnly(project(":hyperverse-nms-1-19")) {
+ runtimeOnly(projects.hyperverseNms119) {
targetConfiguration = "reobf"
}
- runtimeOnly(project(":hyperverse-nms-1-20")) {
+ runtimeOnly(projects.hyperverseNms120) {
targetConfiguration = "reobf"
}
- runtimeOnly(project(":hyperverse-nms-1-20-6")) {
+ runtimeOnly(projects.hyperverseNms1206) {
targetConfiguration = "reobf"
}
- runtimeOnly(project(":hyperverse-nms-1-21")) {
+ runtimeOnly(projects.hyperverseNms121) {
+ targetConfiguration = "reobf"
+ }
+ runtimeOnly(projects.hyperverseNms1213) {
targetConfiguration = "reobf"
}
}
diff --git a/hyperverse-core/src/main/java/org/incendo/hyperverse/Hyperverse.java b/hyperverse-core/src/main/java/org/incendo/hyperverse/Hyperverse.java
index 944714b..dea7604 100644
--- a/hyperverse-core/src/main/java/org/incendo/hyperverse/Hyperverse.java
+++ b/hyperverse-core/src/main/java/org/incendo/hyperverse/Hyperverse.java
@@ -98,7 +98,8 @@ public final class Hyperverse extends JavaPlugin implements HyperverseAPI, Liste
Version.parseMinecraft("1.19.4"),
Version.parseMinecraft("1.20.4"),
Version.parseMinecraft("1.20.6"),
- Version.parseMinecraft("1.21")
+ Version.parseMinecraft("1.21.1"),
+ Version.parseMinecraft("1.21.3")
);
private WorldManager worldManager;
diff --git a/hyperverse-nms-1-21-3/build.gradle.kts b/hyperverse-nms-1-21-3/build.gradle.kts
new file mode 100644
index 0000000..19a0053
--- /dev/null
+++ b/hyperverse-nms-1-21-3/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id("hyperverse.base-conventions")
+ alias(libs.plugins.paperweight.userdev)
+}
+
+indra {
+ javaVersions {
+ minimumToolchain(21)
+ target(21)
+ }
+}
+
+dependencies {
+ paperweight.paperDevBundle("1.21.3-R0.1-SNAPSHOT")
+ compileOnly(projects.hyperverseNmsCommon)
+}
+
+tasks {
+ assemble {
+ dependsOn(reobfJar)
+ }
+}
diff --git a/hyperverse-nms-1-21-3/src/main/java/org/incendo/hyperverse/platform/v1_21_3/NMSImpl.java b/hyperverse-nms-1-21-3/src/main/java/org/incendo/hyperverse/platform/v1_21_3/NMSImpl.java
new file mode 100644
index 0000000..792be08
--- /dev/null
+++ b/hyperverse-nms-1-21-3/src/main/java/org/incendo/hyperverse/platform/v1_21_3/NMSImpl.java
@@ -0,0 +1,255 @@
+//
+// Hyperverse - A minecraft world management plugin
+//
+// 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 .
+//
+
+package org.incendo.hyperverse.platform.v1_21_3;
+
+import co.aikar.taskchain.TaskChainFactory;
+import com.google.inject.Inject;
+import io.papermc.lib.PaperLib;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+
+import net.minecraft.BlockUtil;
+import net.minecraft.core.BlockPos;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.DoubleTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.NbtAccounter;
+import net.minecraft.nbt.NbtIo;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.border.WorldBorder;
+import net.minecraft.world.level.dimension.DimensionType;
+import net.minecraft.world.level.entity.EntityLookup;
+import net.minecraft.world.level.entity.PersistentEntitySectionManager;
+import net.minecraft.world.level.portal.PortalForcer;
+import net.minecraft.world.phys.Vec3;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.filter.RegexFilter;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.entity.CraftEntity;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerRespawnEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.incendo.hyperverse.util.HyperConfigShouldGroupProfiles;
+import org.incendo.hyperverse.util.NMS;
+
+@SuppressWarnings("unused")
+public class NMSImpl implements NMS {
+
+ private final TaskChainFactory taskFactory;
+ private Field entitySectionManager;
+ private Field entityLookup;
+ private org.apache.logging.log4j.core.Logger worldServerLogger;
+
+ @Inject public NMSImpl(final TaskChainFactory taskFactory, final @HyperConfigShouldGroupProfiles boolean hyperConfiguration) {
+ this.taskFactory = taskFactory;
+ if (hyperConfiguration) {
+ try {
+ final Field field = ServerLevel.class.getDeclaredField("LOGGER");
+ field.setAccessible(true);
+ this.worldServerLogger = (Logger) field.get(null);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ try {
+ final RegexFilter regexFilter = RegexFilter
+ .createFilter("[\\S\\s]*Force-added player with duplicate UUID[\\S\\s]*", null, false,
+ Filter.Result.DENY, Filter.Result.ACCEPT);
+ this.worldServerLogger.addFilter(regexFilter);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override @Nullable public Location getOrCreateNetherPortal(@NotNull final org.bukkit.entity.Entity entity,
+ @NotNull final Location origin) {
+ final ServerLevel worldServer = Objects.requireNonNull(((CraftWorld) origin.getWorld()).getHandle());
+ final PortalForcer portalTravelAgent = Objects.requireNonNull(worldServer.getPortalForcer());
+ final Entity nmsEntity = Objects.requireNonNull(((CraftEntity) entity).getHandle());
+ final BlockPos blockPosition = new BlockPos(origin.getBlockX(), origin.getBlockY(), origin.getBlockZ());
+ final WorldBorder worldBorder = worldServer.getWorldBorder();
+ Optional existingPortalPosition = Objects.requireNonNull(portalTravelAgent, "travel agent")
+ .findClosestPortalPosition(Objects.requireNonNull(blockPosition, "position"), worldBorder,128);
+ if (existingPortalPosition.isPresent()) {
+ BlockPos bottomLeft = existingPortalPosition.get();
+ return new Location(origin.getWorld(), bottomLeft.getX(), bottomLeft.getY(), bottomLeft.getZ());
+ }
+ Optional createdPortal = portalTravelAgent.createPortal(blockPosition,
+ nmsEntity.getDirection().getAxis(), nmsEntity,
+ 16);
+ if (createdPortal.isEmpty()) {
+ return null;
+ }
+ final BlockUtil.FoundRectangle rectangle = createdPortal.get();
+ return new Location(origin.getWorld(), rectangle.minCorner.getX() + 1D, rectangle.minCorner.getY() - 1D,
+ rectangle.minCorner.getZ() + 1D);
+ }
+
+ @Override @Nullable public Location getDimensionSpawn(@NotNull final Location origin) {
+ if (Objects.requireNonNull(origin.getWorld()).getEnvironment()
+ == World.Environment.THE_END) {
+ return new Location(origin.getWorld(), 100, 50, 0);
+ }
+ return origin.getWorld().getSpawnLocation();
+ }
+
+ @Override public void writePlayerData(@NotNull final Player player, @NotNull final Path file) {
+ final CompoundTag playerTag = new CompoundTag();
+ final net.minecraft.world.entity.player.Player entityPlayer = ((CraftPlayer) player).getHandle();
+ entityPlayer.save(playerTag);
+
+ if (!playerTag.contains("hyperverse")) {
+ playerTag.put("hyperverse", new CompoundTag());
+ }
+ final CompoundTag hyperverse = playerTag.getCompound("hyperverse");
+ hyperverse.putLong("writeTime", System.currentTimeMillis());
+ hyperverse.putString("version", Bukkit.getPluginManager().getPlugin("Hyperverse").getDescription().getVersion());
+
+ taskFactory.newChain().async(() -> {
+ try (final OutputStream outputStream = Files.newOutputStream(file)) {
+ NbtIo.writeCompressed(playerTag, outputStream);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ }).execute();
+ }
+
+ @Override public void readPlayerData(@NotNull final Player player, @NotNull final Path file, @NotNull final Runnable whenDone) {
+ final Location originLocation = player.getLocation().clone();
+ taskFactory.newChain().asyncFirst(() -> {
+ try (final InputStream inputStream = Files.newInputStream(file)) {
+ return Optional.of(NbtIo.readCompressed(inputStream, NbtAccounter.unlimitedHeap()));
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ return Optional.empty();
+ }).syncLast((optionalCompound) -> {
+ if (!optionalCompound.isPresent()) {
+ return;
+ }
+ final CompoundTag compound = (CompoundTag) optionalCompound.get();
+ PaperLib.getChunkAtAsync(originLocation).thenAccept(chunk -> {
+ // Health and hunger don't update properly, so we
+ // give them a little help
+ final float health = compound.getFloat("Health");
+ final int foodLevel = compound.getInt("foodLevel");
+
+ // Restore bed spawn
+ final String spawnWorld = compound.getString("SpawnWorld");
+ final int spawnX = compound.getInt("SpawnX");
+ final int spawnY = compound.getInt("SpawnY");
+ final int spawnZ = compound.getInt("SpawnZ");
+ final Location spawnLocation = new Location(Bukkit.getWorld(spawnWorld), spawnX,
+ spawnY, spawnZ);
+
+ final ServerPlayer entityPlayer = ((CraftPlayer) player).getHandle();
+
+ // We re-write the extra Bukkit data as to not
+ // mess up the profile
+ ((CraftPlayer) player).setExtraData(compound);
+ // Set the position to the player's current position
+ Vec3 pos = entityPlayer.position();
+ compound.put("Pos", doubleList(pos.x, pos.y, pos.z));
+ // Set the world to the player's current world
+ compound.putString("world", player.getWorld().getName());
+ // Store persistent values
+ ((CraftPlayer) player).storeBukkitValues(compound);
+
+ // We start by doing a total reset
+ entityPlayer.reset();
+ entityPlayer.load(compound);
+
+ entityPlayer.effectsDirty = true;
+ entityPlayer.onUpdateAbilities();
+ player.teleport(originLocation);
+
+ final ServerLevel worldServer = ((CraftWorld) originLocation.getWorld()).getHandle();
+ final DimensionType dimensionManager = worldServer.dimensionType();
+
+ // Prevent annoying message
+ // Spigot-obf = decouple()
+ entityPlayer.unRide();
+ worldServer.removePlayerImmediately(entityPlayer, Entity.RemovalReason.CHANGED_DIMENSION);
+ // worldServer.removePlayer above should remove the player from the
+ // map, but that doesn't always happen. This is a last effort
+ // attempt to prevent the annoying "Force re-added" message
+ // from appearing
+ try {
+ if (this.entitySectionManager == null) {
+ this.entitySectionManager = worldServer.getClass().getDeclaredField("entityManager");
+ this.entitySectionManager.setAccessible(true);
+ }
+ final PersistentEntitySectionManager esm = (PersistentEntitySectionManager) this.entitySectionManager.get(worldServer);
+ if (this.entityLookup == null) {
+ this.entityLookup = esm.getClass().getDeclaredField("visibleEntityStorage");
+ }
+ final EntityLookup lookup = (EntityLookup) this.entityLookup.get(esm);
+ lookup.remove(entityPlayer);
+ } catch (final NoSuchFieldException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+
+ // pre 1.18 code = PlayerList#moveToWorld
+ entityPlayer.server.getPlayerList().remove(entityPlayer);
+ worldServer.getServer().getPlayerList().respawn(entityPlayer, true,
+ Entity.RemovalReason.CHANGED_DIMENSION, PlayerRespawnEvent.RespawnReason.PLUGIN, originLocation);
+
+ // Apply health and foodLevel
+ player.setHealth(health);
+ player.setFoodLevel(foodLevel);
+ player.setPortalCooldown(40);
+ player.setBedSpawnLocation(spawnLocation, true);
+ });
+ }).execute(whenDone);
+ }
+
+ @Override @Nullable public Location findBedRespawn(@NotNull final Location spawnLocation) {
+ final CraftWorld craftWorld = (CraftWorld) spawnLocation.getWorld();
+ if (craftWorld == null) {
+ return null;
+ }
+
+ return ServerPlayer.findRespawnAndUseSpawnBlock(craftWorld.getHandle(), new BlockPos(spawnLocation.getBlockX(),
+ spawnLocation.getBlockY(), spawnLocation.getBlockZ()), 0, true, false)
+ .map(ServerPlayer.RespawnPosAngle::position)
+ .map(pos -> new Location(spawnLocation.getWorld(), pos.x(), pos.y(), pos.z()))
+ .orElse(null);
+ }
+
+ private static ListTag doubleList(final double... values) {
+ final ListTag tagList = new ListTag();
+ for (final double d : values) {
+ tagList.add(DoubleTag.valueOf(d));
+ }
+ return tagList;
+ }
+
+}
diff --git a/hyperverse-nms-1-21/build.gradle.kts b/hyperverse-nms-1-21/build.gradle.kts
index 07cc6e2..80cb781 100644
--- a/hyperverse-nms-1-21/build.gradle.kts
+++ b/hyperverse-nms-1-21/build.gradle.kts
@@ -11,7 +11,7 @@ indra {
}
dependencies {
- paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT")
+ paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT")
compileOnly(projects.hyperverseNmsCommon)
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a90ecc1..e08e5ae 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,3 +19,4 @@ include(":hyperverse-nms-1-19")
include(":hyperverse-nms-1-20")
include(":hyperverse-nms-1-20-6")
include(":hyperverse-nms-1-21")
+include(":hyperverse-nms-1-21-3")