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:
laforetbrut 2026-03-26 11:39:44 +01:00
parent c63d5849a3
commit 5576d7f7e2

View File

@ -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