diff --git a/ec-core/src/main/java/dev/jpcode/eccore/util/TextUtil.java b/ec-core/src/main/java/dev/jpcode/eccore/util/TextUtil.java index da20cf15..a2c7d59f 100644 --- a/ec-core/src/main/java/dev/jpcode/eccore/util/TextUtil.java +++ b/ec-core/src/main/java/dev/jpcode/eccore/util/TextUtil.java @@ -10,14 +10,11 @@ import com.google.gson.JsonParser; import eu.pb4.placeholders.api.ParserContext; import eu.pb4.placeholders.api.parsers.TagParser; -import org.apache.logging.log4j.Level; import com.mojang.serialization.JsonOps; import net.minecraft.text.*; -import dev.jpcode.eccore.ECCore; - public final class TextUtil { private TextUtil() {} @@ -198,21 +195,7 @@ public static void registerTextParser(StringToTextParser parser) { static { registerTextParser(str -> TextCodecs.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(str)).getOrThrow()); - int javaVersion = Util.getJavaVersion(); - if (javaVersion >= 16) { - ECCore.LOGGER.log(Level.INFO, String.format( - "Detected Java version %d. Enabling Java %d features.", - javaVersion, - 16 - )); - registerTextParser(str -> TagParser.DEFAULT.parseText(str, ParserContext.of())); - } else { - ECCore.LOGGER.log(Level.WARN, String.format( - "Detected Java version %d. Some features require Java %d. Some text formatting features will be disabled.", - javaVersion, - 16 - )); - } + registerTextParser(str -> TagParser.DEFAULT.parseText(str, ParserContext.of())); } public static Text parseText(String textStr) { diff --git a/src/main/java/com/fibermc/essentialcommands/WorldData.java b/src/main/java/com/fibermc/essentialcommands/WorldData.java new file mode 100644 index 00000000..bbe5cf42 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/WorldData.java @@ -0,0 +1,55 @@ +package com.fibermc.essentialcommands; + +import com.fibermc.essentialcommands.codec.Codecs; +import com.fibermc.essentialcommands.types.MinecraftLocation; +import com.fibermc.essentialcommands.types.WarpStorage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtOps; + +public class WorldData { + private @Nullable MinecraftLocation spawnLocation; + private final @NotNull WarpStorage warps; + + WorldData() { + this.spawnLocation = null; + this.warps = new WarpStorage(); + } + + WorldData(@Nullable MinecraftLocation spawnLocation, @NotNull WarpStorage warps) { + this.spawnLocation = spawnLocation; + this.warps = warps; + } + + public WarpStorage warps() { + return this.warps; + } + + public MinecraftLocation getSpawn() { + return this.spawnLocation; + } + + public void setSpawn(@Nullable MinecraftLocation spawn) { + this.spawnLocation = spawn; + } + + public static WorldData fromNbt(NbtCompound nbt) { + return CODEC.parse(NbtOps.INSTANCE, nbt).getOrThrow(); + } + + public NbtCompound toNbt() { + return CODEC.encodeStart(NbtOps.INSTANCE, this).getOrThrow().asCompound().orElseThrow(); + } + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codecs.MINECRAFT_LOCATION.fieldOf("spawn").forGetter(WorldData::getSpawn), + Codecs.WARP_STORAGE.fieldOf("warps").forGetter(WorldData::warps) + ).apply(instance, WorldData::new) + ); +} diff --git a/src/main/java/com/fibermc/essentialcommands/WorldDataManager.java b/src/main/java/com/fibermc/essentialcommands/WorldDataManager.java index 3f943c13..49289c3a 100644 --- a/src/main/java/com/fibermc/essentialcommands/WorldDataManager.java +++ b/src/main/java/com/fibermc/essentialcommands/WorldDataManager.java @@ -21,8 +21,6 @@ import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.NbtSizeTracker; -import net.minecraft.registry.DynamicRegistryManager; -import net.minecraft.registry.RegistryWrapper; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.WorldSavePath; @@ -32,17 +30,12 @@ import net.fabricmc.fabric.api.event.EventFactory; public class WorldDataManager extends PersistentState { - private final WarpStorage warps; - private MinecraftLocation spawnLocation; private Path saveDir; private File worldDataFile; - - private static final String SPAWN_KEY = "spawn"; - private static final String WARPS_KEY = "warps"; + private WorldData data; public WorldDataManager() { - warps = new WarpStorage(); - spawnLocation = null; + this.data = new WorldData(); } public static WorldDataManager createForServer(MinecraftServer server) @@ -68,10 +61,11 @@ public void onServerStart(MinecraftServer server) { // if files was not JUST created, read data from it. var tag = NbtIo.readCompressed(worldDataFile.toPath(), NbtSizeTracker.ofUnlimitedBytes()); // `data` was the main obj key in old mc PersistentState schema - this.fromNbt(tag.getCompound("data").orElse(tag)); + this.data = WorldData.fromNbt(tag.getCompound("data").orElse(tag)); + warpsLoadEvent.invoker().accept(this.data.warps()); } else { this.markDirty(); - this.save(server.getRegistryManager()); + this.save(); } } catch (IOException e) { EssentialCommands.log(Level.ERROR, String.format("An unexpected error occoured while loading the Essential Commands World Data file (Path: '%s')", worldDataFile.getPath())); @@ -83,18 +77,6 @@ private File getDataFile() { return worldDataFile; } - public void fromNbt(NbtCompound tag) { - this.spawnLocation = tag.getCompound(SPAWN_KEY) - .flatMap(spawnTag -> spawnTag.isEmpty() ? Optional.empty() : Optional.of(spawnTag)) - .map(MinecraftLocation::fromNbt) - .orElse(null); - - tag.getCompound(WARPS_KEY) - .ifPresent(warps::loadNbt); - - warpsLoadEvent.invoker().accept(warps); - } - public final Event> warpsLoadEvent = EventFactory.createArrayBacked( Consumer.class, (listeners) -> (warps) -> { @@ -103,9 +85,9 @@ public void fromNbt(NbtCompound tag) { } }); - public void save(RegistryWrapper.WrapperLookup wrapperLookup) { + public void save() { EssentialCommands.log(Level.INFO, "Saving world_data.dat (Spawn/Warps)..."); - NbtCompound data = this.writeNbt(new NbtCompound(), wrapperLookup); + NbtCompound data = this.data.toNbt(); try { NbtIo.writeCompressed(data, this.worldDataFile.toPath()); } catch (IOException e) { @@ -114,65 +96,51 @@ public void save(RegistryWrapper.WrapperLookup wrapperLookup) { EssentialCommands.log(Level.INFO, "world_data.dat saved."); } - public NbtCompound writeNbt(NbtCompound tag, RegistryWrapper.WrapperLookup wrapperLookup) { - // Spawn to NBT - if (spawnLocation != null) { - tag.put(SPAWN_KEY, spawnLocation.asNbt()); - } - - // Warps to NBT - NbtCompound warpsNbt = new NbtCompound(); - warps.writeNbt(warpsNbt); - tag.put(WARPS_KEY, warpsNbt); - - return tag; - } - // Command Actions public void setWarp(String warpName, MinecraftLocation location, boolean requiresPermission) throws CommandSyntaxException { - warps.putCommand(warpName, new WarpLocation( + this.data.warps().putCommand(warpName, new WarpLocation( location, requiresPermission ? warpName : null, warpName )); this.markDirty(); - this.save(DynamicRegistryManager.EMPTY); + this.save(); } public boolean delWarp(String warpName) { - MinecraftLocation prevValue = warps.remove(warpName); + MinecraftLocation prevValue = this.data.warps().remove(warpName); this.markDirty(); - this.save(DynamicRegistryManager.EMPTY); + this.save(); return prevValue != null; } public WarpLocation getWarp(String warpName) { - return warps.get(warpName); + return this.data.warps().get(warpName); } public List getWarpNames() { - return this.warps.keySet().stream().toList(); + return this.data.warps().keySet().stream().toList(); } public Stream getAccessibleWarps(ServerPlayerEntity player) { - var warpsStream = this.warps.values().stream(); + var warpsStream = this.data.warps().values().stream(); return (EssentialCommands.CONFIG.USE_PERMISSIONS_API ? warpsStream.filter(loc -> loc.hasPermission(player)) : warpsStream); } public Set> getWarpEntries() { - return this.warps.entrySet(); + return this.data.warps().entrySet(); } public void setSpawn(MinecraftLocation location) { - spawnLocation = location; + this.data.setSpawn(location); this.markDirty(); - this.save(DynamicRegistryManager.EMPTY); + this.save(); } public Optional getSpawn() { - return Optional.ofNullable(spawnLocation); + return Optional.ofNullable(this.data.getSpawn()); } } diff --git a/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java b/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java new file mode 100644 index 00000000..07c1a988 --- /dev/null +++ b/src/main/java/com/fibermc/essentialcommands/codec/Codecs.java @@ -0,0 +1,95 @@ +package com.fibermc.essentialcommands.codec; + +import java.util.HashMap; +import java.util.Optional; + +import com.fibermc.essentialcommands.WorldData; +import com.fibermc.essentialcommands.types.*; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.world.World; + +public final class Codecs { + private Codecs() {} + + public static final Codec> WORLD_KEY = RegistryKey.createCodec(RegistryKeys.WORLD); + + public static final Codec MINECRAFT_LOCATION = RecordCodecBuilder.create(instance -> + instance.group( + WORLD_KEY.fieldOf("WorldRegistryKey").forGetter(MinecraftLocation::dim), + Codec.DOUBLE.fieldOf("x").forGetter(MinecraftLocation::x), + Codec.DOUBLE.fieldOf("y").forGetter(MinecraftLocation::y), + Codec.DOUBLE.fieldOf("z").forGetter(MinecraftLocation::z), + Codec.FLOAT.optionalFieldOf("headYaw", 0.0f).forGetter(MinecraftLocation::headYaw), + Codec.FLOAT.optionalFieldOf("pitch", 0.0f).forGetter(MinecraftLocation::pitch) + ).apply(instance, MinecraftLocation::new) + ); + + public static final Codec NAMED_MINECRAFT_LOCATION = RecordCodecBuilder.create(instance -> + instance.group( + // Inherit all fields from NamedMinecraftLocation + WORLD_KEY.fieldOf("WorldRegistryKey").forGetter(NamedMinecraftLocation::dim), + Codec.DOUBLE.fieldOf("x").forGetter(MinecraftLocation::x), + Codec.DOUBLE.fieldOf("y").forGetter(MinecraftLocation::y), + Codec.DOUBLE.fieldOf("z").forGetter(MinecraftLocation::z), + Codec.FLOAT.optionalFieldOf("headYaw", 0.0f).forGetter(NamedMinecraftLocation::headYaw), + Codec.FLOAT.optionalFieldOf("pitch", 0.0f).forGetter(NamedMinecraftLocation::pitch), + // loaded from the map + Codec.STRING.optionalFieldOf("name").forGetter(home -> Optional.of(home.getName())) + + ).apply(instance, NamedMinecraftLocation::new) + ); + + public static final Codec WARP_LOCATION = RecordCodecBuilder.create(instance -> + instance.group( + // Inherit all fields from NamedMinecraftLocation + WORLD_KEY.fieldOf("WorldRegistryKey").forGetter(WarpLocation::dim), + Codec.DOUBLE.fieldOf("x").forGetter(MinecraftLocation::x), + Codec.DOUBLE.fieldOf("y").forGetter(MinecraftLocation::y), + Codec.DOUBLE.fieldOf("z").forGetter(MinecraftLocation::z), + Codec.FLOAT.optionalFieldOf("headYaw", 0.0f).forGetter(WarpLocation::headYaw), + Codec.FLOAT.optionalFieldOf("pitch", 0.0f).forGetter(WarpLocation::pitch), + // loaded from the map + Codec.STRING.optionalFieldOf("name").forGetter(warp -> Optional.of(warp.getName())), + + Codec.STRING.optionalFieldOf("permissionString").forGetter(warp -> Optional.ofNullable(warp.getPermissionString())) + + ).apply(instance, WarpLocation::new) + ); + + public static final Codec NAMED_LOCATION_STORAGE = + Codec.unboundedMap(Codec.STRING, MINECRAFT_LOCATION) + .xmap( + // Convert Map to NamedLocationStorage + map -> { + NamedLocationStorage storage = new NamedLocationStorage(); + map.forEach( + (key, value) -> storage.put(key, new NamedMinecraftLocation(value, key)) + ); + return storage; + }, + // Convert NamedLocationStorage to Map for serialization + HashMap::new + ); + + public static final Codec WARP_STORAGE = + Codec.unboundedMap(Codec.STRING, WARP_LOCATION) + .xmap( + // Convert Map to WarpStorage + map -> { + WarpStorage storage = new WarpStorage(); + map.forEach( + (key, value) -> storage.put(key, WarpLocation.setName(value, key)) + ); + return storage; + }, + // Convert WarpStorage to Map for serialization + HashMap::new + ); + + public static final Codec WORLD_DATA = WorldData.CODEC; +} diff --git a/src/main/java/com/fibermc/essentialcommands/commands/HomeOverwriteCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/HomeOverwriteCommand.java index 7808e821..1197b76f 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/HomeOverwriteCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/HomeOverwriteCommand.java @@ -27,7 +27,7 @@ public int run(CommandContext context) throws CommandSyntax playerData.removeHome(homeName); playerData.addHome(homeName, new MinecraftLocation(senderPlayer)); - playerData.save(context.getSource().getServer().getRegistryManager()); + playerData.save(); //inform command sender that the home has been set playerData.sendCommandFeedback("cmd.overwritehome.feedback", homeNameText); diff --git a/src/main/java/com/fibermc/essentialcommands/commands/HomeSetCommand.java b/src/main/java/com/fibermc/essentialcommands/commands/HomeSetCommand.java index f64eb5db..a4e11970 100644 --- a/src/main/java/com/fibermc/essentialcommands/commands/HomeSetCommand.java +++ b/src/main/java/com/fibermc/essentialcommands/commands/HomeSetCommand.java @@ -51,7 +51,7 @@ private static int exec(CommandContext context, String home Text homeNameText = ECText.access(senderPlayer).accent(homeName); playerData.addHome(homeName, new MinecraftLocation(senderPlayer)); - playerData.save(context.getSource().getServer().getRegistryManager()); + playerData.save(); //inform command sender that the home has been set playerData.sendCommandFeedback("cmd.home.set.feedback", homeNameText); } diff --git a/src/main/java/com/fibermc/essentialcommands/mixin/WorldSaveHandlerMixin.java b/src/main/java/com/fibermc/essentialcommands/mixin/WorldSaveHandlerMixin.java index 19e9ef71..421c51ee 100644 --- a/src/main/java/com/fibermc/essentialcommands/mixin/WorldSaveHandlerMixin.java +++ b/src/main/java/com/fibermc/essentialcommands/mixin/WorldSaveHandlerMixin.java @@ -1,7 +1,5 @@ package com.fibermc.essentialcommands.mixin; -import java.util.Objects; - import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -16,7 +14,7 @@ public class WorldSaveHandlerMixin { @Inject(method = "savePlayerData", at = @At("RETURN")) public void onSavePlayerData(PlayerEntity player, CallbackInfo ci) { - ((ServerPlayerEntityAccess) player).ec$getPlayerData().save(Objects.requireNonNull(player.getServer()).getRegistryManager()); + ((ServerPlayerEntityAccess) player).ec$getPlayerData().save(); // System.out.printf("Saved PlayerData for player: %s\n", player.getName().getString()); } diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java index 6de04eb3..689b6986 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerData.java @@ -8,6 +8,7 @@ import com.fibermc.essentialcommands.ECPerms; import com.fibermc.essentialcommands.EssentialCommands; import com.fibermc.essentialcommands.access.ServerPlayerEntityAccess; +import com.fibermc.essentialcommands.codec.Codecs; import com.fibermc.essentialcommands.commands.CommandUtil; import com.fibermc.essentialcommands.commands.InvulnCommand; import com.fibermc.essentialcommands.commands.helpers.IFeedbackReceiver; @@ -27,17 +28,19 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.entity.player.PlayerAbilities; import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.NbtIo; -import net.minecraft.registry.RegistryWrapper; +import net.minecraft.nbt.NbtOps; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.HoverEvent; import net.minecraft.text.MutableText; import net.minecraft.text.Text; +import net.minecraft.text.TextCodecs; import net.minecraft.util.math.Vec3d; import net.minecraft.world.PersistentState; @@ -54,7 +57,7 @@ public class PlayerData extends PersistentState implements IServerPlayerEntityDa // ServerPlayerEntity private ServerPlayerEntity player; private UUID pUuid; - private final File saveFile; + private File saveFile; // Target of tpAsk private final OutgoingTeleportRequests outgoingTeleportRequests = new OutgoingTeleportRequests(); @@ -84,19 +87,8 @@ public class PlayerData extends PersistentState implements IServerPlayerEntityDa private boolean isSleepingFromCommand; public PlayerData(ServerPlayerEntity player, File saveFile) { - this.player = player; - this.lastTickPos = player.getPos(); - this.lastActionTick = player.getServer().getTicks(); - this.pUuid = player.getUuid(); - this.saveFile = saveFile; incomingTeleportRequests = new LinkedHashMap<>(); - homes = new NamedLocationStorage(); - playerActEvent.register((packet) -> { - updateLastActionTick(); - setAfk(false); - }); - // this should never stick around between respawns - Pal.revokeAbility(player, VanillaAbilities.INVULNERABLE, ECAbilitySources.SLEEP_INVULN); + initializeRuntimeState(player, saveFile); } /** @@ -117,6 +109,59 @@ public PlayerData(File saveFile) { homes = new NamedLocationStorage(); } + // ONLY TO BE USED WITH CODECS + private PlayerData() { + this.saveFile = null; // must be set by factory + this.incomingTeleportRequests = new LinkedHashMap<>(); + } + + public static PlayerData createWithData( + NamedLocationStorage homes, + Optional previousLocation, + Optional nickname, + long timeUsedRtpEpochMs, + int tpCooldown + ) { + // This creates a PlayerData with serializable state only + // Runtime state will be initialized by your factory methods + PlayerData pd = new PlayerData(); + pd.homes = homes; + pd.previousLocation = previousLocation.orElse(null); + pd.nickname = nickname.orElse(null); + pd.timeUsedRtp = TimeUtil.epochTimeMsToTicks(timeUsedRtpEpochMs); + pd.tpCooldown = tpCooldown; + return pd; + } + + // Initialize runtime state after deserialization + public void initializeRuntimeState(ServerPlayerEntity player, File saveFile) { + this.player = player; + this.saveFile = saveFile; + this.lastTickPos = player.getPos(); + this.lastActionTick = player.getServer().getTicks(); + this.pUuid = player.getUuid(); + + // Re-register events + playerActEvent.register((packet) -> { + updateLastActionTick(); + setAfk(false); + }); + + // Recalculate derived state + if (this.nickname != null) { + try { + reloadFullNickname(); + } catch (NullPointerException ignore) { + EssentialCommands.LOGGER.warn("Could not refresh player full nickname, as ServerPlayerEntity was null in PlayerData."); + } + } + + // Revoke any abilities that shouldn't persist + Pal.revokeAbility(player, VanillaAbilities.INVULNERABLE, ECAbilitySources.SLEEP_INVULN); + + updatePlayerEntity(player); + } + public OutgoingTeleportRequests getSentTeleportRequests() { return outgoingTeleportRequests; } @@ -338,27 +383,53 @@ private static final class StorageKey { static final String PREVIOUS_LOCATION = "previousLocation"; } - public void fromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup wrapperLookup) { - // `data` was the main obj key in old mc PersistentState schema + public void fromNbt(NbtCompound tag) { + // Handle legacy "data" wrapper if present NbtCompound dataTag = tag.getCompound("data").orElse(tag); - NamedLocationStorage homes = new NamedLocationStorage(); - NbtElement homesTag = dataTag.get(StorageKey.HOMES); - if (homesTag != null) { - homes.loadNbt(homesTag); + var result = CODEC.parse(NbtOps.INSTANCE, dataTag); + + if (result.isSuccess()) { + PlayerData loaded = result.getOrThrow(); + + // Copy serializable state to this instance + this.homes = loaded.homes; + this.previousLocation = loaded.previousLocation; + this.nickname = loaded.nickname; + this.timeUsedRtp = loaded.timeUsedRtp; + this.tpCooldown = loaded.tpCooldown; + + // Recalculate derived state if player is available + if (this.nickname != null && this.player != null) { + try { + reloadFullNickname(); + } catch (NullPointerException ignore) { + EssentialCommands.LOGGER.warn("Could not refresh player full nickname, as ServerPlayerEntity was null in PlayerData."); + } + } + } else { + // Fallback to legacy parsing for backward compatibility + EssentialCommands.LOGGER.warn( + "Failed to parse PlayerData with codec, falling back to legacy parsing: {}", + result.error() + ); + legacyFromNbt(dataTag); } - this.homes = homes; + + if (this.player != null) { + updatePlayerEntity(this.player); + } + } + + // Keep legacy parsing as fallback + private void legacyFromNbt(NbtCompound dataTag) { + this.homes = dataTag.get(StorageKey.HOMES, NamedLocationStorage.CODEC).orElseGet(NamedLocationStorage::new); dataTag.getString(StorageKey.NICKNAME).ifPresent((nick) -> { if ("null".equals(nick)) { return; } this.nickname = TextUtil.parseText(nick); - try { - reloadFullNickname(); - } catch (NullPointerException ignore) { - EssentialCommands.LOGGER.warn("Could not refresh player full nickanme, as ServerPlayerEntity was null in PlayerData."); - } }); dataTag.getLong(StorageKey.TIME_USED_RTP_EPOCH_MS).ifPresent((time) -> { @@ -370,29 +441,18 @@ public void fromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup wrapperLookup this.previousLocation = MinecraftLocation.fromNbt(nbt); }); } - - if (this.player != null) { - updatePlayerEntity(this.player); - } - } - public NbtCompound writeNbt(NbtCompound tag, RegistryWrapper.WrapperLookup wrapperLookup) { - NbtCompound homesNbt = new NbtCompound(); - homes.writeNbt(homesNbt); - tag.put(StorageKey.HOMES, homesNbt); - - if (nickname != null) { - tag.putString(StorageKey.NICKNAME, TextUtil.toJsonString(nickname)); - } - - tag.putLong(StorageKey.TIME_USED_RTP_EPOCH_MS, TimeUtil.tickTimeToEpochMs(timeUsedRtp)); + public NbtCompound toNbt() { + var result = CODEC.encodeStart(NbtOps.INSTANCE, this); - if (CONFIG.PERSIST_BACK_LOCATION && previousLocation != null) { - tag.put(StorageKey.PREVIOUS_LOCATION, previousLocation.asNbt()); + if (result.isSuccess()) { + return result.getOrThrow() + .asCompound() + .orElseThrow(); } - return tag; + throw new RuntimeException("Failed to encode PlayerData with codec: " + result.error()); } public void setPreviousLocation(MinecraftLocation location) { @@ -494,10 +554,10 @@ public int setNickname(Text nickname) { if (nickname == null) { this.nickname = null; resultCode = 1; - EssentialCommands.LOGGER.info(String.format( - "Cleared %s's nickname", + EssentialCommands.LOGGER.info( + "Cleared {}'s nickname", this.player.getGameProfile().getName() - )); + ); } else { // Ensure nickname does not exceed max length if (nickname.getString().length() > CONFIG.NICKNAME_MAX_LENGTH) { @@ -506,18 +566,18 @@ public int setNickname(Text nickname) { // Ensure player has permissions required to set the specified nickname boolean hasRequiredPerms = NicknameTextUtil.checkPerms(nickname, this.player.getCommandSource()); if (!hasRequiredPerms) { - EssentialCommands.LOGGER.info(String.format( - "%s attempted to set nickname to '%s', with insufficient permissions to do so.", + EssentialCommands.LOGGER.info( + "{} attempted to set nickname to '{}', with insufficient permissions to do so.", this.player.getGameProfile().getName(), nickname - )); + ); return -1; } else { - EssentialCommands.LOGGER.info(String.format( - "Set %s's nickname to '%s'.", + EssentialCommands.LOGGER.info( + "Set {}'s nickname to '{}'.", this.player.getGameProfile().getName(), nickname - )); + ); } // Set nickname @@ -532,8 +592,8 @@ public int setNickname(Text nickname) { return resultCode; } - public void save(RegistryWrapper.WrapperLookup wrapperLookup) { - NbtCompound data = this.writeNbt(new NbtCompound(), wrapperLookup); + public void save() { + NbtCompound data = this.toNbt(); try { NbtIo.writeCompressed(data, this.saveFile.toPath()); @@ -583,4 +643,34 @@ public static PlayerData accessFromContextOrThrow(CommandContext CODEC = RecordCodecBuilder.create(instance -> + instance.group( + // Homes storage + NamedLocationStorage.CODEC + .optionalFieldOf("homes", new NamedLocationStorage()) + .forGetter(pd -> pd.homes), + + // Previous location for /back + Codecs.MINECRAFT_LOCATION + .optionalFieldOf("previousLocation") + .forGetter(pd -> Optional.ofNullable(pd.previousLocation)), + + // Nickname + TextCodecs.CODEC + .optionalFieldOf("nickname") + .forGetter(pd -> Optional.ofNullable(pd.nickname)), + + // RTP time (stored as epoch ms for format compatibility) + Codec.LONG + .optionalFieldOf("timeUsedRtpEpochMs", 0L) + .forGetter(pd -> TimeUtil.tickTimeToEpochMs(pd.timeUsedRtp)), + + // TP cooldown + Codec.INT + .optionalFieldOf("tpCooldown", 0) + .forGetter(pd -> pd.tpCooldown) + + ).apply(instance, PlayerData::createWithData) + ); } diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataFactory.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataFactory.java index 0500c71f..3adef8d7 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataFactory.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataFactory.java @@ -14,7 +14,6 @@ import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.NbtSizeTracker; -import net.minecraft.registry.DynamicRegistryManager; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; @@ -35,16 +34,17 @@ private static PlayerData create(ServerPlayerEntity player, File playerDataFile) if (fileExisted && playerDataFile.length() != 0) { try { pData.fromNbt( - NbtIo.readCompressed(playerDataFile.toPath(), NbtSizeTracker.ofUnlimitedBytes()), - DynamicRegistryManager.EMPTY); + NbtIo.readCompressed(playerDataFile.toPath(), NbtSizeTracker.ofUnlimitedBytes()) + ); } catch (IOException e) { - EssentialCommands.log(Level.WARN, + EssentialCommands.log( + Level.WARN, "Failed to load essential_commands player data for {%s}", player.getName().getString()); e.printStackTrace(); } } else { pData.markDirty(); - pData.save(DynamicRegistryManager.EMPTY); + pData.save(); } return pData; @@ -60,7 +60,7 @@ public static PlayerData create(NamedLocationStorage homes, File saveFile) { if (Files.exists(saveFile.toPath()) && saveFile.length() != 0) { try { NbtCompound nbtCompound3 = NbtIo.readCompressed(saveFile.toPath(), NbtSizeTracker.ofUnlimitedBytes()); - pData.fromNbt(nbtCompound3, DynamicRegistryManager.EMPTY); + pData.fromNbt(nbtCompound3); // If a EC data already existed, the homes we just initialized the pData with (from paramater) just got overwritten. // Now, add them back if their keys do not already exist in the set we just loaded from EC save file. pData.homes.putAll(homes); diff --git a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java index 5007dec9..1b0281f7 100644 --- a/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java +++ b/src/main/java/com/fibermc/essentialcommands/playerdata/PlayerDataManager.java @@ -157,7 +157,7 @@ public void tick(MinecraftServer server) { ); changedNicknames.forEach(playerData -> - playerData.save(server.getRegistryManager()) + playerData.save() ); this.changedNicknames.clear(); diff --git a/src/main/java/com/fibermc/essentialcommands/types/MinecraftLocation.java b/src/main/java/com/fibermc/essentialcommands/types/MinecraftLocation.java index 8d6a9b2d..39b3bffe 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/MinecraftLocation.java +++ b/src/main/java/com/fibermc/essentialcommands/types/MinecraftLocation.java @@ -1,9 +1,13 @@ package com.fibermc.essentialcommands.types; +import com.fibermc.essentialcommands.codec.Codecs; import com.fibermc.essentialcommands.playerdata.PlayerProfile; import com.fibermc.essentialcommands.text.TextFormatType; +import com.mojang.serialization.Codec; + import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtOps; import net.minecraft.registry.RegistryKey; import net.minecraft.registry.RegistryKeys; import net.minecraft.server.network.ServerPlayerEntity; @@ -15,6 +19,7 @@ import net.minecraft.world.World; public class MinecraftLocation { + public static final Codec CODEC = Codecs.MINECRAFT_LOCATION; private Vec3d pos; private float pitch; @@ -45,6 +50,13 @@ public MinecraftLocation(RegistryKey dim, Vec3i vec3i, float headYaw, flo this.pitch = pitch; } + public MinecraftLocation(RegistryKey dim, Vec3d pos, float headYaw, float pitch) { + this.dim = dim; + this.pos = pos; + this.headYaw = headYaw; + this.pitch = pitch; + } + public MinecraftLocation(ServerPlayerEntity player) { this.dim = player.getWorld().getRegistryKey(); this.pos = Vec3d.ZERO.add(player.getPos()); @@ -66,39 +78,20 @@ public MinecraftLocation(NbtCompound tag) { this.pitch = tag.getFloat("pitch").orElse(0f); } - public static MinecraftLocation fromNbt(NbtCompound tag) { - var loc = new MinecraftLocation(); - loc.loadNbt(tag); - return loc; - } - - protected void loadNbt(NbtCompound tag) { - this.dim = RegistryKey.of( - RegistryKeys.WORLD, - Identifier.tryParse(tag.getString("WorldRegistryKey").orElseThrow()) - ); - this.pos = new Vec3d( - tag.getDouble("x").orElseThrow(), - tag.getDouble("y").orElseThrow(), - tag.getDouble("z").orElseThrow() - ); - this.headYaw = tag.getFloat("headYaw").orElse(0f); - this.pitch = tag.getFloat("pitch").orElse(0f); - } - public NbtCompound asNbt() { return this.writeNbt(new NbtCompound()); } - public NbtCompound writeNbt(NbtCompound tag) { - tag.putString("WorldRegistryKey", dim().getValue().toString()); - tag.putDouble("x", pos().x); - tag.putDouble("y", pos().y); - tag.putDouble("z", pos().z); - tag.putFloat("headYaw", headYaw()); - tag.putFloat("pitch", pitch()); + public static MinecraftLocation fromNbt(NbtCompound tag) { + return CODEC.parse(NbtOps.INSTANCE, tag) + .getOrThrow(); + } - return tag; + public NbtCompound writeNbt(NbtCompound tag) { + return CODEC.encodeStart(NbtOps.INSTANCE, this) + .getOrThrow() + .asCompound() + .orElseThrow(); } protected MutableText toLiteralTextSimple() { @@ -126,6 +119,18 @@ public float headYaw() { return headYaw; } + public double x() { + return pos.x; + } + + public double y() { + return pos.y; + } + + public double z() { + return pos.z; + } + public RegistryKey dim() { return dim; } diff --git a/src/main/java/com/fibermc/essentialcommands/types/NamedLocationStorage.java b/src/main/java/com/fibermc/essentialcommands/types/NamedLocationStorage.java index 0a3795f0..396d5aca 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/NamedLocationStorage.java +++ b/src/main/java/com/fibermc/essentialcommands/types/NamedLocationStorage.java @@ -2,18 +2,22 @@ import java.util.HashMap; +import com.fibermc.essentialcommands.codec.Codecs; import com.fibermc.essentialcommands.commands.CommandUtil; import com.fibermc.essentialcommands.text.ECText; import com.fibermc.essentialcommands.text.TextFormatType; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.serialization.Codec; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.NbtList; +import net.minecraft.nbt.NbtOps; import net.minecraft.text.Text; public class NamedLocationStorage extends HashMap implements NbtSerializable { + public static final Codec CODEC = Codecs.NAMED_LOCATION_STORAGE; public NamedLocationStorage() {} @@ -22,28 +26,51 @@ public NamedLocationStorage(NbtCompound nbt) { loadNbt(nbt); } - @Override + public static NamedLocationStorage fromNbt(NbtCompound nbt) { + // Try codec first + var result = CODEC.parse(NbtOps.INSTANCE, nbt); + if (result.isSuccess()) { + return result.getOrThrow(); + } + + // Fallback to legacy parsing + NamedLocationStorage storage = new NamedLocationStorage(); + storage.loadNbt(nbt); + return storage; + } + public NbtCompound writeNbt(NbtCompound nbt) { - this.forEach((key, value) -> nbt.put(key, value.asNbt())); - return nbt; + return CODEC.encode(this, NbtOps.INSTANCE, nbt) + .getOrThrow() + .asCompound() + .orElseThrow(); } /** - * @param nbt NbtCompound or NbtList. (Latter is deprecated) + * Legacy NBT loading method - supports both old list format and compound format + * @param nbt NbtCompound or NbtList. (NbtList is deprecated) */ - public void loadNbt(NbtElement nbt) { + private void loadNbt(NbtElement nbt) { if (nbt.getType() == 9) { - // Legacy format + // Legacy format - NbtList NbtList homesNbtList = (NbtList) nbt; for (NbtElement t : homesNbtList) { NbtCompound homeTag = (NbtCompound) t; homeTag.getString("homeName").ifPresent((homeName) -> { - super.put(homeName, NamedMinecraftLocation.fromNbt(homeTag, homeName)); + var location = MinecraftLocation.fromNbt(homeTag); + super.put(homeName, new NamedMinecraftLocation(location, homeName)); }); } } else { + // Legacy compound format NbtCompound nbtCompound = (NbtCompound) nbt; - nbtCompound.getKeys().forEach((key) -> super.put(key, NamedMinecraftLocation.fromNbt(nbtCompound.getCompoundOrEmpty(key), key))); + nbtCompound.getKeys().forEach((key) -> { + var location = NamedMinecraftLocation.fromNbt(nbtCompound.getCompound(key).orElseThrow()); + if (!key.equals(location.getName())) { + throw new RuntimeException("Home key '%s' did not match home name '%s'".formatted(key, location.getName())); + } + super.put(key, location); + }); } } diff --git a/src/main/java/com/fibermc/essentialcommands/types/NamedMinecraftLocation.java b/src/main/java/com/fibermc/essentialcommands/types/NamedMinecraftLocation.java index e1ae372c..2617110b 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/NamedMinecraftLocation.java +++ b/src/main/java/com/fibermc/essentialcommands/types/NamedMinecraftLocation.java @@ -1,11 +1,21 @@ package com.fibermc.essentialcommands.types; +import java.util.Optional; + +import com.fibermc.essentialcommands.codec.Codecs; + +import com.mojang.serialization.Codec; + import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtOps; import net.minecraft.registry.RegistryKey; +import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; public class NamedMinecraftLocation extends MinecraftLocation { - private String name; + public static final Codec CODEC = Codecs.NAMED_MINECRAFT_LOCATION; + + protected String name; protected NamedMinecraftLocation() {} @@ -29,23 +39,34 @@ public NamedMinecraftLocation( double z, float headYaw, float pitch, - String name + Optional name ) { super(dim, x, y, z, headYaw, pitch); - this.name = name; + this.name = name.orElse(null); } - protected void loadNbt(NbtCompound tag, String name) { - super.loadNbt(tag); + public NamedMinecraftLocation( + RegistryKey dim, + Vec3d pos, + float headYaw, + float pitch, + String name + ) { + super(dim, pos.x, pos.y, pos.z, headYaw, pitch); this.name = name; } - public static NamedMinecraftLocation fromNbt(NbtCompound tag, String name) { - var loc = new NamedMinecraftLocation(); - // `data` was the main obj key in old mc PersistentState schema - tag = tag.getCompound("data").orElse(tag); - loc.loadNbt(tag, name); - return loc; + public static NamedMinecraftLocation fromNbt(NbtCompound tag) { + return CODEC.parse(NbtOps.INSTANCE, tag) + .getOrThrow(); + } + + public NbtCompound writeNbt(NbtCompound tag) { + return CODEC.encodeStart(NbtOps.INSTANCE, this) + .getOrThrow() + .asCompound() + .orElseThrow(); + } public String getName() { diff --git a/src/main/java/com/fibermc/essentialcommands/types/WarpLocation.java b/src/main/java/com/fibermc/essentialcommands/types/WarpLocation.java index 971c83be..f3f3163d 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/WarpLocation.java +++ b/src/main/java/com/fibermc/essentialcommands/types/WarpLocation.java @@ -3,12 +3,18 @@ import java.util.Optional; import com.fibermc.essentialcommands.ECPerms; +import com.fibermc.essentialcommands.codec.Codecs; + +import com.mojang.serialization.Codec; import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtOps; +import net.minecraft.registry.RegistryKey; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.world.World; public class WarpLocation extends NamedMinecraftLocation { - + public static final Codec CODEC = Codecs.WARP_LOCATION; private String permissionString; private WarpLocation() {} @@ -26,16 +32,33 @@ public WarpLocation(MinecraftLocation location, String permissionString, String this.permissionString = permissionString; } - public static WarpLocation fromNbt(NbtCompound tag, String name) { - // `data` was the main obj key in old mc PersistentState schema - tag = tag.getCompound("data").orElse(tag); + public WarpLocation( + RegistryKey dim, + double x, + double y, + double z, + float headYaw, + float pitch, + Optional name, + Optional permissionString + ) { + super(dim, x, y, z, headYaw, pitch, name); + this.permissionString = permissionString.orElse(null); + } - var loc = new WarpLocation(); - loc.loadNbt(tag, name); - loc.permissionString = tag.getString("permissionString") - .flatMap(str -> str.isBlank() ? Optional.empty() : Optional.of(str)) - .orElse(null); - return loc; + public static WarpLocation fromNbt(NbtCompound tag) { + var result = CODEC.parse(NbtOps.INSTANCE, tag); + + if (result.isSuccess()) { + return result.getOrThrow(); + } + + throw new RuntimeException("Failed to parse WarpLocation from NBT: " + result.error()); + } + + public static WarpLocation setName(WarpLocation value, String key) { + value.name = key; + return value; } @Override @@ -45,11 +68,10 @@ public NbtCompound asNbt() { @Override public NbtCompound writeNbt(NbtCompound tag) { - super.writeNbt(tag); - if (permissionString != null) { - tag.putString("permissionString", permissionString); - } - return tag; + return CODEC.encodeStart(NbtOps.INSTANCE, this) + .getOrThrow() + .asCompound() + .orElseThrow(); } public String getPermissionString() { diff --git a/src/main/java/com/fibermc/essentialcommands/types/WarpStorage.java b/src/main/java/com/fibermc/essentialcommands/types/WarpStorage.java index 3804f0e4..11dcba94 100644 --- a/src/main/java/com/fibermc/essentialcommands/types/WarpStorage.java +++ b/src/main/java/com/fibermc/essentialcommands/types/WarpStorage.java @@ -1,19 +1,24 @@ package com.fibermc.essentialcommands.types; import java.util.HashMap; +import java.util.Optional; +import com.fibermc.essentialcommands.codec.Codecs; import com.fibermc.essentialcommands.commands.CommandUtil; import com.fibermc.essentialcommands.text.ECText; import com.fibermc.essentialcommands.text.TextFormatType; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.serialization.Codec; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.NbtList; +import net.minecraft.nbt.NbtOps; import net.minecraft.text.Text; public class WarpStorage extends HashMap implements NbtSerializable { + public static Codec CODEC = Codecs.WARP_STORAGE; public WarpStorage() {} @@ -22,27 +27,57 @@ public WarpStorage(NbtCompound nbt) { loadNbt(nbt); } - @Override + public static WarpStorage fromNbt(NbtCompound nbt) { + // Try codec first + var result = CODEC.parse(NbtOps.INSTANCE, nbt); + if (result.isSuccess()) { + return result.getOrThrow(); + } + + // Fallback to legacy parsing + WarpStorage storage = new WarpStorage(); + storage.loadNbt(nbt); + return storage; + } + public NbtCompound writeNbt(NbtCompound nbt) { - this.forEach((key, value) -> nbt.put(key, value.asNbt())); - return nbt; + return CODEC.encode(this, NbtOps.INSTANCE, nbt) + .getOrThrow() + .asCompound() + .orElseThrow(); } /** * @param nbt NbtCompound or NbtList. (Latter is deprecated) */ - public void loadNbt(NbtElement nbt) { + private void loadNbt(NbtElement nbt) { if (nbt.getType() == 9) { // Legacy format NbtList homesNbtList = (NbtList) nbt; for (NbtElement t : homesNbtList) { NbtCompound homeTag = (NbtCompound) t; String name = homeTag.getString("homeName").orElseThrow(); - super.put(name, WarpLocation.fromNbt(homeTag, name)); + var location = MinecraftLocation.fromNbt(homeTag); + super.put( + name, + new WarpLocation( + location, + homeTag.getString("permissionString") + .flatMap(str -> str.isBlank() ? Optional.empty() : Optional.of(str)) + .orElse(null), + name + ) + ); } } else { NbtCompound nbtCompound = (NbtCompound) nbt; - nbtCompound.getKeys().forEach((key) -> super.put(key, WarpLocation.fromNbt(nbtCompound.getCompoundOrEmpty(key), key))); + nbtCompound.getKeys().forEach((key) -> { + var location = WarpLocation.fromNbt(nbtCompound.getCompound(key).orElseThrow()); + if (!key.equals(location.getName())) { + throw new RuntimeException("Warp key '%s' did not match home name '%s'".formatted(key, location.getName())); + } + super.put(key, location); + }); } } diff --git a/src/main/java/com/fibermc/essentialcommands/util/EssentialsConvertor.java b/src/main/java/com/fibermc/essentialcommands/util/EssentialsConvertor.java index ffa0e2a5..182f9020 100644 --- a/src/main/java/com/fibermc/essentialcommands/util/EssentialsConvertor.java +++ b/src/main/java/com/fibermc/essentialcommands/util/EssentialsConvertor.java @@ -101,7 +101,7 @@ public static void homeConvert(MinecraftServer server) { String homeName = entry.getKey(); playerData.addHome(homeName, new MinecraftLocation(world.getRegistryKey(), x, y, z, yaw, pitch)); - playerData.save(player.getServer().getRegistryManager()); + playerData.save(); oldUserDataFile.renameTo(new File(oldUsersDataDictionary, oldUserDataFile.getName() + ".converted")); counter++; diff --git a/src/main/java/com/fibermc/essentialcommands/util/EssentialsXParser.java b/src/main/java/com/fibermc/essentialcommands/util/EssentialsXParser.java index a07244ef..80ce51f3 100644 --- a/src/main/java/com/fibermc/essentialcommands/util/EssentialsXParser.java +++ b/src/main/java/com/fibermc/essentialcommands/util/EssentialsXParser.java @@ -16,7 +16,6 @@ import org.apache.logging.log4j.Level; import org.yaml.snakeyaml.Yaml; -import net.minecraft.registry.DynamicRegistryManager; import net.minecraft.registry.RegistryKey; import net.minecraft.server.MinecraftServer; import net.minecraft.world.World; @@ -99,7 +98,7 @@ public static NamedLocationStorage parsePlayerHomes( (Double) locData.get("z"), ((Double) locData.get("yaw")).floatValue(), ((Double) locData.get("pitch")).floatValue(), - name + Optional.of(name) )); }); @@ -183,7 +182,7 @@ public static void convertPlayerDataDir(File sourceDir, File targetDir, Minecraf LOGGER.info("Creating temporary playerdata for '{}', with {} homes.", file, homes.size()); PlayerData playerData = PlayerDataFactory.create(homes, targetFile); - playerData.save(DynamicRegistryManager.EMPTY); + playerData.save(); filesSucceeded++; } catch (Exception ex) { LOGGER.error("An unexpected error occurred while parsing player data file '{}'", targetFile.getPath(), ex);