diff --git a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java
index b24e0d9..6ae4e51 100644
--- a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java
+++ b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java
@@ -121,6 +121,39 @@ public class VanillaSync {
public static void removePlayerLock(String uuid) {
playerLocks.remove(uuid);
+ lastWrittenSnapshotHash.remove(uuid);
+ }
+
+ /**
+ * PHASE 7 PERF: per-player hash of the last successfully-written snapshot.
+ * Auto-save / periodic / dimension-change BG tasks skip the DB write when
+ * the new snapshot hashes identical to the last-written one — on an idle
+ * server with 35 players this cuts 95%+ of redundant UPDATE traffic.
+ *
+ *
Never used by logout/shutdown/death paths: those MUST always write
+ * to guarantee online=0 atomicity and capture the final state.
+ */
+ private static final ConcurrentHashMap lastWrittenSnapshotHash = new ConcurrentHashMap<>();
+
+ /** Cheap hash over the serialized snapshot. */
+ private static int computeSnapshotHash(PlayerDataSnapshot s) {
+ int h = 17;
+ h = 31 * h + java.util.Objects.hashCode(s.inventory());
+ h = 31 * h + java.util.Objects.hashCode(s.equipment());
+ h = 31 * h + java.util.Objects.hashCode(s.enderChest());
+ h = 31 * h + java.util.Objects.hashCode(s.effects());
+ h = 31 * h + java.util.Objects.hashCode(s.leftHand());
+ h = 31 * h + java.util.Objects.hashCode(s.cursors());
+ h = 31 * h + java.util.Objects.hashCode(s.advancements());
+ h = 31 * h + java.util.Objects.hashCode(s.curiosData());
+ h = 31 * h + java.util.Objects.hashCode(s.accessoriesData());
+ h = 31 * h + java.util.Objects.hashCode(s.cosmeticArmorData());
+ h = 31 * h + java.util.Objects.hashCode(s.attachmentsData());
+ h = 31 * h + s.xp();
+ h = 31 * h + s.foodLevel();
+ h = 31 * h + s.health();
+ h = 31 * h + s.score();
+ return h;
}
/**
@@ -997,7 +1030,16 @@ public class VanillaSync {
return;
}
}
- writeSnapshotToDB(snapshot);
+ // PHASE 7 PERF: skip write when snapshot hashes identical to last-written.
+ // Logout/shutdown/death paths do NOT use this optimization — only auto-save.
+ int newHash = computeSnapshotHash(snapshot);
+ Integer prev = lastWrittenSnapshotHash.get(puuid);
+ if (prev != null && prev == newHash) {
+ return; // identical — no DB write needed
+ }
+ if (writeSnapshotToDB(snapshot)) {
+ lastWrittenSnapshotHash.put(puuid, newHash);
+ }
} catch (Exception e) {
PlayerSync.LOGGER.error("Error writing async SaveToFile snapshot for player {}", puuid, e);
} finally {
@@ -1347,6 +1389,8 @@ public class VanillaSync {
// NOT carry a last_server guard themselves).
boolean persisted = writeSnapshotToDB(snapshot, true);
if (persisted) {
+ // Update hash so post-logout rejoin on same process doesn't double-write.
+ lastWrittenSnapshotHash.put(player_uuid, computeSnapshotHash(snapshot));
ModsSupport.saveBackpackSnapshots(backpackSnapshots);
ModsSupport.saveSSSnapshots(ssSnapshots);
if (!rs2DiskUuids.isEmpty() && rs2Level != null) {
@@ -1880,8 +1924,15 @@ public class VanillaSync {
return;
}
}
+ // PHASE 7 PERF: hash-skip identical snapshots.
+ int newHash = computeSnapshotHash(snapshot);
+ Integer prev = lastWrittenSnapshotHash.get(puuid);
+ if (prev != null && prev == newHash) {
+ return; // no-op
+ }
boolean persisted = writeSnapshotToDB(snapshot);
if (persisted) {
+ lastWrittenSnapshotHash.put(puuid, newHash);
ModsSupport.saveBackpackSnapshots(backpackSnapshots);
} else {
PlayerSync.LOGGER.warn("Staggered auto-save: core write blocked for {}", puuid);
diff --git a/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java b/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java
index d0ec270..7d0bced 100644
--- a/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java
+++ b/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java
@@ -577,11 +577,51 @@ public class ModsSupport {
* Can be called from a background thread (no entity access — data already captured).
*/
public static void saveBackpackSnapshots(Map snapshots) {
+ // PHASE 7 PERF: batch every REPLACE INTO into ONE transaction instead of
+ // N separate round-trips. With 3 backpacks + 2 shulkers + 4 disks a single
+ // logout save used to do 9 sequential commits — now 1.
+ if (snapshots == null || snapshots.isEmpty()) return;
+ List