Fix RS2 disk + SS shulker data loss: use in-memory API, not .dat files

ROOT CAUSE for both:
- RS2: We removed dataStorage.save() to avoid fastasyncworldsave crash,
  but then read the .dat file which had stale data. Disks appeared
  empty because the file didn't contain the latest in-memory state.
- SS: getOrCreateStorageContents() could create empty content if the
  data wasn't loaded yet for that UUID.

FIX RS2:
- Save: Use SavedData.save(CompoundTag, Provider) which serializes
  from MEMORY, not disk. No file I/O, no fastasyncworldsave conflict.
- Restore: Decode entries via RS2's codec (reflection on getMapCodec)
  and inject via repo.set(). Falls back to direct NBT injection if
  codec fails.
- Removed dead code: getRS2DataFile, injectRS2EntryIntoNbt

FIX SS:
- Already using StackStorageWrapper.fromStack() API for UUID extraction
  (DataComponent-based, not CustomData). This was fixed in previous
  commit. If data still missing, the save() logging will show which
  UUIDs fail to find in ItemContentsStorage.

Vyrriox
This commit is contained in:
laforetbrut 2026-03-26 19:12:02 +01:00
parent 7c89df7d1b
commit 6bb8aeba39

View File

@ -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() : "<null>");
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<UUID, ?> decoded = (java.util.Map<UUID, ?>) pair.getFirst();
for (java.util.Map.Entry<UUID, ?> 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<String, ?> cache = (java.util.Map<String, ?>) 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);
}
}