Add /playersync inventory viewer
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 <player> — everything (main + armor + ender + curios)
/playersync inventory <player> main — 36-slot hotbar + main inventory only
/playersync inventory <player> armor — 4 armor slots (0=boots, 1=legs, 2=chest, 3=helm)
/playersync inventory <player> ender — 27 ender chest slots
/playersync inventory <player> 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🔙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 <parse error: ClassName> and the rest continues.
Help listing updated. No other behavior changed.
This commit is contained in:
parent
4597041b1a
commit
131aa64eb1
|
|
@ -87,6 +87,21 @@ public class CommandInit {
|
||||||
.then(Commands.literal("confirm")
|
.then(Commands.literal("confirm")
|
||||||
.executes(CommandInit::runWipe))))
|
.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 ----
|
// ---- Cluster ops ----
|
||||||
.then(Commands.literal("orphans").executes(CommandInit::runOrphans))
|
.then(Commands.literal("orphans").executes(CommandInit::runOrphans))
|
||||||
.then(Commands.literal("clearorphans")
|
.then(Commands.literal("clearorphans")
|
||||||
|
|
@ -465,6 +480,155 @@ public class CommandInit {
|
||||||
return 1;
|
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<CommandSourceStack> ctx, String section)
|
||||||
|
throws CommandSyntaxException {
|
||||||
|
Collection<com.mojang.authlib.GameProfile> 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<Integer,String>). 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<Integer, String> map;
|
||||||
|
try {
|
||||||
|
map = vip.fubuki.playersync.util.LocalJsonUtil.StringToEntryMap(raw);
|
||||||
|
} catch (Exception e) {
|
||||||
|
src.sendSuccess(() -> Component.literal(header + "§7: §c<parse error: " + e.getMessage() + ">"), 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<Integer, String> 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<String, String> map;
|
||||||
|
try {
|
||||||
|
map = vip.fubuki.playersync.util.LocalJsonUtil.StringToMap(raw);
|
||||||
|
} catch (Exception e) {
|
||||||
|
src.sendSuccess(() -> Component.literal("§6Curios§7: §c<parse error>"), 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<String, String> 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<parse error: " + t.getClass().getSimpleName() + ">";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int runHelp(com.mojang.brigadier.context.CommandContext<CommandSourceStack> ctx) {
|
private static int runHelp(com.mojang.brigadier.context.CommandContext<CommandSourceStack> ctx) {
|
||||||
CommandSourceStack src = ctx.getSource();
|
CommandSourceStack src = ctx.getSource();
|
||||||
src.sendSuccess(() -> Component.literal("§a=== /playersync command reference ==="), false);
|
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 poolstats §7— log pool stats immediately",
|
||||||
"§e/playersync flush [player] §7— force save all / one",
|
"§e/playersync flush [player] §7— force save all / one",
|
||||||
"§e/playersync info <player> §7— DB row metadata",
|
"§e/playersync info <player> §7— DB row metadata",
|
||||||
|
"§e/playersync inventory <player> [main|armor|ender|curios|all] §7— pretty-print stored inventory",
|
||||||
"§e/playersync dump <player> §7— dump DB row to server log",
|
"§e/playersync dump <player> §7— dump DB row to server log",
|
||||||
"§e/playersync resync <player> §7— kick to force re-sync",
|
"§e/playersync resync <player> §7— kick to force re-sync",
|
||||||
"§e/playersync wipe <player> confirm §7— DELETE rows (DANGER)",
|
"§e/playersync wipe <player> confirm §7— DELETE rows (DANGER)",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user