net.p3pp3rf1y.sophisticatedbackpacks throws a NoClassDefFoundError when
sophisticated backpacks is not installed.
This exception never reaches the logs for unknown reasons.
Checking explicitly for ModList.get().isLoaded() ensures that the mod is
loaded.
Fixes regression of 439c7ee5bb
393 lines
18 KiB
Java
393 lines
18 KiB
Java
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;
|
|
import net.minecraft.server.MinecraftServer;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.world.InteractionHand;
|
|
import net.minecraft.world.effect.MobEffect;
|
|
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.minecraftforge.api.distmarker.Dist;
|
|
import net.minecraftforge.event.TickEvent;
|
|
import net.minecraftforge.event.entity.player.PlayerEvent;
|
|
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.server.ServerLifecycleHooks;
|
|
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;
|
|
|
|
@Mod.EventBusSubscriber
|
|
public class VanillaSync {
|
|
|
|
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();
|
|
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 (!rs1.next()){
|
|
store(event.getEntity(), true, Dist.CLIENT.isDedicatedServer());
|
|
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"));
|
|
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"));
|
|
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<Integer, String> equipment = LocalJsonUtil.StringToEntryMap(armor_data);
|
|
for (Map.Entry<Integer, String> entry : equipment.entrySet()) {
|
|
serverPlayer.getInventory().armor.set(entry.getKey(), deserialize(entry));
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
// Restore Effects
|
|
String effectData = rs2.getString("effects");
|
|
if (effectData.length() > 2) {
|
|
serverPlayer.removeAllEffects();
|
|
Map<Integer, String> effects = LocalJsonUtil.StringToEntryMap(effectData);
|
|
for (Map.Entry<Integer, String> entry : effects.entrySet()) {
|
|
CompoundTag effectTag = NbtUtils.snbtToStructure(deserializeString(entry.getValue()));
|
|
MobEffectInstance mobEffectInstance = MobEffectInstance.load(effectTag);
|
|
if (mobEffectInstance != null) {
|
|
serverPlayer.addEffect(mobEffectInstance);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 (!advancements.exists()) {
|
|
advancements.createNewFile();
|
|
}
|
|
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 = rs2.getString("advancements").getBytes();
|
|
Files.write(file.toPath(), bytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mod support
|
|
ModsSupport modsSupport = new ModsSupport();
|
|
modsSupport.onPlayerJoin(serverPlayer);
|
|
serverPlayer.addTag("player_synced");
|
|
|
|
rs2.close();
|
|
}
|
|
|
|
@SubscribeEvent
|
|
public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) {
|
|
executorService.submit(() -> {
|
|
try {
|
|
doPlayerJoin(event);
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
@SubscribeEvent
|
|
public static void onPlayerSaveToFile(PlayerEvent.SaveToFile event) {
|
|
executorService.submit(() -> {
|
|
try {
|
|
doPlayerSaveToFile(event);
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
});
|
|
}
|
|
|
|
@SubscribeEvent
|
|
public static void onServerShutdown(ServerStoppedEvent event) throws SQLException {
|
|
JDBCsetUp.executeUpdate("UPDATE server_info SET enable= '0' WHERE id=" + JdbcConfig.SERVER_ID.get());
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
@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();
|
|
}
|
|
});
|
|
}
|
|
|
|
public static void store(Player player, boolean init, boolean isServer) throws SQLException, IOException {
|
|
String player_uuid = player.getUUID().toString();
|
|
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
|
|
String left_hand = serialize(player.getItemInHand(InteractionHand.OFF_HAND).serializeNBT().toString());
|
|
// Cursor
|
|
String cursors = serialize(player.containerMenu.getCarried().serializeNBT().toString());
|
|
// 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);
|
|
if (itemStack.isEmpty()) continue;
|
|
equipment.put(i, serialize(itemStack.serializeNBT().toString()));
|
|
}
|
|
// 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()));
|
|
}
|
|
// 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()));
|
|
}
|
|
|
|
if(ModList.get().isLoaded("sophisticatedbackpacks")){
|
|
ModsSupport.storeSophisticatedBackpacks(player);
|
|
}
|
|
|
|
// Effects
|
|
Map<MobEffect, MobEffectInstance> effects = player.getActiveEffectsMap();
|
|
Map<Integer, String> effectMap = new HashMap<>();
|
|
for (Map.Entry<MobEffect, MobEffectInstance> entry : effects.entrySet()) {
|
|
CompoundTag effectTag = entry.getValue().save(new CompoundTag());
|
|
effectMap.put(MobEffect.getId(entry.getKey()), serialize(effectTag.toString()));
|
|
}
|
|
// 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[] files = scanAdvancementsFile(player_uuid, gameDir);
|
|
long latestModifiedDate = 0;
|
|
for (File file : files) {
|
|
if (file == null) continue;
|
|
if (file.lastModified() > latestModifiedDate) {
|
|
latestModifiedDate = file.lastModified();
|
|
advancements = file;
|
|
}
|
|
}
|
|
}
|
|
byte[] bytes = new byte[0];
|
|
if (advancements != null) {
|
|
bytes = Files.readAllBytes(advancements.toPath());
|
|
}
|
|
String json = new String(bytes, StandardCharsets.UTF_8);
|
|
|
|
// 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;
|
|
files[i] = advanceFile;
|
|
}
|
|
return files;
|
|
}
|
|
|
|
static int tick = 0;
|
|
|
|
@SubscribeEvent
|
|
public static void onUpdate(TickEvent.LevelTickEvent event) throws SQLException {
|
|
tick++;
|
|
if (tick == 1800) {
|
|
tick = 0;
|
|
long current = System.currentTimeMillis();
|
|
JDBCsetUp.executeUpdate("UPDATE server_info SET last_update =" + current + " WHERE id= " + JdbcConfig.SERVER_ID.get());
|
|
}
|
|
}
|
|
|
|
|
|
// New fields for auto-save
|
|
private static int autoSaveTickCounter = 0;
|
|
private static final int AUTO_SAVE_INTERVAL_TICKS = 1200; // Every Minute
|
|
|
|
//AutoSave
|
|
@SubscribeEvent
|
|
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
|
// Run at the end phase to avoid interfering with game logic
|
|
if (event.phase == TickEvent.Phase.END) {
|
|
autoSaveTickCounter++;
|
|
if (autoSaveTickCounter >= AUTO_SAVE_INTERVAL_TICKS) {
|
|
autoSaveTickCounter = 0;
|
|
// Retrieve the current server instance
|
|
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
|
if (server != null) {
|
|
// Iterate through all online players
|
|
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
|
executorService.submit(() -> {
|
|
try {
|
|
// Call the same store method used in logout and file save events.
|
|
store(player, false, server.isDedicatedServer());
|
|
} catch (Exception e) {
|
|
PlayerSync.LOGGER.error("Error auto-saving player " + player.getUUID(), e);
|
|
}
|
|
});
|
|
executorService.submit(() -> {
|
|
try {
|
|
new ModsSupport().StoreCurios(player, false);
|
|
} catch (SQLException e) {
|
|
PlayerSync.LOGGER.error("Error auto-saving Curios data for player " + player.getUUID(), e);
|
|
}
|
|
});
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|