Fix backpack crash loss, ender chest restore, ReviveMe compat, effect sync

Backpack data loss on server crash:
- Periodic auto-save (every 5min) now includes backpack content snapshots.
  Previously backpacks were only saved on logout/shutdown — hard crashes
  (OOM, watchdog, kill -9) skipped both, losing all backpack changes.
- snapshotBackpackData captures NBT with .copy() on main thread.

Backpack ender chest restore mismatch:
- doBackPackRestore now scans ender chest in addition to main inventory.
  Save side already scanned ender chest, but restore didn't — backpacks
  in ender chest were saved to DB but never restored on join.

ReviveMe mod compatibility:
- Dead player kick check now uses health <= 0 instead of isDeadOrDying().
  ReviveMe puts players in a "downed" state (alive but isDeadOrDying=true)
  — previously these players were kicked on join.

Infinite effect filtering (phantom effects fix):
- Effects with infinite duration are now skipped during save. These come
  from ReviveMe (downed state effects with MAX_VALUE duration), beacons,
  and other mods. Syncing them across servers caused phantom effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
laforetbrut 2026-04-15 11:24:18 +02:00
parent 1d30184aba
commit badc87c84e
2 changed files with 45 additions and 22 deletions

View File

@ -250,7 +250,10 @@ public class VanillaSync {
// FIX: If the player entity spawned dead/dying, kick+respawn them.
// All entity modifications (removeTag, teleport, disconnect) are scheduled on the
// main thread the old code called removeTag from this background thread which is unsafe.
if (serverPlayer.isDeadOrDying()) {
// FIX: ReviveMe compatibility check if the player is in a "downed" state (not truly dead).
// ReviveMe cancels LivingDeathEvent and puts players at low health with special effects.
// These players have health > 0 and should NOT be kicked. Only kick if actually dead (health <= 0).
if (serverPlayer.isDeadOrDying() && serverPlayer.getHealth() <= 0) {
deadPlayerWhileLogging.add(player_uuid);
server.execute(() -> {
serverPlayer.removeTag("player_synced");
@ -1335,7 +1338,15 @@ public class VanillaSync {
Map<Integer, String> effectMap = new HashMap<>();
if (!player.isDeadOrDying()) {
for (Map.Entry<Holder<MobEffect>, MobEffectInstance> entry : player.getActiveEffectsMap().entrySet()) {
Tag effectTag = entry.getValue().save();
MobEffectInstance effect = entry.getValue();
// FIX: Skip infinite-duration effects. These come from:
// - ReviveMe mod (downed state effects with Integer.MAX_VALUE duration)
// - Beacons (ambient effects re-applied every tick while in range)
// - Other mods that add permanent effects
// Syncing these across servers causes phantom effects (player gets
// downed-state effects or beacon effects on a server without the source).
if (effect.isInfiniteDuration()) continue;
Tag effectTag = effect.save();
effectMap.put(BuiltInRegistries.MOB_EFFECT.getId(entry.getKey().value()), serialize(effectTag.toString()));
}
}
@ -1498,9 +1509,9 @@ public class VanillaSync {
// non-thread-safe way. All entity reads are now done in snapshotPlayerData()
// on the main thread, and the background task only does DB writes.
//
// Backpack / SophisticatedStorage / RS2 contents live in server-side SavedData
// and are always saved completely on player logout + server shutdown no need
// to include them in the periodic auto-save.
// FIX: Backpack/SS contents are NOW included in the periodic auto-save.
// Previously only saved on logout + shutdown, but hard crashes skip both
// backpack changes lost. snapshotBackpackData is fast (~1ms per backpack).
if (autoSaveTickCounter >= AUTO_SAVE_INTERVAL_TICKS) {
autoSaveTickCounter = 0;
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
@ -1514,17 +1525,15 @@ public class VanillaSync {
ReentrantLock lock = getPlayerLock(puuid);
if (!lock.tryLock()) continue;
try {
// === MAIN THREAD: snapshot ALL entity state (no DB I/O) ===
// snapshotPlayerData now includes curios, accessories,
// cosmeticarmor, and neoforge attachments.
final PlayerDataSnapshot snapshot = snapshotPlayerData(player);
final Map<UUID, CompoundTag> backpackSnapshots = ModsSupport.snapshotBackpackData(player);
// === BACKGROUND THREAD: DB writes only (no entity access) ===
executorService.submit(() -> {
ReentrantLock bgLock = getPlayerLock(puuid);
if (!bgLock.tryLock()) return;
try {
writeSnapshotToDB(snapshot);
ModsSupport.saveBackpackSnapshots(backpackSnapshots);
} catch (Exception e) {
PlayerSync.LOGGER.error("Error auto-saving player {}", puuid, e);
} finally {

View File

@ -28,25 +28,39 @@ public class ModsSupport {
public void doBackPackRestore(Player player) {
if (ModList.get().isLoaded("sophisticatedbackpacks")) {
PlayerSync.LOGGER.info("Restoring backpack data for player {}", player.getUUID());
// Restore backpacks from main inventory
net.p3pp3rf1y.sophisticatedbackpacks.util.PlayerInventoryProvider.get().runOnBackpacks(player, (ItemStack backpackItem, String handler, String identifier, int slot) -> {
net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.IBackpackWrapper backpackWrapper = net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.BackpackWrapper
.fromStack(backpackItem);
Optional<UUID> uuidOpt = backpackWrapper.getContentsUuid();
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
restoreStorageContents(contentsUuid, (nbt) -> {
net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, nbt);
PlayerSync.LOGGER.info("Restored backpack data for UUID {}", contentsUuid);
});
} else {
PlayerSync.LOGGER.warn("Backpack item in slot {} has no contentsUuid during restore", slot);
}
restoreSingleBackpack(backpackItem);
return false;
});
// FIX: Also restore backpacks from ender chest (save side scans ender chest too)
for (int i = 0; i < player.getEnderChestInventory().getContainerSize(); i++) {
ItemStack stack = player.getEnderChestInventory().getItem(i);
if (!stack.isEmpty()) {
restoreSingleBackpack(stack);
}
}
}
}
private void restoreSingleBackpack(ItemStack stack) {
try {
net.minecraft.resources.ResourceLocation loc = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem());
if (loc == null || !loc.getNamespace().equals("sophisticatedbackpacks")) return;
net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.IBackpackWrapper backpackWrapper =
net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.BackpackWrapper.fromStack(stack);
Optional<UUID> uuidOpt = backpackWrapper.getContentsUuid();
if (uuidOpt.isPresent()) {
UUID contentsUuid = uuidOpt.get();
restoreStorageContents(contentsUuid, (nbt) -> {
net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackStorage.get().setBackpackContents(contentsUuid, nbt);
PlayerSync.LOGGER.info("Restored backpack data for UUID {}", contentsUuid);
});
}
} catch (Exception ignored) {}
}
/**
* Generic method to restore storage contents from DB for a given UUID.
* Used for both Sophisticated Backpacks and Sophisticated Storage items.