PlayerSync/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java
EoD 6ac6f297af encode unknown items using Paper
This allows using PlayerSync with different minecraft versions and
even different sets of mods.

All unknown items are replaced by Paper with its original NBT data
encoded into the paper item.
2025-05-04 16:48:57 +00:00

629 lines
29 KiB
Java

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;
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.minecraft.world.item.Items;
import net.minecraft.world.level.storage.WorldData;
import net.minecraftforge.event.OnDatapackSyncEvent;
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.registries.ForgeRegistries;
import net.minecraftforge.server.ServerLifecycleHooks;
import vip.fubuki.playersync.PlayerSync;
import vip.fubuki.playersync.config.JdbcConfig;
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.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;
@Mod.EventBusSubscriber
public class VanillaSync {
public static void register() {}
static ExecutorService executorService = Executors.newCachedThreadPool(new PSThreadPoolFactory("PlayerSync"));
@SubscribeEvent
public static void onDataPackSyncEvent(OnDatapackSyncEvent event) throws SQLException, IOException {
if (!JdbcConfig.SYNC_ADVANCEMENTS.get())
return; // advancement sync disabled
final ServerPlayer serverPlayer = event.getPlayer();
if (serverPlayer == null) {
PlayerSync.LOGGER.debug("No player joining");
return;
}
final String player_uuid = serverPlayer.getUUID().toString();
PlayerSync.LOGGER.info("Player entity joining level " + player_uuid);
JDBCsetUp.QueryResult advancementsQuery = JDBCsetUp
.executeQuery("SELECT advancements FROM player_data WHERE uuid='" + player_uuid + "'");
ResultSet advancementsResultSet = advancementsQuery.resultSet();
if (!advancementsResultSet.next()) {
PlayerSync.LOGGER.debug("No advancements found for player " + player_uuid);
advancementsResultSet.close();
return;
}
// Restore Advancements
File gameDir = Objects.requireNonNull(serverPlayer.getServer()).getServerDirectory();
final MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server != null && server.isDedicatedServer()) {
PlayerSync.LOGGER.debug("Attempting to write dedicated server advancement file");
File advancements = new File(gameDir,
getSyncWorldForServer() + "/advancements" + "/" + player_uuid + ".json");
byte[] bytes = advancementsResultSet.getString("advancements").getBytes();
advancementsResultSet.close();
// only create advancements file if at least "{}" has been stored in the field
if (bytes.length < 2) {
PlayerSync.LOGGER.debug("Skip writing advancements for player " + player_uuid);
return;
}
File advancementsDir = advancements.getParentFile();
if (advancementsDir != null && !advancementsDir.exists()) {
PlayerSync.LOGGER.info("Creating advancements directory " + advancementsDir.getPath());
boolean createdDir = advancementsDir.mkdirs();
if (!createdDir) {
PlayerSync.LOGGER.error("Aborting advancements sync. Failed to create advancements "
+ "directory at " + advancementsDir.getPath());
return;
}
}
if (!advancements.exists()) {
try {
PlayerSync.LOGGER.info("Creating new advancement file for player " + player_uuid);
advancements.createNewFile();
} catch (IOException e) {
PlayerSync.LOGGER.error("Aborting advancements sync. Failed to create advancements file at "
+ advancements.getAbsolutePath(), e);
return;
}
}
PlayerSync.LOGGER.debug("Writing advancement file " + advancements.toPath() + " for player " + player_uuid);
PlayerSync.LOGGER.trace("Writing advancement file for player " + player_uuid + ": "
+ new String(bytes, StandardCharsets.UTF_8));
Files.write(advancements.toPath(), bytes);
// reload the json files on the server after updating them
PlayerAdvancements playeradvancements = serverPlayer.getAdvancements();
playeradvancements.reload(server.getAdvancements());
} else {
PlayerSync.LOGGER.debug("Writing non-dedicated server advancement files");
File[] files = scanAdvancementsFile(player_uuid, gameDir);
for (File file : files) {
if (file == null)
continue;
byte[] bytes = advancementsResultSet.getString("advancements").getBytes();
Files.write(file.toPath(), bytes);
}
advancementsResultSet.close();
}
}
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);
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");
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<Integer, String> equipment = LocalJsonUtil.StringToEntryMap(armor_data);
for (Map.Entry<Integer, String> entry : equipment.entrySet()) {
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(), 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(), deserializeAndCreatePlaceholderIfNeeded(entry.getValue()));
}
// 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);
}
}
}
}
// 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();
}
});
}
// 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;
}
/**
* Deserializes a string from the database back into an NBT string.
* Handles both the new Base64 format (prefixed with "B64:") and the old custom format.
* @param encoded The string retrieved from the database.
* @return The deserialized NBT string.
*/
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("~", "'");
}
/**
* Serializes an NBT string for database storage.
* Uses Base64 encoding by default (prefixed with "B64:").
* If USE_LEGACY_SERIALIZATION config is true, uses the old custom replacement format.
* @param object The NBT string to serialize.
* @return The serialized string.
*/
public static String serialize(String object) {
// Check the config option for backwards compatibility during writing
if (JdbcConfig.USE_LEGACY_SERIALIZATION.get()) {
// Use old custom replacement logic
return object.replace(",", "|")
.replace("\"", "^")
.replace("{", "<")
.replace("}", ">")
.replace("'", "~");
}
// 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);
}
@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);
}
@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();
}
});
}
// 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 + ")");
// 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 = getNbtForStorage(player.getItemInHand(InteractionHand.OFF_HAND));
// Cursor
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, getNbtForStorage(itemStack));
}
// Inventory
Inventory inventory = player.getInventory();
Map<Integer, String> inventoryMap = new HashMap<>();
for (int i = 0; i < inventory.items.size(); i++) {
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++) {
ender_chest.put(i, getNbtForStorage(player.getEnderChestInventory().getItem(i)));
}
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;
byte[] advancementBytes = new byte[0];
if (JdbcConfig.SYNC_ADVANCEMENTS.get()) {
File gameDir = Objects.requireNonNull(player.getServer()).getServerDirectory();
final MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server != null && server.isDedicatedServer() ) {
PlayerSync.LOGGER.trace("Reading dedicated server advancements");
advancements = new File(gameDir, getSyncWorldForServer() + "/advancements" + "/" + player_uuid + ".json");
} else {
PlayerSync.LOGGER.debug("Reading non-dedicated server advancements");
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;
}
}
}
if (!advancements.exists()) {
PlayerSync.LOGGER.warn("Advancements file for " + player_uuid + " does not exist (yet).");
}
if (advancements != null && advancements.exists()) {
PlayerSync.LOGGER.debug("Storing advancements for " + player_uuid + " from " + advancements.toPath());
advancementBytes = Files.readAllBytes(advancements.toPath());
} else {
PlayerSync.LOGGER.error("Unable to save advancements for player " + player_uuid);
}
}
String json = new String(advancementBytes, StandardCharsets.UTF_8);
PlayerSync.LOGGER.trace("Storing advancements for player " + player_uuid + ": " + json);
// 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 String getSyncWorldForServer() {
if (!JdbcConfig.SYNC_WORLD.get().isEmpty()) {
PlayerSync.LOGGER.warn("Using configuration 'sync_world' on servers is deprecated. Please leave the array empty. Falling back to first entry.");
return JdbcConfig.SYNC_WORLD.get().get(0);
}
final MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server == null) {
PlayerSync.LOGGER.error("Unable to get current server. Assuming default level-name 'world'.");
return "world";
}
final WorldData worldData = server.getWorldData();
final String levelName = worldData.getLevelName();
PlayerSync.LOGGER.debug("Using server level-name: " + levelName);
return levelName;
}
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);
} 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);
}
});
}
}
}
}
}
}