Add anti-duplication locks, shutdown save, and security hardening
- Per-player ReentrantLock prevents concurrent save/restore operations,
eliminating race conditions that could cause item duplication
- Save ALL online players on ServerStoppingEvent (before disconnect) to
prevent data loss from server shutdowns/restarts
- Lock acquired before restore on join, released in finally block
- Lock acquired before save on logout, cleaned up after completion
- Verified compatibility with 430-mod Arcadia V2 modpack:
- All item DataComponents from all mods preserved via BNBT serialization
- Curios items (Artifacts, Elytra Slot, Charm of Undying, etc.) synced
- Accessories items (Aether, Deep Aether) synced
- Server-specific data (FTB Quests/Chunks, Waystones, Lootr) correctly
NOT synced as intended
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c63d5849a3
commit
5576d7f7e2
|
|
@ -37,7 +37,7 @@ 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.server.ServerStoppingEvent;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
import net.neoforged.neoforge.server.ServerLifecycleHooks;
|
||||
import vip.fubuki.playersync.PlayerSync;
|
||||
|
|
@ -60,6 +60,7 @@ import java.util.*;
|
|||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@EventBusSubscriber(modid = PlayerSync.MODID)
|
||||
public class VanillaSync {
|
||||
|
|
@ -68,6 +69,17 @@ public class VanillaSync {
|
|||
|
||||
static ExecutorService executorService = Executors.newCachedThreadPool(new PSThreadPoolFactory("PlayerSync"));
|
||||
|
||||
// Per-player locks to prevent concurrent save/restore operations (anti-duplication)
|
||||
private static final ConcurrentHashMap<String, ReentrantLock> playerLocks = new ConcurrentHashMap<>();
|
||||
|
||||
private static ReentrantLock getPlayerLock(String uuid) {
|
||||
return playerLocks.computeIfAbsent(uuid, k -> new ReentrantLock());
|
||||
}
|
||||
|
||||
public static void removePlayerLock(String uuid) {
|
||||
playerLocks.remove(uuid);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onDataPackSyncEvent(OnDatapackSyncEvent event) throws SQLException, IOException {
|
||||
if (!JdbcConfig.SYNC_ADVANCEMENTS.get())
|
||||
|
|
@ -236,6 +248,9 @@ public class VanillaSync {
|
|||
return;
|
||||
}
|
||||
|
||||
// Acquire per-player lock to prevent concurrent save/restore (anti-duplication)
|
||||
ReentrantLock lock = getPlayerLock(player_uuid);
|
||||
lock.lock();
|
||||
try {
|
||||
PlayerSync.LOGGER.info("Starting synchronization for player {}", player_uuid);
|
||||
|
||||
|
|
@ -362,6 +377,8 @@ public class VanillaSync {
|
|||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Internal Exception detected!", e);
|
||||
syncNotCompletedPlayer.remove(player_uuid);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -582,15 +599,49 @@ public class VanillaSync {
|
|||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onServerShutdown(ServerStoppedEvent event) throws SQLException {
|
||||
public static void onServerShutdown(ServerStoppingEvent event) throws SQLException {
|
||||
// Save ALL online players before shutdown to prevent data loss
|
||||
// Uses ServerStoppingEvent (not ServerStoppedEvent) because players are still connected
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
if (server != null) {
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
if (player.getTags().contains("player_synced") && !player.isDeadOrDying()) {
|
||||
try {
|
||||
store(player, false);
|
||||
if (ModList.get().isLoaded("curios")) {
|
||||
new ModsSupport().StoreCurios(player, false);
|
||||
}
|
||||
ModCompatSync.storeAll(player);
|
||||
if (ModList.get().isLoaded("sophisticatedbackpacks")) {
|
||||
ModsSupport.storeSophisticatedBackpacks(player);
|
||||
}
|
||||
if (ModList.get().isLoaded("sophisticatedstorage")) {
|
||||
ModsSupport.storeSophisticatedStorageItems(player);
|
||||
}
|
||||
JDBCsetUp.executePreparedUpdate("UPDATE player_data SET online=0 WHERE uuid=?", player.getUUID().toString());
|
||||
PlayerSync.LOGGER.info("Saved player {} data on server shutdown", player.getUUID());
|
||||
} catch (Exception e) {
|
||||
PlayerSync.LOGGER.error("Error saving player {} on shutdown", player.getUUID(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
JDBCsetUp.executePreparedUpdate("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();
|
||||
// FIX: Save data BEFORE marking offline to prevent data loss on quick reconnect
|
||||
store(event.getEntity(), false);
|
||||
JDBCsetUp.executePreparedUpdate("UPDATE player_data SET online=0 WHERE uuid=?", player_uuid);
|
||||
// Acquire per-player lock to prevent concurrent save/restore (anti-duplication)
|
||||
ReentrantLock lock = getPlayerLock(player_uuid);
|
||||
lock.lock();
|
||||
try {
|
||||
// FIX: Save data BEFORE marking offline to prevent data loss on quick reconnect
|
||||
store(event.getEntity(), false);
|
||||
JDBCsetUp.executePreparedUpdate("UPDATE player_data SET online=0 WHERE uuid=?", player_uuid);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
removePlayerLock(player_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user