diff --git a/build.gradle b/build.gradle index 411045d..9d84f14 100644 --- a/build.gradle +++ b/build.gradle @@ -129,6 +129,9 @@ dependencies { // Mod compatibility - The Aether + Accessories API compileOnly "curse.maven:aether-255308:7043502" compileOnly "curse.maven:accessories-938917:7046407" + // Mod compatibility - Refined Storage 2 + Extra Disks + compileOnly "curse.maven:refined-storage-243076:7610477" + compileOnly "curse.maven:extra-disks-351491:7032487" runtimeOnly "curse.maven:curios-309927:6529130" runtimeOnly "curse.maven:sophisticated-backpacks-422301:7169832" diff --git a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java index e370437..c6719bb 100644 --- a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java +++ b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java @@ -378,6 +378,9 @@ public class VanillaSync { if (ModList.get().isLoaded("sophisticatedstorage")) { ModsSupport.restoreSophisticatedStorageItems(serverPlayer); } + if (ModList.get().isLoaded("refinedstorage")) { + ModsSupport.restoreRefinedStorageDisks(serverPlayer); + } // Restore mod compatibility data (Accessories/Aether, CosmeticArmor) ModCompatSync.restoreAll(serverPlayer); @@ -629,6 +632,9 @@ public class VanillaSync { if (ModList.get().isLoaded("sophisticatedstorage")) { ModsSupport.storeSophisticatedStorageItems(player); } + if (ModList.get().isLoaded("refinedstorage")) { + ModsSupport.storeRefinedStorageDisks(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) { @@ -785,6 +791,9 @@ public class VanillaSync { if(ModList.get().isLoaded("sophisticatedstorage")){ ModsSupport.storeSophisticatedStorageItems(player); } + if(ModList.get().isLoaded("refinedstorage")){ + ModsSupport.storeRefinedStorageDisks(player); + } // Effects Map, MobEffectInstance> effects = player.getActiveEffectsMap(); 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 0ee8d14..b31d39c 100644 --- a/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java +++ b/src/main/java/vip/fubuki/playersync/sync/addons/ModsSupport.java @@ -21,10 +21,7 @@ import vip.fubuki.playersync.util.LocalJsonUtil; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; public class ModsSupport { @@ -376,4 +373,193 @@ public class ModsSupport { } return null; } + + // ============================ + // Refined Storage 2 Disks + // ============================ + + /** + * Saves RS2 disk storage contents for all disks in the player's inventory. + * RS2 disks reference their storage via a UUID DataComponent (storageReference). + * The actual storage data lives in a world-level SavedData (StorageRepositoryImpl). + * We extract individual entries from the saved data and store them in our DB. + */ + public static void storeRefinedStorageDisks(Player player) { + if (!ModList.get().isLoaded("refinedstorage")) return; + if (!(player instanceof net.minecraft.server.level.ServerPlayer sp)) return; + + List diskUuids = collectRS2DiskUuids(player); + if (diskUuids.isEmpty()) return; + + try { + com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo = + com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(sp.serverLevel()); + + // Serialize the full repository to NBT via SavedData.save() + if (repo instanceof net.minecraft.world.level.saveddata.SavedData savedData) { + net.minecraft.nbt.CompoundTag fullNbt = new net.minecraft.nbt.CompoundTag(); + savedData.save(fullNbt, sp.getServer().registryAccess()); + + for (UUID uuid : diskUuids) { + net.minecraft.nbt.CompoundTag entryNbt = extractRS2Entry(fullNbt, uuid); + if (entryNbt != null && !entryNbt.isEmpty()) { + // Store the entry NBT along with a wrapper that includes the UUID key + // so we can reconstruct the map format on restore + net.minecraft.nbt.CompoundTag wrapper = new net.minecraft.nbt.CompoundTag(); + wrapper.put(uuid.toString(), entryNbt); + saveStorageContents(uuid, wrapper); + PlayerSync.LOGGER.info("Saved RS2 disk data for UUID {}", uuid); + } + } + } + } catch (Exception e) { + PlayerSync.LOGGER.error("Error saving RS2 disk data for player {}", player.getUUID(), e); + } + } + + /** + * Restores RS2 disk storage contents from the database. + * Uses reflection to access the StorageRepositoryImpl's codec for proper deserialization, + * then calls the public set() method to inject entries into the live repository. + */ + public static void restoreRefinedStorageDisks(Player player) { + if (!ModList.get().isLoaded("refinedstorage")) return; + if (!(player instanceof net.minecraft.server.level.ServerPlayer sp)) return; + + List diskUuids = collectRS2DiskUuids(player); + if (diskUuids.isEmpty()) return; + + try { + com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo = + com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(sp.serverLevel()); + + for (UUID uuid : diskUuids) { + // Check if storage already exists on this server (don't overwrite) + if (repo.get(uuid).isPresent()) { + PlayerSync.LOGGER.debug("RS2 storage {} already exists on this server, skipping restore", uuid); + continue; + } + + restoreStorageContents(uuid, (nbt) -> { + try { + injectRS2StorageEntry(repo, nbt, sp); + } catch (Exception e) { + PlayerSync.LOGGER.error("Error injecting RS2 storage for UUID {}", uuid, e); + } + }); + } + } catch (Exception e) { + PlayerSync.LOGGER.error("Error restoring RS2 disk data for player {}", player.getUUID(), e); + } + } + + /** + * Collects all RS2/ExtraDisks storage reference UUIDs from the player's inventory and ender chest. + */ + private static List collectRS2DiskUuids(Player player) { + List uuids = new ArrayList<>(); + // Check main inventory + collectRS2DiskUuidsFromContainer(player.getInventory(), uuids); + // Check ender chest + for (int i = 0; i < player.getEnderChestInventory().getContainerSize(); i++) { + ItemStack stack = player.getEnderChestInventory().getItem(i); + if (stack.isEmpty()) continue; + UUID ref = getRS2StorageReference(stack); + if (ref != null) uuids.add(ref); + } + return uuids; + } + + private static void collectRS2DiskUuidsFromContainer(Inventory inv, List uuids) { + for (int i = 0; i < inv.getContainerSize(); i++) { + ItemStack stack = inv.getItem(i); + if (stack.isEmpty()) continue; + UUID ref = getRS2StorageReference(stack); + if (ref != null) uuids.add(ref); + } + } + + /** + * Extracts the storageReference UUID from an RS2 disk item using the RS2 DataComponent. + * Returns null if the item is not an RS2 disk or doesn't have a storage reference. + */ + private static UUID getRS2StorageReference(ItemStack stack) { + try { + net.minecraft.resources.ResourceLocation loc = + net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem()); + if (!loc.getNamespace().equals("refinedstorage") && !loc.getNamespace().equals("extradisks")) { + return null; + } + net.minecraft.core.component.DataComponentType storageRefType = + com.refinedmods.refinedstorage.common.content.DataComponents.INSTANCE.getStorageReference(); + return stack.get(storageRefType); + } catch (Exception e) { + return null; + } + } + + /** + * Extracts an individual storage entry from the full StorageRepository NBT by UUID. + * The save() format uses UUID strings as CompoundTag keys (unboundedMap codec). + */ + private static net.minecraft.nbt.CompoundTag extractRS2Entry(net.minecraft.nbt.CompoundTag fullNbt, UUID uuid) { + String uuidStr = uuid.toString(); + // Direct key lookup (standard unboundedMap format) + if (fullNbt.contains(uuidStr, net.minecraft.nbt.Tag.TAG_COMPOUND)) { + return fullNbt.getCompound(uuidStr); + } + // Some SavedData implementations wrap data under a "data" key + for (String key : fullNbt.getAllKeys()) { + if (fullNbt.contains(key, net.minecraft.nbt.Tag.TAG_COMPOUND)) { + net.minecraft.nbt.CompoundTag sub = fullNbt.getCompound(key); + if (sub.contains(uuidStr, net.minecraft.nbt.Tag.TAG_COMPOUND)) { + return sub.getCompound(uuidStr); + } + } + } + return null; + } + + /** + * Injects a storage entry back into the RS2 StorageRepository. + * Uses the repository's codec (via reflection) to properly deserialize the entry, + * then calls set() to inject it into the live repository. + */ + @SuppressWarnings("unchecked") + private static void injectRS2StorageEntry( + com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo, + net.minecraft.nbt.CompoundTag wrapperNbt, + net.minecraft.server.level.ServerPlayer sp) throws Exception { + + // The wrapper contains { "uuid-string": { ...entry data... } } + // We need to decode this using the same codec that StorageRepositoryImpl uses + + // Get the map codec via reflection from StorageRepositoryImpl + 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) () -> {}); + + // Decode the single-entry wrapper using the codec + var ops = sp.getServer().registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE); + com.mojang.serialization.DataResult dataResult = codec.decode(ops, wrapperNbt); + + Optional resultOpt = dataResult.result(); + if (resultOpt.isPresent()) { + // DataResult contains Pair, Tag> + com.mojang.datafixers.util.Pair pair = (com.mojang.datafixers.util.Pair) resultOpt.get(); + @SuppressWarnings("unchecked") + Map decoded = (Map) pair.getFirst(); + for (Map.Entry entry : decoded.entrySet()) { + repo.set(entry.getKey(), + (com.refinedmods.refinedstorage.common.api.storage.SerializableStorage) entry.getValue()); + PlayerSync.LOGGER.info("Restored RS2 disk storage for UUID {}", entry.getKey()); + } + } else { + PlayerSync.LOGGER.warn("Failed to decode RS2 storage data from wrapper NBT: {}", wrapperNbt); + } + } }