diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f9839dd..658cffb7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ ktlint = "0.50.0" checkstyle = "10.12.5" kotlin = "1.9.20" shadow = "8.1.1" -paperweight = "1.7.0" +paperweight = "1.7.1" run-paper = "2.3.0" pluginyml = "0.6.0" diff --git a/hyperverse-core/build.gradle.kts b/hyperverse-core/build.gradle.kts index 8e637693..10cbea78 100644 --- a/hyperverse-core/build.gradle.kts +++ b/hyperverse-core/build.gradle.kts @@ -59,6 +59,9 @@ dependencies { runtimeOnly(project(":hyperverse-nms-1-20-6")) { targetConfiguration = "reobf" } + runtimeOnly(project(":hyperverse-nms-1-21")) { + targetConfiguration = "reobf" + } } bukkit { @@ -119,6 +122,7 @@ tasks { exclude(project(":hyperverse-nms-1-19")) exclude(project(":hyperverse-nms-1-20")) exclude(project(":hyperverse-nms-1-20-6")) + exclude(project(":hyperverse-nms-1-21")) } mergeServiceFiles() @@ -156,6 +160,6 @@ tasks { runServer { java.toolchain.languageVersion.set(JavaLanguageVersion.of(21)) - minecraftVersion("1.20.6") + minecraftVersion("1.21") } } 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 73c4cb80..944714b7 100644 --- a/hyperverse-core/src/main/java/org/incendo/hyperverse/Hyperverse.java +++ b/hyperverse-core/src/main/java/org/incendo/hyperverse/Hyperverse.java @@ -93,11 +93,12 @@ public final class Hyperverse extends JavaPlugin implements HyperverseAPI, Liste private final ServicePipeline servicePipeline = ServicePipeline.builder().build(); private final List supportedVersions = List.of( - Version.parse("1.17.1"), - Version.parse("1.18.2"), - Version.parse("1.19.4"), - Version.parse("1.20.4"), - Version.parse("1.20.6") + Version.parseMinecraft("1.17.1"), + Version.parseMinecraft("1.18.2"), + Version.parseMinecraft("1.19.4"), + Version.parseMinecraft("1.20.4"), + Version.parseMinecraft("1.20.6"), + Version.parseMinecraft("1.21") ); private WorldManager worldManager; diff --git a/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/Version.java b/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/Version.java index 73d6868f..ba07b792 100644 --- a/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/Version.java +++ b/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/Version.java @@ -19,6 +19,7 @@ import org.jetbrains.annotations.NotNull; +import java.util.StringJoiner; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,8 +32,24 @@ public record Version(@NotNull String original, @NotNull VersionData versionData public static final Pattern SEMVER_PATTERN = Pattern.compile( "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"); + public static @NotNull Version parseMinecraft(@NotNull final String version) throws IllegalArgumentException { + String[] split = version.split("\\."); + if (split.length < 2) { + throw new IllegalArgumentException("Invalid minecraft version: " + version); + } + if (split.length == 2) { + StringJoiner joiner = new StringJoiner("."); + // insert a .0 to make it correctly formatted + joiner.add(split[0]); + joiner.add("0"); + joiner.add(split[1]); + Version formatted = parseSemVer(joiner.toString()); + return new Version(version, formatted.versionData()); + } + return parseSemVer(version); + } - public static @NotNull Version parse(@NotNull final String version) throws IllegalArgumentException { + public static @NotNull Version parseSemVer(@NotNull final String version) throws IllegalArgumentException { Matcher matcher = SEMVER_PATTERN.matcher(version); if (!matcher.find()) { throw new IllegalArgumentException("Invalid version: " + version); diff --git a/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionData.java b/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionData.java index 7bf1b76e..2908b94e 100644 --- a/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionData.java +++ b/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionData.java @@ -19,7 +19,7 @@ import org.jetbrains.annotations.NotNull; -public record VersionData(int major, int minor, int patch, PreReleaseType preReleaseType) +public record VersionData(int major, int minor, Integer patch, PreReleaseType preReleaseType) implements Comparable { public VersionData(final int major, final int minor, final int patch) { diff --git a/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionUtil.java b/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionUtil.java index 5c5cf1ab..075e2b34 100644 --- a/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionUtil.java +++ b/hyperverse-core/src/main/java/org/incendo/hyperverse/util/versioning/VersionUtil.java @@ -34,7 +34,7 @@ private VersionUtil() { } String strippedVersion = minecraftVersion.substring(0, length - stripLength); try { - return Version.parse(strippedVersion); + return Version.parseMinecraft(strippedVersion); } catch (IllegalArgumentException ex) { throw new IllegalArgumentException("Invalid minecraft version: " + minecraftVersion, ex); } diff --git a/hyperverse-nms-1-21/build.gradle.kts b/hyperverse-nms-1-21/build.gradle.kts new file mode 100644 index 00000000..07cc6e2a --- /dev/null +++ b/hyperverse-nms-1-21/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-R0.1-SNAPSHOT") + compileOnly(projects.hyperverseNmsCommon) +} + +tasks { + assemble { + dependsOn(reobfJar) + } +} diff --git a/hyperverse-nms-1-21/src/main/java/org/incendo/hyperverse/platform/v1_21/NMSImpl.java b/hyperverse-nms-1-21/src/main/java/org/incendo/hyperverse/platform/v1_21/NMSImpl.java new file mode 100644 index 00000000..ea5f6e46 --- /dev/null +++ b/hyperverse-nms-1-21/src/main/java/org/incendo/hyperverse/platform/v1_21/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; + +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/settings.gradle.kts b/settings.gradle.kts index e1b5fb09..a90ecc1d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,3 +18,4 @@ include(":hyperverse-nms-1-18") include(":hyperverse-nms-1-19") include(":hyperverse-nms-1-20") include(":hyperverse-nms-1-20-6") +include(":hyperverse-nms-1-21")