From 6ac6f297afe95b885ccb70f1e75afb07ed2282f0 Mon Sep 17 00:00:00 2001 From: EoD <293499+EoD@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:58:37 +0000 Subject: [PATCH] encode unknown items using Paper This allows using PlayerSync with different minecraft versions and even different sets of mods. All unknown items are replaced by Paper with its original NBT data encoded into the paper item. --- .../fubuki/playersync/config/JdbcConfig.java | 9 ++ .../fubuki/playersync/sync/VanillaSync.java | 153 +++++++++++++++--- .../assets/playersync/lang/en_us.json | 4 +- 3 files changed, 146 insertions(+), 20 deletions(-) diff --git a/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java b/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java index c584882..4abd602 100644 --- a/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java +++ b/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java @@ -20,6 +20,8 @@ public class JdbcConfig { public static ForgeConfigSpec.BooleanValue USE_SSL; public static ForgeConfigSpec.BooleanValue SYNC_CHAT; public static ForgeConfigSpec.BooleanValue IS_CHAT_SERVER; + public static final ForgeConfigSpec.ConfigValue ITEM_PLACEHOLDER_TITLE_OVERRIDE; + public static final ForgeConfigSpec.ConfigValue ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE; public static ForgeConfigSpec.ConfigValue CHAT_SERVER_IP; public static ForgeConfigSpec.IntValue CHAT_SERVER_PORT; public static ForgeConfigSpec.BooleanValue USE_LEGACY_SERIALIZATION; @@ -50,6 +52,13 @@ public class JdbcConfig { "This only affects writing data, the mod can read both Base64 and pre-Base64 serialization.", "New installations should leave this as 'false'." ).define("use_legacy_serialization", false); + ITEM_PLACEHOLDER_TITLE_OVERRIDE = COMMON_BUILDER + .comment("Override the title of placeholder items which are unavailable on the current server.") + .define("item_placeholder_title_override", ""); + ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE = COMMON_BUILDER + .comment("Override the description of placeholder items which are unavailable on the current server.") + .define("item_placeholder_description_override", ""); + COMMON_BUILDER.pop(); COMMON_CONFIG = COMMON_BUILDER.build(); } diff --git a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java index a3ea524..86b7552 100644 --- a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java +++ b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java @@ -1,9 +1,16 @@ package vip.fubuki.playersync.sync; import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PlayerAdvancements; import net.minecraft.server.level.ServerPlayer; @@ -13,6 +20,7 @@ import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.level.storage.WorldData; import net.minecraftforge.event.OnDatapackSyncEvent; import net.minecraftforge.event.TickEvent; @@ -21,6 +29,7 @@ import net.minecraftforge.event.server.ServerStoppedEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.server.ServerLifecycleHooks; import vip.fubuki.playersync.PlayerSync; import vip.fubuki.playersync.config.JdbcConfig; @@ -38,6 +47,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -182,36 +192,33 @@ public class VanillaSync { // Restore left-hand item String leftHandEncoded = rs2.getString("left_hand"); - String leftHandNBT = deserializeString(leftHandEncoded); serverPlayer.setItemInHand(InteractionHand.OFF_HAND, - ItemStack.of(NbtUtils.snbtToStructure(leftHandNBT))); + deserializeAndCreatePlaceholderIfNeeded(leftHandEncoded)); // Restore cursor item String cursorsEncoded = rs2.getString("cursors"); - String cursorsNBT = deserializeString(cursorsEncoded); serverPlayer.containerMenu.setCarried( - ItemStack.of(NbtUtils.snbtToStructure(cursorsNBT)) - ); + deserializeAndCreatePlaceholderIfNeeded(cursorsEncoded)); // Restore armor String armor_data = rs2.getString("armor"); if (armor_data.length() > 2) { Map equipment = LocalJsonUtil.StringToEntryMap(armor_data); for (Map.Entry entry : equipment.entrySet()) { - serverPlayer.getInventory().armor.set(entry.getKey(), deserialize(entry)); + serverPlayer.getInventory().armor.set(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); } } // Restore inventory Map inventory = LocalJsonUtil.StringToEntryMap(rs2.getString("inventory")); for (Map.Entry entry : inventory.entrySet()) { - serverPlayer.getInventory().setItem(entry.getKey(), deserialize(entry)); + serverPlayer.getInventory().setItem(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); } // Restore Ender Chest Map ender_chest = LocalJsonUtil.StringToEntryMap(rs2.getString("enderchest")); for (Map.Entry entry : ender_chest.entrySet()) { - serverPlayer.getEnderChestInventory().setItem(entry.getKey(), deserialize(entry)); + serverPlayer.getEnderChestInventory().setItem(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); } // Restore Effects @@ -248,10 +255,106 @@ public class VanillaSync { }); } - public static ItemStack deserialize(Map.Entry entry) throws CommandSyntaxException { - String nbt = deserializeString(entry.getValue()); - CompoundTag compoundTag = NbtUtils.snbtToStructure(nbt); - return ItemStack.of(compoundTag); + // deserialize item and potentially create placeholders + private static ItemStack deserializeAndCreatePlaceholderIfNeeded(String serializedNbt) + throws CommandSyntaxException { + if (serializedNbt == null || serializedNbt.isEmpty() || serializedNbt.equals("B64:e30=")) { + // Check for empty NBT (Base64 encoded '{}') + return ItemStack.EMPTY; + } + + String nbtString = deserializeString(serializedNbt); + CompoundTag compoundTag = NbtUtils.snbtToStructure(nbtString); + + if (compoundTag == null || compoundTag.isEmpty() || !compoundTag.contains("id", Tag.TAG_STRING)) { + return ItemStack.EMPTY; // Invalid or empty tag + } + + ResourceLocation registryName = ResourceLocation.tryParse(compoundTag.getString("id")); + + if (registryName == null) { + PlayerSync.LOGGER.warn("Failed to parse registry name from NBT: {}", nbtString); + return ItemStack.EMPTY; // Cannot determine item type + } + + if (ForgeRegistries.ITEMS.containsKey(registryName)) { + // Item exists (could be vanilla or a loaded mod item), restore normally + try { + ItemStack restoredItem = ItemStack.of(compoundTag); + // Only return the restored item if the ItemStack.of did not unexpectedly + // returned an empty item + // Either the item is not empty, or it is empty and the original tag was also + // empty or it was an empty inventory slot + if (!restoredItem.isEmpty() || compoundTag.isEmpty() + || registryName.equals(ResourceLocation.parse("air"))) { + return restoredItem; + } + // ItemStack.of unexpectedly returned empty for a known, non-air item. + PlayerSync.LOGGER.warn( + "ItemStack.of returned EMPTY for known item {} with NBT: {}. Creating placeholder as fallback.", + registryName, nbtString); + } catch (Exception e) { + PlayerSync.LOGGER.error( + "Error creating ItemStack for known item {} with NBT: {}. Creating placeholder as fallback.", + registryName, nbtString, e); + } + } + + // Create placeholder + PlayerSync.LOGGER.debug("Item {} not found in registry. Creating placeholder.", registryName); + ItemStack placeholder = new ItemStack(Items.PAPER); + + CompoundTag placeholderNbt = placeholder.getOrCreateTag(); + // Store the original serialized NBT string, not the parsed CompoundTag string + placeholderNbt.putString("playersync:original_item_nbt", serializedNbt); + placeholderNbt.putString("playersync:original_item_id", registryName.toString()); + + // Add a unique UUID to ensure the item is unstackable + // Stacked placerholders would be converted into a single item when restoring item + placeholderNbt.putUUID("playersync:unique_id", UUID.randomUUID()); + + // Add display name and lore + CompoundTag displayTag = placeholderNbt.getCompound("display"); + if (!placeholderNbt.contains("display")) + placeholderNbt.put("display", displayTag); + + String placeholderItemTitleOverride = JdbcConfig.ITEM_PLACEHOLDER_TITLE_OVERRIDE.get(); + displayTag.putString("Name", Component.Serializer.toJson( + Component + .literal(placeholderItemTitleOverride != null && !placeholderItemTitleOverride.isBlank() + ? placeholderItemTitleOverride + : Component.translatable("playersync.item_placeholder_title").getString()) + .setStyle(Style.EMPTY.withColor(ChatFormatting.RED).withItalic(true)))); + + ListTag loreList = new ListTag(); + String placeholderItemDetails = registryName.toString(); + + // add a stack size if it is available + PlayerSync.LOGGER.warn("Item {}: {}", registryName, compoundTag); + int placeholderItemAmount = compoundTag.getInt("Count"); + if (placeholderItemAmount > 1) { + placeholderItemDetails = placeholderItemAmount + "x " + placeholderItemDetails; + } + + loreList.add(StringTag.valueOf(Component.Serializer.toJson( + Component.literal(placeholderItemDetails) + .setStyle(Style.EMPTY.withColor(ChatFormatting.GRAY).withItalic(false))))); + // add newline + loreList.add(StringTag.valueOf(Component.Serializer.toJson(Component.literal("")))); + + String placeholderItemDescriptionOverride = JdbcConfig.ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE.get(); + String placeholderItemDescriptionLines = placeholderItemDescriptionOverride != null && ! placeholderItemDescriptionOverride.isBlank() + ? placeholderItemDescriptionOverride + : Component.translatable("playersync.item_placeholder_description").getString(); + + for (String descriptionLine : placeholderItemDescriptionLines.split("\n")) { + loreList.add(StringTag.valueOf(Component.Serializer.toJson( + Component.literal(descriptionLine) + .setStyle(Style.EMPTY.withColor(ChatFormatting.DARK_GRAY))))); + } + displayTag.put("Lore", loreList); + + return placeholder; } /** @@ -342,6 +445,18 @@ public class VanillaSync { }); } + // Helper function to get the NBT string to be saved + // If item is a placeholder, get original NBT; otherwise, get current NBT + private static String getNbtForStorage(ItemStack itemStack) { + if (itemStack.is(Items.PAPER) && itemStack.hasTag() && itemStack.getTag().contains("playersync:original_item_nbt", Tag.TAG_STRING)) { + // It's our placeholder, retrieve the original NBT string + return itemStack.getTag().getString("playersync:original_item_nbt"); + } else { + // It's a normal item or empty, serialize its current NBT + return serialize(itemStack.serializeNBT().toString()); + } + } + public static void store(Player player, boolean init) throws SQLException, IOException { String player_uuid = player.getUUID().toString(); PlayerSync.LOGGER.info("Storing data for player " + player_uuid + " (init=" + init + ")"); @@ -352,27 +467,27 @@ public class VanillaSync { int food_level = player.getFoodData().getFoodLevel(); int health = (int) player.getHealth(); // Left Hand - String left_hand = serialize(player.getItemInHand(InteractionHand.OFF_HAND).serializeNBT().toString()); + String left_hand = getNbtForStorage(player.getItemInHand(InteractionHand.OFF_HAND)); + // Cursor - String cursors = serialize(player.containerMenu.getCarried().serializeNBT().toString()); + String cursors = getNbtForStorage(player.containerMenu.getCarried()); + // Equipment (Armor) Map equipment = new HashMap<>(); for (int i = 0; i < player.getInventory().armor.size(); i++) { ItemStack itemStack = player.getInventory().armor.get(i); - equipment.put(i, serialize(itemStack.serializeNBT().toString())); + equipment.put(i, getNbtForStorage(itemStack)); } // Inventory Inventory inventory = player.getInventory(); Map inventoryMap = new HashMap<>(); for (int i = 0; i < inventory.items.size(); i++) { - CompoundTag itemNBT = inventory.items.get(i).serializeNBT(); - inventoryMap.put(i, serialize(itemNBT.toString())); + inventoryMap.put(i, getNbtForStorage(inventory.items.get(i))); } // Ender Chest Map ender_chest = new HashMap<>(); for (int i = 0; i < player.getEnderChestInventory().getContainerSize(); i++) { - CompoundTag itemNBT = player.getEnderChestInventory().getItem(i).serializeNBT(); - ender_chest.put(i, serialize(itemNBT.toString())); + ender_chest.put(i, getNbtForStorage(player.getEnderChestInventory().getItem(i))); } if(ModList.get().isLoaded("sophisticatedbackpacks")){ diff --git a/src/main/resources/assets/playersync/lang/en_us.json b/src/main/resources/assets/playersync/lang/en_us.json index a320d1a..fceecfd 100644 --- a/src/main/resources/assets/playersync/lang/en_us.json +++ b/src/main/resources/assets/playersync/lang/en_us.json @@ -1,3 +1,5 @@ { + "playersync.item_placeholder_description": "Item is unknown on this server. This can either\nbe a modded item, an added, or a removed vanilla\nitem.\nThis voucher will automatically be replaced with\nthe corresponding item when joining a server\nwhere the item is known.","playersync.placeholder_titel_override": "Item Voucher", + "playersync.item_placeholder_title": "Item Voucher", "playersync.already_online": "You can't join more than one synchronization server at the same time." -} \ No newline at end of file +}