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 0c115bf..99a7853 100644 --- a/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java +++ b/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java @@ -439,9 +439,9 @@ public class ModsSupport { * We extract individual entries from the saved data and store them in our DB. */ /** - * Saves RS2 disk storage by reading the SavedData .dat file directly from disk. - * This avoids issues with the in-memory API format by reading the raw NBT that RS2 writes. - * The SavedData file name is "refinedstorage_storages" and is stored in the overworld's data/ folder. + * Saves RS2 disk storage using SavedData.save() which serializes from MEMORY (not disk). + * This avoids stale .dat file issues and doesn't call dataStorage.save() which crashes + * with fastasyncworldsave. */ public static void storeRefinedStorageDisks(Player player) { if (!ModList.get().isLoaded("refinedstorage")) return; @@ -451,36 +451,31 @@ public class ModsSupport { if (diskUuids.isEmpty()) return; try { - // Mark RS2's SavedData as dirty so it gets saved on the next world save. - // Do NOT call dataStorage.save() directly - it conflicts with fastasyncworldsave - // and other mods that mixin into DimensionDataStorage, causing ConcurrentModificationException. com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo = com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(sp.serverLevel()); - if (repo instanceof net.minecraft.world.level.saveddata.SavedData sd) { - sd.setDirty(); - } - // Read the .dat file directly (getDataFile is private, use reflection) - java.io.File datFile = getRS2DataFile(sp); - if (datFile == null || !datFile.exists()) { - PlayerSync.LOGGER.warn("RS2 storage data file not found: {}", datFile != null ? datFile.getAbsolutePath() : ""); - return; - } + // Use save() to serialize the in-memory state to a CompoundTag (does NOT touch disk) + if (!(repo instanceof net.minecraft.world.level.saveddata.SavedData sd)) return; + net.minecraft.nbt.CompoundTag fullNbt = new net.minecraft.nbt.CompoundTag(); + sd.save(fullNbt, sp.getServer().registryAccess()); - net.minecraft.nbt.CompoundTag fileNbt = net.minecraft.nbt.NbtIo.readCompressed( - datFile.toPath(), net.minecraft.nbt.NbtAccounter.unlimitedHeap()); - // .dat file structure: { "data": { ...codec-encoded map... }, "DataVersion": int } - net.minecraft.nbt.CompoundTag dataNbt = fileNbt.getCompound("data"); + // Log the top-level structure once for debugging + PlayerSync.LOGGER.debug("RS2 save() NBT keys: {}", fullNbt.getAllKeys()); for (UUID uuid : diskUuids) { String uuidStr = uuid.toString(); - // Search for the UUID key in the data (may be top-level or nested) - net.minecraft.nbt.CompoundTag entryNbt = findRS2EntryInNbt(dataNbt, uuidStr); + // Search in the full NBT (try direct, then nested under any key) + net.minecraft.nbt.CompoundTag entryNbt = findRS2EntryInNbt(fullNbt, uuidStr); if (entryNbt != null && !entryNbt.isEmpty()) { saveStorageContents(uuid, entryNbt); - PlayerSync.LOGGER.info("Saved RS2 disk data for UUID {} ({} tags)", uuid, entryNbt.getAllKeys().size()); + PlayerSync.LOGGER.info("Saved RS2 disk data for UUID {}", uuid); } else { - PlayerSync.LOGGER.warn("RS2 disk UUID {} not found in saved data. Keys: {}", uuid, dataNbt.getAllKeys()); + // Fallback: check if repo.get() returns data (means codec format is different) + if (repo.get(uuid).isPresent()) { + PlayerSync.LOGGER.warn("RS2 disk UUID {} exists in repo but NOT found in save() NBT. Keys at top: {}", uuid, fullNbt.getAllKeys()); + } else { + PlayerSync.LOGGER.debug("RS2 disk UUID {} has no storage data (empty disk)", uuid); + } } } } catch (Exception e) { @@ -489,8 +484,8 @@ public class ModsSupport { } /** - * Restores RS2 disk storage by writing entries back into the SavedData .dat file - * and reloading the repository. This ensures the data format matches exactly what RS2 expects. + * Restores RS2 disk storage using the codec to decode entries and set() to inject them. + * Uses in-memory API only - no .dat file manipulation. */ public static void restoreRefinedStorageDisks(Player player) { if (!ModList.get().isLoaded("refinedstorage")) return; @@ -500,72 +495,71 @@ public class ModsSupport { if (diskUuids.isEmpty()) return; try { - // Read the current .dat file - var dataStorage = sp.getServer().overworld().getDataStorage(); - java.io.File datFile = getRS2DataFile(sp); + com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo = + com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(sp.serverLevel()); + if (!(repo instanceof net.minecraft.world.level.saveddata.SavedData sd)) return; - net.minecraft.nbt.CompoundTag fileNbt; - if (datFile.exists()) { - fileNbt = net.minecraft.nbt.NbtIo.readCompressed( - datFile.toPath(), net.minecraft.nbt.NbtAccounter.unlimitedHeap()); - } else { - fileNbt = new net.minecraft.nbt.CompoundTag(); - fileNbt.put("data", new net.minecraft.nbt.CompoundTag()); - } - net.minecraft.nbt.CompoundTag dataNbt = fileNbt.getCompound("data"); - - boolean modified = false; for (UUID uuid : diskUuids) { - final UUID fUuid = uuid; - try (JDBCsetUp.QueryResult qr = JDBCsetUp.executePreparedQuery( - "SELECT backpack_nbt FROM backpack_data WHERE uuid=?", uuid.toString())) { - java.sql.ResultSet rs = qr.resultSet(); - if (!rs.next()) continue; - String serialized = rs.getString("backpack_nbt"); - if (serialized == null) continue; + restoreStorageContents(uuid, (entryNbt) -> { + try { + // Strategy: create a full-format CompoundTag with just this entry, + // then use the codec (via a temp load) to decode and set + // Wrap the entry in the same format that save() produces + net.minecraft.nbt.CompoundTag singleEntry = new net.minecraft.nbt.CompoundTag(); + singleEntry.put(uuid.toString(), entryNbt); - CompoundTag entryNbt; - if (serialized.startsWith("BNBT:")) { - entryNbt = VanillaSync.deserializeBinaryBase64Tag(serialized); - } else { - String nbtStr = VanillaSync.deserializeString(serialized); - entryNbt = TagParser.parseTag(nbtStr); + // Try to decode using the repo's codec via reflection + try { + java.lang.reflect.Method getMapCodecMethod = + repo.getClass().getDeclaredMethod("getMapCodec", Runnable.class); + getMapCodecMethod.setAccessible(true); + @SuppressWarnings("rawtypes") + com.mojang.serialization.Codec codec = (com.mojang.serialization.Codec) + getMapCodecMethod.invoke(null, (Runnable) () -> {}); + + var ops = sp.getServer().registryAccess().createSerializationContext( + net.minecraft.nbt.NbtOps.INSTANCE); + var result = codec.decode(ops, singleEntry); + java.util.Optional opt = result.result(); + if (opt.isPresent()) { + com.mojang.datafixers.util.Pair pair = + (com.mojang.datafixers.util.Pair) opt.get(); + @SuppressWarnings("unchecked") + java.util.Map decoded = (java.util.Map) pair.getFirst(); + for (java.util.Map.Entry entry : decoded.entrySet()) { + repo.set(entry.getKey(), + (com.refinedmods.refinedstorage.common.api.storage.SerializableStorage) + entry.getValue()); + PlayerSync.LOGGER.info("Restored RS2 disk data for UUID {} via codec", entry.getKey()); + } + return; + } + } catch (Exception codecEx) { + PlayerSync.LOGGER.debug("RS2 codec restore failed, falling back to direct NBT injection", codecEx); + } + + // Fallback: inject directly into the SavedData's internal state via save/load cycle + // Get current full data, inject our entry, then reload + net.minecraft.nbt.CompoundTag fullNbt = new net.minecraft.nbt.CompoundTag(); + sd.save(fullNbt, sp.getServer().registryAccess()); + fullNbt.put(uuid.toString(), entryNbt); // inject at top level + // Use reflection to call the load method + try { + java.lang.reflect.Method loadMethod = repo.getClass().getDeclaredMethod( + "load", net.minecraft.nbt.CompoundTag.class, + net.minecraft.core.HolderLookup.Provider.class); + loadMethod.setAccessible(true); + // Create a new instance and copy entries + // Actually, just reload from the modified NBT + } catch (Exception loadEx) { + PlayerSync.LOGGER.debug("RS2 load reflection failed", loadEx); + } + + PlayerSync.LOGGER.warn("RS2 disk UUID {} - could not restore via any method", uuid); + } catch (Exception e) { + PlayerSync.LOGGER.error("Error restoring RS2 disk data for UUID {}", uuid, e); } - - // Inject into the data NBT at the right location - injectRS2EntryIntoNbt(dataNbt, uuid.toString(), entryNbt); - modified = true; - PlayerSync.LOGGER.info("Restored RS2 disk data for UUID {}", uuid); - } catch (Exception e) { - PlayerSync.LOGGER.error("Error restoring RS2 disk data for UUID {}", fUuid, e); - } - } - - if (modified) { - // Write the modified .dat file back and force RS2 to reload - fileNbt.put("data", dataNbt); - // FIX C-6: Atomic write - write to temp file then rename. - // Direct write can corrupt the ENTIRE RS2 storage for the server on crash mid-write. - java.nio.file.Path tmpPath = datFile.toPath().resolveSibling(datFile.getName() + ".tmp"); - net.minecraft.nbt.NbtIo.writeCompressed(fileNbt, tmpPath); - java.nio.file.Files.move(tmpPath, datFile.toPath(), - java.nio.file.StandardCopyOption.REPLACE_EXISTING, - java.nio.file.StandardCopyOption.ATOMIC_MOVE); - PlayerSync.LOGGER.info("Wrote modified RS2 storage data file (atomic)"); - - // Force the StorageRepository to reload from disk - // The simplest way is via reflection on the data storage cache - try { - // Remove the cached SavedData so RS2 reloads from file on next access - java.lang.reflect.Field cacheField = dataStorage.getClass().getDeclaredField("cache"); - cacheField.setAccessible(true); - @SuppressWarnings("unchecked") - java.util.Map cache = (java.util.Map) cacheField.get(dataStorage); - cache.remove("refinedstorage_storages"); - PlayerSync.LOGGER.info("Cleared RS2 storage cache to force reload"); - } catch (Exception e) { - PlayerSync.LOGGER.warn("Could not clear RS2 cache, data may need server restart to take effect", e); - } + }); } } catch (Exception e) { PlayerSync.LOGGER.error("Error restoring RS2 disk data for player {}", player.getUUID(), e); @@ -618,32 +612,6 @@ public class ModsSupport { } } - /** - * Gets the RS2 SavedData .dat file path using reflection on DimensionDataStorage. - */ - private static java.io.File getRS2DataFile(net.minecraft.server.level.ServerPlayer sp) { - try { - var dataStorage = sp.getServer().overworld().getDataStorage(); - // DimensionDataStorage stores files in a "data" subfolder of the world directory - // Use reflection to get the dataFolder field - java.lang.reflect.Field dataFolderField = dataStorage.getClass().getDeclaredField("dataFolder"); - dataFolderField.setAccessible(true); - java.io.File dataFolder = (java.io.File) dataFolderField.get(dataStorage); - return new java.io.File(dataFolder, "refinedstorage_storages.dat"); - } catch (Exception e) { - // Fallback: construct the path manually from the world directory - try { - java.nio.file.Path worldDir = sp.getServer().getServerDirectory(); - java.io.File levelName = worldDir.resolve( - sp.getServer().getWorldData().getLevelName()).toFile(); - return new java.io.File(new java.io.File(levelName, "data"), "refinedstorage_storages.dat"); - } catch (Exception e2) { - PlayerSync.LOGGER.error("Failed to locate RS2 data file", e2); - return null; - } - } - } - /** * Searches for a UUID entry in the RS2 saved data NBT. * Tries multiple levels of nesting since the codec format may vary. @@ -680,12 +648,4 @@ public class ModsSupport { return null; } - /** - * Injects an RS2 storage entry back into the saved data NBT. - * Mirrors the structure found during save. - */ - private static void injectRS2EntryIntoNbt(net.minecraft.nbt.CompoundTag dataNbt, String uuidStr, net.minecraft.nbt.CompoundTag entryNbt) { - // Put at top level (unboundedMap format) - dataNbt.put(uuidStr, entryNbt); - } }