diff --git a/src/main/java/vip/fubuki/playersync/PlayerSync.java b/src/main/java/vip/fubuki/playersync/PlayerSync.java index de37b07..f9d5efc 100644 --- a/src/main/java/vip/fubuki/playersync/PlayerSync.java +++ b/src/main/java/vip/fubuki/playersync/PlayerSync.java @@ -177,6 +177,11 @@ public class PlayerSync { rsAdvCol.close(); // ----- END NEW BLOCK ----- + try { + JDBCsetUp.executeUpdate("UPDATE player_data SET online=0 WHERE last_server=" + JdbcConfig.SERVER_ID.get() +" AND online=1 LIMIT 1000"); + } catch (Exception e) { + LOGGER.error("An exception occurred while trying change wrong player-status\n" + e.getMessage()); + } LOGGER.info("PlayerSync is ready!"); } diff --git a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java index b89e97e..33f6b66 100644 --- a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java +++ b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java @@ -2,6 +2,7 @@ package vip.fubuki.playersync.sync; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponents; @@ -11,9 +12,11 @@ import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PlayerAdvancements; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.effect.MobEffect; @@ -24,18 +27,23 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.component.CustomData; import net.minecraft.world.item.component.ItemLore; +import net.minecraft.world.level.Level; import net.minecraft.world.level.storage.WorldData; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.ModList; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.event.OnDatapackSyncEvent; +import net.neoforged.neoforge.event.entity.living.LivingDeathEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.entity.player.PlayerNegotiationEvent; import net.neoforged.neoforge.event.server.ServerStoppedEvent; import net.neoforged.neoforge.event.tick.LevelTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent; import net.neoforged.neoforge.server.ServerLifecycleHooks; import vip.fubuki.playersync.PlayerSync; import vip.fubuki.playersync.config.JdbcConfig; +import vip.fubuki.playersync.sync.addons.CuriosCache; +import vip.fubuki.playersync.sync.addons.ModsSupport; import vip.fubuki.playersync.util.JDBCsetUp; import vip.fubuki.playersync.util.LocalJsonUtil; import vip.fubuki.playersync.util.PSThreadPoolFactory; @@ -47,6 +55,7 @@ import java.nio.file.Files; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -141,106 +150,223 @@ public class VanillaSync { } } - public static void doPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) throws SQLException, CommandSyntaxException, IOException { - String player_uuid = event.getEntity().getUUID().toString(); - PlayerSync.LOGGER.info("Starting synchronization for player " + player_uuid); + public static void doPlayerConnect(PlayerNegotiationEvent event) { + try { + String player_uuid = event.getProfile().getId().toString(); + PlayerSync.LOGGER.info("Detected connection from player" + player_uuid + ",starting checking"); + boolean online; + int lastServer; - // 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(); + // First query: check basic player data and check whether player can join into server. + JDBCsetUp.QueryResult qr1 = JDBCsetUp.executeQuery("SELECT online, last_server FROM player_data WHERE uuid='" + player_uuid + "'"); - // Mod support - ModsSupport modsSupport = new ModsSupport(); - modsSupport.onPlayerJoin(serverPlayer); - - if (!rs1.next()){ - store(event.getEntity(), true); - return; - } - boolean online = rs1.getBoolean("online"); - int lastServer = rs1.getInt("last_server"); - - // 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")); + try (ResultSet rs1 = qr1.resultSet()) { + if (!rs1.next()) { + PlayerSync.LOGGER.info("A new-player connection detected"); + qr1.connection().close(); return; } - JDBCsetUp.executeUpdate("UPDATE server_info SET enable= '0' WHERE id=" + lastServer); + online = rs1.getBoolean("online"); + lastServer = rs1.getInt("last_server"); + qr1.connection().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 (rs2.next()) { - // Restore basic attributes - serverPlayer.setHealth(rs2.getInt("health")); - serverPlayer.getFoodData().setFoodLevel(rs2.getInt("food_level")); - - setXpForPlayer(serverPlayer, rs2.getInt("xp")); - serverPlayer.setScore(rs2.getInt("score")); - - // Restore left-hand item - String leftHandEncoded = rs2.getString("left_hand"); - serverPlayer.setItemInHand(InteractionHand.OFF_HAND, - deserializeAndCreatePlaceholderIfNeeded(leftHandEncoded)); - - // Restore cursor item - String cursorsEncoded = rs2.getString("cursors"); - serverPlayer.containerMenu.setCarried( - 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(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); + // Second query: Check if player is already online on another server + if (online && lastServer != JdbcConfig.SERVER_ID.get()) { + JDBCsetUp.QueryResult qr2 = JDBCsetUp.executeQuery("SELECT last_update,enable FROM server_info WHERE id='" + lastServer + "'"); + try (ResultSet rs2 = qr2.resultSet()) { + if (rs2.next()) { + long last_update = rs2.getLong("last_update"); + boolean enable = rs2.getBoolean("enable"); + if (enable && System.currentTimeMillis() < last_update + 300000.0) { + event.getConnection().disconnect(Component.translatable("playersync.already_online")); + qr2.connection().close(); + return; + } + JDBCsetUp.executeUpdate("UPDATE server_info SET enable= '0' WHERE id=" + lastServer); + } + qr2.connection().close(); } } + } catch (Exception e) { + PlayerSync.LOGGER.error("SqlException detected!", e); + event.getConnection().disconnect(Component.translatable("playersync.sqlexception")); + } + } - // Restore inventory - Map inventory = LocalJsonUtil.StringToEntryMap(rs2.getString("inventory")); - for (Map.Entry entry : inventory.entrySet()) { - serverPlayer.getInventory().setItem(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); + // Use string uuid as key + public static Set deadPlayerWhileLogging = ConcurrentHashMap.newKeySet(); + public static Set syncNotCompletedPlayer = ConcurrentHashMap.newKeySet(); + + public static void doPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) throws SQLException, CommandSyntaxException, IOException { + ServerPlayer joinedPlayer = (ServerPlayer) event.getEntity(); + String player_uuid = joinedPlayer.getUUID().toString(); + if (joinedPlayer.isDeadOrDying()) { + deadPlayerWhileLogging.add(player_uuid); + joinedPlayer.removeTag("player_synced"); + + // Simulate normal death behavior + MinecraftServer server = joinedPlayer.getServer(); + if (server != null) { + ResourceKey respawnLevel = joinedPlayer.getRespawnDimension(); + BlockPos respawnPos = joinedPlayer.getRespawnPosition(); + double respawnX; + double respawnY; + double respawnZ; + if (respawnPos != null && respawnLevel != null) { + ServerLevel level = server.getLevel(respawnLevel); + respawnX = respawnPos.getX(); + respawnY = respawnPos.getY(); + respawnZ = respawnPos.getZ(); + if (level != null) { + joinedPlayer.teleportTo(level, respawnX, respawnY + 1, respawnZ, 0, 0); + } + } else { + PlayerSync.LOGGER.debug("Player " + player_uuid + " has no respawn point"); + } + } else { + PlayerSync.LOGGER.warn("Trying to get server,but got a null"); } - // Restore Ender Chest - Map ender_chest = LocalJsonUtil.StringToEntryMap(rs2.getString("enderchest")); - for (Map.Entry entry : ender_chest.entrySet()) { - serverPlayer.getEnderChestInventory().setItem(entry.getKey(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); + joinedPlayer.setHealth(1); + try { + 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 + "'"); + } catch (SQLException e) { + PlayerSync.LOGGER.error("An error occurred while trying to execute a dead or dying player" + e.getMessage()); } + joinedPlayer.connection.disconnect(Component.translatableWithFallback("playersync.wrong_entity_status","An error occurred while creating playerEntity in the world,please login again.")); + return; + } - // 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(deserializeString(entry.getValue())); - MobEffectInstance mobEffectInstance = MobEffectInstance.load(effectTag); - if (mobEffectInstance != null) { - serverPlayer.addEffect(mobEffectInstance); + try { + 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(); + + // Mod support + ModsSupport modsSupport = new ModsSupport(); + modsSupport.onPlayerJoin(serverPlayer); + + if (!rs1.next()) { + store(event.getEntity(), true); + 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 + "'"); + rs1.close(); + qr1.close(); + PlayerSync.LOGGER.info("New player detected,init completed."); + syncNotCompletedPlayer.remove(player_uuid); + return; + } + boolean online = rs1.getBoolean("online"); + int lastServer = rs1.getInt("last_server"); + + // 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.translatableWithFallback("playersync.already_online", "You can't join more than one synchronization server at the same time.")); + return; + } + JDBCsetUp.executeUpdate("UPDATE server_info SET enable= '0' WHERE id=" + lastServer); + } + 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 (rs2.next()) { + // Restore basic attributes + serverPlayer.setHealth(rs2.getInt("health")); + serverPlayer.getFoodData().setFoodLevel(rs2.getInt("food_level")); + + setXpForPlayer(serverPlayer, rs2.getInt("xp")); + serverPlayer.setScore(rs2.getInt("score")); + + // Restore left-hand item + String leftHandEncoded = rs2.getString("left_hand"); + serverPlayer.setItemInHand(InteractionHand.OFF_HAND, + deserializeAndCreatePlaceholderIfNeeded(leftHandEncoded)); + + // Restore cursor item + String cursorsEncoded = rs2.getString("cursors"); + serverPlayer.containerMenu.setCarried( + 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(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); + } + } + + // Restore inventory + Map inventory = LocalJsonUtil.StringToEntryMap(rs2.getString("inventory")); + for (Map.Entry entry : inventory.entrySet()) { + 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(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue())); + } + + // 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(deserializeString(entry.getValue())); + MobEffectInstance mobEffectInstance = MobEffectInstance.load(effectTag); + if (mobEffectInstance != null) { + serverPlayer.addEffect(mobEffectInstance); + } } } } + + serverPlayer.addTag("player_synced"); + + PlayerSync.LOGGER.info("Sync data for player {} completed.", player_uuid); + + rs2.close(); + qr2.close(); + rs1.close(); + qr1.close(); + + PlayerSync.LOGGER.info("Sync data for player {} completed.", player_uuid); + syncNotCompletedPlayer.remove(player_uuid); + } catch (Exception e) { + PlayerSync.LOGGER.error("Internal Exception detected!", e); + syncNotCompletedPlayer.remove(player_uuid); } + } - serverPlayer.addTag("player_synced"); - - rs2.close(); + @SubscribeEvent + public static void onPlayerConnect(PlayerNegotiationEvent event) { + executorService.submit(() -> { + try { + doPlayerConnect(event); + } catch (Exception e) { + e.printStackTrace(); + } + }); } @SubscribeEvent @@ -430,16 +556,23 @@ public class VanillaSync { @SubscribeEvent public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) throws SQLException { - // Mod support - ModsSupport modsSupport = new ModsSupport(); - modsSupport.onPlayerLeave(event.getEntity()); - executorService.submit(() -> { - try { - doPlayerLogout(event); - } catch (Exception e) { - e.printStackTrace(); - } - }); + String player_uuid = event.getEntity().getUUID().toString(); + if (deadPlayerWhileLogging.contains(player_uuid)) { + PlayerSync.LOGGER.warn("A dead or dying player was kicked,which uuid is:" + player_uuid); + JDBCsetUp.executeUpdate("UPDATE player_data SET online= '0' WHERE uuid='" + player_uuid + "'"); + deadPlayerWhileLogging.remove(player_uuid); + } else { + // Mod support + ModsSupport modsSupport = new ModsSupport(); + modsSupport.onPlayerLeave(event.getEntity()); + executorService.submit(() -> { + try { + doPlayerLogout(event); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } } // Helper function to get the NBT string to be saved @@ -599,12 +732,15 @@ public class VanillaSync { // New fields for auto-save private static int autoSaveTickCounter = 0; private static final int AUTO_SAVE_INTERVAL_TICKS = 1200; // Every Minute + private static int autoCleanCuriosCacheTickCounter = 0; + private static final int AUTO_CLEAN_CURIOS_CACHE_INTERVAL_TICKS = 36000; // Every 30 min //AutoSave @SubscribeEvent public static void onServerTick(ServerTickEvent.Post event) { // Run at the end phase to avoid interfering with game logic autoSaveTickCounter++; + autoCleanCuriosCacheTickCounter++; if (autoSaveTickCounter >= AUTO_SAVE_INTERVAL_TICKS) { autoSaveTickCounter = 0; // Retrieve the current server instance @@ -629,8 +765,18 @@ public class VanillaSync { }); } - } } + } + if (autoCleanCuriosCacheTickCounter >= AUTO_CLEAN_CURIOS_CACHE_INTERVAL_TICKS) { + autoCleanCuriosCacheTickCounter = 0; + executorService.submit(() -> { + try { + CuriosCache.RemoveExpiredCuriosCache(); + } catch (Exception e) { + PlayerSync.LOGGER.error("An error occurred while cleaning curios cache:" + e.getMessage()); + } + }); + } } private static void setXpForPlayer(ServerPlayer serverPlayer, int databaseXp) { @@ -680,4 +826,12 @@ public class VanillaSync { return totalXp; } + + @SubscribeEvent + //Don't know what will happen if a fake player is killed,need more test. + public static void onPlayerDeath(LivingDeathEvent event) { + if (event.getEntity() instanceof ServerPlayer player && !deadPlayerWhileLogging.contains(event.getEntity().getUUID().toString())) { + CuriosCache.tryStoreCuriosToCache(player); + } + } } diff --git a/src/main/java/vip/fubuki/playersync/sync/addons/CuriosCache.java b/src/main/java/vip/fubuki/playersync/sync/addons/CuriosCache.java new file mode 100644 index 0000000..acb04a8 --- /dev/null +++ b/src/main/java/vip/fubuki/playersync/sync/addons/CuriosCache.java @@ -0,0 +1,119 @@ +package vip.fubuki.playersync.sync.addons; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameRules; + +import net.neoforged.fml.ModList; +import top.theillusivec4.curios.api.CuriosApi; +import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler; +import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler; +import vip.fubuki.playersync.PlayerSync; +import vip.fubuki.playersync.sync.VanillaSync; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class CuriosCache { + private static final long CACHE_EXPIRY_MS = 3600000; + public static final ConcurrentHashMap curiosCache = new ConcurrentHashMap<>(); + + public static class CuriosCacheEntry { + final long timeStamp; + final String serializedData; + + CuriosCacheEntry(String data) { + this.timeStamp = System.currentTimeMillis(); + this.serializedData = data; + } + + boolean isExpired() { + return System.currentTimeMillis() - timeStamp > CACHE_EXPIRY_MS; + } + } + + //If player logged out by "Title Screen" button,you will not be able to get the handlerOpt,and it will make the curios inventory sync failed. + //Create a method to store temporary curios data when player is dead. + //Then check player status in the logged out event,and take a normal sync if player is alive. + //If player is dead or dying,the cache will be used to prevent the empty data from the failure of getting handlerOpt. + public static void tryStoreCuriosToCache(net.minecraft.world.entity.player.Player player) { + if (!ModList.get().isLoaded("curios") || !CuriosCache.isKeepInventoryActive(player)) { + return; + } + + try { + Optional handlerOpt = CuriosApi.getCuriosInventory(player); + if (handlerOpt.isEmpty()) { + PlayerSync.LOGGER.error("Obtain the curios api failed,cannot create the cache."); + return; + } + + ICuriosItemHandler handler = handlerOpt.get(); + String serializedData = serializeCuriosInventory(handler); + + if (serializedData.startsWith("{}")) { + PlayerSync.LOGGER.debug("No curios data found,skipping the step of creating cache"); + return; + } + + UUID playerUuid = player.getUUID(); + curiosCache.put(playerUuid, new CuriosCacheEntry(serializedData)); + } catch (Exception e) { + PlayerSync.LOGGER.error("An error occurred while creating curios cache:" + e.getMessage()); + } + } + + private static String serializeCuriosInventory(ICuriosItemHandler handler) { + Map flatMap = new HashMap<>(); + try { + handler.getCurios().forEach((slotType, stacksHandler) -> { + IDynamicStackHandler dynStacks = stacksHandler.getStacks(); + for (int i = 0; i < dynStacks.getSlots(); i++) { + ItemStack stack = dynStacks.getStackInSlot(i); + if (!stack.isEmpty()) { + String serialized = VanillaSync.serialize(VanillaSync.serializeNBT(stack).toString()); + flatMap.put(slotType + ":" + i, serialized); + } + } + }); + } catch (Exception e) { + PlayerSync.LOGGER.error("Failed to serialize curios data:" + e.getMessage()); + } + return flatMap.isEmpty() ? "{}" : flatMap.toString(); + } + + public static boolean isKeepInventoryActive(Player player) { + MinecraftServer server = player.getServer(); + if (server == null) { + PlayerSync.LOGGER.error("Trying to get the gamerule(KeepInventory),but server is null"); + return false; + } + return server.getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY); + } + + public static void RemoveExpiredCuriosCache() { + long startMs = System.currentTimeMillis(); + int cacheSize = curiosCache.size(); + + if (cacheSize == 0) { + PlayerSync.LOGGER.debug("No curios caches,skipping cleaning"); + return; + } + + int removed = 0; + Iterator> iterator = curiosCache.entrySet().iterator(); + + while (iterator.hasNext()) { + if (iterator.next().getValue().isExpired()) { + iterator.remove(); + removed ++; + } + } + + if (removed > 0) { + PlayerSync.LOGGER.info("Cleaned {} curios cache(s),{} left,took {} Ms", + removed, curiosCache.size(), System.currentTimeMillis() - startMs); + } + } +} diff --git a/src/main/java/vip/fubuki/playersync/sync/ModsSupport.java b/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java similarity index 98% rename from src/main/java/vip/fubuki/playersync/sync/ModsSupport.java rename to src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java index 8603fef..25917ec 100644 --- a/src/main/java/vip/fubuki/playersync/sync/ModsSupport.java +++ b/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java @@ -1,4 +1,4 @@ -package vip.fubuki.playersync.sync; +package vip.fubuki.playersync.sync.addons; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.nbt.CompoundTag; @@ -12,6 +12,7 @@ import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler; import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler; import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler; import vip.fubuki.playersync.PlayerSync; +import vip.fubuki.playersync.sync.VanillaSync; import vip.fubuki.playersync.util.JDBCsetUp; import vip.fubuki.playersync.util.LocalJsonUtil; @@ -116,6 +117,7 @@ public class ModsSupport { CompoundTag backpackNbt = NbtUtils.snbtToStructure(nbtString); // Update BackpackStorage with the retrieved NBT net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, backpackNbt); + net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setDirty(); PlayerSync.LOGGER.info("Restored backpack data for UUID " + contentsUuid); } rsBackpack.close(); diff --git a/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncClient.java b/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncClient.java index b207915..cb76a6b 100644 --- a/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncClient.java +++ b/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncClient.java @@ -24,9 +24,6 @@ public class ChatSyncClient { private static final int RECONNECT_DELAY = 5000; private static final int MAX_RECONNECT_ATTEMPTS = 10; - private static volatile long lastHeartbeat = System.currentTimeMillis(); - private static final long HEARTBEAT_INTERVAL = 15000; - public void run() { int reconnectAttempts = 0; @@ -49,33 +46,25 @@ public class ChatSyncClient { 15000 ); - clientSocket.setSoTimeout(30000); + clientSocket.setSoTimeout(0); out = new PrintWriter(new BufferedWriter( new OutputStreamWriter(clientSocket.getOutputStream())), true); PlayerSync.LOGGER.info("Successfully connected to chat server"); reconnectAttempts = 0; - lastHeartbeat = System.currentTimeMillis(); - - startHeartbeatMonitor(); BufferedReader in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); String serverMessage; while (running && (serverMessage = in.readLine()) != null) { - lastHeartbeat = System.currentTimeMillis(); - - if ("".equals(serverMessage)) { - continue; - } - - PlayerSync.LOGGER.info("Received message from chat server: " + serverMessage); Component textComponents = Component.nullToEmpty(serverMessage); if(playerList != null){ playerList.getServer().execute(() -> playerList.broadcastSystemMessage(textComponents, false)); + }else { + PlayerSync.LOGGER.info("Received message from chat server: " + serverMessage); } } @@ -105,32 +94,6 @@ public class ChatSyncClient { } } - private void startHeartbeatMonitor() { - Thread heartbeatThread = new Thread(() -> { - while (running && clientSocket != null && !clientSocket.isClosed()) { - try { - Thread.sleep(10000); // 每10秒检查一次 - - long now = System.currentTimeMillis(); - if (now - lastHeartbeat > HEARTBEAT_INTERVAL) { - PlayerSync.LOGGER.warn("No heartbeat for {}ms, sending test message", - now - lastHeartbeat); - - // 发送测试消息检查连接 - if (out != null) { - out.println(""); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - }, "ChatSync-Heartbeat"); - heartbeatThread.setDaemon(true); - heartbeatThread.start(); - } - private void closeConnection() { try { if (out != null) { diff --git a/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncServer.java b/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncServer.java index 6697d6b..db382bd 100644 --- a/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncServer.java +++ b/src/main/java/vip/fubuki/playersync/sync/chat/ChatSyncServer.java @@ -26,12 +26,10 @@ public class ChatSyncServer { serverSocket.setReuseAddress(true); PlayerSync.LOGGER.info("Chat server started successfully on port {}", JdbcConfig.CHAT_SERVER_PORT.get()); - startHeartbeatBroadcast(); - while (running && !Thread.currentThread().isInterrupted()) { try { Socket newSocket = serverSocket.accept(); - newSocket.setSoTimeout(30000); + newSocket.setSoTimeout(0); SocketList.add(newSocket); executorService.submit(() -> handleClient(newSocket)); PlayerSync.LOGGER.info("New client connected, total clients: {}", SocketList.size()); @@ -54,7 +52,6 @@ public class ChatSyncServer { String message; while (running && (message = reader.readLine()) != null) { - PlayerSync.LOGGER.info("Received message from {}: {}", clientInfo, message); broadcastMessage(socket, message); } @@ -96,47 +93,6 @@ public class ChatSyncServer { } } - private void startHeartbeatBroadcast() { - Thread heartbeatThread = new Thread(() -> { - while (running) { - try { - Thread.sleep(20000); - broadcastHeartbeat(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - }, "ChatSync-Server-Heartbeat"); - heartbeatThread.setDaemon(true); - heartbeatThread.start(); - } - - private void broadcastHeartbeat() { - Iterator iterator = SocketList.iterator(); - while (iterator.hasNext()) { - Socket socket = iterator.next(); - if (!socket.isClosed()) { - try { - PrintWriter writer = new PrintWriter( - new BufferedWriter( - new OutputStreamWriter(socket.getOutputStream())), true); - writer.println(""); - } catch (IOException e) { - PlayerSync.LOGGER.warn("Failed to send heartbeat to client, removing: {}", e.getMessage()); - iterator.remove(); - try { - socket.close(); - } catch (IOException ex) { - // Ignore - } - } - } else { - iterator.remove(); - } - } - } - public void shutdown() { running = false; try { diff --git a/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java b/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java index fea7fd7..eb00d58 100644 --- a/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java +++ b/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java @@ -1,13 +1,11 @@ package vip.fubuki.playersync.util; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; import vip.fubuki.playersync.config.JdbcConfig; import java.sql.*; -import org.slf4j.Logger; - -import com.mojang.logging.LogUtils; - public class JDBCsetUp { private static final Logger LOGGER = LogUtils.getLogger(); @@ -22,14 +20,14 @@ public class JDBCsetUp { 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()) { + if (selectDatabase && !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()) { + if (selectDatabase && !dbName.isEmpty()) { try (Statement st = conn.createStatement()) { st.execute("USE " + dbName); } @@ -45,18 +43,20 @@ public class JDBCsetUp { /** * Executes a query using a connection that includes the database. */ - public static QueryResult executeQuery(String sql) throws SQLException { + public static QueryResult executeQuery(String sqlFormatString, Object... args) throws SQLException { + String sql = String.format(sqlFormatString, args); LOGGER.trace(sql); 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, queryStatement, resultSet); } /** - * Executes an update using a connection that includes the database. + * Executes an update using a connection with or without the database within the JDBC URL */ - public static void executeUpdate(String sql) throws SQLException { + private static void executeUpdate(boolean selectDatabase, String sqlFormatString, Object... args) throws SQLException { + String sql = String.format(sqlFormatString, args); LOGGER.trace(sql); try (Connection connection = getConnection()) { // With database selected try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { @@ -65,6 +65,13 @@ public class JDBCsetUp { } } + /** + * Executes an update using a connection that includes the database in the JDBC URL + */ + public static void executeUpdate(String sqlFormatString, Object... args) throws SQLException { + executeUpdate(true, sqlFormatString, args); + } + /** * 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 ..." @@ -92,6 +99,32 @@ public class JDBCsetUp { } } - public record QueryResult(Connection connection, ResultSet resultSet) { + public record QueryResult(Connection connection,PreparedStatement preparedStatement, ResultSet resultSet) implements AutoCloseable { + @Override + public void close() { + if (resultSet != null) { + try { + resultSet.close(); + } catch (SQLException e) { + LOGGER.error("Error closing ResultSet", e); + } + } + + if (preparedStatement != null) { + try { + preparedStatement.close(); + } catch (SQLException e) { + LOGGER.error("Error closing PreparedStatement", e); + } + } + + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + LOGGER.error("Error closing Connection", e); + } + } + } } } diff --git a/src/main/resources/assets/playersync/lang/en_us.json b/src/main/resources/assets/playersync/lang/en_us.json index 4b03f8e..fc0c438 100644 --- a/src/main/resources/assets/playersync/lang/en_us.json +++ b/src/main/resources/assets/playersync/lang/en_us.json @@ -1,5 +1,7 @@ { "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.item_placeholder_title": "Item Voucher", - "playersync.already_online": "You can't join more than one synchronization server at the same time." + "playersync.already_online": "You can't join more than one synchronization server at the same time.", + "playersync.sqlexception": "SqlException detected!Connection lost,please contact with your admin.", + "playersync.wrong_entity_status": "An error occurred while creating playerEntity in the world,please login again." } diff --git a/src/main/resources/assets/playersync/lang/zh_cn.json b/src/main/resources/assets/playersync/lang/zh_cn.json index f206090..474767f 100644 --- a/src/main/resources/assets/playersync/lang/zh_cn.json +++ b/src/main/resources/assets/playersync/lang/zh_cn.json @@ -1,5 +1,7 @@ { "playersync.item_placeholder_description": "物品在此服务器未知。这可能是一个模组物品,或是不同版本的原版物品。\n这张券将会在加入可识别此物品的服务器后自动替换为对应物品。", "playersync.item_placeholder_title": "物品券", - "playersync.already_online": "你不能同时加入多个同步的服务器。" + "playersync.already_online": "你不能同时加入多个同步的服务器。", + "playersync.sqlexception": "检测到Sql异常!连接已中断,请联系管理员", + "playersync.wrong_entity_status": "在世界中尝试创建玩家实体时发生了错误,请尝试重新进入" }