diff --git a/build.gradle b/build.gradle index 5991097..404409f 100644 --- a/build.gradle +++ b/build.gradle @@ -128,6 +128,8 @@ dependencies { implementation fg.deobf("curse.maven:curios-309927:5266541") implementation fg.deobf("curse.maven:MySQL-561280:3685108") + implementation fg.deobf("curse.maven:sophisticated-backpacks-422301:6303388") + implementation fg.deobf("curse.maven:sophisticated-core-618298:6317048") } // Example for how to get properties into the manifest for reading at runtime. diff --git a/src/main/java/vip/fubuki/playersync/PlayerSync.java b/src/main/java/vip/fubuki/playersync/PlayerSync.java index 4bcb923..0b35648 100644 --- a/src/main/java/vip/fubuki/playersync/PlayerSync.java +++ b/src/main/java/vip/fubuki/playersync/PlayerSync.java @@ -17,16 +17,17 @@ import vip.fubuki.playersync.sync.ChatSync; import vip.fubuki.playersync.sync.VanillaSync; import vip.fubuki.playersync.util.JDBCsetUp; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; @Mod(PlayerSync.MODID) -public class PlayerSync -{ +public class PlayerSync { public static final String MODID = "playersync"; public static final Logger LOGGER = LogUtils.getLogger(); - public PlayerSync() - { + + public PlayerSync() { IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, JdbcConfig.COMMON_CONFIG); modEventBus.addListener(this::commonSetup); @@ -35,71 +36,106 @@ public class PlayerSync private void commonSetup(final FMLCommonSetupEvent event) { VanillaSync.register(); - if(JdbcConfig.SYNC_CHAT.get()){ + if (JdbcConfig.SYNC_CHAT.get()) { ChatSync.register(); } } @SubscribeEvent public void onServerStarting(ServerStartingEvent event) throws SQLException { - JDBCsetUp.executeUpdate("CREATE DATABASE IF NOT EXISTS "+JdbcConfig.DATABASE_NAME.get(),1); + String dbName = JdbcConfig.DATABASE_NAME.get(); - JDBCsetUp.executeUpdate(""" - CREATE TABLE IF NOT EXISTS `player_data` ( - `uuid` char(36) NOT NULL, - `inventory` mediumblob, - `armor` blob, - `advancements` blob, - `enderchest` mediumblob, - `effects` blob, - `left_hand` blob, - `cursors` blob, - `xp` int DEFAULT NULL, - `food_level` int DEFAULT NULL, - `score` int DEFAULT NULL, - `health` int DEFAULT NULL, - `online` tinyint(1) DEFAULT NULL, - `last_server` int DEFAULT NULL, - PRIMARY KEY (`uuid`) - );"""); + // Step 1: Create the database using a connection that does not select a database. + JDBCsetUp.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName, 1); - JDBCsetUp.QueryResult queryResult = JDBCsetUp.executeQuery(""" - SELECT COUNT(*) AS column_count - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_NAME = 'player_data'; - """); + // Step 2: Explicitly select the database on a connection obtained without default database. + try (Connection conn = JDBCsetUp.getConnection(false); + Statement st = conn.createStatement()) { + st.execute("USE " + dbName); + } catch (SQLException e) { + LOGGER.error("Error selecting database " + dbName, e); + throw e; + } + // Step 3: Create and alter tables using fully qualified names. + // Create player_data table + JDBCsetUp.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + dbName + ".`player_data` (" + + "`uuid` char(36) NOT NULL," + + "`inventory` mediumblob," + + "`armor` blob," + + "`advancements` blob," + + "`enderchest` mediumblob," + + "`effects` blob," + + "`left_hand` blob," + + "`cursors` blob," + + "`xp` int DEFAULT NULL," + + "`food_level` int DEFAULT NULL," + + "`score` int DEFAULT NULL," + + "`health` int DEFAULT NULL," + + "`online` tinyint(1) DEFAULT NULL," + + "`last_server` int DEFAULT NULL," + + "PRIMARY KEY (`uuid`)" + + ");" + ); + + // Check and alter player_data table if columns are missing + JDBCsetUp.QueryResult queryResult = JDBCsetUp.executeQuery( + "SELECT COUNT(*) AS column_count " + + "FROM INFORMATION_SCHEMA.COLUMNS " + + "WHERE TABLE_SCHEMA = '" + dbName + "' " + + "AND TABLE_NAME = 'player_data';" + ); ResultSet resultSet = queryResult.resultSet(); int columnCount = 0; - if(resultSet.next()) { + if (resultSet.next()) { columnCount = resultSet.getInt("column_count"); } - - if(columnCount<14){ - JDBCsetUp.executeUpdate(""" - ALTER TABLE player_data - ADD COLUMN left_hand blob, - ADD COLUMN cursors blob; - """); + if (columnCount < 14) { + JDBCsetUp.executeUpdate( + "ALTER TABLE " + dbName + ".player_data " + + "ADD COLUMN left_hand blob, " + + "ADD COLUMN cursors blob;" + ); } - JDBCsetUp.executeUpdate(""" - CREATE TABLE IF NOT EXISTS server_info ( - `id` INT NOT NULL, - `enable` boolean NOT NULL, - `last_update` BIGINT NOT NULL, - PRIMARY KEY (`id`));"""); + // Create server_info table + JDBCsetUp.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + dbName + ".server_info (" + + "`id` INT NOT NULL," + + "`enable` boolean NOT NULL," + + "`last_update` BIGINT NOT NULL," + + "PRIMARY KEY (`id`)" + + ");" + ); long current = System.currentTimeMillis(); - JDBCsetUp.executeUpdate("INSERT INTO server_info(id,enable,last_update) " + - "VALUES(" + JdbcConfig.SERVER_ID.get() + ",true," + current + ") " + - "ON DUPLICATE KEY UPDATE id= " + JdbcConfig.SERVER_ID.get() +",enable = 1," + - "last_update=" + current + ";"); - JDBCsetUp.executeUpdate("UPDATE server_info SET enable= 1 WHERE id= "+ JdbcConfig.SERVER_ID.get()); + JDBCsetUp.executeUpdate( + "INSERT INTO " + dbName + ".server_info(id,enable,last_update) " + + "VALUES(" + JdbcConfig.SERVER_ID.get() + ",true," + current + ") " + + "ON DUPLICATE KEY UPDATE id= " + JdbcConfig.SERVER_ID.get() + ",enable = 1," + + "last_update=" + current + ";" + ); + JDBCsetUp.executeUpdate( + "UPDATE " + dbName + ".server_info SET last_update=" + System.currentTimeMillis() + + " WHERE id='" + JdbcConfig.SERVER_ID.get() + "'" + ); - if(ModList.get().isLoaded("curios")) { - JDBCsetUp.executeUpdate("CREATE TABLE IF NOT EXISTS curios (uuid CHAR(36) NOT NULL,curios_item BLOB, PRIMARY KEY (uuid))"); + // Create curios table if the Curios mod is loaded + if (ModList.get().isLoaded("curios")) { + JDBCsetUp.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + dbName + ".curios (" + + "uuid CHAR(36) NOT NULL, curios_item BLOB, PRIMARY KEY (uuid)" + + ")" + ); } + + // Create backpack_data table + JDBCsetUp.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + dbName + ".backpack_data (" + + "uuid CHAR(36) NOT NULL, backpack_nbt MEDIUMBLOB, PRIMARY KEY (uuid)" + + ");", 1 + ); + LOGGER.info("PlayerSync is ready!"); } - } diff --git a/src/main/java/vip/fubuki/playersync/sync/ChatSync.java b/src/main/java/vip/fubuki/playersync/sync/ChatSync.java index 52d8b20..4f14b63 100644 --- a/src/main/java/vip/fubuki/playersync/sync/ChatSync.java +++ b/src/main/java/vip/fubuki/playersync/sync/ChatSync.java @@ -103,7 +103,7 @@ public class ChatSync { } private static void reconnectClient() { - //TODO + //TODO } @SubscribeEvent diff --git a/src/main/java/vip/fubuki/playersync/sync/ModsSupport.java b/src/main/java/vip/fubuki/playersync/sync/ModsSupport.java index 35c9b3d..6dd6d70 100644 --- a/src/main/java/vip/fubuki/playersync/sync/ModsSupport.java +++ b/src/main/java/vip/fubuki/playersync/sync/ModsSupport.java @@ -48,10 +48,10 @@ public class ModsSupport { throw new RuntimeException(e); } } - }); + }); - resultSet.close(); - queryResult.connection().close(); + resultSet.close(); + queryResult.connection().close(); }else{ StoreCurios(player,true); } @@ -60,7 +60,7 @@ public class ModsSupport { public void onPlayerLeave(Player player) throws SQLException { if (ModList.get().isLoaded("curios")) { - StoreCurios(player, false); + StoreCurios(player, false); } } diff --git a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java index 0bbd07a..1381ee3 100644 --- a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java +++ b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java @@ -1,6 +1,20 @@ package vip.fubuki.playersync.sync; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtUtils; import net.minecraft.network.chat.Component; @@ -17,164 +31,227 @@ import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.server.ServerStoppedEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; +import vip.fubuki.playersync.PlayerSync; import vip.fubuki.playersync.config.JdbcConfig; +import vip.fubuki.playersync.sync.ModsSupport; import vip.fubuki.playersync.util.JDBCsetUp; import vip.fubuki.playersync.util.LocalJsonUtil; import vip.fubuki.playersync.util.PSThreadPoolFactory; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - @Mod.EventBusSubscriber public class VanillaSync { - public static void register(){} + public static void register() {} static ExecutorService executorService = Executors.newCachedThreadPool(new PSThreadPoolFactory("PlayerSync")); public static void doPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) throws SQLException, CommandSyntaxException, IOException { String player_uuid = event.getEntity().getUUID().toString(); - JDBCsetUp.QueryResult queryResult=JDBCsetUp.executeQuery("SELECT online, last_server FROM player_data WHERE uuid='"+player_uuid+"'"); - ResultSet resultSet=queryResult.resultSet(); + PlayerSync.LOGGER.info("Starting synchronization for player " + player_uuid); + + // First query: check basic player data + JDBCsetUp.QueryResult qr1 = JDBCsetUp.executeQuery("SELECT online, last_server FROM player_data WHERE uuid='" + player_uuid + "'"); + ResultSet rs1 = qr1.resultSet(); ServerPlayer serverPlayer = (ServerPlayer) event.getEntity(); - if(!resultSet.next()){ - store(event.getEntity(),true,Dist.CLIENT.isDedicatedServer()); + if (!rs1.next()){ + store(event.getEntity(), true, Dist.CLIENT.isDedicatedServer()); return; } - boolean online = resultSet.getBoolean("online"); - int lastServer = resultSet.getInt("last_server"); - queryResult=JDBCsetUp.executeQuery("SELECT * FROM player_data WHERE uuid='"+player_uuid+"'"); - resultSet= queryResult.resultSet(); - if(online && lastServer != JdbcConfig.SERVER_ID.get()) { + boolean online = rs1.getBoolean("online"); + int lastServer = rs1.getInt("last_server"); - queryResult=JDBCsetUp.executeQuery("SELECT last_update,enable FROM server_info WHERE id='"+lastServer+"'"); - ResultSet getServerInfo = queryResult.resultSet(); - if(getServerInfo.next()){ - long last_update = getServerInfo.getLong("last_update"); - boolean enable = getServerInfo.getBoolean("enable"); - if(enable && System.currentTimeMillis() < last_update + 300000.0){ + // Second query: retrieve full player data + JDBCsetUp.QueryResult qr2 = JDBCsetUp.executeQuery("SELECT * FROM player_data WHERE uuid='" + player_uuid + "'"); + ResultSet rs2 = qr2.resultSet(); + + // Check if player is already online on another server + if (online && lastServer != JdbcConfig.SERVER_ID.get()) { + JDBCsetUp.QueryResult qr3 = JDBCsetUp.executeQuery("SELECT last_update,enable FROM server_info WHERE id='" + lastServer + "'"); + ResultSet rs3 = qr3.resultSet(); + if (rs3.next()){ + long last_update = rs3.getLong("last_update"); + boolean enable = rs3.getBoolean("enable"); + if (enable && System.currentTimeMillis() < last_update + 300000.0){ event.getEntity().removeTag("player_synced"); serverPlayer.connection.disconnect(Component.translatable("playersync.already_online")); return; } JDBCsetUp.executeUpdate("UPDATE server_info SET enable= '0' WHERE id=" + lastServer); } - - getServerInfo.close(); - - + rs3.close(); } JDBCsetUp.executeUpdate("UPDATE server_info SET last_update=" + System.currentTimeMillis() + " WHERE id=" + JdbcConfig.SERVER_ID.get()); - JDBCsetUp.executeUpdate("UPDATE player_data SET online= '1',last_server=" + JdbcConfig.SERVER_ID.get() + " WHERE uuid='"+player_uuid+"'"); - if(resultSet.next()) { - //Easy Part - serverPlayer.setHealth(resultSet.getInt("health")); - serverPlayer.getFoodData().setFoodLevel(resultSet.getInt("food_level")); - serverPlayer.totalExperience=0; - serverPlayer.experienceLevel=0; - serverPlayer.experienceProgress=0; - serverPlayer.giveExperiencePoints(resultSet.getInt("xp")); - serverPlayer.setScore(resultSet.getInt("score")); - //Left Hand - serverPlayer.setItemInHand(InteractionHand.OFF_HAND,ItemStack.of(NbtUtils.snbtToStructure(resultSet.getString("left_hand").replace("|",",").replace("^","\"").replace("<","{").replace(">","}").replace("~", "'")))); - //Cursor - serverPlayer.containerMenu.setCarried(ItemStack.of(NbtUtils.snbtToStructure(resultSet.getString("cursors").replace("|",",").replace("^","\"").replace("<","{").replace(">","}").replace("~", "'")))); - //Equipment - String armor_data=resultSet.getString("armor"); - if(armor_data.length()>2) { + JDBCsetUp.executeUpdate("UPDATE player_data SET online= '1',last_server=" + JdbcConfig.SERVER_ID.get() + " WHERE uuid='" + player_uuid + "'"); + + if (rs2.next()) { + // Restore basic attributes + serverPlayer.setHealth(rs2.getInt("health")); + serverPlayer.getFoodData().setFoodLevel(rs2.getInt("food_level")); + serverPlayer.totalExperience = 0; + serverPlayer.experienceLevel = 0; + serverPlayer.experienceProgress = 0; + serverPlayer.giveExperiencePoints(rs2.getInt("xp")); + serverPlayer.setScore(rs2.getInt("score")); + + // Restore left-hand item + String leftHandEncoded = rs2.getString("left_hand"); + String leftHandNBT = deserializeString(leftHandEncoded); + serverPlayer.setItemInHand(InteractionHand.OFF_HAND, + ItemStack.of(NbtUtils.snbtToStructure(leftHandNBT))); + + // Restore cursor item + String cursorsEncoded = rs2.getString("cursors"); + String cursorsNBT = deserializeString(cursorsEncoded); + serverPlayer.containerMenu.setCarried( + ItemStack.of(NbtUtils.snbtToStructure(cursorsNBT)) + ); + + // 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)); } } - //Inventory - Map inventory = LocalJsonUtil.StringToEntryMap(resultSet.getString("inventory")); + + // Restore inventory + Map inventory = LocalJsonUtil.StringToEntryMap(rs2.getString("inventory")); for (Map.Entry entry : inventory.entrySet()) { serverPlayer.getInventory().setItem(entry.getKey(), deserialize(entry)); } - //Ender chest - Map ender_chest = LocalJsonUtil.StringToEntryMap(resultSet.getString("enderchest")); + + // 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)); } - //Effects - String effectData=resultSet.getString("effects"); - if(effectData.length()>2) { + + // Restore Effects + String effectData = rs2.getString("effects"); + if (effectData.length() > 2) { serverPlayer.removeAllEffects(); Map effects = LocalJsonUtil.StringToEntryMap(effectData); for (Map.Entry entry : effects.entrySet()) { - CompoundTag effectTag = NbtUtils.snbtToStructure(entry.getValue().replace("|", ",")); + CompoundTag effectTag = NbtUtils.snbtToStructure(deserializeString(entry.getValue())); MobEffectInstance mobEffectInstance = MobEffectInstance.load(effectTag); - assert mobEffectInstance != null; - serverPlayer.addEffect(mobEffectInstance); + if (mobEffectInstance != null) { + serverPlayer.addEffect(mobEffectInstance); + } } } - //Advancements + + // Restore Advancements File gameDir = Objects.requireNonNull(serverPlayer.getServer()).getServerDirectory(); - if(Dist.CLIENT.isDedicatedServer()){ - File advancements = new File(gameDir, JdbcConfig.SYNC_WORLD.get().get(0)+"/advancements"+"/"+player_uuid+".json"); + if (Dist.CLIENT.isDedicatedServer()){ + File advancements = new File(gameDir, JdbcConfig.SYNC_WORLD.get().get(0) + "/advancements" + "/" + player_uuid + ".json"); if (!advancements.exists()) { advancements.createNewFile(); } - byte [] bytes=resultSet.getString("advancements").getBytes(); - Files.write(advancements.toPath(),bytes); - }else{ - File[] files= scanAdvancementsFile(player_uuid, gameDir); + byte[] bytes = rs2.getString("advancements").getBytes(); + Files.write(advancements.toPath(), bytes); + } else { + File[] files = scanAdvancementsFile(player_uuid, gameDir); for (File file : files) { - if(file==null) continue; - byte [] bytes=resultSet.getString("advancements").getBytes(); - Files.write(file.toPath(),bytes); + if (file == null) continue; + byte[] bytes = rs2.getString("advancements").getBytes(); + Files.write(file.toPath(), bytes); } } } - //Mod support + + // Mod support ModsSupport modsSupport = new ModsSupport(); modsSupport.onPlayerJoin(serverPlayer); serverPlayer.addTag("player_synced"); - resultSet.close(); + // --- Begin Backpack Data Restore --- + PlayerSync.LOGGER.info("Restoring backpack data for player " + player_uuid); + net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(serverPlayer, (ItemStack backpackItem, String handler, String identifier, int slot) -> { + backpackItem.getCapability(net.p3pp3rf1y.sophisticatedbackpacks.api.CapabilityBackpackWrapper.getCapabilityInstance()) + .ifPresent(wrapper -> { + // Retrieve the contents UUID from the backpack's NBT using NBTHelper + Optional uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper.getUniqueId(wrapper.getBackpack(), "contentsUuid"); + if (uuidOpt.isPresent()) { + UUID contentsUuid = uuidOpt.get(); + try { + JDBCsetUp.QueryResult qrBackpack = JDBCsetUp.executeQuery("SELECT backpack_nbt FROM backpack_data WHERE uuid='" + contentsUuid.toString() + "'"); + ResultSet rsBackpack = qrBackpack.resultSet(); + if (rsBackpack.next()) { + String serialized = rsBackpack.getString("backpack_nbt"); + String nbtString = deserializeString(serialized); + CompoundTag backpackNbt = NbtUtils.snbtToStructure(nbtString); + // Update BackpackStorage with the retrieved NBT + net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, backpackNbt); + PlayerSync.LOGGER.info("Restored backpack data for UUID " + contentsUuid); + } + rsBackpack.close(); + qrBackpack.connection().close(); + } catch (SQLException e) { + PlayerSync.LOGGER.error("Error restoring backpack data for UUID " + contentsUuid, e); + } catch (CommandSyntaxException e) { + throw new RuntimeException(e); + } + } else { + PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid during restore"); + } + }); + return false; + }); + // --- End Backpack Data Restore --- + + rs2.close(); } @SubscribeEvent - public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event){ - executorService.submit(()->{ + public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) { + executorService.submit(() -> { try { doPlayerJoin(event); } catch (Exception e) { e.printStackTrace(); } }); - } public static ItemStack deserialize(Map.Entry entry) throws CommandSyntaxException { - String nbt= entry.getValue().replace("|",",").replace("^","\"").replace("<","{").replace(">","}").replace("~", "'"); + String nbt = deserializeString(entry.getValue()); CompoundTag compoundTag = NbtUtils.snbtToStructure(nbt); return ItemStack.of(compoundTag); } - public static String serialize(String object){ - return object.replace(",","|").replace("\"","^").replace("{","<").replace("}",">").replace("'","~"); + public static String deserializeString(String encoded) { + if (encoded.startsWith("B64:")) { + String base64 = encoded.substring(4); + try { + return new String(Base64.getDecoder().decode(base64), StandardCharsets.UTF_8); + } catch (IllegalArgumentException ex) { + PlayerSync.LOGGER.error("Base64 decoding failed for data: " + encoded, ex); + // fallback to legacy decoding below + } + } + // Legacy fallback using custom replacement + return encoded.replace("|", ",") + .replace("^", "\"") + .replace("<", "{") + .replace(">", "}") + .replace("~", "'"); + } + + public static String serialize(String object) { + // Base64 encode with a "B64:" marker for new data + return "B64:" + Base64.getEncoder().encodeToString(object.getBytes(StandardCharsets.UTF_8)); } public static void doPlayerSaveToFile(PlayerEvent.SaveToFile event) throws SQLException, IOException { JDBCsetUp.executeUpdate("UPDATE server_info SET last_update=" + System.currentTimeMillis() + " WHERE id=" + JdbcConfig.SERVER_ID.get()); - if(!event.getEntity().getTags().contains("player_synced")) return; - store(event.getEntity(),false,Dist.CLIENT.isDedicatedServer()); + if (!event.getEntity().getTags().contains("player_synced")) return; + store(event.getEntity(), false, Dist.CLIENT.isDedicatedServer()); } @SubscribeEvent public static void onPlayerSaveToFile(PlayerEvent.SaveToFile event) { - executorService.submit(()->{ + executorService.submit(() -> { try { doPlayerSaveToFile(event); } catch (Exception e) { @@ -190,77 +267,102 @@ public class VanillaSync { public static void doPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) throws SQLException, IOException { String player_uuid = event.getEntity().getUUID().toString(); - JDBCsetUp.executeUpdate("UPDATE player_data SET online= '0' WHERE uuid='"+player_uuid+"'"); - store(event.getEntity(),false,Dist.CLIENT.isDedicatedServer()); - + JDBCsetUp.executeUpdate("UPDATE player_data SET online= '0' WHERE uuid='" + player_uuid + "'"); + store(event.getEntity(), false, Dist.CLIENT.isDedicatedServer()); } @SubscribeEvent public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) throws SQLException { - //Mod support + // Mod support ModsSupport modsSupport = new ModsSupport(); modsSupport.onPlayerLeave(event.getEntity()); - executorService.submit(()->{ + executorService.submit(() -> { try { doPlayerLogout(event); } catch (Exception e) { e.printStackTrace(); } }); - } public static void store(Player player, boolean init, boolean isServer) throws SQLException, IOException { String player_uuid = player.getUUID().toString(); - //Easy part + PlayerSync.LOGGER.info("Storing data for player " + player_uuid + " (init=" + init + ")"); + + // Basic Attributes int XP = player.totalExperience; - int score=player.getScore(); - int food_level=player.getFoodData().getFoodLevel(); - int health=(int) player.getHealth(); - //Left hand + int score = player.getScore(); + int food_level = player.getFoodData().getFoodLevel(); + int health = (int) player.getHealth(); + // Left Hand String left_hand = serialize(player.getItemInHand(InteractionHand.OFF_HAND).serializeNBT().toString()); - //Cursor + // Cursor String cursors = serialize(player.containerMenu.getCarried().serializeNBT().toString()); - //Equipment - Map equipment =new HashMap<>() ; + // Equipment (Armor) + Map equipment = new HashMap<>(); for (int i = 0; i < player.getInventory().armor.size(); i++) { ItemStack itemStack = player.getInventory().armor.get(i); - if(itemStack.isEmpty()) continue; - equipment.put(i,serialize(itemStack.serializeNBT().toString())); + if (itemStack.isEmpty()) continue; + equipment.put(i, serialize(itemStack.serializeNBT().toString())); } - //inventory + // Inventory Inventory inventory = player.getInventory(); - Map inventoryMap = new HashMap<>(); + 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, serialize(itemNBT.toString())); } - //EnderChest + // Ender Chest Map ender_chest = new HashMap<>(); - for (int i=0;i< player.getEnderChestInventory().getContainerSize();i++) { + 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, serialize(itemNBT.toString())); } - //Effects - Map effects= player.getActiveEffectsMap(); - Map effectMap=new HashMap<>(); + + // --- Begin Backpack Data Sync (Store) --- + PlayerSync.LOGGER.info("Storing backpack data for player " + player.getUUID()); + net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(player, (ItemStack backpackItem, String handler, String identifier, int slot) -> { + backpackItem.getCapability(net.p3pp3rf1y.sophisticatedbackpacks.api.CapabilityBackpackWrapper.getCapabilityInstance()) + .ifPresent(wrapper -> { + // Retrieve the contents UUID from the backpack's NBT using NBTHelper + Optional uuidOpt = net.p3pp3rf1y.sophisticatedcore.util.NBTHelper.getUniqueId(wrapper.getBackpack(), "contentsUuid"); + if (uuidOpt.isPresent()) { + UUID contentsUuid = uuidOpt.get(); + // Get internal backpack data from BackpackStorage (creates it if missing) + CompoundTag backpackNbt = net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().getOrCreateBackpackContents(contentsUuid); + String serialized = VanillaSync.serialize(backpackNbt.toString()); + try { + // Use REPLACE INTO so existing records are updated + JDBCsetUp.executeUpdate("REPLACE INTO backpack_data (uuid, backpack_nbt) VALUES ('" + contentsUuid.toString() + "', '" + serialized + "')"); + PlayerSync.LOGGER.info("Saved backpack data for UUID " + contentsUuid); + } catch (SQLException e) { + PlayerSync.LOGGER.error("Error saving backpack data for UUID " + contentsUuid, e); + } + } else { + PlayerSync.LOGGER.warn("Backpack item in slot " + slot + " has no contentsUuid"); + } + }); + return false; // Continue processing all backpack items. + }); + // --- End Backpack Data Sync (Store) --- + + // Effects + Map effects = player.getActiveEffectsMap(); + Map effectMap = new HashMap<>(); for (Map.Entry entry : effects.entrySet()) { - CompoundTag effectTag= entry.getValue().save(new CompoundTag()); - effectMap.put(MobEffect.getId(entry.getKey()),effectTag.toString().replace(",","|")); + CompoundTag effectTag = entry.getValue().save(new CompoundTag()); + effectMap.put(MobEffect.getId(entry.getKey()), serialize(effectTag.toString())); } - //Advancements - //File root = serverPlayer.getServer().getServerDirectory(); + // Advancements File advancements = null; File gameDir = Objects.requireNonNull(player.getServer()).getServerDirectory(); - if(isServer){ - advancements = new File(gameDir, JdbcConfig.SYNC_WORLD.get().get(0)+"/advancements"+"/"+player_uuid+".json"); - }else{ -// File gameDir = Minecraft.getInstance().gameDirectory; - File[] files= scanAdvancementsFile(player_uuid, gameDir); - //Get LastModified + if (isServer) { + advancements = new File(gameDir, JdbcConfig.SYNC_WORLD.get().get(0) + "/advancements" + "/" + player_uuid + ".json"); + } else { + File[] files = scanAdvancementsFile(player_uuid, gameDir); long latestModifiedDate = 0; for (File file : files) { - if(file==null) continue; + if (file == null) continue; if (file.lastModified() > latestModifiedDate) { latestModifiedDate = file.lastModified(); advancements = file; @@ -273,17 +375,19 @@ public class VanillaSync { } String json = new String(bytes, StandardCharsets.UTF_8); - //SQL Operation - if(init){ - JDBCsetUp.executeUpdate("INSERT INTO player_data (uuid,armor,inventory,enderchest,advancements,effects,xp,food_level,health,score,left_hand,cursors,online) VALUES ('"+player_uuid+"','"+equipment+"','"+inventoryMap+"','"+ender_chest+"','"+advancements+"','"+effectMap+"','"+XP+"','"+food_level+"','"+health+"','"+score+"','"+left_hand+"','"+cursors+"',online=true)"); - }else JDBCsetUp.executeUpdate("UPDATE player_data SET inventory = '"+inventoryMap+"',armor='"+equipment+"' ,xp='"+XP+"',effects='"+effectMap+"',enderchest='"+ender_chest+"',score='"+score+"',food_level='"+food_level+"',health='"+health+"',advancements='"+json+"',left_hand='"+left_hand+"',cursors='"+cursors+"' WHERE uuid = '"+player_uuid+"'"); + // SQL Operation for player data + if (init) { + JDBCsetUp.executeUpdate("INSERT INTO player_data (uuid,armor,inventory,enderchest,advancements,effects,xp,food_level,health,score,left_hand,cursors,online) VALUES ('" + player_uuid + "','" + equipment + "','" + inventoryMap + "','" + ender_chest + "','" + advancements + "','" + effectMap + "','" + XP + "','" + food_level + "','" + health + "','" + score + "','" + left_hand + "','" + cursors + "',online=true)"); + } else { + JDBCsetUp.executeUpdate("UPDATE player_data SET inventory = '" + inventoryMap + "',armor='" + equipment + "' ,xp='" + XP + "',effects='" + effectMap + "',enderchest='" + ender_chest + "',score='" + score + "',food_level='" + food_level + "',health='" + health + "',advancements='" + json + "',left_hand='" + left_hand + "',cursors='" + cursors + "' WHERE uuid = '" + player_uuid + "'"); + } } private static File[] scanAdvancementsFile(String player_uuid, File gameDir) { File[] files = new File[JdbcConfig.SYNC_WORLD.get().size()]; for (int i = 0; i < JdbcConfig.SYNC_WORLD.get().size(); i++) { - File advanceFile=new File(gameDir, "saves/"+JdbcConfig.SYNC_WORLD.get().get(i)+"/advancements"+"/"+player_uuid+".json"); - if(!advanceFile.exists()) continue; + File advanceFile = new File(gameDir, "saves/" + JdbcConfig.SYNC_WORLD.get().get(i) + "/advancements" + "/" + player_uuid + ".json"); + if (!advanceFile.exists()) continue; files[i] = advanceFile; } return files; @@ -294,12 +398,10 @@ public class VanillaSync { @SubscribeEvent public static void onUpdate(TickEvent.LevelTickEvent event) throws SQLException { tick++; - if(tick == 1800) { - tick=0; + if (tick == 1800) { + tick = 0; long current = System.currentTimeMillis(); - JDBCsetUp.executeUpdate("UPDATE server_info SET last_update ="+current+" WHERE id= "+ JdbcConfig.SERVER_ID.get()); + JDBCsetUp.executeUpdate("UPDATE server_info SET last_update =" + current + " WHERE id= " + JdbcConfig.SERVER_ID.get()); } } - } - diff --git a/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java b/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java index 436e750..40bc697 100644 --- a/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java +++ b/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java @@ -4,61 +4,84 @@ import vip.fubuki.playersync.config.JdbcConfig; import java.sql.*; - public class JDBCsetUp { - public static Connection getConnection() throws SQLException { - String url= "jdbc:mysql://"+JdbcConfig.HOST.get()+":"+JdbcConfig.PORT.get()+"?useUnicode=true&characterEncoding=utf-8&useSSL="+JdbcConfig.USE_SSL.get()+"&serverTimezone=UTC&allowPublicKeyRetrieval=true"; - return DriverManager.getConnection(url, JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get()); + /** + * Returns a connection to the MySQL server. + * @param selectDatabase if true, the returned URL includes the configured database name. + * @return a Connection object with the database explicitly selected. + * @throws SQLException if a database access error occurs. + */ + public static Connection getConnection(boolean selectDatabase) throws SQLException { + String dbName = JdbcConfig.DATABASE_NAME.get(); + // Build the base URL + String url = "jdbc:mysql://" + JdbcConfig.HOST.get() + ":" + JdbcConfig.PORT.get(); + if (selectDatabase && dbName != null && !dbName.isEmpty()) { + url += "/" + dbName; + } + url += "?useUnicode=true&characterEncoding=utf-8&useSSL=" + JdbcConfig.USE_SSL.get() + + "&serverTimezone=UTC&allowPublicKeyRetrieval=true"; + Connection conn = DriverManager.getConnection(url, JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get()); + // Ensure that the connection uses the desired database by explicitly issuing "USE dbName" + if (selectDatabase && dbName != null && !dbName.isEmpty()) { + try (Statement st = conn.createStatement()) { + st.execute("USE " + dbName); + } + } + return conn; } - public static QueryResult executeQuery(String sql) throws SQLException{ - Connection connection = getConnection(); + // Default connection always includes the database. + public static Connection getConnection() throws SQLException { + return getConnection(true); + } - try (Statement useStatement = connection.createStatement()) { - useStatement.execute("USE " + JdbcConfig.DATABASE_NAME.get()); - } + /** + * Executes a query using a connection that includes the database. + */ + public static QueryResult executeQuery(String sql) throws SQLException { + Connection connection = getConnection(); // With database selected (and "USE" already run) PreparedStatement queryStatement = connection.prepareStatement(sql); ResultSet resultSet = queryStatement.executeQuery(); - return new QueryResult(connection,resultSet); + return new QueryResult(connection, resultSet); } - public static void executeUpdate(String sql) throws SQLException{ - try (Connection connection = getConnection()) { - - try (Statement useStatement = connection.createStatement()) { - useStatement.execute("USE " + JdbcConfig.DATABASE_NAME.get()); - } - + /** + * Executes an update using a connection that includes the database. + */ + public static void executeUpdate(String sql) throws SQLException { + try (Connection connection = getConnection()) { // With database selected try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { updateStatement.executeUpdate(); } } } - public static void update(String sql, String... argument) throws SQLException{ - Connection connection = getConnection(); - - try (Statement useStatement = connection.createStatement()) { - useStatement.execute("USE " + JdbcConfig.DATABASE_NAME.get()); - } - - PreparedStatement updateStatement = connection.prepareStatement(sql); - for (int i = 1; i <= argument.length; i++) { - updateStatement.setString(i,argument[i]); - } - updateStatement.executeUpdate(); - } - - public static void executeUpdate(String sql, int i) throws SQLException{ - try (Connection connection = getConnection()) { - + /** + * Executes an update using a connection that does NOT include a default database. + * This method is used for commands like "CREATE DATABASE IF NOT EXISTS ..." + */ + public static void executeUpdate(String sql, int dummy) throws SQLException { + try (Connection connection = getConnection(false)) { // Without default database try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { updateStatement.executeUpdate(); } } } + /** + * A helper method for updates with parameters. + */ + public static void update(String sql, String... argument) throws SQLException { + try (Connection connection = getConnection()) { // With database selected + PreparedStatement updateStatement = connection.prepareStatement(sql); + for (int i = 0; i < argument.length; i++) { + updateStatement.setString(i + 1, argument[i]); + } + updateStatement.executeUpdate(); + } + } + public record QueryResult(Connection connection, ResultSet resultSet) { } }