From 131aa64eb1fa5354ff19be01ccc2d07904e9b565 Mon Sep 17 00:00:00 2001 From: laforetbrut Date: Wed, 22 Apr 2026 07:03:08 +0200 Subject: [PATCH] Add /playersync inventory viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New op command to pretty-print a player's stored inventory from the DB. Works on offline players — reads the serialized columns directly and deserializes each slot through the same deserializeAndCreatePlaceholderIfNeeded path used by the normal restore. Usage: /playersync inventory — everything (main + armor + ender + curios) /playersync inventory main — 36-slot hotbar + main inventory only /playersync inventory armor — 4 armor slots (0=boots, 1=legs, 2=chest, 3=helm) /playersync inventory ender — 27 ender chest slots /playersync inventory curios — Curios slots (funct + cosmetic), composite-keyed Output per section lists only non-empty slots: [5] minecraft:diamond_sword x1 [8] sophisticatedbackpacks:backpack x1 (Gilded Backpack) [cos:back:0] [placeholder] minecraft:paper x1 <- cross-server missing mod Placeholder items (items from a mod not loaded on this server) are tagged [placeholder] in magenta so admins can see at a glance which slots contain 'travelling' items. Parse errors on a single slot don't break the listing — the affected slot shows and the rest continues. Help listing updated. No other behavior changed. --- .../vip/fubuki/playersync/CommandInit.java | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/src/main/java/vip/fubuki/playersync/CommandInit.java b/src/main/java/vip/fubuki/playersync/CommandInit.java index 2872113..b2d8959 100644 --- a/src/main/java/vip/fubuki/playersync/CommandInit.java +++ b/src/main/java/vip/fubuki/playersync/CommandInit.java @@ -87,6 +87,21 @@ public class CommandInit { .then(Commands.literal("confirm") .executes(CommandInit::runWipe)))) + // ---- Inventory viewer ---- + .then(Commands.literal("inventory") + .then(Commands.argument("player", GameProfileArgument.gameProfile()) + .executes(ctx -> runInventoryView(ctx, "all")) + .then(Commands.literal("main") + .executes(ctx -> runInventoryView(ctx, "main"))) + .then(Commands.literal("armor") + .executes(ctx -> runInventoryView(ctx, "armor"))) + .then(Commands.literal("ender") + .executes(ctx -> runInventoryView(ctx, "ender"))) + .then(Commands.literal("curios") + .executes(ctx -> runInventoryView(ctx, "curios"))) + .then(Commands.literal("all") + .executes(ctx -> runInventoryView(ctx, "all"))))) + // ---- Cluster ops ---- .then(Commands.literal("orphans").executes(CommandInit::runOrphans)) .then(Commands.literal("clearorphans") @@ -465,6 +480,155 @@ public class CommandInit { return 1; } + /** + * Pretty-prints a player's inventory / armor / ender chest / curios from the DB. + * Works on offline players too — reads the serialized columns directly instead + * of requiring the entity to be online. Output is compact, per-section, with + * item ID and count per non-empty slot. + */ + private static int runInventoryView(com.mojang.brigadier.context.CommandContext ctx, String section) + throws CommandSyntaxException { + Collection profiles = + GameProfileArgument.getGameProfiles(ctx, "player"); + if (profiles.isEmpty()) { + ctx.getSource().sendFailure(Component.literal("§cNo matching player")); + return 0; + } + com.mojang.authlib.GameProfile profile = profiles.iterator().next(); + UUID uuid = profile.getId(); + String name = profile.getName(); + + CommandSourceStack src = ctx.getSource(); + + String inventoryRaw = null, armorRaw = null, enderRaw = null; + try (JDBCsetUp.QueryResult qr = JDBCsetUp.executePreparedQuery( + "SELECT inventory, armor, enderchest FROM " + Tables.playerData() + " WHERE uuid=?", + uuid.toString())) { + ResultSet rs = qr.resultSet(); + if (!rs.next()) { + src.sendFailure(Component.literal("§cNo DB row for " + name + " (" + uuid + ")")); + return 0; + } + inventoryRaw = rs.getString("inventory"); + armorRaw = rs.getString("armor"); + enderRaw = rs.getString("enderchest"); + } catch (Exception e) { + src.sendFailure(Component.literal("§cDB query failed: " + e.getMessage())); + return 0; + } + + String curiosRaw = null; + if ("all".equals(section) || "curios".equals(section)) { + try (JDBCsetUp.QueryResult qr = JDBCsetUp.executePreparedQuery( + "SELECT curios_item FROM " + Tables.curios() + " WHERE uuid=?", uuid.toString())) { + ResultSet rs = qr.resultSet(); + if (rs.next()) curiosRaw = rs.getString("curios_item"); + } catch (Exception ignored) {} + } + + src.sendSuccess(() -> Component.literal("§a=== Inventory of §f" + name + " §7(" + uuid + ")"), false); + + int totalShown = 0; + if ("all".equals(section) || "main".equals(section)) { + totalShown += printSection(src, "§6Main inventory", inventoryRaw, 36); + } + if ("all".equals(section) || "armor".equals(section)) { + totalShown += printSection(src, "§6Armor §8(0=boots,1=legs,2=chest,3=helm)", armorRaw, 4); + } + if ("all".equals(section) || "ender".equals(section)) { + totalShown += printSection(src, "§6Ender chest", enderRaw, 27); + } + if ("all".equals(section) || "curios".equals(section)) { + totalShown += printCurios(src, curiosRaw); + } + + final int shown = totalShown; + src.sendSuccess(() -> Component.literal("§7— §f" + shown + " §7non-empty slot(s) shown"), false); + return 1; + } + + /** Prints a vanilla-style slot section (Map). Returns non-empty count. */ + private static int printSection(CommandSourceStack src, String header, String raw, int expectedSize) { + if (raw == null || raw.length() <= 2) { + src.sendSuccess(() -> Component.literal(header + "§7: §8(empty)"), false); + return 0; + } + java.util.Map map; + try { + map = vip.fubuki.playersync.util.LocalJsonUtil.StringToEntryMap(raw); + } catch (Exception e) { + src.sendSuccess(() -> Component.literal(header + "§7: §c"), false); + return 0; + } + if (map.isEmpty()) { + src.sendSuccess(() -> Component.literal(header + "§7: §8(empty)"), false); + return 0; + } + src.sendSuccess(() -> Component.literal(header + "§7 (" + map.size() + " slot(s) filled of " + expectedSize + "):"), false); + int shown = 0; + for (java.util.Map.Entry e : new java.util.TreeMap<>(map).entrySet()) { + String line = formatSlotLine(e.getKey().toString(), e.getValue()); + if (line != null) { + src.sendSuccess(() -> Component.literal(line), false); + shown++; + } + } + return shown; + } + + /** Curios has composite keys ("slotType:index" and "cos:slotType:index"). */ + private static int printCurios(CommandSourceStack src, String raw) { + if (raw == null || raw.length() <= 2) { + src.sendSuccess(() -> Component.literal("§6Curios§7: §8(empty)"), false); + return 0; + } + java.util.Map map; + try { + map = vip.fubuki.playersync.util.LocalJsonUtil.StringToMap(raw); + } catch (Exception e) { + src.sendSuccess(() -> Component.literal("§6Curios§7: §c"), false); + return 0; + } + if (map.isEmpty()) { + src.sendSuccess(() -> Component.literal("§6Curios§7: §8(empty)"), false); + return 0; + } + src.sendSuccess(() -> Component.literal("§6Curios§7 (" + map.size() + " slot(s) filled):"), false); + int shown = 0; + for (java.util.Map.Entry e : new java.util.TreeMap<>(map).entrySet()) { + String line = formatSlotLine(e.getKey(), e.getValue()); + if (line != null) { + src.sendSuccess(() -> Component.literal(line), false); + shown++; + } + } + return shown; + } + + /** Deserializes a single slot payload into a human-readable line. */ + private static String formatSlotLine(String slotKey, String payload) { + try { + net.minecraft.world.item.ItemStack stack = + vip.fubuki.playersync.sync.VanillaSync.deserializeAndCreatePlaceholderIfNeeded(payload); + if (stack == null || stack.isEmpty()) return null; + net.minecraft.resources.ResourceLocation id = + net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem()); + String idStr = id == null ? "unknown" : id.toString(); + String display = stack.getHoverName().getString(); + // Placeholder items (items from a mod not loaded on this server) show up with their + // original id preserved inside CustomData — the deserializer already handled that. + boolean placeholder = idStr.equals("minecraft:paper") + && stack.getComponents().has(net.minecraft.core.component.DataComponents.CUSTOM_DATA) + && stack.getComponents().get(net.minecraft.core.component.DataComponents.CUSTOM_DATA) + .copyTag().contains("playersync:original_item_nbt"); + String prefix = placeholder ? "§d[placeholder] " : "§f"; + return "§7 [" + slotKey + "] " + prefix + idStr + "§7 x§f" + stack.getCount() + + (display.equals(stack.getItem().getDescription().getString()) ? "" : " §8(" + display + ")"); + } catch (Throwable t) { + return "§7 [" + slotKey + "] §c"; + } + } + private static int runHelp(com.mojang.brigadier.context.CommandContext ctx) { CommandSourceStack src = ctx.getSource(); src.sendSuccess(() -> Component.literal("§a=== /playersync command reference ==="), false); @@ -473,6 +637,7 @@ public class CommandInit { "§e/playersync poolstats §7— log pool stats immediately", "§e/playersync flush [player] §7— force save all / one", "§e/playersync info §7— DB row metadata", + "§e/playersync inventory [main|armor|ender|curios|all] §7— pretty-print stored inventory", "§e/playersync dump §7— dump DB row to server log", "§e/playersync resync §7— kick to force re-sync", "§e/playersync wipe confirm §7— DELETE rows (DANGER)",