Merge pull request #62 from EoD/version-compat

encode unknown items using Paper
This commit is contained in:
mlus 2025-05-05 02:03:03 +08:00 committed by GitHub
commit 606b9e1c1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 147 additions and 21 deletions

View File

@ -34,7 +34,7 @@ mod_name=PlayerSync
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=GPL-3.0 license
# The mod version. See https://semver.org/
mod_version=2.0.0
mod_version=2.1.0
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -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<String> ITEM_PLACEHOLDER_TITLE_OVERRIDE;
public static final ForgeConfigSpec.ConfigValue<String> ITEM_PLACEHOLDER_DESCRIPTION_OVERRIDE;
public static ForgeConfigSpec.ConfigValue<String> 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();
}

View File

@ -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<Integer, String> equipment = LocalJsonUtil.StringToEntryMap(armor_data);
for (Map.Entry<Integer, String> entry : equipment.entrySet()) {
serverPlayer.getInventory().armor.set(entry.getKey(), deserialize(entry));
serverPlayer.getInventory().armor.set(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue()));
}
}
// Restore inventory
Map<Integer, String> inventory = LocalJsonUtil.StringToEntryMap(rs2.getString("inventory"));
for (Map.Entry<Integer, String> entry : inventory.entrySet()) {
serverPlayer.getInventory().setItem(entry.getKey(), deserialize(entry));
serverPlayer.getInventory().setItem(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue()));
}
// Restore Ender Chest
Map<Integer, String> ender_chest = LocalJsonUtil.StringToEntryMap(rs2.getString("enderchest"));
for (Map.Entry<Integer, String> 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<Integer, String> 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<Integer, String> 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<Integer, String> 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<Integer, String> 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")){

View File

@ -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."
}
}