From 24b408ce96605d166a3fe7735620a1ca1fe3e58a Mon Sep 17 00:00:00 2001 From: Huang Jinjie <38885760+xiaohuang2004@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:45:49 +0800 Subject: [PATCH 01/39] Add Chinese translation file to the project --- .../multiverse-inventories_zh.properties | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/main/resources/multiverse-inventories_zh.properties diff --git a/src/main/resources/multiverse-inventories_zh.properties b/src/main/resources/multiverse-inventories_zh.properties new file mode 100644 index 00000000..cf452f59 --- /dev/null +++ b/src/main/resources/multiverse-inventories_zh.properties @@ -0,0 +1,103 @@ +mv-inventories.test.string=资源文件中的一个测试字符串 + +# Generic Strings +mv-inventories.generic.sorry=抱歉…… +mv-inventories.generic.page=页面 +mv-inventories.generic.of=of +mv-inventories.generic.unloaded=卸载 +mv-inventories.generic.plugindisabled=插件已被禁用! +mv-inventories.generic.error=[错误] +mv-inventories.generic.success=[成功] +mv-inventories.generic.info=[信息] +mv-inventories.generic.help=[帮助] +mv-inventories.generic.commandnopermission=你没有权限执行 {command}。 ({permission}) +mv-inventories.generic.theconsole=控制台 +mv-inventories.generic.notloggedin={player} 目前处于离线状态! +mv-inventories.generic.off=关闭 + +# Errors +mv-inventories.error.configload=加载配置文件时遇到错误。正在禁用…… +mv-inventories.error.dataload=加载数据文件时遇到错误。正在禁用…… +mv-inventories.error.nogroup=&6没有名为 &f{group}&6 的组 +mv-inventories.error.noworld=&6没有名为 &f{world}&6 的世界 +mv-inventories.error.noworldprofile=&6世界 &f{world}&6 没有配置文件 +mv-inventories.error.nosharesspecified=&c你没有指定任何有效的共享! + +# Conflicts +mv-inventories.conflict.results=在以下组中检测到冲突: ''{group1}'' 和 ''{group2}'',因为它们在世界: ''{worlds}'' 中都共享了: ''{shares}'' +mv-inventories.conflict.checking=正在检查组中的冲突…… +mv-inventories.conflict.found=已经发现冲突……如果这些冲突没有解决,你的数据可能会遇到问题。 +mv-inventories.conflict.notfound=没有发现组冲突! + +# Commands +## Info Command +mv-inventories.info.world=&b===[ 世界信息: &6{world}&b ]=== +mv-inventories.info.world.info=&6组:&f {groups} +mv-inventories.info.group=&b===[ 组信息: &6{group}&b ]=== +mv-inventories.info.group.info=&6世界:&f {worlds} +mv-inventories.info.group.infoshares=&b共享:&f {shares} +mv-inventories.info.group.infonegativeshares=&b禁用的共享:&f {shares} +mv-inventories.info.zeroarg=你只能在游戏中使用该命令的无参数版本! + +# List Command +mv-inventories.list.groups=&b===[ 组列表 ]=== +mv-inventories.list.groups.info=&6组:&f {groups} + +# Reload Command +mv-inventories.reload.complete=&b===[ 重新加载完成! ]=== + +# Addworld Command +mv-inventories.addworld.worldadded=&6世界:&f {world} &6已被添加到组: &f{group} +mv-inventories.addworld.worldalreadyexists=&6世界:&f {world} &6已在以下组中: &f{group} + +# Removeworld Command +mv-inventories.removeworld.worldremoved=&6世界: &f {world} &6已从以下组中移除: &f{group} +mv-inventories.removeworld.worldnotingroup=&6世界: &f {world} &6不在以下组中: &f{group} + +# Add/remove shares command +mv-inventories.shares.nowsharing=&6组: &f{group} &6正在共享: &f{shares} &6禁用的共享: &f{negativeshares} + +# Add/remove disabled shares command +mv-inventories.disabledshares.nowsharing=&6组 &f{group} &6的以下共享已禁用: &f{shares} + +# Spawn command +mv-inventories.spawn.teleporting=正在传送到当前世界组的出生点…… +mv-inventories.spawn.teleportedby=你已被玩家 {player} 传送。 +mv-inventories.spawn.teleportconsoleerror=从控制台运行该指令时,你必须指定玩家! + +# Debug command +mv-inventories.debug.invaliddebug=&f无效的调试等级。请使用数字 0-3。 &b(3 会出现很多很多信息!) +mv-inventories.debug.set=调试模式已设置为 {mode}。 + +# Toggle command +mv-inventories.toggle.nowusingoptional=&6当玩家的世界改变时,&f{share} &6将会被考虑。 +mv-inventories.toggle.nownotusingoptional=&6当玩家的世界改变时,&f{share} &6将不在被考虑。 +mv-inventories.toggle.nooptionalshares=&f{share} &6 不是一个可选择的共享! + +# Group command +mv-inventories.group.commandprompt=&6你想做什么? &f创建(Create)&6, &f编辑(Edit) &6或者 &f删除(Delete)&6。 输入 &f##&6 取消当前操作。 +mv-inventories.group.createprompt=&6请输入新建组的名称: +mv-inventories.group.editprompt=&6想要编辑哪个组? {groups} +mv-inventories.group.deleteprompt=&6想要删除哪个组? {groups} +mv-inventories.group.modifyprompt=&6你想要修改 &e{group}&6 的哪个属性? &f世界(Worlds) &6或者 &f共享(Shares)&6。 输入 &f##&6 结束。 +mv-inventories.group.worldsprompt=&6输入你想要添加到组 &f{group}&6 的世界 或者输入 &f@&6 继续。 如果需要移除世界,在名称前加减号。(例如: &f-worldname&6)。 当前世界: {worlds} +mv-inventories.group.sharesprompt=&6输入全部(&fall&6)或者一个指定的共享加入到组 &f{group}&6,或者输入 &f@&6 继续。如果需要移除共享,在名称前加减号。(例如: &f-inventory&6). 当前的共享:{shares} +mv-inventories.group.invalidname=&c名称无效! 只能包含字母、数字和下划线。 +mv-inventories.group.exists=&c组已经存在!(&f{group}&c) +mv-inventories.group.removed=&2已移除组: &f{group} +mv-inventories.group.worldsempty=&c您可能没有不包含任何世界的组,请添加世界或输入&f##&c取消。 +mv-inventories.group.creationcomplete=&2你创建了新的组 '{group}'! +mv-inventories.group.updated=&2组已经更新! +mv-inventories.group.nonconversable=无法访问会话(远程控制台?) +mv-inventories.group.invalidoption=&c这不是一个有效的选项!输入&f##&c停止处理组。 + +# Migrate command +mv-inventories.migrate.pluginnotenabled=&f{plugin} &6未启用,因此你可能无法从中导入数据! +mv-inventories.migrate.unsupportedplugin=&6抱歉, ''&f{plugin}&6'' 不支持导入。 +mv-inventories.migrate.confirmprompt=&6你确认从 &f{plugin}&6 导入数据吗?这将覆盖 Multiverse-Inventories 中已存在的玩家数据!!! +mv-inventories.migrate.success=&2成功从 &f{plugin} &2中导入数据! +mv-inventories.migrate.failed=&c从 &f{plugin} &c中导入数据失败! + +# Deletegroup command +mv-inventories.deletegroup.confirmprompt=您确定要删除组 &f{group}&6 吗? +mv-inventories.deletegroup.success=&2成功删除组: &f{group}! \ No newline at end of file From afb9f84e50715546b4ea1295658043548268c5c6 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sat, 19 Jul 2025 15:30:10 -0400 Subject: [PATCH 02/39] Implement view inventory command for online players only --- .../commands/InventoryViewCommand.java | 103 ++++++++++++++++++ .../multiverse/inventories/InjectionTest.kt | 2 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java new file mode 100644 index 00000000..b809b253 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -0,0 +1,103 @@ +package org.mvplugins.multiverse.inventories.commands; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.world.MultiverseWorld; +import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; +import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; +import org.mvplugins.multiverse.external.acf.commands.annotation.Description; +import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand; +import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; +import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; + +@Service +public class InventoryViewCommand extends InventoriesCommand{ + + private final ProfileContainerStoreProvider profileContainerStoreProvider; + @Inject + InventoryViewCommand( + @NotNull ProfileContainerStoreProvider profileContainerStoreProvider + ) { + this.profileContainerStoreProvider = profileContainerStoreProvider; + } + + @Subcommand("view") + @CommandPermission("multiverse.inventories.view") + @CommandCompletion("@players @mvworlds") + @Syntax(" ") + @Description("View a player's inventory in a specific world.") + + void onInventoryViewCommand( + @NotNull MVCommandIssuer issuer, + // TODO add capability for offline players + @Syntax("") + @Description("Online player") + Player target, + + @Syntax("") + @Description("The world the player's inventory is in") + MultiverseWorld[] worlds + ) { + if (!(issuer.getIssuer() instanceof Player viewer)) { + issuer.sendMessage(ChatColor.RED + "Only players can view inventories."); + return; + } + + if (worlds == null || worlds.length == 0 || worlds[0] == null) { + issuer.sendMessage(ChatColor.RED + "You must specify a valid world."); + return; + } + + + String worldName = worlds[0].getName(); + + // Load the container for this world + ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName); + + if (container == null) { + issuer.sendError("Could not load profile container for world: " + worldName); + return; + } + + // Load the target's profile key from the container + var profile = container.getPlayerProfileNow(target); + if (profile == null) { + issuer.sendError("No inventory data found for " + target.getName() + " in world " + worldName); + return; + } + + // Get the contents of the player's inventory from the profile + ItemStack[] contents = target.getInventory().getContents(); // This is the main inventory (0–35) + ItemStack[] armor = target.getInventory().getArmorContents(); // Armor contents (helmet, chest, etc.) + ItemStack offHand = target.getInventory().getItemInOffHand(); // Offhand slot + + // Create a new inventory with enough space (you can customize this layout) + Inventory inv = Bukkit.createInventory(null, 54, target.getName() + " @ " + worldName); + + // Copy inventory contents into the first 36 slots + for (int i = 0; i < contents.length && i < 36; i++) { + inv.setItem(i, contents[i]); + } + + // Optionally add armor and offhand to specific GUI slots + inv.setItem(36, armor.length > 3 ? armor[3] : null); // Boots + inv.setItem(37, armor.length > 2 ? armor[2] : null); // Leggings + inv.setItem(38, armor.length > 1 ? armor[1] : null); // Chestplate + inv.setItem(39, armor.length > 0 ? armor[0] : null); // Helmet + inv.setItem(40, offHand); // Offhand (shield) + + // Open the GUI for the viewer + viewer.openInventory(inv); + } +} diff --git a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt index 53dad107..ae65cfd2 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt @@ -15,7 +15,7 @@ class InjectionTest : TestWithMockBukkit() { @Test fun `InventoriesCommand are available as a service`() { - assertEquals(28, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) + assertEquals(29, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) } @Test From ecdbc1ae70f65ab3e0a14b035781917e30d38f1c Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sat, 19 Jul 2025 18:16:58 -0400 Subject: [PATCH 03/39] Add logic for offline players and command completion. Enforce read-only access. --- .../commands/InventoryViewCommand.java | 166 +++++++++++++++--- .../listeners/InventoryViewListener.java | 60 +++++++ .../inventories/listeners/MVInvListener.java | 2 +- .../multiverse/inventories/InjectionTest.kt | 2 +- 4 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index b809b253..9d6102b4 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -2,6 +2,7 @@ import org.bukkit.Bukkit; import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; @@ -16,48 +17,67 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; +import org.mvplugins.multiverse.inventories.listeners.InventoryViewListener; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; +import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; +import org.mvplugins.multiverse.inventories.share.Sharables; + +import java.util.concurrent.CompletionException; @Service -public class InventoryViewCommand extends InventoriesCommand{ +public class InventoryViewCommand extends InventoriesCommand { private final ProfileContainerStoreProvider profileContainerStoreProvider; + private final MultiverseInventories inventories; + @Inject InventoryViewCommand( - @NotNull ProfileContainerStoreProvider profileContainerStoreProvider + @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, + @NotNull MultiverseInventories inventories ) { this.profileContainerStoreProvider = profileContainerStoreProvider; + this.inventories = inventories; } @Subcommand("view") @CommandPermission("multiverse.inventories.view") - @CommandCompletion("@players @mvworlds") + @CommandCompletion("@mvinvplayernames @mvworlds") @Syntax(" ") @Description("View a player's inventory in a specific world.") - void onInventoryViewCommand( @NotNull MVCommandIssuer issuer, - // TODO add capability for offline players + @Syntax("") - @Description("Online player") - Player target, + @Description("Online or offline player") + //Player targetPlayer, + OfflinePlayer targetPlayer, @Syntax("") @Description("The world the player's inventory is in") MultiverseWorld[] worlds ) { + // Check if the command sender is a player if (!(issuer.getIssuer() instanceof Player viewer)) { - issuer.sendMessage(ChatColor.RED + "Only players can view inventories."); + issuer.sendError(ChatColor.RED + "Only players can view inventories."); return; } + // Validate world argument if (worlds == null || worlds.length == 0 || worlds[0] == null) { - issuer.sendMessage(ChatColor.RED + "You must specify a valid world."); + issuer.sendError(ChatColor.RED + "You must specify a valid world."); return; } + // Validate targetPlayer player + if (targetPlayer == null || targetPlayer.getName() == null) { + issuer.sendError(ChatColor.RED + "You must specify a valid player."); + return; + } String worldName = worlds[0].getName(); @@ -70,34 +90,124 @@ void onInventoryViewCommand( return; } - // Load the target's profile key from the container - var profile = container.getPlayerProfileNow(target); - if (profile == null) { - issuer.sendError("No inventory data found for " + target.getName() + " in world " + worldName); - return; + // Load the targetPlayer's profile key from the container + //var profile = container.getPlayerProfileNow(targetPlayer); + PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); + ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; // Default to SURVIVAL + + if (tempProfile == null) { + // If SURVIVAL profile not found, iterate through other known types as a fallback + // to find ANY PlayerProfile and use its type. + for (ProfileType type : ProfileTypes.getTypes()) { + if (type.equals(ProfileTypes.SURVIVAL)) { + continue; // Skip SURVIVAL as we already tried it + } + tempProfile = container.getPlayerProfileNow(type, targetPlayer); + if (tempProfile != null) { + profileTypeToUse = type; // Use the type of the found profile + break; + } + } } - // Get the contents of the player's inventory from the profile - ItemStack[] contents = target.getInventory().getContents(); // This is the main inventory (0–35) - ItemStack[] armor = target.getInventory().getArmorContents(); // Armor contents (helmet, chest, etc.) - ItemStack offHand = target.getInventory().getItemInOffHand(); // Offhand slot + if (tempProfile == null) { + issuer.sendError("No inventory data found for " + targetPlayer.getName() + " in world " + worldName); + return; + } +/* + // Get inventory data + var contents = profile.getInventoryContents(); + var armor = profile.getArmorContents(); + var offHand = profile.getOffHandItem(); - // Create a new inventory with enough space (you can customize this layout) - Inventory inv = Bukkit.createInventory(null, 54, target.getName() + " @ " + worldName); + // Create an inventory for viewing + Inventory inv = Bukkit.createInventory(null, 54, targetPlayer.getName() + " @ " + worldName); - // Copy inventory contents into the first 36 slots - for (int i = 0; i < contents.length && i < 36; i++) { + // Fill in inventory slots (0–35) + for (int i = 0; i < Math.min(contents.length, 36); i++) { inv.setItem(i, contents[i]); } - // Optionally add armor and offhand to specific GUI slots - inv.setItem(36, armor.length > 3 ? armor[3] : null); // Boots - inv.setItem(37, armor.length > 2 ? armor[2] : null); // Leggings - inv.setItem(38, armor.length > 1 ? armor[1] : null); // Chestplate - inv.setItem(39, armor.length > 0 ? armor[0] : null); // Helmet - inv.setItem(40, offHand); // Offhand (shield) +// Fill in armor slots (36–39) and offhand (40) + if (armor != null && armor.length >= 4) { + inv.setItem(39, armor[0]); // Helmet (from profile) -> Slot 39 (viewing inv) + inv.setItem(38, armor[1]); // Chestplate (from profile) -> Slot 38 (viewing inv) + inv.setItem(37, armor[2]); // Leggings (from profile) -> Slot 37 (viewing inv) + inv.setItem(36, armor[3]); // Boots (from profile) -> Slot 36 (viewing inv) + } + + // Fill in offhand slot (40) + if (offHand != null) { + inv.setItem(40, offHand); + } + + inv.setItem(40, offHand); // offhand // Open the GUI for the viewer viewer.openInventory(inv); } } +*/ + ItemStack[] contents = null; + ItemStack[] armor = null; + ItemStack offHand = null; + + try { + // Read main inventory contents + contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY) + .read() + .join(); // Use .join() to block until the future completes + + // Read armor contents + armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR) + .read() + .join(); + + // Read off-hand item + offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND) + .read() + .join(); + + } catch (CompletionException e) { + issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); + e.printStackTrace(); // Log the full stack trace for debugging + return; + } + + // Create an inventory for viewing. Size 54 (6 rows) is good for main inventory + armor + offhand. + // This links the inventory to our listener, making it read-only. + Inventory inv = Bukkit.createInventory(new InventoryViewListener.ReadOnlyInventoryHolder(), 54, targetPlayer.getName() + " @ " + worldName); + + // Fill in main inventory slots (0–35) + // Ensure we don't go out of bounds if contents is smaller than expected + if (contents != null) { + for (int i = 0; i < Math.min(contents.length, 36); i++) { + inv.setItem(i, contents[i]); + } + } + + // --- Corrected armor slot mapping --- + // Bukkit's PlayerInventory armor slots are (from index 0): boots, leggings, chestplate, helmet. + // However, Multiverse-Inventories' Sharables.ARMOR returns [helmet, chestplate, leggings, boots]. + // We need to map them correctly to the viewing inventory's display slots. + // Standard viewing inventory layout for armor is: + // Slot 36: Boots + // Slot 37: Leggings + // Slot 38: Chestplate + // Slot 39: Helmet + if (armor != null && armor.length >= 4) { + inv.setItem(39, armor[0]); // Helmet (from profile) -> Slot 39 (viewing inv) + inv.setItem(38, armor[1]); // Chestplate (from profile) -> Slot 38 (viewing inv) + inv.setItem(37, armor[2]); // Leggings (from profile) -> Slot 37 (viewing inv) + inv.setItem(36, armor[3]); // Boots (from profile) -> Slot 36 (viewing inv) + } + + // Fill in offhand slot (40) + if (offHand != null) { + inv.setItem(40, offHand); + } + + // Open the GUI for the viewer + viewer.openInventory(inv); + } +} \ No newline at end of file diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java new file mode 100644 index 00000000..a612e67a --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -0,0 +1,60 @@ +package org.mvplugins.multiverse.inventories.listeners; + +import org.bukkit.event.EventPriority; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority; +import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.MultiverseInventories; + +@Service +public final class InventoryViewListener implements MVInvListener { + + private final MultiverseInventories inventories; + + @Inject + InventoryViewListener( + @NotNull MultiverseInventories inventories) { + this.inventories = inventories; + } + + // This class acts as a marker. When an inventory is created with this holder, + // the event listener can identify it as a read-only inventory. + public static class ReadOnlyInventoryHolder implements InventoryHolder { + @Override + public @NotNull Inventory getInventory() { + // This method is required by the interface but isn't strictly used for our marker purpose. + // The actual inventory is obtained from the InventoryClickEvent itself. + return null; + } + } + // This listener will cancel any clicks or drags in inventories that have the ReadOnlyInventoryHolder marker. + @EventMethod + @DefaultEventPriority(EventPriority.NORMAL) + public void onInventoryClick(InventoryClickEvent event) { + // Check if the inventory being clicked has the custom holder + if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder) { + event.setCancelled(true); + } + // This covers cases where a player might click in their own inventory + // but the action is intended to move an item into the read-only inventory (e.g., shift-click). + else if (event.getClickedInventory() != null && event.getClickedInventory().getHolder() instanceof ReadOnlyInventoryHolder) { + event.setCancelled(true); + } + } + + // Also cancel drag events to prevent items from being dragged into/out of the inventory + @EventMethod + @DefaultEventPriority(EventPriority.NORMAL) + public void onInventoryDrag(InventoryDragEvent event) { + if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder) { + event.setCancelled(true); + } + } +} + diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java index 6e760c37..713bd006 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java @@ -4,5 +4,5 @@ import org.mvplugins.multiverse.core.dynamiclistener.DynamicListener; @Contract -public sealed interface MVInvListener extends DynamicListener permits MVEventsListener, RespawnListener, ShareHandleListener, SpawnChangeListener { +public sealed interface MVInvListener extends DynamicListener permits InventoryViewListener, MVEventsListener, RespawnListener, ShareHandleListener, SpawnChangeListener { } diff --git a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt index ae65cfd2..f2489d70 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt @@ -25,7 +25,7 @@ class InjectionTest : TestWithMockBukkit() { @Test fun `InventoriesListener is available as a service`() { - assertEquals(4, serviceLocator.getAllActiveServices(MVInvListener::class.java).size) + assertEquals(5, serviceLocator.getAllActiveServices(MVInvListener::class.java).size) } @Test From e844df9dbfd732fa4f4c00cb62850d990602281e Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sat, 19 Jul 2025 18:26:08 -0400 Subject: [PATCH 04/39] Remove old code --- .../commands/InventoryViewCommand.java | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 9d6102b4..afaf57b6 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -114,40 +114,7 @@ void onInventoryViewCommand( issuer.sendError("No inventory data found for " + targetPlayer.getName() + " in world " + worldName); return; } -/* - // Get inventory data - var contents = profile.getInventoryContents(); - var armor = profile.getArmorContents(); - var offHand = profile.getOffHandItem(); - - // Create an inventory for viewing - Inventory inv = Bukkit.createInventory(null, 54, targetPlayer.getName() + " @ " + worldName); - - // Fill in inventory slots (0–35) - for (int i = 0; i < Math.min(contents.length, 36); i++) { - inv.setItem(i, contents[i]); - } -// Fill in armor slots (36–39) and offhand (40) - if (armor != null && armor.length >= 4) { - inv.setItem(39, armor[0]); // Helmet (from profile) -> Slot 39 (viewing inv) - inv.setItem(38, armor[1]); // Chestplate (from profile) -> Slot 38 (viewing inv) - inv.setItem(37, armor[2]); // Leggings (from profile) -> Slot 37 (viewing inv) - inv.setItem(36, armor[3]); // Boots (from profile) -> Slot 36 (viewing inv) - } - - // Fill in offhand slot (40) - if (offHand != null) { - inv.setItem(40, offHand); - } - - inv.setItem(40, offHand); // offhand - - // Open the GUI for the viewer - viewer.openInventory(inv); - } -} -*/ ItemStack[] contents = null; ItemStack[] armor = null; ItemStack offHand = null; @@ -186,15 +153,6 @@ void onInventoryViewCommand( } } - // --- Corrected armor slot mapping --- - // Bukkit's PlayerInventory armor slots are (from index 0): boots, leggings, chestplate, helmet. - // However, Multiverse-Inventories' Sharables.ARMOR returns [helmet, chestplate, leggings, boots]. - // We need to map them correctly to the viewing inventory's display slots. - // Standard viewing inventory layout for armor is: - // Slot 36: Boots - // Slot 37: Leggings - // Slot 38: Chestplate - // Slot 39: Helmet if (armor != null && armor.length >= 4) { inv.setItem(39, armor[0]); // Helmet (from profile) -> Slot 39 (viewing inv) inv.setItem(38, armor[1]); // Chestplate (from profile) -> Slot 38 (viewing inv) From fa50a1f3ca82f082ba55bb66934bffb9b68cd79e Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sat, 19 Jul 2025 19:31:33 -0400 Subject: [PATCH 05/39] Added modify inventory functionality --- .../commands/InventoryModifyCommand.java | 143 ++++++++++++++++++ .../commands/InventoryViewCommand.java | 1 - .../listeners/InventoryViewListener.java | 117 +++++++++++++- .../multiverse/inventories/InjectionTest.kt | 2 +- 4 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java new file mode 100644 index 00000000..cf9ee75b --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -0,0 +1,143 @@ +package org.mvplugins.multiverse.inventories.commands; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.world.MultiverseWorld; +import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; +import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; +import org.mvplugins.multiverse.external.acf.commands.annotation.Description; +import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand; +import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; +import org.mvplugins.multiverse.inventories.listeners.InventoryViewListener; +import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; +import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; +import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; +import org.mvplugins.multiverse.inventories.share.Sharables; + +import java.util.concurrent.CompletionException; + +@Service +public class InventoryModifyCommand extends InventoriesCommand { + + private final ProfileContainerStoreProvider profileContainerStoreProvider; + private final MultiverseInventories inventories; + + @Inject + InventoryModifyCommand( + @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, + @NotNull MultiverseInventories inventories + ) { + this.profileContainerStoreProvider = profileContainerStoreProvider; + this.inventories = inventories; + } + + // This method contains the logic for the /mvinv modify command + @Subcommand("modify") + @CommandPermission("multiverse.inventories.view.modify") // Specific permission for modification + @CommandCompletion("@mvinvplayernames @mvworlds") + @Syntax(" ") + @Description("Modify a player's inventory in a specific world.") + void onInventoryModifyCommand( + @NotNull MVCommandIssuer issuer, + @Syntax("") + @Description("Online or offline player") + OfflinePlayer target, + + @Syntax("") + @Description("The world the player's inventory is in") + MultiverseWorld[] worlds + ) { + if (!(issuer.getIssuer() instanceof Player viewer)) { + issuer.sendError(ChatColor.RED + "Only players can modify inventories."); + return; + } + if (worlds == null || worlds.length == 0 || worlds[0] == null) { + issuer.sendError(ChatColor.RED + "You must specify a valid world."); + return; + } + if (target == null || target.getName() == null) { + issuer.sendError(ChatColor.RED + "You must specify a valid player."); + return; + } + + String worldName = worlds[0].getName(); + ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName); + if (container == null) { + issuer.sendError(ChatColor.RED + "Could not load profile container for world: " + worldName); + return; + } + + PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, target); + ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; + if (tempProfile == null) { + for (ProfileType type : ProfileTypes.getTypes()) { + if (type.equals(ProfileTypes.SURVIVAL)) continue; + tempProfile = container.getPlayerProfileNow(type, target); + if (tempProfile != null) { + profileTypeToUse = type; + break; + } + } + } + if (tempProfile == null) { + issuer.sendError(ChatColor.RED + "No player data found for " + target.getName() + " in world " + worldName + ". Cannot modify inventory."); + return; + } + + ItemStack[] contents = null; + ItemStack[] armor = null; + ItemStack offHand = null; + + try { + contents = SingleShareReader.of(inventories, target, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); + armor = SingleShareReader.of(inventories, target, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); + offHand = SingleShareReader.of(inventories, target, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); + } catch (CompletionException e) { + issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); + e.printStackTrace(); + return; + } + + // Create inventory with ModifiableInventoryHolder + Inventory inv = Bukkit.createInventory( + new InventoryViewListener.ModifiableInventoryHolder(target, worldName, profileTypeToUse, inventories), + 54, + "Modifying " + target.getName() + " @ " + worldName + ); + + // Fill inventory + if (contents != null) { + for (int i = 0; i < Math.min(contents.length, 36); i++) { + inv.setItem(i, contents[i]); + } + } + if (armor != null && armor.length >= 4) { + inv.setItem(39, armor[0]); + inv.setItem(38, armor[1]); + inv.setItem(37, armor[2]); + inv.setItem(36, armor[3]); + } + if (offHand != null) { + inv.setItem(40, offHand); + } + + viewer.openInventory(inv); + issuer.sendInfo(ChatColor.GREEN + "Opened editable inventory for " + target.getName() + + " in world " + ChatColor.YELLOW + worldName + ChatColor.GREEN + ". Changes will save on close."); + } +} + diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index afaf57b6..57b520f1 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -91,7 +91,6 @@ void onInventoryViewCommand( } // Load the targetPlayer's profile key from the container - //var profile = container.getPlayerProfileNow(targetPlayer); PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; // Default to SURVIVAL diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index a612e67a..3c88aeba 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -1,16 +1,27 @@ package org.mvplugins.multiverse.inventories.listeners; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; import org.bukkit.event.EventPriority; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority; import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.share.Sharables; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; @Service public final class InventoryViewListener implements MVInvListener { @@ -33,6 +44,7 @@ public static class ReadOnlyInventoryHolder implements InventoryHolder { return null; } } + // This listener will cancel any clicks or drags in inventories that have the ReadOnlyInventoryHolder marker. @EventMethod @DefaultEventPriority(EventPriority.NORMAL) @@ -56,5 +68,108 @@ public void onInventoryDrag(InventoryDragEvent event) { event.setCancelled(true); } } -} + // This holder stores context needed to save the inventory when it's closed. + public static class ModifiableInventoryHolder implements InventoryHolder { + private final OfflinePlayer targetPlayer; + private final String worldName; + private final ProfileType profileType; + private final MultiverseInventories inventories; + + public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull ProfileType profileType, + @NotNull MultiverseInventories inventories) { + this.targetPlayer = targetPlayer; + this.worldName = worldName; + this.profileType = profileType; + this.inventories = inventories; + } + + public @NotNull OfflinePlayer getTargetPlayer() { + return targetPlayer; + } + + public @NotNull String getWorldName() { + return worldName; + } + + public @NotNull ProfileType getProfileType() { + return profileType; + } + + public @NotNull MultiverseInventories getInventories() { + return inventories; + } + + @Override + public @NotNull Inventory getInventory() { + return null; // The actual inventory is passed via the event + } + } + + // Event handler for InventoryCloseEvent to save changes + @EventMethod + @DefaultEventPriority(EventPriority.NORMAL) + public void onInventoryClose(InventoryCloseEvent event) { + // Check if the closed inventory has the custom ModifiableInventoryHolder class + if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder holder) { + final OfflinePlayer targetPlayer = holder.getTargetPlayer(); + final String worldName = holder.getWorldName(); + final ProfileType profileType = holder.getProfileType(); + final MultiverseInventories plugin = holder.getInventories(); + final Inventory closedInventory = event.getInventory(); + + // Extract contents: 0-35 for inventory, 36-39 for armor, 40 for off-hand + ItemStack[] newContents = Arrays.copyOfRange(closedInventory.getContents(), 0, 36); + ItemStack[] newArmor = new ItemStack[4]; + // Map GUI armor slots back to Multiverse-Inventories' expected order [helmet, chestplate, leggings, boots] + newArmor[0] = closedInventory.getItem(39); // Helmet + newArmor[1] = closedInventory.getItem(38); // Chestplate + newArmor[2] = closedInventory.getItem(37); // Leggings + newArmor[3] = closedInventory.getItem(36); // Boots + ItemStack newOffHand = closedInventory.getItem(40); + + // Save the updated inventory, armor, and off-hand contents asynchronously + CompletableFuture saveFuture = CompletableFuture.allOf( + SingleShareWriter.of(plugin, targetPlayer, worldName, profileType, Sharables.INVENTORY) + .write(newContents, true) // true to update if player is online + .thenRun(() -> plugin.getLogger().fine("Saved inventory for " + targetPlayer.getName() + " in " + worldName)), + SingleShareWriter.of(plugin, targetPlayer, worldName, profileType, Sharables.ARMOR) + .write(newArmor, true) + .thenRun(() -> plugin.getLogger().fine("Saved armor for " + targetPlayer.getName() + " in " + worldName)), + SingleShareWriter.of(plugin, targetPlayer, worldName, profileType, Sharables.OFF_HAND) + .write(newOffHand, true) + .thenRun(() -> plugin.getLogger().fine("Saved off-hand for " + targetPlayer.getName() + " in " + worldName)) + ); + + saveFuture.thenRun(() -> { + plugin.getLogger().info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); + }).exceptionally(throwable -> { + plugin.getLogger().severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; + }); + + // If the target player is online, update their live inventory + if (targetPlayer.isOnline()) { + Player onlinePlayer = targetPlayer.getPlayer(); + if (onlinePlayer != null) { + // Check if the online player is in the world whose inventory was modified + // This is important to avoid overwriting their current inventory if they are in a different world + if (onlinePlayer.getWorld().getName().equalsIgnoreCase(worldName)) { + Bukkit.getScheduler().runTask(plugin, () -> { + onlinePlayer.getInventory().setContents(newContents); + onlinePlayer.getInventory().setArmorContents(newArmor); + onlinePlayer.getInventory().setItemInOffHand(newOffHand); + onlinePlayer.updateInventory(); // Ensure client sees changes + plugin.getLogger().info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); + }); + } else { + plugin.getLogger().info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt index f2489d70..dd0add30 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt @@ -15,7 +15,7 @@ class InjectionTest : TestWithMockBukkit() { @Test fun `InventoriesCommand are available as a service`() { - assertEquals(29, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) + assertEquals(30, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) } @Test From 55ca31c26ec6928617dc8fed1eabea23b0edb2c7 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sat, 19 Jul 2025 19:54:24 -0400 Subject: [PATCH 06/39] Show live inventory update if the player is online --- .../commands/InventoryViewCommand.java | 160 ++++++++++++------ 1 file changed, 105 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 57b520f1..460bcebc 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -80,64 +80,115 @@ void onInventoryViewCommand( } String worldName = worlds[0].getName(); - - // Load the container for this world - ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) - .getContainer(worldName); - - if (container == null) { - issuer.sendError("Could not load profile container for world: " + worldName); - return; - } - - // Load the targetPlayer's profile key from the container - PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); - ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; // Default to SURVIVAL - - if (tempProfile == null) { - // If SURVIVAL profile not found, iterate through other known types as a fallback - // to find ANY PlayerProfile and use its type. - for (ProfileType type : ProfileTypes.getTypes()) { - if (type.equals(ProfileTypes.SURVIVAL)) { - continue; // Skip SURVIVAL as we already tried it + ItemStack[] contents = null; + ItemStack[] armor = null; + ItemStack offHand = null; + String statusMessage; + + // Check if target player is online + if (targetPlayer.isOnline()) { + Player onlineTarget = targetPlayer.getPlayer(); + if (onlineTarget != null) { + // If the player is online, retrieve their current live inventory + contents = onlineTarget.getInventory().getContents(); + armor = onlineTarget.getInventory().getArmorContents(); + offHand = onlineTarget.getInventory().getItemInOffHand(); + statusMessage = "Displaying LIVE inventory for " + targetPlayer.getName() + "."; + } else { + // Should not happen if isOnline() is true, but as a fallback + statusMessage = "Error: Could not get online player instance. Falling back to stored data."; + + // Fallback to stored data if online player instance somehow isn't available + // This block is now correctly placed within the 'else' for onlineTarget != null + ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName); + if (container == null) { + issuer.sendError(ChatColor.RED + "Could not load profile container for world: " + worldName); + return; // Exit if container cannot be loaded + } + PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); + ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; + if (tempProfile == null) { + for (ProfileType type : ProfileTypes.getTypes()) { + if (type.equals(ProfileTypes.SURVIVAL)) continue; + tempProfile = container.getPlayerProfileNow(type, targetPlayer); + if (tempProfile != null) { + profileTypeToUse = type; + break; + } + } } - tempProfile = container.getPlayerProfileNow(type, targetPlayer); - if (tempProfile != null) { - profileTypeToUse = type; // Use the type of the found profile - break; + if (tempProfile == null) { + issuer.sendError(ChatColor.RED + "No player data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); + return; // Exit if no profile found } + try { + contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); + armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); + offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); + } catch (CompletionException e) { + issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); + e.printStackTrace(); + return; // Exit on loading error + } + statusMessage = "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + ". (Fallback)"; + } + } else { // if the target player is offline, load the offline inventory data + + // Load the container for this world + ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName); + if (container == null) { + issuer.sendError(ChatColor.RED + "Could not load profile container for world: " + worldName); + return; // Exit if container cannot be loaded } - } - if (tempProfile == null) { - issuer.sendError("No inventory data found for " + targetPlayer.getName() + " in world " + worldName); - return; - } + // Load the targetPlayer's profile key from the container + PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); + ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; // Default to SURVIVAL + + if (tempProfile == null) { + // If SURVIVAL profile not found, iterate through other known types as a fallback + // to find ANY PlayerProfile and use its type. + for (ProfileType type : ProfileTypes.getTypes()) { + if (type.equals(ProfileTypes.SURVIVAL)) { + continue; // Skip SURVIVAL as we already tried it + } + tempProfile = container.getPlayerProfileNow(type, targetPlayer); + if (tempProfile != null) { + profileTypeToUse = type; // Use the type of the found profile + break; + } + } + } - ItemStack[] contents = null; - ItemStack[] armor = null; - ItemStack offHand = null; + if (tempProfile == null) { + issuer.sendError(ChatColor.RED + "No inventory data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); + return; // Exit if no profile found + } - try { - // Read main inventory contents - contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY) - .read() - .join(); // Use .join() to block until the future completes - - // Read armor contents - armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR) - .read() - .join(); - - // Read off-hand item - offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND) - .read() - .join(); - - } catch (CompletionException e) { - issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); - e.printStackTrace(); // Log the full stack trace for debugging - return; + try { + // Read main inventory contents + contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY) + .read() + .join(); // Use .join() to block until the future completes + + // Read armor contents + armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR) + .read() + .join(); + + // Read off-hand item + offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND) + .read() + .join(); + + } catch (CompletionException e) { + issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); + e.printStackTrace(); // Log the full stack trace for debugging + return; // Exit on loading error + } + statusMessage = "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + "."; } // Create an inventory for viewing. Size 54 (6 rows) is good for main inventory + armor + offhand. @@ -158,13 +209,12 @@ void onInventoryViewCommand( inv.setItem(37, armor[2]); // Leggings (from profile) -> Slot 37 (viewing inv) inv.setItem(36, armor[3]); // Boots (from profile) -> Slot 36 (viewing inv) } - // Fill in offhand slot (40) if (offHand != null) { inv.setItem(40, offHand); } - // Open the GUI for the viewer viewer.openInventory(inv); + issuer.sendInfo(ChatColor.GREEN + statusMessage); } -} \ No newline at end of file +} From f16d4c218f5408f538129261f4708dba9988e25a Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 13:25:42 -0400 Subject: [PATCH 07/39] Implement InventoryDataProvider for business logic --- .../commands/InventoryModifyCommand.java | 133 +++++------- .../commands/InventoryViewCommand.java | 194 ++++------------- .../profile/InventoryDataProvider.java | 200 ++++++++++++++++++ 3 files changed, 299 insertions(+), 228 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index cf9ee75b..61469270 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -1,11 +1,11 @@ package org.mvplugins.multiverse.inventories.commands; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandIssuer; import org.mvplugins.multiverse.core.world.MultiverseWorld; @@ -17,30 +17,21 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; -import org.mvplugins.multiverse.inventories.listeners.InventoryViewListener; -import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; -import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; -import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; -import org.mvplugins.multiverse.inventories.profile.key.ContainerType; -import org.mvplugins.multiverse.inventories.profile.key.ProfileType; -import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; -import org.mvplugins.multiverse.inventories.share.Sharables; - -import java.util.concurrent.CompletionException; +import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; +import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; @Service -public class InventoryModifyCommand extends InventoriesCommand { +final class InventoryModifyCommand extends InventoriesCommand { - private final ProfileContainerStoreProvider profileContainerStoreProvider; + private final InventoryDataProvider inventoryDataProvider; private final MultiverseInventories inventories; @Inject InventoryModifyCommand( - @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, + @NotNull InventoryDataProvider inventoryDataProvider, @NotNull MultiverseInventories inventories ) { - this.profileContainerStoreProvider = profileContainerStoreProvider; + this.inventoryDataProvider = inventoryDataProvider; this.inventories = inventories; } @@ -54,7 +45,7 @@ void onInventoryModifyCommand( @NotNull MVCommandIssuer issuer, @Syntax("") @Description("Online or offline player") - OfflinePlayer target, + OfflinePlayer targetPlayer, @Syntax("") @Description("The world the player's inventory is in") @@ -68,76 +59,60 @@ void onInventoryModifyCommand( issuer.sendError(ChatColor.RED + "You must specify a valid world."); return; } - if (target == null || target.getName() == null) { + if (targetPlayer == null || targetPlayer.getName() == null) { issuer.sendError(ChatColor.RED + "You must specify a valid player."); return; } String worldName = worlds[0].getName(); - ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) - .getContainer(worldName); - if (container == null) { - issuer.sendError(ChatColor.RED + "Could not load profile container for world: " + worldName); - return; - } - - PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, target); - ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; - if (tempProfile == null) { - for (ProfileType type : ProfileTypes.getTypes()) { - if (type.equals(ProfileTypes.SURVIVAL)) continue; - tempProfile = container.getPlayerProfileNow(type, target); - if (tempProfile != null) { - profileTypeToUse = type; - break; - } - } - } - if (tempProfile == null) { - issuer.sendError(ChatColor.RED + "No player data found for " + target.getName() + " in world " + worldName + ". Cannot modify inventory."); - return; - } + // Asynchronously load data using InventoryDataProvider + issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); - ItemStack[] contents = null; - ItemStack[] armor = null; - ItemStack offHand = null; + inventoryDataProvider.loadPlayerInventoryData(targetPlayer, worldName) + .thenAccept(playerInventoryData -> { + // Ensure GUI operations run on the main thread + Bukkit.getScheduler().runTask(inventories, () -> { + // Create inventory with ModifiableInventoryHolder + // Pass all necessary context to the holder for saving on close. + Component title = Component.text("Modifiying " + targetPlayer.getName() + " @ " + worldName); + Inventory inv = Bukkit.createInventory( + new ModifiableInventoryHolder( + targetPlayer, + worldName, + playerInventoryData.profileTypeUsed, // Use the determined profile type + inventories + ), + 54, + title + ); - try { - contents = SingleShareReader.of(inventories, target, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); - armor = SingleShareReader.of(inventories, target, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); - offHand = SingleShareReader.of(inventories, target, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); - } catch (CompletionException e) { - issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); - e.printStackTrace(); - return; - } - - // Create inventory with ModifiableInventoryHolder - Inventory inv = Bukkit.createInventory( - new InventoryViewListener.ModifiableInventoryHolder(target, worldName, profileTypeToUse, inventories), - 54, - "Modifying " + target.getName() + " @ " + worldName - ); - - // Fill inventory - if (contents != null) { - for (int i = 0; i < Math.min(contents.length, 36); i++) { - inv.setItem(i, contents[i]); - } - } - if (armor != null && armor.length >= 4) { - inv.setItem(39, armor[0]); - inv.setItem(38, armor[1]); - inv.setItem(37, armor[2]); - inv.setItem(36, armor[3]); - } - if (offHand != null) { - inv.setItem(40, offHand); - } + // Fill inventory + if (playerInventoryData.contents != null) { + for (int i = 0; i < Math.min(playerInventoryData.contents.length, 36); i++) { + inv.setItem(i, playerInventoryData.contents[i]); + } + } + if (playerInventoryData.armor != null && playerInventoryData.armor.length >= 4) { + inv.setItem(39, playerInventoryData.armor[0]); + inv.setItem(38, playerInventoryData.armor[1]); + inv.setItem(37, playerInventoryData.armor[2]); + inv.setItem(36, playerInventoryData.armor[3]); + } + if (playerInventoryData.offHand != null) { + inv.setItem(40, playerInventoryData.offHand); + } - viewer.openInventory(inv); - issuer.sendInfo(ChatColor.GREEN + "Opened editable inventory for " + target.getName() + - " in world " + ChatColor.YELLOW + worldName + ChatColor.GREEN + ". Changes will save on close."); + viewer.openInventory(inv); + issuer.sendInfo(ChatColor.GREEN + "Opened editable inventory for " + targetPlayer.getName() + " in world " + worldName + ". Changes will save on close."); + }); // End of Bukkit.getScheduler().runTask() + }) + .exceptionally(throwable -> { + // This block runs if an exception occurs during data loading + issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); + inventories.getLogger().severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; // Must return null for CompletableFuture in exceptionally + }); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 460bcebc..85a24b80 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -1,11 +1,12 @@ package org.mvplugins.multiverse.inventories.commands; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandIssuer; import org.mvplugins.multiverse.core.world.MultiverseWorld; @@ -17,31 +18,22 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; -import org.mvplugins.multiverse.inventories.listeners.InventoryViewListener; -import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; -import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; -import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; -import org.mvplugins.multiverse.inventories.profile.key.ContainerType; -import org.mvplugins.multiverse.inventories.profile.key.ProfileType; -import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; -import org.mvplugins.multiverse.inventories.share.Sharables; - -import java.util.concurrent.CompletionException; +import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; +import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; @Service -public class InventoryViewCommand extends InventoriesCommand { +final class InventoryViewCommand extends InventoriesCommand { - private final ProfileContainerStoreProvider profileContainerStoreProvider; + private final InventoryDataProvider inventoryDataProvider; private final MultiverseInventories inventories; @Inject InventoryViewCommand( - @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, + @NotNull InventoryDataProvider inventoryDataProvider, @NotNull MultiverseInventories inventories ) { - this.profileContainerStoreProvider = profileContainerStoreProvider; this.inventories = inventories; + this.inventoryDataProvider = inventoryDataProvider; } @Subcommand("view") @@ -80,141 +72,45 @@ void onInventoryViewCommand( } String worldName = worlds[0].getName(); - ItemStack[] contents = null; - ItemStack[] armor = null; - ItemStack offHand = null; - String statusMessage; - - // Check if target player is online - if (targetPlayer.isOnline()) { - Player onlineTarget = targetPlayer.getPlayer(); - if (onlineTarget != null) { - // If the player is online, retrieve their current live inventory - contents = onlineTarget.getInventory().getContents(); - armor = onlineTarget.getInventory().getArmorContents(); - offHand = onlineTarget.getInventory().getItemInOffHand(); - statusMessage = "Displaying LIVE inventory for " + targetPlayer.getName() + "."; - } else { - // Should not happen if isOnline() is true, but as a fallback - statusMessage = "Error: Could not get online player instance. Falling back to stored data."; - // Fallback to stored data if online player instance somehow isn't available - // This block is now correctly placed within the 'else' for onlineTarget != null - ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) - .getContainer(worldName); - if (container == null) { - issuer.sendError(ChatColor.RED + "Could not load profile container for world: " + worldName); - return; // Exit if container cannot be loaded - } - PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); - ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; - if (tempProfile == null) { - for (ProfileType type : ProfileTypes.getTypes()) { - if (type.equals(ProfileTypes.SURVIVAL)) continue; - tempProfile = container.getPlayerProfileNow(type, targetPlayer); - if (tempProfile != null) { - profileTypeToUse = type; - break; + // Asynchronously load data using InventoryDataProvider + issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); + + inventoryDataProvider.loadPlayerInventoryData(targetPlayer, worldName) + .thenAccept(playerInventoryData -> { + // Ensure GUI operations run on the main thread + Bukkit.getScheduler().runTask(inventories, () -> { + // Create an inventory for viewing. + Component title = Component.text(targetPlayer.getName() + " @ " + worldName); + Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), 54, title); + + // Fill in main inventory slots (0–35) + if (playerInventoryData.contents != null) { + for (int i = 0; i < Math.min(playerInventoryData.contents.length, 36); i++) { + inv.setItem(i, playerInventoryData.contents[i]); + } + } + // Armor slot mapping for display in the GUI + if (playerInventoryData.armor != null && playerInventoryData.armor.length >= 4) { + inv.setItem(39, playerInventoryData.armor[0]); // Helmet + inv.setItem(38, playerInventoryData.armor[1]); // Chestplate + inv.setItem(37, playerInventoryData.armor[2]); // Leggings + inv.setItem(36, playerInventoryData.armor[3]); // Boots + } + if (playerInventoryData.offHand != null) { + inv.setItem(40, playerInventoryData.offHand); } - } - } - if (tempProfile == null) { - issuer.sendError(ChatColor.RED + "No player data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); - return; // Exit if no profile found - } - try { - contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); - armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); - offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); - } catch (CompletionException e) { - issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); - e.printStackTrace(); - return; // Exit on loading error - } - statusMessage = "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + ". (Fallback)"; - } - } else { // if the target player is offline, load the offline inventory data - - // Load the container for this world - ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) - .getContainer(worldName); - if (container == null) { - issuer.sendError(ChatColor.RED + "Could not load profile container for world: " + worldName); - return; // Exit if container cannot be loaded - } - - // Load the targetPlayer's profile key from the container - PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); - ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; // Default to SURVIVAL - - if (tempProfile == null) { - // If SURVIVAL profile not found, iterate through other known types as a fallback - // to find ANY PlayerProfile and use its type. - for (ProfileType type : ProfileTypes.getTypes()) { - if (type.equals(ProfileTypes.SURVIVAL)) { - continue; // Skip SURVIVAL as we already tried it - } - tempProfile = container.getPlayerProfileNow(type, targetPlayer); - if (tempProfile != null) { - profileTypeToUse = type; // Use the type of the found profile - break; - } - } - } - - if (tempProfile == null) { - issuer.sendError(ChatColor.RED + "No inventory data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); - return; // Exit if no profile found - } - - try { - // Read main inventory contents - contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY) - .read() - .join(); // Use .join() to block until the future completes - - // Read armor contents - armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR) - .read() - .join(); - - // Read off-hand item - offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND) - .read() - .join(); - - } catch (CompletionException e) { - issuer.sendError(ChatColor.RED + "Error loading inventory data: " + e.getCause().getMessage()); - e.printStackTrace(); // Log the full stack trace for debugging - return; // Exit on loading error - } - statusMessage = "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + "."; - } - - // Create an inventory for viewing. Size 54 (6 rows) is good for main inventory + armor + offhand. - // This links the inventory to our listener, making it read-only. - Inventory inv = Bukkit.createInventory(new InventoryViewListener.ReadOnlyInventoryHolder(), 54, targetPlayer.getName() + " @ " + worldName); - - // Fill in main inventory slots (0–35) - // Ensure we don't go out of bounds if contents is smaller than expected - if (contents != null) { - for (int i = 0; i < Math.min(contents.length, 36); i++) { - inv.setItem(i, contents[i]); - } - } - if (armor != null && armor.length >= 4) { - inv.setItem(39, armor[0]); // Helmet (from profile) -> Slot 39 (viewing inv) - inv.setItem(38, armor[1]); // Chestplate (from profile) -> Slot 38 (viewing inv) - inv.setItem(37, armor[2]); // Leggings (from profile) -> Slot 37 (viewing inv) - inv.setItem(36, armor[3]); // Boots (from profile) -> Slot 36 (viewing inv) - } - // Fill in offhand slot (40) - if (offHand != null) { - inv.setItem(40, offHand); - } - // Open the GUI for the viewer - viewer.openInventory(inv); - issuer.sendInfo(ChatColor.GREEN + statusMessage); + viewer.openInventory(inv); + issuer.sendInfo(ChatColor.GREEN + playerInventoryData.statusMessage); + }); // End of Bukkit.getScheduler().runTask() + }) + .exceptionally(throwable -> { + // This block runs if an exception occurs during data loading + issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); + inventories.getLogger().severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; // Must return null for CompletableFuture in exceptionally + }); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java new file mode 100644 index 00000000..e244b1bc --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -0,0 +1,200 @@ +package org.mvplugins.multiverse.inventories.profile; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; +import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; +import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; +import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; +import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; +import org.mvplugins.multiverse.inventories.share.Sharables; + +import java.util.concurrent.CompletableFuture; // Needed for async operations +import java.util.concurrent.CompletionException; // Needed for async error handling + +/** + * Provides methods for asynchronously loading player inventory data. + * This class encapsulates the business logic for fetching inventory, armor, and off-hand contents. + */ +@Service +public final class InventoryDataProvider { + + private final ProfileContainerStoreProvider profileContainerStoreProvider; + private final MultiverseInventories inventories; + + @Inject + public InventoryDataProvider( + @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, + @NotNull MultiverseInventories inventories + ) { + this.profileContainerStoreProvider = profileContainerStoreProvider; + this.inventories = inventories; + } + + /** + * Represents the loaded inventory data. + */ + public static class PlayerInventoryData { + public final ItemStack[] contents; + public final ItemStack[] armor; + public final ItemStack offHand; + public final String statusMessage; // To indicate if it's live or stored data + public final ProfileType profileTypeUsed; // To pass back which profile type was used for stored data + + public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, String statusMessage, ProfileType profileTypeUsed) { + this.contents = contents; + this.armor = armor; + this.offHand = offHand; + this.statusMessage = statusMessage; + this.profileTypeUsed = profileTypeUsed; + } + } + + /** + * Asynchronously loads a player's inventory data. + * If the player is online AND in the specified world, it attempts to get their live inventory. + * Otherwise (offline or online in a different world), it loads from Multiverse-Inventories' stored profiles. + * + * @param targetPlayer The OfflinePlayer whose inventory data to load. + * @param worldName The name of the world to load the inventory from (either live or stored). + * @return A CompletableFuture that will complete with PlayerInventoryData or an exception. + */ + public CompletableFuture loadPlayerInventoryData( + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName + ) { + // If the player is online, prioritize getting their live inventory + if (targetPlayer.isOnline()) { + Player onlineTarget = targetPlayer.getPlayer(); + // Ensure onlineTarget is not null and their current world matches the requested worldName + if (onlineTarget != null && onlineTarget.getWorld().getName().equalsIgnoreCase(worldName)) { + // Get the actual ProfileType for the online player + ProfileType profileType = ProfileTypes.forPlayer(onlineTarget); + // Return immediately with live data + return CompletableFuture.completedFuture(new PlayerInventoryData( + onlineTarget.getInventory().getContents(), + onlineTarget.getInventory().getArmorContents(), + onlineTarget.getInventory().getItemInOffHand(), + "Displaying LIVE inventory for " + targetPlayer.getName() + worldName + ".", + profileType + )); + } + // If online but in a different world, or getPlayer() returned null, fall through to stored data logic + if (onlineTarget != null) { + inventories.getLogger().fine("Player " + targetPlayer.getName() + " is online but in world " + onlineTarget.getWorld().getName() + ". Loading stored data for " + worldName + "."); + } else { + inventories.getLogger().warning("Player " + targetPlayer.getName() + " is online but getPlayer() returned null. Falling back to stored data."); + } + } + // If the player is offline or online in a different world, or live data failed, load from Multiverse-Inventories' stored profiles + return CompletableFuture.supplyAsync(() -> { + ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName); + if (container == null) { + throw new IllegalStateException("Could not load profile container for world: " + worldName); + } + + PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); + ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; + + if (tempProfile == null) { + for (ProfileType type : ProfileTypes.getTypes()) { + if (type.equals(ProfileTypes.SURVIVAL)) continue; + tempProfile = container.getPlayerProfileNow(type, targetPlayer); + if (tempProfile != null) { + profileTypeToUse = type; + break; + } + } + } + + if (tempProfile == null) { + throw new IllegalStateException("No player data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); + } + + try { + ItemStack[] contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); + ItemStack[] armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); + ItemStack offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); + + return new PlayerInventoryData(contents, armor, offHand, "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + ".", profileTypeToUse); + } catch (CompletionException e) { + // Unwrap CompletionException to get the actual cause + throw new IllegalStateException("Error loading inventory data: " + e.getCause().getMessage(), e.getCause()); + } + }); + } + + + /** + * Asynchronously saves a player's inventory data to their Multiverse-Inventories profile. + * If the player is online and in the target world, their live inventory is also updated. + * + * @param targetPlayer The OfflinePlayer whose inventory data to save. + * @param worldName The name of the world the inventory belongs to. + * @param profileType The ProfileType (e.g., SURVIVAL) associated with this inventory data. + * @param newContents The new main inventory contents (slots 0-35). + * @param newArmor The new armor contents (helmet, chestplate, leggings, boots). + * @param newOffHand The new off-hand item. + * @return A CompletableFuture that completes when saving is done, or an exception occurs. + */ + public CompletableFuture savePlayerInventoryData( + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull ProfileType profileType, + @NotNull ItemStack[] newContents, + @NotNull ItemStack[] newArmor, + @NotNull ItemStack newOffHand + ) { + // Save the updated inventory, armor, and off-hand contents asynchronously + CompletableFuture saveFuture = CompletableFuture.allOf( + SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.INVENTORY) + .write(newContents, true) // true to update if player is online + .thenRun(() -> inventories.getLogger().fine("Saved inventory for " + targetPlayer.getName() + " in " + worldName)), + SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.ARMOR) + .write(newArmor, true) + .thenRun(() -> inventories.getLogger().fine("Saved armor for " + targetPlayer.getName() + " in " + worldName)), + SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.OFF_HAND) + .write(newOffHand, true) + .thenRun(() -> inventories.getLogger().fine("Saved off-hand for " + targetPlayer.getName() + " in " + worldName)) + ); + + return saveFuture.thenRun(() -> { + inventories.getLogger().info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); + + // If the target player is online, update their live inventory + if (targetPlayer.isOnline()) { + Player onlinePlayer = targetPlayer.getPlayer(); + if (onlinePlayer != null) { + // Check if the online player is in the world whose inventory was modified + // This is important to avoid overwriting their current inventory if they are in a different world + if (onlinePlayer.getWorld().getName().equalsIgnoreCase(worldName)) { + // Run Bukkit API calls on the main thread + Bukkit.getScheduler().runTask(inventories, () -> { + onlinePlayer.getInventory().setContents(newContents); + onlinePlayer.getInventory().setArmorContents(newArmor); + onlinePlayer.getInventory().setItemInOffHand(newOffHand); + onlinePlayer.updateInventory(); // Ensure client sees changes + inventories.getLogger().info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); + }); + } else { + inventories.getLogger().info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); + } + } + } + }).exceptionally(throwable -> { + inventories.getLogger().severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; + }); + } +} \ No newline at end of file From 06f469dd2c31b1f7d3386a91ef7846af9b9010e5 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 13:26:23 -0400 Subject: [PATCH 08/39] Create new package for ModifiableInventoryHolder and ReadOnlyInventoryHolder --- .../listeners/InventoryViewListener.java | 116 ++++-------------- .../inventories/listeners/MVInvListener.java | 1 + .../view/ModifiableInventoryHolder.java | 53 ++++++++ .../view/ReadOnlyInventoryHolder.java | 19 +++ 4 files changed, 96 insertions(+), 93 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 3c88aeba..d26dd33c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -17,34 +17,30 @@ import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; +import org.mvplugins.multiverse.inventories.listeners.MVInvListener; +import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import org.mvplugins.multiverse.inventories.share.Sharables; +import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; +import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; import java.util.Arrays; import java.util.concurrent.CompletableFuture; @Service -public final class InventoryViewListener implements MVInvListener { +final class InventoryViewListener implements MVInvListener { private final MultiverseInventories inventories; + private final InventoryDataProvider inventoryDataProvider; @Inject InventoryViewListener( - @NotNull MultiverseInventories inventories) { + @NotNull MultiverseInventories inventories, + @NotNull InventoryDataProvider inventoryDataProvider + ) { this.inventories = inventories; + this.inventoryDataProvider = inventoryDataProvider; } - - // This class acts as a marker. When an inventory is created with this holder, - // the event listener can identify it as a read-only inventory. - public static class ReadOnlyInventoryHolder implements InventoryHolder { - @Override - public @NotNull Inventory getInventory() { - // This method is required by the interface but isn't strictly used for our marker purpose. - // The actual inventory is obtained from the InventoryClickEvent itself. - return null; - } - } - // This listener will cancel any clicks or drags in inventories that have the ReadOnlyInventoryHolder marker. @EventMethod @DefaultEventPriority(EventPriority.NORMAL) @@ -53,8 +49,7 @@ public void onInventoryClick(InventoryClickEvent event) { if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder) { event.setCancelled(true); } - // This covers cases where a player might click in their own inventory - // but the action is intended to move an item into the read-only inventory (e.g., shift-click). + // If the clicked inventory is read-only, cancel the event (e.g., shift-click into it) else if (event.getClickedInventory() != null && event.getClickedInventory().getHolder() instanceof ReadOnlyInventoryHolder) { event.setCancelled(true); } @@ -69,45 +64,6 @@ public void onInventoryDrag(InventoryDragEvent event) { } } - // This holder stores context needed to save the inventory when it's closed. - public static class ModifiableInventoryHolder implements InventoryHolder { - private final OfflinePlayer targetPlayer; - private final String worldName; - private final ProfileType profileType; - private final MultiverseInventories inventories; - - public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, - @NotNull String worldName, - @NotNull ProfileType profileType, - @NotNull MultiverseInventories inventories) { - this.targetPlayer = targetPlayer; - this.worldName = worldName; - this.profileType = profileType; - this.inventories = inventories; - } - - public @NotNull OfflinePlayer getTargetPlayer() { - return targetPlayer; - } - - public @NotNull String getWorldName() { - return worldName; - } - - public @NotNull ProfileType getProfileType() { - return profileType; - } - - public @NotNull MultiverseInventories getInventories() { - return inventories; - } - - @Override - public @NotNull Inventory getInventory() { - return null; // The actual inventory is passed via the event - } - } - // Event handler for InventoryCloseEvent to save changes @EventMethod @DefaultEventPriority(EventPriority.NORMAL) @@ -130,46 +86,20 @@ public void onInventoryClose(InventoryCloseEvent event) { newArmor[3] = closedInventory.getItem(36); // Boots ItemStack newOffHand = closedInventory.getItem(40); - // Save the updated inventory, armor, and off-hand contents asynchronously - CompletableFuture saveFuture = CompletableFuture.allOf( - SingleShareWriter.of(plugin, targetPlayer, worldName, profileType, Sharables.INVENTORY) - .write(newContents, true) // true to update if player is online - .thenRun(() -> plugin.getLogger().fine("Saved inventory for " + targetPlayer.getName() + " in " + worldName)), - SingleShareWriter.of(plugin, targetPlayer, worldName, profileType, Sharables.ARMOR) - .write(newArmor, true) - .thenRun(() -> plugin.getLogger().fine("Saved armor for " + targetPlayer.getName() + " in " + worldName)), - SingleShareWriter.of(plugin, targetPlayer, worldName, profileType, Sharables.OFF_HAND) - .write(newOffHand, true) - .thenRun(() -> plugin.getLogger().fine("Saved off-hand for " + targetPlayer.getName() + " in " + worldName)) - ); - - saveFuture.thenRun(() -> { - plugin.getLogger().info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); - }).exceptionally(throwable -> { - plugin.getLogger().severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); + // Delegate saving to InventoryDataProvider + inventoryDataProvider.savePlayerInventoryData( + targetPlayer, + worldName, + profileType, + newContents, + newArmor, + newOffHand + ).exceptionally(throwable -> { + // Error logging is now handled within InventoryDataProvider, but we can add a general one here too + inventories.getLogger().severe("Error during inventory save process for " + targetPlayer.getName() + ": " + throwable.getMessage()); throwable.printStackTrace(); return null; }); - - // If the target player is online, update their live inventory - if (targetPlayer.isOnline()) { - Player onlinePlayer = targetPlayer.getPlayer(); - if (onlinePlayer != null) { - // Check if the online player is in the world whose inventory was modified - // This is important to avoid overwriting their current inventory if they are in a different world - if (onlinePlayer.getWorld().getName().equalsIgnoreCase(worldName)) { - Bukkit.getScheduler().runTask(plugin, () -> { - onlinePlayer.getInventory().setContents(newContents); - onlinePlayer.getInventory().setArmorContents(newArmor); - onlinePlayer.getInventory().setItemInOffHand(newOffHand); - onlinePlayer.updateInventory(); // Ensure client sees changes - plugin.getLogger().info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); - }); - } else { - plugin.getLogger().info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); - } - } } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java index 713bd006..88518e82 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/MVInvListener.java @@ -2,6 +2,7 @@ import org.jvnet.hk2.annotations.Contract; import org.mvplugins.multiverse.core.dynamiclistener.DynamicListener; +import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; @Contract public sealed interface MVInvListener extends DynamicListener permits InventoryViewListener, MVEventsListener, RespawnListener, ShareHandleListener, SpawnChangeListener { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java new file mode 100644 index 00000000..14ace172 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java @@ -0,0 +1,53 @@ +package org.mvplugins.multiverse.inventories.view; + +import org.bukkit.OfflinePlayer; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; + +/** + * A custom InventoryHolder that serves as a marker for modifiable inventories. + * It stores the necessary context (player, world, profile type, plugin instance) + * to save changes back to the player's profile when the inventory is closed. + */ +public final class ModifiableInventoryHolder implements InventoryHolder { + private final OfflinePlayer targetPlayer; + private final String worldName; + private final ProfileType profileType; + private final MultiverseInventories inventories; + + public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull ProfileType profileType, + @NotNull MultiverseInventories inventories) { + this.targetPlayer = targetPlayer; + this.worldName = worldName; + this.profileType = profileType; + this.inventories = inventories; + } + + public @NotNull OfflinePlayer getTargetPlayer() { + return targetPlayer; + } + + public @NotNull String getWorldName() { + return worldName; + } + + public @NotNull ProfileType getProfileType() { + return profileType; + } + + public @NotNull MultiverseInventories getInventories() { + return inventories; + } + + @Override + public @NotNull Inventory getInventory() { + // This method is required by the interface but is not directly used for the marker purpose. + // Throwing UnsupportedOperationException clearly indicates it's not meant to be called. + throw new UnsupportedOperationException("ModifiableInventoryHolder does not provide an Inventory directly."); + } +} \ No newline at end of file diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java b/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java new file mode 100644 index 00000000..ee15950b --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java @@ -0,0 +1,19 @@ +package org.mvplugins.multiverse.inventories.view; // New package + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; + +/** + * A custom InventoryHolder that serves as a marker for read-only inventories. + * Inventories created with this holder will have their interactions cancelled by the InventoryViewListener. + */ +public final class ReadOnlyInventoryHolder implements InventoryHolder { + + @Override + public @NotNull Inventory getInventory() { + // This method is required by the interface but is not directly used for the marker purpose. + // Throwing UnsupportedOperationException clearly indicates it's not meant to be called. + throw new UnsupportedOperationException("ReadOnlyInventoryHolder does not provide an Inventory directly."); + } +} \ No newline at end of file From 0a95cf79b380dea8d5eb5415f15d7f631354c785 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 13:39:56 -0400 Subject: [PATCH 09/39] Minor update, remove unused code --- .../inventories/commands/InventoryViewCommand.java | 1 - .../listeners/InventoryViewListener.java | 13 +++---------- .../inventories/profile/InventoryDataProvider.java | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 85a24b80..01f18bb6 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -5,7 +5,6 @@ import org.bukkit.ChatColor; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandIssuer; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index d26dd33c..12de231c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -1,14 +1,11 @@ package org.mvplugins.multiverse.inventories.listeners; -import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; import org.bukkit.event.EventPriority; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority; @@ -16,16 +13,12 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; -import org.mvplugins.multiverse.inventories.listeners.MVInvListener; import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; -import org.mvplugins.multiverse.inventories.share.Sharables; import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; import java.util.Arrays; -import java.util.concurrent.CompletableFuture; @Service final class InventoryViewListener implements MVInvListener { @@ -41,6 +34,7 @@ final class InventoryViewListener implements MVInvListener { this.inventories = inventories; this.inventoryDataProvider = inventoryDataProvider; } + // This listener will cancel any clicks or drags in inventories that have the ReadOnlyInventoryHolder marker. @EventMethod @DefaultEventPriority(EventPriority.NORMAL) @@ -73,7 +67,6 @@ public void onInventoryClose(InventoryCloseEvent event) { final OfflinePlayer targetPlayer = holder.getTargetPlayer(); final String worldName = holder.getWorldName(); final ProfileType profileType = holder.getProfileType(); - final MultiverseInventories plugin = holder.getInventories(); final Inventory closedInventory = event.getInventory(); // Extract contents: 0-35 for inventory, 36-39 for armor, 40 for off-hand @@ -100,6 +93,6 @@ public void onInventoryClose(InventoryCloseEvent event) { throwable.printStackTrace(); return null; }); - } } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index e244b1bc..8dc9c51f 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -84,7 +84,7 @@ public CompletableFuture loadPlayerInventoryData( onlineTarget.getInventory().getContents(), onlineTarget.getInventory().getArmorContents(), onlineTarget.getInventory().getItemInOffHand(), - "Displaying LIVE inventory for " + targetPlayer.getName() + worldName + ".", + "Displaying LIVE inventory for " + targetPlayer.getName() + " in world " + worldName + ".", profileType )); } From 9e7873780c84e434575f96eabf08fce9c41c03a7 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 14:44:33 -0400 Subject: [PATCH 10/39] Implement helper class to insert filler armor/off-hand items in the inventory GUI --- .../commands/InventoryModifyCommand.java | 56 +++++++- .../commands/InventoryViewCommand.java | 43 ++++-- .../listeners/InventoryViewListener.java | 123 +++++++++++++++++- .../inventories/view/InventoryGUIHelper.java | 112 ++++++++++++++++ 4 files changed, 317 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 61469270..d701d2c0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -1,11 +1,15 @@ package org.mvplugins.multiverse.inventories.commands; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.ChatColor; +import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandIssuer; import org.mvplugins.multiverse.core.world.MultiverseWorld; @@ -18,21 +22,27 @@ import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; +import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; +import java.util.Arrays; + @Service final class InventoryModifyCommand extends InventoriesCommand { private final InventoryDataProvider inventoryDataProvider; private final MultiverseInventories inventories; + private final InventoryGUIHelper inventoryGUIHelper; @Inject InventoryModifyCommand( @NotNull InventoryDataProvider inventoryDataProvider, - @NotNull MultiverseInventories inventories + @NotNull MultiverseInventories inventories, + @NotNull InventoryGUIHelper inventoryGUIHelper ) { this.inventoryDataProvider = inventoryDataProvider; this.inventories = inventories; + this.inventoryGUIHelper = inventoryGUIHelper; } // This method contains the logic for the /mvinv modify command @@ -92,13 +102,36 @@ void onInventoryModifyCommand( inv.setItem(i, playerInventoryData.contents[i]); } } - if (playerInventoryData.armor != null && playerInventoryData.armor.length >= 4) { + // Armor slot mapping for display in the GUI and add fillers if empty + // Slot 39: Helmet + if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { + inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper + } else { inv.setItem(39, playerInventoryData.armor[0]); + } + // Slot 38: Chestplate + if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { + inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper + } else { inv.setItem(38, playerInventoryData.armor[1]); + } + // Slot 37: Leggings + if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { + inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37)); // Use helper + } else { inv.setItem(37, playerInventoryData.armor[2]); + } + // Slot 36: Boots + if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { + inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper + } else { inv.setItem(36, playerInventoryData.armor[3]); } - if (playerInventoryData.offHand != null) { + + // Off-hand slot (40) and add filler if empty + if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { + inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40)); // Use helper + } else { inv.setItem(40, playerInventoryData.offHand); } @@ -114,5 +147,22 @@ void onInventoryModifyCommand( return null; // Must return null for CompletableFuture in exceptionally }); } + /** + * Helper method to create a filler item for GUI slots. + * @param material The material of the filler item. + * @param name The display name of the item. + * @param lore The lore text for the item. + * @return The created ItemStack. + */ + private ItemStack createFillerItem(Material material, String name, String lore) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(name, NamedTextColor.GOLD)); // Use Component for name + meta.lore(Arrays.asList(Component.text(lore, NamedTextColor.GRAY))); // Use Component for lore + item.setItemMeta(meta); + } + return item; + } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 01f18bb6..3b40d71e 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -3,6 +3,7 @@ import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.ChatColor; +import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; @@ -17,6 +18,7 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; @@ -25,14 +27,17 @@ final class InventoryViewCommand extends InventoriesCommand { private final InventoryDataProvider inventoryDataProvider; private final MultiverseInventories inventories; + private final InventoryGUIHelper inventoryGUIHelper; @Inject InventoryViewCommand( @NotNull InventoryDataProvider inventoryDataProvider, - @NotNull MultiverseInventories inventories + @NotNull MultiverseInventories inventories, + @NotNull InventoryGUIHelper inventoryGUIHelper ) { this.inventories = inventories; this.inventoryDataProvider = inventoryDataProvider; + this.inventoryGUIHelper = inventoryGUIHelper; } @Subcommand("view") @@ -89,14 +94,36 @@ void onInventoryViewCommand( inv.setItem(i, playerInventoryData.contents[i]); } } - // Armor slot mapping for display in the GUI - if (playerInventoryData.armor != null && playerInventoryData.armor.length >= 4) { - inv.setItem(39, playerInventoryData.armor[0]); // Helmet - inv.setItem(38, playerInventoryData.armor[1]); // Chestplate - inv.setItem(37, playerInventoryData.armor[2]); // Leggings - inv.setItem(36, playerInventoryData.armor[3]); // Boots + // Armor slot mapping for display in the GUI and add fillers if empty + // Slot 39: Helmet + if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { + inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper + } else { + inv.setItem(39, playerInventoryData.armor[0]); } - if (playerInventoryData.offHand != null) { + // Slot 38: Chestplate + if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { + inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper + } else { + inv.setItem(38, playerInventoryData.armor[1]); + } + // Slot 37: Leggings + if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { + inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37)); // Use helper + } else { + inv.setItem(37, playerInventoryData.armor[2]); + } + // Slot 36: Boots + if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { + inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper + } else { + inv.setItem(36, playerInventoryData.armor[3]); + } + + // Off-hand slot (40) and add filler if empty + if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { + inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40)); // Use helper + } else { inv.setItem(40, playerInventoryData.offHand); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 12de231c..2e3abf45 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -1,7 +1,11 @@ package org.mvplugins.multiverse.inventories.listeners; +import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; import org.bukkit.event.EventPriority; +import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; @@ -15,6 +19,7 @@ import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; @@ -25,27 +30,101 @@ final class InventoryViewListener implements MVInvListener { private final MultiverseInventories inventories; private final InventoryDataProvider inventoryDataProvider; + private final InventoryGUIHelper inventoryGUIHelper; @Inject InventoryViewListener( @NotNull MultiverseInventories inventories, - @NotNull InventoryDataProvider inventoryDataProvider + @NotNull InventoryDataProvider inventoryDataProvider, + @NotNull InventoryGUIHelper inventoryGUIHelper ) { this.inventories = inventories; this.inventoryDataProvider = inventoryDataProvider; + this.inventoryGUIHelper = inventoryGUIHelper; } // This listener will cancel any clicks or drags in inventories that have the ReadOnlyInventoryHolder marker. @EventMethod @DefaultEventPriority(EventPriority.NORMAL) public void onInventoryClick(InventoryClickEvent event) { - // Check if the inventory being clicked has the custom holder - if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder) { + // If it's a read-only inventory, cancel all clicks. + if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder || + (event.getClickedInventory() != null && event.getClickedInventory().getHolder() instanceof ReadOnlyInventoryHolder)) { event.setCancelled(true); + return; } - // If the clicked inventory is read-only, cancel the event (e.g., shift-click into it) - else if (event.getClickedInventory() != null && event.getClickedInventory().getHolder() instanceof ReadOnlyInventoryHolder) { - event.setCancelled(true); + + // If it's a modifiable inventory, apply specific restrictions for armor/off-hand slots. + if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder) { + int clickedSlot = event.getRawSlot(); + ItemStack cursorItem = event.getCursor(); // Item held by the cursor + ItemStack currentItem = event.getCurrentItem(); // Item in the clicked slot + + // Define the special slots + boolean isSpecialSlot = (clickedSlot >= 36 && clickedSlot <= 40); // Armor (36-39) and Off-hand (40) + + // --- Logic to prevent moving filler items AND restore them if slot becomes empty --- + if (isSpecialSlot) { + // Check if the current item in the slot is a filler item (e.g., stained glass pane) + // This is a simple check; a more robust check might involve NBT tags if fillers are complex. + boolean isFillerInSlot = currentItem != null && + (currentItem.getType() == Material.GRAY_STAINED_GLASS_PANE || + currentItem.getType() == Material.LIGHT_GRAY_STAINED_GLASS_PANE); + + // Scenario 1: Player tries to take out a filler item or replace it with an invalid item + if (isFillerInSlot && (cursorItem == null || cursorItem.getType() == Material.AIR || !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot))) { + event.setCancelled(true); // Prevent taking out filler or putting invalid item on top of it + // If they tried to take it out (cursor empty), put it back immediately + if (cursorItem == null || cursorItem.getType() == Material.AIR) { + Bukkit.getScheduler().runTaskLater(inventories, () -> { + event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); // Use helper + }, 1L); // Run one tick later to avoid conflicts with Bukkit's internal inventory updates + } + return; + } + + // Scenario 2: Player tries to place an invalid item into a special slot (even if it's empty or has a valid item) + if (cursorItem != null && cursorItem.getType() != Material.AIR && !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { // Use helper + event.setCancelled(true); + return; + } + + // Scenario 3 + // If the item in the slot *before* the click was a filler, and a valid item is being placed, + // we need to clear the cursor after Bukkit handles the swap. + if (isFillerInSlot && cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { + // Schedule a task to run after the event has fully processed + Bukkit.getScheduler().runTaskLater(inventories, () -> { + Player player = (Player) event.getWhoClicked(); + // Check if the item on the player's cursor is now the filler item + if (player.getItemOnCursor() != null && inventoryGUIHelper.isFillerItem(player.getItemOnCursor())) { + player.setItemOnCursor(null); // Clear the cursor + player.updateInventory(); // Update client to reflect cursor change + } + // Also ensure the slot has a filler if it became empty (e.g., if the placed item was a stack of 1) + if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { + event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); + } + }, 1L); + return; // Run one tick later to ensure Bukkit's updates have settled + } + + // Scenario 4: Player is shift-clicking a valid item *from* a special slot. + // Or picking up a valid item from a special slot. + // We need to ensure filler reappears if the slot becomes empty. + if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY || + event.getAction() == InventoryAction.PICKUP_ALL || + event.getAction() == InventoryAction.PICKUP_HALF || + event.getAction() == InventoryAction.PICKUP_ONE || + event.getAction() == InventoryAction.PICKUP_SOME) { + Bukkit.getScheduler().runTaskLater(inventories, () -> { + // After Bukkit processes the click, if the slot is now empty, put the filler back. + if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { + event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); + } + }, 1L); + } + } } } @@ -53,9 +132,41 @@ else if (event.getClickedInventory() != null && event.getClickedInventory().getH @EventMethod @DefaultEventPriority(EventPriority.NORMAL) public void onInventoryDrag(InventoryDragEvent event) { + // If it is a read-only inventory, cancel all drags if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder) { event.setCancelled(true); } + + // If it's a modifiable inventory, apply specific restrictions for armor/off-hand slots. + if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder) { + ItemStack draggedItem = event.getCursor(); // The item being dragged (after the drag operation) + + // If nothing is being dragged, or dragging air, no restriction needed. + if (draggedItem == null || draggedItem.getType() == Material.AIR) { + return; + } + for (int slot : event.getRawSlots()) { + // Define the special slots + boolean isSpecialSlot = (slot >= 36 && slot <= 40); + + if (isSpecialSlot) { + // Check if the dragged item is valid for this special slot + if (!inventoryGUIHelper.isValidItemForSlot(draggedItem, slot)) { // Use helper + event.setCancelled(true); + return; // Cancel the entire drag event + } + } + } + + // After a drag, check if any special slots became empty and replace with filler + Bukkit.getScheduler().runTaskLater(inventories, () -> { + for (int slot : event.getRawSlots()) { + if ((slot >= 36 && slot <= 40) && (event.getInventory().getItem(slot) == null || event.getInventory().getItem(slot).getType() == Material.AIR)) { + event.getInventory().setItem(slot, inventoryGUIHelper.createFillerItemForSlot(slot)); // Use helper + } + } + }, 1L); // Run one tick later + } } // Event handler for InventoryCloseEvent to save changes diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java new file mode 100644 index 00000000..88c252c4 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -0,0 +1,112 @@ +package org.mvplugins.multiverse.inventories.view; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; // Mark as a service for injection +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.inventories.MultiverseInventories; + +import java.util.Collections; + +/** + * A helper class for creating and validating items within the custom inventory GUIs. + * This centralizes logic for filler items and slot-specific item validation. + */ +@Service // Allows this class to be injected +public final class InventoryGUIHelper { // Made public and final + + private final NamespacedKey IS_FILLER_KEY; // Key to mark filler items + + @Inject // Inject MultiverseInventories to create NamespacedKey + public InventoryGUIHelper(@NotNull MultiverseInventories inventories) { + this.IS_FILLER_KEY = new NamespacedKey(inventories, "is_mvinv_filler"); + } + /** + * Creates a generic filler item for GUI slots. + * @param material The material of the filler item. + * @param name The display name of the item. + * @param lore The lore text for the item. + * @return The created ItemStack. + */ + public ItemStack createFillerItem(Material material, String name, String lore) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.displayName(Component.text(name, NamedTextColor.GOLD)); + meta.lore(Collections.singletonList(Component.text(lore, NamedTextColor.GRAY))); + meta.getPersistentDataContainer().set(IS_FILLER_KEY, PersistentDataType.BYTE, (byte) 1); // store 1 for true + item.setItemMeta(meta); + } + return item; + } + + /** + * Checks if a given ItemStack is a filler item created by this helper. + * @param item The ItemStack to check. + * @return True if the item is a filler, false otherwise. + */ + public boolean isFillerItem(@NotNull ItemStack item) { + if (!item.hasItemMeta()) { + return false; + } + return item.getItemMeta().getPersistentDataContainer().has(IS_FILLER_KEY, PersistentDataType.BYTE) && + item.getItemMeta().getPersistentDataContainer().get(IS_FILLER_KEY, PersistentDataType.BYTE) == (byte) 1; + } + + /** + * Determines if an ItemStack is valid for a given special inventory slot in the custom GUI. + * This method is used for both armor and off-hand slot validation. + * @param item The ItemStack to check. + * @param slot The raw slot number (36-40 for armor/off-hand). + * @return True if the item is valid for the slot, false otherwise. + */ + public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { + if (item.getType() == Material.AIR) { + return true; // Air is always valid (it means the slot is empty) + } + // If it's a filler item, it's considered valid for its own slot context (e.g., when checking if slot is empty) + if (isFillerItem(item)) { + return true; + } + + switch (slot) { + case 39: // Helmet slot + return item.getType().name().endsWith("_HELMET"); + case 38: // Chestplate slot + return item.getType().name().endsWith("_CHESTPLATE"); + case 37: // Leggings slot + return item.getType().name().endsWith("_LEGGINGS"); + case 36: // Boots slot + return item.getType().name().endsWith("_BOOTS"); + case 40: // Off-hand slot + // Off-hand is very permissive in vanilla. Allow any non-air item. + // If you want to restrict this further (e.g., only shields/totems), + // add more specific Material checks here. + return true; + default: + return true; // For non-special slots (main inventory), any item is generally allowed. + } + } + + /** + * Creates the appropriate filler item for a given special slot in the custom GUI. + * @param slot The raw slot number (36-40). + * @return The specific filler ItemStack for that slot. + */ + public ItemStack createFillerItemForSlot(int slot) { + switch (slot) { + case 39: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); + case 38: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", "Place Chestplate Here"); + case 37: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); + case 36: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); + case 40: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", "Place Off-Hand Item Here"); + default: return new ItemStack(Material.AIR); // Should not happen for these slots + } + } +} \ No newline at end of file From f24dd31322b269c0f1a5cb970e82059db7d097b2 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 16:00:35 -0400 Subject: [PATCH 11/39] Fix bugs associated with the item filler. Remove ability for players to modify their own inventory --- .../commands/InventoryModifyCommand.java | 52 ++++----- .../commands/InventoryViewCommand.java | 30 +++--- .../listeners/InventoryViewListener.java | 100 ++++++++++-------- .../profile/InventoryDataProvider.java | 3 +- .../inventories/view/InventoryGUIHelper.java | 16 +-- 5 files changed, 101 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index d701d2c0..898a8920 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -73,6 +73,10 @@ void onInventoryModifyCommand( issuer.sendError(ChatColor.RED + "You must specify a valid player."); return; } + if (viewer.getUniqueId().equals(targetPlayer.getUniqueId())) { + issuer.sendError(ChatColor.RED + "You cannot modify your own inventory using this command. Use your regular inventory."); + return; + } String worldName = worlds[0].getName(); // Asynchronously load data using InventoryDataProvider @@ -103,31 +107,30 @@ void onInventoryModifyCommand( } } // Armor slot mapping for display in the GUI and add fillers if empty - // Slot 39: Helmet - if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper - } else { - inv.setItem(39, playerInventoryData.armor[0]); - } - // Slot 38: Chestplate - if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper + // Slot 36: Helmet + if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { + inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper } else { - inv.setItem(38, playerInventoryData.armor[1]); + inv.setItem(36, playerInventoryData.armor[3]); } - // Slot 37: Leggings + // Slot 37: Chestplate if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37)); // Use helper } else { inv.setItem(37, playerInventoryData.armor[2]); } - // Slot 36: Boots - if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper + // Slot 38: Leggings + if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { + inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper } else { - inv.setItem(36, playerInventoryData.armor[3]); + inv.setItem(38, playerInventoryData.armor[1]); + } + // Slot 39: Boots + if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { + inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper + } else { + inv.setItem(39, playerInventoryData.armor[0]); } - // Off-hand slot (40) and add filler if empty if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40)); // Use helper @@ -147,22 +150,5 @@ void onInventoryModifyCommand( return null; // Must return null for CompletableFuture in exceptionally }); } - /** - * Helper method to create a filler item for GUI slots. - * @param material The material of the filler item. - * @param name The display name of the item. - * @param lore The lore text for the item. - * @return The created ItemStack. - */ - private ItemStack createFillerItem(Material material, String name, String lore) { - ItemStack item = new ItemStack(material); - ItemMeta meta = item.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text(name, NamedTextColor.GOLD)); // Use Component for name - meta.lore(Arrays.asList(Component.text(lore, NamedTextColor.GRAY))); // Use Component for lore - item.setItemMeta(meta); - } - return item; - } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 3b40d71e..a5de2a37 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -95,29 +95,29 @@ void onInventoryViewCommand( } } // Armor slot mapping for display in the GUI and add fillers if empty - // Slot 39: Helmet - if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper - } else { - inv.setItem(39, playerInventoryData.armor[0]); - } - // Slot 38: Chestplate - if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper + // Slot 36: Helmet + if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { + inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper } else { - inv.setItem(38, playerInventoryData.armor[1]); + inv.setItem(36, playerInventoryData.armor[3]); } - // Slot 37: Leggings + // Slot 37: Chestplate if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37)); // Use helper } else { inv.setItem(37, playerInventoryData.armor[2]); } - // Slot 36: Boots - if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper + // Slot 38: Leggings + if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { + inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper } else { - inv.setItem(36, playerInventoryData.armor[3]); + inv.setItem(38, playerInventoryData.armor[1]); + } + // Slot 39: Boots + if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { + inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper + } else { + inv.setItem(39, playerInventoryData.armor[0]); } // Off-hand slot (40) and add filler if empty diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 2e3abf45..fa216bc8 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -59,57 +59,61 @@ public void onInventoryClick(InventoryClickEvent event) { int clickedSlot = event.getRawSlot(); ItemStack cursorItem = event.getCursor(); // Item held by the cursor ItemStack currentItem = event.getCurrentItem(); // Item in the clicked slot + Player player = (Player) event.getWhoClicked(); // The player who clicked // Define the special slots boolean isSpecialSlot = (clickedSlot >= 36 && clickedSlot <= 40); // Armor (36-39) and Off-hand (40) - // --- Logic to prevent moving filler items AND restore them if slot becomes empty --- + // --- Logic for special slots (armor/off-hand) --- if (isSpecialSlot) { - // Check if the current item in the slot is a filler item (e.g., stained glass pane) - // This is a simple check; a more robust check might involve NBT tags if fillers are complex. - boolean isFillerInSlot = currentItem != null && - (currentItem.getType() == Material.GRAY_STAINED_GLASS_PANE || - currentItem.getType() == Material.LIGHT_GRAY_STAINED_GLASS_PANE); - - // Scenario 1: Player tries to take out a filler item or replace it with an invalid item - if (isFillerInSlot && (cursorItem == null || cursorItem.getType() == Material.AIR || !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot))) { - event.setCancelled(true); // Prevent taking out filler or putting invalid item on top of it - // If they tried to take it out (cursor empty), put it back immediately - if (cursorItem == null || cursorItem.getType() == Material.AIR) { - Bukkit.getScheduler().runTaskLater(inventories, () -> { - event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); // Use helper - }, 1L); // Run one tick later to avoid conflicts with Bukkit's internal inventory updates - } - return; + boolean currentItemIsFiller = currentItem != null && inventoryGUIHelper.isFillerItem(currentItem); + + // Scenario 1: Player tries to pick up a filler item (cursor is empty) + if (currentItemIsFiller && (cursorItem == null || cursorItem.getType() == Material.AIR)) { + event.setCancelled(true); + return; // Prevent pickup, filler stays in place } - // Scenario 2: Player tries to place an invalid item into a special slot (even if it's empty or has a valid item) - if (cursorItem != null && cursorItem.getType() != Material.AIR && !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { // Use helper + // Scenario 2: Player tries to place an invalid item into a special slot + if (cursorItem != null && cursorItem.getType() != Material.AIR && !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { event.setCancelled(true); - return; + return; // Prevent invalid placement } - // Scenario 3 - // If the item in the slot *before* the click was a filler, and a valid item is being placed, - // we need to clear the cursor after Bukkit handles the swap. - if (isFillerInSlot && cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { - // Schedule a task to run after the event has fully processed - Bukkit.getScheduler().runTaskLater(inventories, () -> { - Player player = (Player) event.getWhoClicked(); - // Check if the item on the player's cursor is now the filler item - if (player.getItemOnCursor() != null && inventoryGUIHelper.isFillerItem(player.getItemOnCursor())) { - player.setItemOnCursor(null); // Clear the cursor - player.updateInventory(); // Update client to reflect cursor change - } - // Also ensure the slot has a filler if it became empty (e.g., if the placed item was a stack of 1) - if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { - event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); - } - }, 1L); - return; // Run one tick later to ensure Bukkit's updates have settled + // Scenario 3: Player places a valid item into a special slot that currently holds a filler or a valid item. + // Manually handle the swap to ensure fillers don't leave the GUI. + if (cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { + event.setCancelled(true); // Take full control of the event + + // If there was a filler in the slot, it needs to be put back after the new item is placed. + // If there was a valid item, it goes to the cursor. + ItemStack itemToReturnToCursor = currentItem; // This could be the filler or a valid item + + // Place the new item from cursor into the clicked slot + event.getInventory().setItem(clickedSlot, cursorItem); + + // Clear the player's cursor + player.setItemOnCursor(null); + + // If the item that was in the slot (itemToReturnToCursor) was a filler, + // we don't want it to go anywhere. It should effectively be "discarded" from the event. + // If it was a valid item, it should go to the player's cursor. + if (itemToReturnToCursor != null && !inventoryGUIHelper.isFillerItem(itemToReturnToCursor)) { + player.setItemOnCursor(itemToReturnToCursor); + } else if (itemToReturnToCursor != null && inventoryGUIHelper.isFillerItem(itemToReturnToCursor)) { + // If it was a filler, ensure the slot gets a fresh filler if it becomes empty + // (though we just put cursorItem there, this is a safeguard) + Bukkit.getScheduler().runTaskLater(inventories, () -> { + if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { + event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); + } + }, 1L); + } + player.updateInventory(); // Update client to reflect changes + return; // Event handled } - // Scenario 4: Player is shift-clicking a valid item *from* a special slot. + // Scenario 4: Player is shift-clicking a valid item from a special slot. // Or picking up a valid item from a special slot. // We need to ensure filler reappears if the slot becomes empty. if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY || @@ -184,12 +188,22 @@ public void onInventoryClose(InventoryCloseEvent event) { ItemStack[] newContents = Arrays.copyOfRange(closedInventory.getContents(), 0, 36); ItemStack[] newArmor = new ItemStack[4]; // Map GUI armor slots back to Multiverse-Inventories' expected order [helmet, chestplate, leggings, boots] - newArmor[0] = closedInventory.getItem(39); // Helmet - newArmor[1] = closedInventory.getItem(38); // Chestplate - newArmor[2] = closedInventory.getItem(37); // Leggings - newArmor[3] = closedInventory.getItem(36); // Boots + newArmor[3] = closedInventory.getItem(36); // Helmet + newArmor[2] = closedInventory.getItem(37); // Chestplate + newArmor[1] = closedInventory.getItem(38); // Leggings + newArmor[0] = closedInventory.getItem(39); // Boots ItemStack newOffHand = closedInventory.getItem(40); + // Before saving, ensure any filler items are removed from the actual data + for (int i = 0; i < newArmor.length; i++) { + if (newArmor[i] != null && inventoryGUIHelper.isFillerItem(newArmor[i])) { + newArmor[i] = null; // Replace filler with null for saving + } + } + if (newOffHand != null && inventoryGUIHelper.isFillerItem(newOffHand)) { + newOffHand = null; // Replace filler with null for saving + } + // Delegate saving to InventoryDataProvider inventoryDataProvider.savePlayerInventoryData( targetPlayer, diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 8dc9c51f..b7da5084 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -7,6 +7,7 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.external.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; @@ -153,7 +154,7 @@ public CompletableFuture savePlayerInventoryData( @NotNull ProfileType profileType, @NotNull ItemStack[] newContents, @NotNull ItemStack[] newArmor, - @NotNull ItemStack newOffHand + @Nullable ItemStack newOffHand ) { // Save the updated inventory, armor, and off-hand contents asynchronously CompletableFuture saveFuture = CompletableFuture.allOf( diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 88c252c4..13205e80 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -76,13 +76,13 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { } switch (slot) { - case 39: // Helmet slot + case 36: // Helmet slot return item.getType().name().endsWith("_HELMET"); - case 38: // Chestplate slot + case 37: // Chestplate slot return item.getType().name().endsWith("_CHESTPLATE"); - case 37: // Leggings slot + case 38: // Leggings slot return item.getType().name().endsWith("_LEGGINGS"); - case 36: // Boots slot + case 39: // Boots slot return item.getType().name().endsWith("_BOOTS"); case 40: // Off-hand slot // Off-hand is very permissive in vanilla. Allow any non-air item. @@ -101,10 +101,10 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { */ public ItemStack createFillerItemForSlot(int slot) { switch (slot) { - case 39: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); - case 38: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", "Place Chestplate Here"); - case 37: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); - case 36: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); + case 36: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); + case 37: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", "Place Chestplate Here"); + case 38: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); + case 39: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); case 40: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", "Place Off-Hand Item Here"); default: return new ItemStack(Material.AIR); // Should not happen for these slots } From 10a69ff69d9762e613cfa18b49f54cf90a1ce882 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 16:08:12 -0400 Subject: [PATCH 12/39] Simplify scenario 3 and remove import --- .../commands/InventoryModifyCommand.java | 5 ----- .../listeners/InventoryViewListener.java | 18 ------------------ 2 files changed, 23 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 898a8920..2ca4d709 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -1,15 +1,12 @@ package org.mvplugins.multiverse.inventories.commands; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandIssuer; import org.mvplugins.multiverse.core.world.MultiverseWorld; @@ -25,8 +22,6 @@ import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; -import java.util.Arrays; - @Service final class InventoryModifyCommand extends InventoriesCommand { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index fa216bc8..c4e217ac 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -85,30 +85,12 @@ public void onInventoryClick(InventoryClickEvent event) { if (cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { event.setCancelled(true); // Take full control of the event - // If there was a filler in the slot, it needs to be put back after the new item is placed. - // If there was a valid item, it goes to the cursor. - ItemStack itemToReturnToCursor = currentItem; // This could be the filler or a valid item - // Place the new item from cursor into the clicked slot event.getInventory().setItem(clickedSlot, cursorItem); // Clear the player's cursor player.setItemOnCursor(null); - // If the item that was in the slot (itemToReturnToCursor) was a filler, - // we don't want it to go anywhere. It should effectively be "discarded" from the event. - // If it was a valid item, it should go to the player's cursor. - if (itemToReturnToCursor != null && !inventoryGUIHelper.isFillerItem(itemToReturnToCursor)) { - player.setItemOnCursor(itemToReturnToCursor); - } else if (itemToReturnToCursor != null && inventoryGUIHelper.isFillerItem(itemToReturnToCursor)) { - // If it was a filler, ensure the slot gets a fresh filler if it becomes empty - // (though we just put cursorItem there, this is a safeguard) - Bukkit.getScheduler().runTaskLater(inventories, () -> { - if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { - event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); - } - }, 1L); - } player.updateInventory(); // Update client to reflect changes return; // Event handled } From 9ab5c10ea9acacd4d2481f06cd205804642aed5f Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 17:43:49 -0400 Subject: [PATCH 13/39] Make custom inventory have fewer slots. Empty slots replaced with barriers --- .../commands/InventoryModifyCommand.java | 8 +++++-- .../commands/InventoryViewCommand.java | 6 +++++- .../listeners/InventoryViewListener.java | 21 ++++++++++++++++++- .../inventories/view/InventoryGUIHelper.java | 15 ++++++++++--- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 2ca4d709..b69a14ec 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -83,7 +83,7 @@ void onInventoryModifyCommand( Bukkit.getScheduler().runTask(inventories, () -> { // Create inventory with ModifiableInventoryHolder // Pass all necessary context to the holder for saving on close. - Component title = Component.text("Modifiying " + targetPlayer.getName() + " @ " + worldName); + Component title = Component.text("Modify " + targetPlayer.getName() + " @ " + worldName); Inventory inv = Bukkit.createInventory( new ModifiableInventoryHolder( targetPlayer, @@ -91,7 +91,7 @@ void onInventoryModifyCommand( playerInventoryData.profileTypeUsed, // Use the determined profile type inventories ), - 54, + 45, title ); @@ -132,6 +132,10 @@ void onInventoryModifyCommand( } else { inv.setItem(40, playerInventoryData.offHand); } + // Add the remaining slots as non-interactable filler items + for (int i = 41; i <= 44; i++) { + inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i)); + } viewer.openInventory(inv); issuer.sendInfo(ChatColor.GREEN + "Opened editable inventory for " + targetPlayer.getName() + " in world " + worldName + ". Changes will save on close."); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index a5de2a37..21c58cce 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -86,7 +86,7 @@ void onInventoryViewCommand( Bukkit.getScheduler().runTask(inventories, () -> { // Create an inventory for viewing. Component title = Component.text(targetPlayer.getName() + " @ " + worldName); - Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), 54, title); + Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), 45, title); // Fill in main inventory slots (0–35) if (playerInventoryData.contents != null) { @@ -126,6 +126,10 @@ void onInventoryViewCommand( } else { inv.setItem(40, playerInventoryData.offHand); } + // Add the remaining slots as non-interactable filler items + for (int i = 41; i <= 44; i++) { + inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i)); + } viewer.openInventory(inv); issuer.sendInfo(ChatColor.GREEN + playerInventoryData.statusMessage); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index c4e217ac..3da987a7 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -59,10 +59,20 @@ public void onInventoryClick(InventoryClickEvent event) { int clickedSlot = event.getRawSlot(); ItemStack cursorItem = event.getCursor(); // Item held by the cursor ItemStack currentItem = event.getCurrentItem(); // Item in the clicked slot + Inventory customGUI = event.getInventory(); // The custom inventory Player player = (Player) event.getWhoClicked(); // The player who clicked // Define the special slots - boolean isSpecialSlot = (clickedSlot >= 36 && clickedSlot <= 40); // Armor (36-39) and Off-hand (40) + boolean isSpecialSlot = (clickedSlot >= 36 && clickedSlot <= 40); // Armor (36-39) and Off-hand (40) + + // Determine if the slot is one of the padding slots (41-44) + boolean isPaddingSlot = (clickedSlot >= 41 && clickedSlot <= 44); + + if (isPaddingSlot) { + // Clicks on padding slots are always cancelled and do nothing else. + event.setCancelled(true); + return; + } // --- Logic for special slots (armor/off-hand) --- if (isSpecialSlot) { @@ -135,6 +145,15 @@ public void onInventoryDrag(InventoryDragEvent event) { // Define the special slots boolean isSpecialSlot = (slot >= 36 && slot <= 40); + // Determine if the slot is one of the padding slots (41-44) + boolean isPaddingSlot = (slot >= 41 && slot <= 44); + + if (isPaddingSlot) { + // Clicks on padding slots are always cancelled and do nothing else. + event.setCancelled(true); + return; + } + if (isSpecialSlot) { // Check if the dragged item is valid for this special slot if (!inventoryGUIHelper.isValidItemForSlot(draggedItem, slot)) { // Use helper diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 13205e80..32afe28a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -8,7 +8,7 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.NotNull; -import org.jvnet.hk2.annotations.Service; // Mark as a service for injection +import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.inventories.MultiverseInventories; @@ -18,8 +18,8 @@ * A helper class for creating and validating items within the custom inventory GUIs. * This centralizes logic for filler items and slot-specific item validation. */ -@Service // Allows this class to be injected -public final class InventoryGUIHelper { // Made public and final +@Service +public final class InventoryGUIHelper { private final NamespacedKey IS_FILLER_KEY; // Key to mark filler items @@ -89,6 +89,11 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { // If you want to restrict this further (e.g., only shields/totems), // add more specific Material checks here. return true; + case 41: // Padding slot + case 42: // Padding slot + case 43: // Padding slot + case 44: // Padding slot + return false; // Cannot place items in padding slots default: return true; // For non-special slots (main inventory), any item is generally allowed. } @@ -106,6 +111,10 @@ public ItemStack createFillerItemForSlot(int slot) { case 38: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); case 39: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); case 40: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", "Place Off-Hand Item Here"); + case 41: + case 42: + case 43: + case 44: return createFillerItem(Material.BARRIER, " ", " "); // Padding slots default: return new ItemStack(Material.AIR); // Should not happen for these slots } } From a9ae11e27f675768f23b227b0b43a6479703d196 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Sun, 20 Jul 2025 17:59:10 -0400 Subject: [PATCH 14/39] Change public void to void --- .../inventories/listeners/InventoryViewListener.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 3da987a7..027fb07a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -46,7 +46,7 @@ final class InventoryViewListener implements MVInvListener { // This listener will cancel any clicks or drags in inventories that have the ReadOnlyInventoryHolder marker. @EventMethod @DefaultEventPriority(EventPriority.NORMAL) - public void onInventoryClick(InventoryClickEvent event) { + void onInventoryClick(InventoryClickEvent event) { // If it's a read-only inventory, cancel all clicks. if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder || (event.getClickedInventory() != null && event.getClickedInventory().getHolder() instanceof ReadOnlyInventoryHolder)) { @@ -127,7 +127,7 @@ public void onInventoryClick(InventoryClickEvent event) { // Also cancel drag events to prevent items from being dragged into/out of the inventory @EventMethod @DefaultEventPriority(EventPriority.NORMAL) - public void onInventoryDrag(InventoryDragEvent event) { + void onInventoryDrag(InventoryDragEvent event) { // If it is a read-only inventory, cancel all drags if (event.getInventory().getHolder() instanceof ReadOnlyInventoryHolder) { event.setCancelled(true); @@ -177,7 +177,7 @@ public void onInventoryDrag(InventoryDragEvent event) { // Event handler for InventoryCloseEvent to save changes @EventMethod @DefaultEventPriority(EventPriority.NORMAL) - public void onInventoryClose(InventoryCloseEvent event) { + void onInventoryClose(InventoryCloseEvent event) { // Check if the closed inventory has the custom ModifiableInventoryHolder class if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder holder) { final OfflinePlayer targetPlayer = holder.getTargetPlayer(); From 4d49bcf7c7d1676bcbf0c80da14cb4539128be85 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:53:49 +0800 Subject: [PATCH 15/39] Add api version to javadocs and fix minor formating --- .../profile/InventoryDataProvider.java | 27 ++++++++++- .../inventories/view/InventoryGUIHelper.java | 21 +++++++++ .../view/ModifiableInventoryHolder.java | 47 ++++++++++++++++++- .../view/ReadOnlyInventoryHolder.java | 10 +++- 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index b7da5084..cf0dc65c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -4,6 +4,7 @@ import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; @@ -25,7 +26,10 @@ /** * Provides methods for asynchronously loading player inventory data. * This class encapsulates the business logic for fetching inventory, armor, and off-hand contents. + * + * @since 5.2 */ +@ApiStatus.AvailableSince("5.2") @Service public final class InventoryDataProvider { @@ -43,7 +47,10 @@ public InventoryDataProvider( /** * Represents the loaded inventory data. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public static class PlayerInventoryData { public final ItemStack[] contents; public final ItemStack[] armor; @@ -51,6 +58,17 @@ public static class PlayerInventoryData { public final String statusMessage; // To indicate if it's live or stored data public final ProfileType profileTypeUsed; // To pass back which profile type was used for stored data + /** + * + * @param contents + * @param armor + * @param offHand + * @param statusMessage + * @param profileTypeUsed + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, String statusMessage, ProfileType profileTypeUsed) { this.contents = contents; this.armor = armor; @@ -68,7 +86,10 @@ public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack of * @param targetPlayer The OfflinePlayer whose inventory data to load. * @param worldName The name of the world to load the inventory from (either live or stored). * @return A CompletableFuture that will complete with PlayerInventoryData or an exception. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public CompletableFuture loadPlayerInventoryData( @NotNull OfflinePlayer targetPlayer, @NotNull String worldName @@ -135,7 +156,6 @@ public CompletableFuture loadPlayerInventoryData( }); } - /** * Asynchronously saves a player's inventory data to their Multiverse-Inventories profile. * If the player is online and in the target world, their live inventory is also updated. @@ -147,7 +167,10 @@ public CompletableFuture loadPlayerInventoryData( * @param newArmor The new armor contents (helmet, chestplate, leggings, boots). * @param newOffHand The new off-hand item. * @return A CompletableFuture that completes when saving is done, or an exception occurs. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public CompletableFuture savePlayerInventoryData( @NotNull OfflinePlayer targetPlayer, @NotNull String worldName, @@ -198,4 +221,4 @@ public CompletableFuture savePlayerInventoryData( return null; }); } -} \ No newline at end of file +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 32afe28a..1bce2d27 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -7,6 +7,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; @@ -17,7 +18,10 @@ /** * A helper class for creating and validating items within the custom inventory GUIs. * This centralizes logic for filler items and slot-specific item validation. + * + * @since 5.2 */ +@ApiStatus.AvailableSince("5.2") @Service public final class InventoryGUIHelper { @@ -27,13 +31,18 @@ public final class InventoryGUIHelper { public InventoryGUIHelper(@NotNull MultiverseInventories inventories) { this.IS_FILLER_KEY = new NamespacedKey(inventories, "is_mvinv_filler"); } + /** * Creates a generic filler item for GUI slots. + * * @param material The material of the filler item. * @param name The display name of the item. * @param lore The lore text for the item. * @return The created ItemStack. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public ItemStack createFillerItem(Material material, String name, String lore) { ItemStack item = new ItemStack(material); ItemMeta meta = item.getItemMeta(); @@ -48,9 +57,13 @@ public ItemStack createFillerItem(Material material, String name, String lore) { /** * Checks if a given ItemStack is a filler item created by this helper. + * * @param item The ItemStack to check. * @return True if the item is a filler, false otherwise. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public boolean isFillerItem(@NotNull ItemStack item) { if (!item.hasItemMeta()) { return false; @@ -62,10 +75,14 @@ public boolean isFillerItem(@NotNull ItemStack item) { /** * Determines if an ItemStack is valid for a given special inventory slot in the custom GUI. * This method is used for both armor and off-hand slot validation. + * * @param item The ItemStack to check. * @param slot The raw slot number (36-40 for armor/off-hand). * @return True if the item is valid for the slot, false otherwise. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { if (item.getType() == Material.AIR) { return true; // Air is always valid (it means the slot is empty) @@ -101,9 +118,13 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { /** * Creates the appropriate filler item for a given special slot in the custom GUI. + * * @param slot The raw slot number (36-40). * @return The specific filler ItemStack for that slot. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public ItemStack createFillerItemForSlot(int slot) { switch (slot) { case 36: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java index 14ace172..09b48eb0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java @@ -3,6 +3,7 @@ import org.bukkit.OfflinePlayer; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; @@ -11,13 +12,26 @@ * A custom InventoryHolder that serves as a marker for modifiable inventories. * It stores the necessary context (player, world, profile type, plugin instance) * to save changes back to the player's profile when the inventory is closed. + * + * @since 5.2 */ +@ApiStatus.AvailableSince("5.2") public final class ModifiableInventoryHolder implements InventoryHolder { private final OfflinePlayer targetPlayer; private final String worldName; private final ProfileType profileType; private final MultiverseInventories inventories; + /** + * + * @param targetPlayer + * @param worldName + * @param profileType + * @param inventories + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, @NotNull String worldName, @NotNull ProfileType profileType, @@ -28,26 +42,57 @@ public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, this.inventories = inventories; } + /** + * + * @return + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") public @NotNull OfflinePlayer getTargetPlayer() { return targetPlayer; } + /** + * + * @return + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") public @NotNull String getWorldName() { return worldName; } + /** + * + * @return + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") public @NotNull ProfileType getProfileType() { return profileType; } + /** + * + * @return + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") public @NotNull MultiverseInventories getInventories() { return inventories; } + /** + * {@inheritDoc} + */ @Override public @NotNull Inventory getInventory() { // This method is required by the interface but is not directly used for the marker purpose. // Throwing UnsupportedOperationException clearly indicates it's not meant to be called. throw new UnsupportedOperationException("ModifiableInventoryHolder does not provide an Inventory directly."); } -} \ No newline at end of file +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java b/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java index ee15950b..9828440c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/ReadOnlyInventoryHolder.java @@ -2,18 +2,24 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * A custom InventoryHolder that serves as a marker for read-only inventories. * Inventories created with this holder will have their interactions cancelled by the InventoryViewListener. + * + * @since 5.2 */ +@ApiStatus.AvailableSince("5.2") public final class ReadOnlyInventoryHolder implements InventoryHolder { - + /** + * {@inheritDoc} + */ @Override public @NotNull Inventory getInventory() { // This method is required by the interface but is not directly used for the marker purpose. // Throwing UnsupportedOperationException clearly indicates it's not meant to be called. throw new UnsupportedOperationException("ReadOnlyInventoryHolder does not provide an Inventory directly."); } -} \ No newline at end of file +} From 131cd51fcf5a653707c13a245db3c8e4f011d9d5 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:55:36 +0800 Subject: [PATCH 16/39] Make constructor of services non-public --- .../multiverse/inventories/profile/InventoryDataProvider.java | 2 +- .../multiverse/inventories/view/InventoryGUIHelper.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index cf0dc65c..5be3a195 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -37,7 +37,7 @@ public final class InventoryDataProvider { private final MultiverseInventories inventories; @Inject - public InventoryDataProvider( + InventoryDataProvider( @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, @NotNull MultiverseInventories inventories ) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 1bce2d27..d925ec4a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -27,8 +27,8 @@ public final class InventoryGUIHelper { private final NamespacedKey IS_FILLER_KEY; // Key to mark filler items - @Inject // Inject MultiverseInventories to create NamespacedKey - public InventoryGUIHelper(@NotNull MultiverseInventories inventories) { + @Inject + InventoryGUIHelper(@NotNull MultiverseInventories inventories) { this.IS_FILLER_KEY = new NamespacedKey(inventories, "is_mvinv_filler"); } From e3d7e5422a11dfcd75e639f3c8efe3035023b7ad Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:40:15 +0800 Subject: [PATCH 17/39] Refactor InventoryDataProvider and InventoryGUIHelper to improve readability --- .../profile/InventoryDataProvider.java | 230 +++++++++++------- .../inventories/view/InventoryGUIHelper.java | 69 +++--- 2 files changed, 176 insertions(+), 123 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 5be3a195..61861957 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -95,66 +95,91 @@ public CompletableFuture loadPlayerInventoryData( @NotNull String worldName ) { // If the player is online, prioritize getting their live inventory - if (targetPlayer.isOnline()) { - Player onlineTarget = targetPlayer.getPlayer(); - // Ensure onlineTarget is not null and their current world matches the requested worldName - if (onlineTarget != null && onlineTarget.getWorld().getName().equalsIgnoreCase(worldName)) { - // Get the actual ProfileType for the online player - ProfileType profileType = ProfileTypes.forPlayer(onlineTarget); - // Return immediately with live data - return CompletableFuture.completedFuture(new PlayerInventoryData( - onlineTarget.getInventory().getContents(), - onlineTarget.getInventory().getArmorContents(), - onlineTarget.getInventory().getItemInOffHand(), - "Displaying LIVE inventory for " + targetPlayer.getName() + " in world " + worldName + ".", - profileType - )); + if (!targetPlayer.isOnline()) { + return loadInventoryDataFromProfileStorage(targetPlayer, worldName); + } + + Player onlineTarget = targetPlayer.getPlayer(); + // Ensure onlineTarget is not null and their current world matches the requested worldName + if (onlineTarget != null && onlineTarget.getWorld().getName().equalsIgnoreCase(worldName)) { + return loadInventoryDataFromPlayer(onlineTarget, worldName); + } + // If online but in a different world, or getPlayer() returned null, fall through to stored data logic + if (onlineTarget != null) { + inventories.getLogger().fine("Player " + targetPlayer.getName() + " is online but in world " + onlineTarget.getWorld().getName() + ". Loading stored data for " + worldName + "."); + } else { + inventories.getLogger().warning("Player " + targetPlayer.getName() + " is online but getPlayer() returned null. Falling back to stored data."); + } + // If the player is offline or online in a different world, or live data failed, load from Multiverse-Inventories' stored profiles + return loadInventoryDataFromProfileStorage(targetPlayer, worldName); + } + + private CompletableFuture loadInventoryDataFromPlayer( + @NotNull Player onlineTarget, + @NotNull String worldName + ) { + // Get the actual ProfileType for the online player + ProfileType profileType = ProfileTypes.forPlayer(onlineTarget); + // Return immediately with live data + return CompletableFuture.completedFuture(new PlayerInventoryData( + onlineTarget.getInventory().getContents(), + onlineTarget.getInventory().getArmorContents(), + onlineTarget.getInventory().getItemInOffHand(), + "Displaying LIVE inventory for " + onlineTarget.getName() + " in world " + worldName + ".", + profileType + )); + } + + private CompletableFuture loadInventoryDataFromProfileStorage( + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName + ) { + return CompletableFuture.supplyAsync(() -> { + ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) + .getContainer(worldName); + if (container == null) { + throw new IllegalStateException("Could not load profile container for world: " + worldName); + } + + PlayerProfile tempProfile = loadMVInvPlayerProfile(container, targetPlayer); + if (tempProfile == null) { + throw new IllegalStateException("No player data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); } - // If online but in a different world, or getPlayer() returned null, fall through to stored data logic - if (onlineTarget != null) { - inventories.getLogger().fine("Player " + targetPlayer.getName() + " is online but in world " + onlineTarget.getWorld().getName() + ". Loading stored data for " + worldName + "."); - } else { - inventories.getLogger().warning("Player " + targetPlayer.getName() + " is online but getPlayer() returned null. Falling back to stored data."); + ProfileType profileTypeToUse = tempProfile.getProfileType(); + try { + ItemStack[] contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); + ItemStack[] armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); + ItemStack offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); + + return new PlayerInventoryData( + contents, + armor, + offHand, + "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + ".", + profileTypeToUse + ); + } catch (CompletionException e) { + // Unwrap CompletionException to get the actual cause + throw new IllegalStateException("Error loading inventory data: " + e.getCause().getMessage(), e.getCause()); } + }); + } + + @Nullable + private PlayerProfile loadMVInvPlayerProfile(ProfileContainer container, OfflinePlayer targetPlayer) { + PlayerProfile survivalProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); + if (survivalProfile != null) { + return survivalProfile; } - // If the player is offline or online in a different world, or live data failed, load from Multiverse-Inventories' stored profiles - return CompletableFuture.supplyAsync(() -> { - ProfileContainer container = profileContainerStoreProvider.getStore(ContainerType.WORLD) - .getContainer(worldName); - if (container == null) { - throw new IllegalStateException("Could not load profile container for world: " + worldName); - } - - PlayerProfile tempProfile = container.getPlayerProfileNow(ProfileTypes.SURVIVAL, targetPlayer); - ProfileType profileTypeToUse = ProfileTypes.SURVIVAL; - - if (tempProfile == null) { - for (ProfileType type : ProfileTypes.getTypes()) { - if (type.equals(ProfileTypes.SURVIVAL)) continue; - tempProfile = container.getPlayerProfileNow(type, targetPlayer); - if (tempProfile != null) { - profileTypeToUse = type; - break; - } - } - } - - if (tempProfile == null) { - throw new IllegalStateException("No player data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); - } - - try { - ItemStack[] contents = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.INVENTORY).read().join(); - ItemStack[] armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); - ItemStack offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); - - return new PlayerInventoryData(contents, armor, offHand, "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + ".", profileTypeToUse); - } catch (CompletionException e) { - // Unwrap CompletionException to get the actual cause - throw new IllegalStateException("Error loading inventory data: " + e.getCause().getMessage(), e.getCause()); - } - }); + for (ProfileType type : ProfileTypes.getTypes()) { + if (type.equals(ProfileTypes.SURVIVAL)) continue; + PlayerProfile profile = container.getPlayerProfileNow(type, targetPlayer); + if (profile != null) { + return profile; + } } + return null; + } /** * Asynchronously saves a player's inventory data to their Multiverse-Inventories profile. @@ -180,7 +205,69 @@ public CompletableFuture savePlayerInventoryData( @Nullable ItemStack newOffHand ) { // Save the updated inventory, armor, and off-hand contents asynchronously - CompletableFuture saveFuture = CompletableFuture.allOf( + CompletableFuture saveFuture = writeInventoryDataToProfile( + targetPlayer, + worldName, + profileType, + newContents, + newArmor, + newOffHand + ); + + return saveFuture.thenRun(() -> { + inventories.getLogger().info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); + updateOnlinePlayerInventoryData(targetPlayer, worldName, newContents, newArmor, newOffHand); + }).exceptionally(throwable -> { + inventories.getLogger().severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; + }); + } + + private void updateOnlinePlayerInventoryData( + OfflinePlayer targetPlayer, + String worldName, + @NotNull ItemStack[] newContents, + @NotNull ItemStack[] newArmor, + ItemStack newOffHand + ) { + // If the target player is online, update their live inventory + if (!targetPlayer.isOnline()) { + return; + } + + Player onlinePlayer = targetPlayer.getPlayer(); + if (onlinePlayer == null) { + return; + } + + // Check if the online player is in the world whose inventory was modified + // This is important to avoid overwriting their current inventory if they are in a different world + if (!onlinePlayer.getWorld().getName().equalsIgnoreCase(worldName)) { + inventories.getLogger().info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); + return; + } + + // Run Bukkit API calls on the main thread + Bukkit.getScheduler().runTask(inventories, () -> { + onlinePlayer.getInventory().setContents(newContents); + onlinePlayer.getInventory().setArmorContents(newArmor); + onlinePlayer.getInventory().setItemInOffHand(newOffHand); + onlinePlayer.updateInventory(); // Ensure client sees changes + inventories.getLogger().info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); + }); + } + + private CompletableFuture writeInventoryDataToProfile( + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull ProfileType profileType, + @NotNull ItemStack[] newContents, + @NotNull ItemStack[] newArmor, + @Nullable ItemStack newOffHand + ) { + // Save the updated inventory, armor, and off-hand contents asynchronously + return CompletableFuture.allOf( SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.INVENTORY) .write(newContents, true) // true to update if player is online .thenRun(() -> inventories.getLogger().fine("Saved inventory for " + targetPlayer.getName() + " in " + worldName)), @@ -191,34 +278,5 @@ public CompletableFuture savePlayerInventoryData( .write(newOffHand, true) .thenRun(() -> inventories.getLogger().fine("Saved off-hand for " + targetPlayer.getName() + " in " + worldName)) ); - - return saveFuture.thenRun(() -> { - inventories.getLogger().info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); - - // If the target player is online, update their live inventory - if (targetPlayer.isOnline()) { - Player onlinePlayer = targetPlayer.getPlayer(); - if (onlinePlayer != null) { - // Check if the online player is in the world whose inventory was modified - // This is important to avoid overwriting their current inventory if they are in a different world - if (onlinePlayer.getWorld().getName().equalsIgnoreCase(worldName)) { - // Run Bukkit API calls on the main thread - Bukkit.getScheduler().runTask(inventories, () -> { - onlinePlayer.getInventory().setContents(newContents); - onlinePlayer.getInventory().setArmorContents(newArmor); - onlinePlayer.getInventory().setItemInOffHand(newOffHand); - onlinePlayer.updateInventory(); // Ensure client sees changes - inventories.getLogger().info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); - }); - } else { - inventories.getLogger().info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); - } - } - } - }).exceptionally(throwable -> { - inventories.getLogger().severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); - throwable.printStackTrace(); - return null; - }); } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index d925ec4a..6e366148 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -6,6 +6,7 @@ import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -68,8 +69,9 @@ public boolean isFillerItem(@NotNull ItemStack item) { if (!item.hasItemMeta()) { return false; } - return item.getItemMeta().getPersistentDataContainer().has(IS_FILLER_KEY, PersistentDataType.BYTE) && - item.getItemMeta().getPersistentDataContainer().get(IS_FILLER_KEY, PersistentDataType.BYTE) == (byte) 1; + PersistentDataContainer persistentDataContainer = item.getItemMeta().getPersistentDataContainer(); + return persistentDataContainer.has(IS_FILLER_KEY, PersistentDataType.BYTE) && + persistentDataContainer.getOrDefault(IS_FILLER_KEY, PersistentDataType.BYTE, (byte) 0) == (byte) 1; } /** @@ -92,28 +94,24 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { return true; } - switch (slot) { - case 36: // Helmet slot - return item.getType().name().endsWith("_HELMET"); - case 37: // Chestplate slot - return item.getType().name().endsWith("_CHESTPLATE"); - case 38: // Leggings slot - return item.getType().name().endsWith("_LEGGINGS"); - case 39: // Boots slot - return item.getType().name().endsWith("_BOOTS"); - case 40: // Off-hand slot - // Off-hand is very permissive in vanilla. Allow any non-air item. - // If you want to restrict this further (e.g., only shields/totems), - // add more specific Material checks here. - return true; - case 41: // Padding slot - case 42: // Padding slot - case 43: // Padding slot - case 44: // Padding slot - return false; // Cannot place items in padding slots - default: - return true; // For non-special slots (main inventory), any item is generally allowed. - } + return switch (slot) { + case 36 -> item.getType().name().endsWith("_HELMET"); + case 37 -> item.getType().name().endsWith("_CHESTPLATE"); + case 38 -> item.getType().name().endsWith("_LEGGINGS"); + case 39 -> item.getType().name().endsWith("_BOOTS"); + + // Off-hand is very permissive in vanilla. Allow any non-air item. + // If you want to restrict this further (e.g., only shields/totems), + // add more specific Material checks here. + case 40 -> true; + + // Padding slot + // Cannot place items in padding slots + case 41, 42, 43, 44 -> false; + + // For non-special slots (main inventory), any item is generally allowed. + default -> true; + }; } /** @@ -126,17 +124,14 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { */ @ApiStatus.AvailableSince("5.2") public ItemStack createFillerItemForSlot(int slot) { - switch (slot) { - case 36: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); - case 37: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", "Place Chestplate Here"); - case 38: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); - case 39: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); - case 40: return createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", "Place Off-Hand Item Here"); - case 41: - case 42: - case 43: - case 44: return createFillerItem(Material.BARRIER, " ", " "); // Padding slots - default: return new ItemStack(Material.AIR); // Should not happen for these slots - } + return switch (slot) { + case 36 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); + case 37 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", "Place Chestplate Here"); + case 38 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); + case 39 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); + case 40 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", "Place Off-Hand Item Here"); + case 41, 42, 43, 44 -> createFillerItem(Material.BARRIER, " ", " "); // Padding slots + default -> new ItemStack(Material.AIR); // Should not happen for these slots + }; } -} \ No newline at end of file +} From a09f33e8859d4026db1ac2e6d7af3d6308bd7337 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:45:43 +0800 Subject: [PATCH 18/39] Reduce need for validation checks in view and modify commands --- .../commands/InventoryModifyCommand.java | 27 +++++++---------- .../commands/InventoryViewCommand.java | 30 +++++-------------- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index b69a14ec..296906eb 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -13,6 +13,7 @@ import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; import org.mvplugins.multiverse.external.acf.commands.annotation.Description; +import org.mvplugins.multiverse.external.acf.commands.annotation.Flags; import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand; import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax; import org.mvplugins.multiverse.external.jakarta.inject.Inject; @@ -48,32 +49,25 @@ final class InventoryModifyCommand extends InventoriesCommand { @Description("Modify a player's inventory in a specific world.") void onInventoryModifyCommand( @NotNull MVCommandIssuer issuer, + + // to make sure the command is only run by players + @Flags("resolve=issuerOnly") + @NotNull Player player, + @Syntax("") @Description("Online or offline player") OfflinePlayer targetPlayer, @Syntax("") @Description("The world the player's inventory is in") - MultiverseWorld[] worlds + MultiverseWorld world ) { - if (!(issuer.getIssuer() instanceof Player viewer)) { - issuer.sendError(ChatColor.RED + "Only players can modify inventories."); - return; - } - if (worlds == null || worlds.length == 0 || worlds[0] == null) { - issuer.sendError(ChatColor.RED + "You must specify a valid world."); - return; - } - if (targetPlayer == null || targetPlayer.getName() == null) { - issuer.sendError(ChatColor.RED + "You must specify a valid player."); - return; - } - if (viewer.getUniqueId().equals(targetPlayer.getUniqueId())) { + if (player.getUniqueId().equals(targetPlayer.getUniqueId())) { issuer.sendError(ChatColor.RED + "You cannot modify your own inventory using this command. Use your regular inventory."); return; } - String worldName = worlds[0].getName(); + String worldName = world.getName(); // Asynchronously load data using InventoryDataProvider issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); @@ -137,7 +131,7 @@ void onInventoryModifyCommand( inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i)); } - viewer.openInventory(inv); + player.openInventory(inv); issuer.sendInfo(ChatColor.GREEN + "Opened editable inventory for " + targetPlayer.getName() + " in world " + worldName + ". Changes will save on close."); }); // End of Bukkit.getScheduler().runTask() }) @@ -150,4 +144,3 @@ void onInventoryModifyCommand( }); } } - diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 21c58cce..2b9020fc 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -13,6 +13,7 @@ import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; import org.mvplugins.multiverse.external.acf.commands.annotation.Description; +import org.mvplugins.multiverse.external.acf.commands.annotation.Flags; import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand; import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax; import org.mvplugins.multiverse.external.jakarta.inject.Inject; @@ -48,34 +49,19 @@ final class InventoryViewCommand extends InventoriesCommand { void onInventoryViewCommand( @NotNull MVCommandIssuer issuer, + // Ensure the command is run by a player + @Flags("resolve=issuerOnly") + @NotNull Player player, + @Syntax("") @Description("Online or offline player") - //Player targetPlayer, OfflinePlayer targetPlayer, @Syntax("") @Description("The world the player's inventory is in") - MultiverseWorld[] worlds + MultiverseWorld world ) { - // Check if the command sender is a player - if (!(issuer.getIssuer() instanceof Player viewer)) { - issuer.sendError(ChatColor.RED + "Only players can view inventories."); - return; - } - - // Validate world argument - if (worlds == null || worlds.length == 0 || worlds[0] == null) { - issuer.sendError(ChatColor.RED + "You must specify a valid world."); - return; - } - - // Validate targetPlayer player - if (targetPlayer == null || targetPlayer.getName() == null) { - issuer.sendError(ChatColor.RED + "You must specify a valid player."); - return; - } - - String worldName = worlds[0].getName(); + String worldName = world.getName(); // Asynchronously load data using InventoryDataProvider issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); @@ -131,7 +117,7 @@ void onInventoryViewCommand( inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i)); } - viewer.openInventory(inv); + player.openInventory(inv); issuer.sendInfo(ChatColor.GREEN + playerInventoryData.statusMessage); }); // End of Bukkit.getScheduler().runTask() }) From 6f8e81e37be21ca064eb24a751a6174bb6b199d6 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 12:17:48 -0400 Subject: [PATCH 19/39] Remove adventure text API --- .../inventories/commands/InventoryModifyCommand.java | 3 +-- .../inventories/commands/InventoryViewCommand.java | 3 +-- .../multiverse/inventories/view/InventoryGUIHelper.java | 7 +++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 296906eb..ce2a1ed0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -1,6 +1,5 @@ package org.mvplugins.multiverse.inventories.commands; -import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -77,7 +76,7 @@ void onInventoryModifyCommand( Bukkit.getScheduler().runTask(inventories, () -> { // Create inventory with ModifiableInventoryHolder // Pass all necessary context to the holder for saving on close. - Component title = Component.text("Modify " + targetPlayer.getName() + " @ " + worldName); + String title = "Modify " + targetPlayer.getName() + " @ " + worldName; Inventory inv = Bukkit.createInventory( new ModifiableInventoryHolder( targetPlayer, diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 2b9020fc..0e2e8cb5 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -1,6 +1,5 @@ package org.mvplugins.multiverse.inventories.commands; -import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -71,7 +70,7 @@ void onInventoryViewCommand( // Ensure GUI operations run on the main thread Bukkit.getScheduler().runTask(inventories, () -> { // Create an inventory for viewing. - Component title = Component.text(targetPlayer.getName() + " @ " + worldName); + String title = targetPlayer.getName() + " @ " + worldName; Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), 45, title); // Fill in main inventory slots (0–35) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 6e366148..66d831d7 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -1,7 +1,6 @@ package org.mvplugins.multiverse.inventories.view; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; @@ -48,8 +47,8 @@ public ItemStack createFillerItem(Material material, String name, String lore) { ItemStack item = new ItemStack(material); ItemMeta meta = item.getItemMeta(); if (meta != null) { - meta.displayName(Component.text(name, NamedTextColor.GOLD)); - meta.lore(Collections.singletonList(Component.text(lore, NamedTextColor.GRAY))); + meta.setDisplayName(ChatColor.GOLD + name); + meta.setLore(Collections.singletonList(ChatColor.GRAY + lore)); meta.getPersistentDataContainer().set(IS_FILLER_KEY, PersistentDataType.BYTE, (byte) 1); // store 1 for true item.setItemMeta(meta); } From 83bb05e96b1efd2cc14673ec1ad7929997813adb Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 12:31:57 -0400 Subject: [PATCH 20/39] Add "No [Item] Here" for the read-only case --- .../commands/InventoryModifyCommand.java | 12 ++++++------ .../commands/InventoryViewCommand.java | 12 ++++++------ .../listeners/InventoryViewListener.java | 4 ++-- .../inventories/view/InventoryGUIHelper.java | 18 ++++++++++++------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index ce2a1ed0..b557b2b3 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -97,37 +97,37 @@ void onInventoryModifyCommand( // Armor slot mapping for display in the GUI and add fillers if empty // Slot 36: Helmet if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper + inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36, true)); // Use helper } else { inv.setItem(36, playerInventoryData.armor[3]); } // Slot 37: Chestplate if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { - inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37)); // Use helper + inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37, true)); // Use helper } else { inv.setItem(37, playerInventoryData.armor[2]); } // Slot 38: Leggings if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper + inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38, true)); // Use helper } else { inv.setItem(38, playerInventoryData.armor[1]); } // Slot 39: Boots if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper + inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39, true)); // Use helper } else { inv.setItem(39, playerInventoryData.armor[0]); } // Off-hand slot (40) and add filler if empty if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { - inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40)); // Use helper + inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40, true)); // Use helper } else { inv.setItem(40, playerInventoryData.offHand); } // Add the remaining slots as non-interactable filler items for (int i = 41; i <= 44; i++) { - inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i)); + inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i, true)); } player.openInventory(inv); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 0e2e8cb5..697fb2e8 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -82,38 +82,38 @@ void onInventoryViewCommand( // Armor slot mapping for display in the GUI and add fillers if empty // Slot 36: Helmet if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36)); // Use helper + inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36, false)); // Use helper } else { inv.setItem(36, playerInventoryData.armor[3]); } // Slot 37: Chestplate if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { - inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37)); // Use helper + inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37, false)); // Use helper } else { inv.setItem(37, playerInventoryData.armor[2]); } // Slot 38: Leggings if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38)); // Use helper + inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38, false)); // Use helper } else { inv.setItem(38, playerInventoryData.armor[1]); } // Slot 39: Boots if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39)); // Use helper + inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39, false)); // Use helper } else { inv.setItem(39, playerInventoryData.armor[0]); } // Off-hand slot (40) and add filler if empty if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { - inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40)); // Use helper + inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40, false)); // Use helper } else { inv.setItem(40, playerInventoryData.offHand); } // Add the remaining slots as non-interactable filler items for (int i = 41; i <= 44; i++) { - inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i)); + inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i, false)); } player.openInventory(inv); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 027fb07a..9625eeb7 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -116,7 +116,7 @@ void onInventoryClick(InventoryClickEvent event) { Bukkit.getScheduler().runTaskLater(inventories, () -> { // After Bukkit processes the click, if the slot is now empty, put the filler back. if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { - event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot)); + event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot, true)); } }, 1L); } @@ -167,7 +167,7 @@ void onInventoryDrag(InventoryDragEvent event) { Bukkit.getScheduler().runTaskLater(inventories, () -> { for (int slot : event.getRawSlots()) { if ((slot >= 36 && slot <= 40) && (event.getInventory().getItem(slot) == null || event.getInventory().getItem(slot).getType() == Material.AIR)) { - event.getInventory().setItem(slot, inventoryGUIHelper.createFillerItemForSlot(slot)); // Use helper + event.getInventory().setItem(slot, inventoryGUIHelper.createFillerItemForSlot(slot, true)); // Use helper } } }, 1L); // Run one tick later diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 66d831d7..347f0de7 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -122,13 +122,19 @@ public boolean isValidItemForSlot(@NotNull ItemStack item, int slot) { * @since 5.2 */ @ApiStatus.AvailableSince("5.2") - public ItemStack createFillerItemForSlot(int slot) { + public ItemStack createFillerItemForSlot(int slot, boolean isModifiable) { + String helmetLore = isModifiable ? "Place Helmet Here" : "No Helmet"; + String chestplateLore = isModifiable ? "Place Chestplate Here" : "No Chestplate"; + String leggingsLore = isModifiable ? "Place Leggings Here" : "No Leggings"; + String bootsLore = isModifiable ? "Place Boots Here" : "No Boots"; + String offHandLore = isModifiable ? "Place Off-Hand Item Here" : "No Off-Hand Item"; + return switch (slot) { - case 36 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", "Place Helmet Here"); - case 37 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", "Place Chestplate Here"); - case 38 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", "Place Leggings Here"); - case 39 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", "Place Boots Here"); - case 40 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", "Place Off-Hand Item Here"); + case 36 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Helmet Slot", helmetLore); + case 37 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Chestplate Slot", chestplateLore); + case 38 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Leggings Slot", leggingsLore); + case 39 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Boots Slot", bootsLore); + case 40 -> createFillerItem(Material.GRAY_STAINED_GLASS_PANE, "Off-Hand Slot", offHandLore); case 41, 42, 43, 44 -> createFillerItem(Material.BARRIER, " ", " "); // Padding slots default -> new ItemStack(Material.AIR); // Should not happen for these slots }; From d5be772b307fcc13f64c4eb6c65c4b5c1ef32941 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 12:35:47 -0400 Subject: [PATCH 21/39] Remove inventories field --- .../view/ModifiableInventoryHolder.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java index 09b48eb0..8da4bc11 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java @@ -20,7 +20,6 @@ public final class ModifiableInventoryHolder implements InventoryHolder { private final OfflinePlayer targetPlayer; private final String worldName; private final ProfileType profileType; - private final MultiverseInventories inventories; /** * @@ -34,12 +33,10 @@ public final class ModifiableInventoryHolder implements InventoryHolder { @ApiStatus.AvailableSince("5.2") public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, @NotNull String worldName, - @NotNull ProfileType profileType, - @NotNull MultiverseInventories inventories) { + @NotNull ProfileType profileType) { this.targetPlayer = targetPlayer; this.worldName = worldName; this.profileType = profileType; - this.inventories = inventories; } /** @@ -75,17 +72,6 @@ public ModifiableInventoryHolder(@NotNull OfflinePlayer targetPlayer, return profileType; } - /** - * - * @return - * - * @since 5.2 - */ - @ApiStatus.AvailableSince("5.2") - public @NotNull MultiverseInventories getInventories() { - return inventories; - } - /** * {@inheritDoc} */ From b03482e1f6b59e750a522fd514f47e966c0c7c79 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 12:38:31 -0400 Subject: [PATCH 22/39] Remove old references to the inventories field --- .../inventories/commands/InventoryModifyCommand.java | 3 +-- .../multiverse/inventories/view/ModifiableInventoryHolder.java | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index b557b2b3..158d68d1 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -81,8 +81,7 @@ void onInventoryModifyCommand( new ModifiableInventoryHolder( targetPlayer, worldName, - playerInventoryData.profileTypeUsed, // Use the determined profile type - inventories + playerInventoryData.profileTypeUsed // Use the determined profile type ), 45, title diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java index 8da4bc11..2fb80fdd 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/ModifiableInventoryHolder.java @@ -26,7 +26,6 @@ public final class ModifiableInventoryHolder implements InventoryHolder { * @param targetPlayer * @param worldName * @param profileType - * @param inventories * * @since 5.2 */ From b69e2b4685516cc0dbbaa25bbd1a4ba914d1baa9 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 13:25:49 -0400 Subject: [PATCH 23/39] Change status message to enum type --- .../commands/InventoryModifyCommand.java | 2 +- .../commands/InventoryViewCommand.java | 2 +- .../profile/InventoryDataProvider.java | 14 +++--- .../inventories/profile/InventoryStatus.java | 45 +++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 158d68d1..78aecc7d 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -130,7 +130,7 @@ void onInventoryModifyCommand( } player.openInventory(inv); - issuer.sendInfo(ChatColor.GREEN + "Opened editable inventory for " + targetPlayer.getName() + " in world " + worldName + ". Changes will save on close."); + issuer.sendInfo(playerInventoryData.status.getFormattedMessage(targetPlayer.getName(), worldName) + ". Changes will save on close."); }); // End of Bukkit.getScheduler().runTask() }) .exceptionally(throwable -> { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 697fb2e8..e35d89f1 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -117,7 +117,7 @@ void onInventoryViewCommand( } player.openInventory(inv); - issuer.sendInfo(ChatColor.GREEN + playerInventoryData.statusMessage); + issuer.sendInfo(playerInventoryData.status.getFormattedMessage(targetPlayer.getName(),worldName)); }); // End of Bukkit.getScheduler().runTask() }) .exceptionally(throwable -> { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 61861957..c3cfd965 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -55,7 +55,7 @@ public static class PlayerInventoryData { public final ItemStack[] contents; public final ItemStack[] armor; public final ItemStack offHand; - public final String statusMessage; // To indicate if it's live or stored data + public final InventoryStatus status; // To indicate if it's live or stored data public final ProfileType profileTypeUsed; // To pass back which profile type was used for stored data /** @@ -63,17 +63,17 @@ public static class PlayerInventoryData { * @param contents * @param armor * @param offHand - * @param statusMessage + * @param status * @param profileTypeUsed * * @since 5.2 */ @ApiStatus.AvailableSince("5.2") - public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, String statusMessage, ProfileType profileTypeUsed) { + public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, ProfileType profileTypeUsed) { this.contents = contents; this.armor = armor; this.offHand = offHand; - this.statusMessage = statusMessage; + this.status = status; this.profileTypeUsed = profileTypeUsed; } } @@ -125,7 +125,7 @@ private CompletableFuture loadInventoryDataFromPlayer( onlineTarget.getInventory().getContents(), onlineTarget.getInventory().getArmorContents(), onlineTarget.getInventory().getItemInOffHand(), - "Displaying LIVE inventory for " + onlineTarget.getName() + " in world " + worldName + ".", + InventoryStatus.LIVE_INVENTORY, profileType )); } @@ -143,7 +143,7 @@ private CompletableFuture loadInventoryDataFromProfileStora PlayerProfile tempProfile = loadMVInvPlayerProfile(container, targetPlayer); if (tempProfile == null) { - throw new IllegalStateException("No player data found for " + targetPlayer.getName() + " in world " + worldName + ". Try checking a different world or ensure the player has played in this world."); + throw new IllegalStateException(InventoryStatus.NO_DATA_FOUND.getFormattedMessage(targetPlayer.getName(), worldName)); } ProfileType profileTypeToUse = tempProfile.getProfileType(); try { @@ -155,7 +155,7 @@ private CompletableFuture loadInventoryDataFromProfileStora contents, armor, offHand, - "Displaying STORED inventory for " + targetPlayer.getName() + " in world " + worldName + ".", + InventoryStatus.STORED_INVENTORY, profileTypeToUse ); } catch (CompletionException e) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java new file mode 100644 index 00000000..d5cca0e1 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java @@ -0,0 +1,45 @@ +package org.mvplugins.multiverse.inventories.profile; + +import org.bukkit.ChatColor; +import org.jetbrains.annotations.NotNull; + +/** + * Represents the status of an inventory data load operation. + * Provides a fixed set of states for clarity and type safety. + */ +public enum InventoryStatus { + /** + * Indicates that live inventory data from an online player was displayed. + */ + LIVE_INVENTORY(ChatColor.GREEN + "Displaying LIVE inventory"), + + /** + * Indicates that stored inventory data from Multiverse-Inventories profiles was displayed. + */ + STORED_INVENTORY(ChatColor.GREEN + "Displaying STORED inventory"), + + /** + * Indicates that no player data was found for the specified world/player. + */ + NO_DATA_FOUND(ChatColor.RED + "No player data found"); + + private final String message; + + InventoryStatus(@NotNull String message) { + this.message = message; + } + + /** + * Gets the full status message including player and world context. + * + * @param playerName The name of the target player. + * @param worldName The name of the target world. + * @return The formatted status message. + */ + public @NotNull String getFormattedMessage(@NotNull String playerName, @NotNull String worldName) { + if (this == NO_DATA_FOUND) { + return this.message + " for " + playerName + " in world" + worldName + ". Try checking a different world or ensure the player has played in this world."; + } + return this.message + " for " + playerName + " in world " + worldName; + } +} From 893ac2f5e348ac6e6abe4e3be187c44439b173bb Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 13:52:49 -0400 Subject: [PATCH 24/39] Move duplicate logic in the view and modify command to populateInventoryGUI method --- .../commands/InventoryModifyCommand.java | 44 +------------ .../commands/InventoryViewCommand.java | 45 +------------- .../inventories/view/InventoryGUIHelper.java | 62 +++++++++++++++++++ 3 files changed, 66 insertions(+), 85 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 78aecc7d..d1ade629 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -87,48 +87,8 @@ void onInventoryModifyCommand( title ); - // Fill inventory - if (playerInventoryData.contents != null) { - for (int i = 0; i < Math.min(playerInventoryData.contents.length, 36); i++) { - inv.setItem(i, playerInventoryData.contents[i]); - } - } - // Armor slot mapping for display in the GUI and add fillers if empty - // Slot 36: Helmet - if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36, true)); // Use helper - } else { - inv.setItem(36, playerInventoryData.armor[3]); - } - // Slot 37: Chestplate - if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { - inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37, true)); // Use helper - } else { - inv.setItem(37, playerInventoryData.armor[2]); - } - // Slot 38: Leggings - if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38, true)); // Use helper - } else { - inv.setItem(38, playerInventoryData.armor[1]); - } - // Slot 39: Boots - if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39, true)); // Use helper - } else { - inv.setItem(39, playerInventoryData.armor[0]); - } - // Off-hand slot (40) and add filler if empty - if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { - inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40, true)); // Use helper - } else { - inv.setItem(40, playerInventoryData.offHand); - } - // Add the remaining slots as non-interactable filler items - for (int i = 41; i <= 44; i++) { - inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i, true)); - } - + // Call the helper method to populate the GUI + inventoryGUIHelper.populateInventoryGUI(inv, playerInventoryData, true); player.openInventory(inv); issuer.sendInfo(playerInventoryData.status.getFormattedMessage(targetPlayer.getName(), worldName) + ". Changes will save on close."); }); // End of Bukkit.getScheduler().runTask() diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index e35d89f1..b73e44d8 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -73,49 +73,8 @@ void onInventoryViewCommand( String title = targetPlayer.getName() + " @ " + worldName; Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), 45, title); - // Fill in main inventory slots (0–35) - if (playerInventoryData.contents != null) { - for (int i = 0; i < Math.min(playerInventoryData.contents.length, 36); i++) { - inv.setItem(i, playerInventoryData.contents[i]); - } - } - // Armor slot mapping for display in the GUI and add fillers if empty - // Slot 36: Helmet - if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, inventoryGUIHelper.createFillerItemForSlot(36, false)); // Use helper - } else { - inv.setItem(36, playerInventoryData.armor[3]); - } - // Slot 37: Chestplate - if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { - inv.setItem(37, inventoryGUIHelper.createFillerItemForSlot(37, false)); // Use helper - } else { - inv.setItem(37, playerInventoryData.armor[2]); - } - // Slot 38: Leggings - if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, inventoryGUIHelper.createFillerItemForSlot(38, false)); // Use helper - } else { - inv.setItem(38, playerInventoryData.armor[1]); - } - // Slot 39: Boots - if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, inventoryGUIHelper.createFillerItemForSlot(39, false)); // Use helper - } else { - inv.setItem(39, playerInventoryData.armor[0]); - } - - // Off-hand slot (40) and add filler if empty - if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { - inv.setItem(40, inventoryGUIHelper.createFillerItemForSlot(40, false)); // Use helper - } else { - inv.setItem(40, playerInventoryData.offHand); - } - // Add the remaining slots as non-interactable filler items - for (int i = 41; i <= 44; i++) { - inv.setItem(i, inventoryGUIHelper.createFillerItemForSlot(i, false)); - } - + // Call the helper method to populate the GUI + inventoryGUIHelper.populateInventoryGUI(inv, playerInventoryData, false); player.openInventory(inv); issuer.sendInfo(playerInventoryData.status.getFormattedMessage(targetPlayer.getName(),worldName)); }); // End of Bukkit.getScheduler().runTask() diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 347f0de7..2183b32a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -3,6 +3,7 @@ import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataContainer; @@ -12,6 +13,7 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; import java.util.Collections; @@ -139,4 +141,64 @@ public ItemStack createFillerItemForSlot(int slot, boolean isModifiable) { default -> new ItemStack(Material.AIR); // Should not happen for these slots }; } + + /** + * Populates the given custom inventory GUI with player inventory data and appropriate filler items. + * @param inv The Inventory GUI to populate. + * @param playerInventoryData The data containing player's contents, armor, and off-hand. + * @param isModifiable True if the inventory is modifiable, false for read-only. + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public void populateInventoryGUI(@NotNull Inventory inv, + @NotNull InventoryDataProvider.PlayerInventoryData playerInventoryData, + boolean isModifiable) { + // Fill main inventory slots (0–35) + if (playerInventoryData.contents != null) { + for (int i = 0; i < Math.min(playerInventoryData.contents.length, 36); i++) { + inv.setItem(i, playerInventoryData.contents[i]); + } + } + // Armor slot mapping for display in the GUI and add fillers if empty + // GUI Slots: 36=Helmet, 37=Chestplate, 38=Leggings, 39=Boots + // Minecraft Internal: armor[3]=Helmet, armor[2]=Chestplate, armor[1]=Leggings, armor[0]=Boots + + // Slot 36: Helmet + if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { + inv.setItem(36, createFillerItemForSlot(36, isModifiable)); + } else { + inv.setItem(36, playerInventoryData.armor[3]); + } + // Slot 37: Chestplate + if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { + inv.setItem(37, createFillerItemForSlot(37, isModifiable)); + } else { + inv.setItem(37, playerInventoryData.armor[2]); + } + // Slot 38: Leggings + if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { + inv.setItem(38, createFillerItemForSlot(38, isModifiable)); + } else { + inv.setItem(38, playerInventoryData.armor[1]); + } + // Slot 39: Boots + if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { + inv.setItem(39, createFillerItemForSlot(39, isModifiable)); + } else { + inv.setItem(39, playerInventoryData.armor[0]); + } + + // Off-hand slot (40) and add filler if empty + if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { + inv.setItem(40, createFillerItemForSlot(40, isModifiable)); + } else { + inv.setItem(40, playerInventoryData.offHand); + } + + // Fill the remaining slots (41-44) with non-interactable filler items + for (int i = 41; i <= 44; i++) { + inv.setItem(i, createFillerItemForSlot(i, isModifiable)); // Use createFillerItemForSlot for padding too + } + } } From 1a05c0e433f3d5578f691f91fe2af16834ab2a35 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 14:03:12 -0400 Subject: [PATCH 25/39] Change inventories.getLogger() to Logging --- .../commands/InventoryModifyCommand.java | 3 ++- .../commands/InventoryViewCommand.java | 3 ++- .../listeners/InventoryViewListener.java | 3 ++- .../profile/InventoryDataProvider.java | 19 ++++++++++--------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index d1ade629..fe62cbf2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.commands; +import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -96,7 +97,7 @@ void onInventoryModifyCommand( .exceptionally(throwable -> { // This block runs if an exception occurs during data loading issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); - inventories.getLogger().severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); + Logging.severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); throwable.printStackTrace(); return null; // Must return null for CompletableFuture in exceptionally }); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index b73e44d8..427d7547 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.commands; +import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; @@ -82,7 +83,7 @@ void onInventoryViewCommand( .exceptionally(throwable -> { // This block runs if an exception occurs during data loading issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); - inventories.getLogger().severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); + Logging.severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); throwable.printStackTrace(); return null; // Must return null for CompletableFuture in exceptionally }); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 9625eeb7..329f73a0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.listeners; +import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.OfflinePlayer; @@ -215,7 +216,7 @@ void onInventoryClose(InventoryCloseEvent event) { newOffHand ).exceptionally(throwable -> { // Error logging is now handled within InventoryDataProvider, but we can add a general one here too - inventories.getLogger().severe("Error during inventory save process for " + targetPlayer.getName() + ": " + throwable.getMessage()); + Logging.severe("Error during inventory save process for " + targetPlayer.getName() + ": " + throwable.getMessage()); throwable.printStackTrace(); return null; }); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index c3cfd965..385567dc 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.profile; +import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -106,9 +107,9 @@ public CompletableFuture loadPlayerInventoryData( } // If online but in a different world, or getPlayer() returned null, fall through to stored data logic if (onlineTarget != null) { - inventories.getLogger().fine("Player " + targetPlayer.getName() + " is online but in world " + onlineTarget.getWorld().getName() + ". Loading stored data for " + worldName + "."); + Logging.fine("Player " + targetPlayer.getName() + " is online but in world " + onlineTarget.getWorld().getName() + ". Loading stored data for " + worldName + "."); } else { - inventories.getLogger().warning("Player " + targetPlayer.getName() + " is online but getPlayer() returned null. Falling back to stored data."); + Logging.warning("Player " + targetPlayer.getName() + " is online but getPlayer() returned null. Falling back to stored data."); } // If the player is offline or online in a different world, or live data failed, load from Multiverse-Inventories' stored profiles return loadInventoryDataFromProfileStorage(targetPlayer, worldName); @@ -215,10 +216,10 @@ public CompletableFuture savePlayerInventoryData( ); return saveFuture.thenRun(() -> { - inventories.getLogger().info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); + Logging.info("Inventory for player " + targetPlayer.getName() + " in world " + worldName + " has been modified and saved."); updateOnlinePlayerInventoryData(targetPlayer, worldName, newContents, newArmor, newOffHand); }).exceptionally(throwable -> { - inventories.getLogger().severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); + Logging.severe("Failed to save inventory for " + targetPlayer.getName() + " in world " + worldName + ": " + throwable.getMessage()); throwable.printStackTrace(); return null; }); @@ -244,7 +245,7 @@ private void updateOnlinePlayerInventoryData( // Check if the online player is in the world whose inventory was modified // This is important to avoid overwriting their current inventory if they are in a different world if (!onlinePlayer.getWorld().getName().equalsIgnoreCase(worldName)) { - inventories.getLogger().info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); + Logging.info("Player " + onlinePlayer.getName() + " is online but in a different world (" + onlinePlayer.getWorld().getName() + "), not updating live inventory."); return; } @@ -254,7 +255,7 @@ private void updateOnlinePlayerInventoryData( onlinePlayer.getInventory().setArmorContents(newArmor); onlinePlayer.getInventory().setItemInOffHand(newOffHand); onlinePlayer.updateInventory(); // Ensure client sees changes - inventories.getLogger().info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); + Logging.info("Updated live inventory for online player " + onlinePlayer.getName() + " in world " + worldName); }); } @@ -270,13 +271,13 @@ private CompletableFuture writeInventoryDataToProfile( return CompletableFuture.allOf( SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.INVENTORY) .write(newContents, true) // true to update if player is online - .thenRun(() -> inventories.getLogger().fine("Saved inventory for " + targetPlayer.getName() + " in " + worldName)), + .thenRun(() -> Logging.fine("Saved inventory for " + targetPlayer.getName() + " in " + worldName)), SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.ARMOR) .write(newArmor, true) - .thenRun(() -> inventories.getLogger().fine("Saved armor for " + targetPlayer.getName() + " in " + worldName)), + .thenRun(() -> Logging.fine("Saved armor for " + targetPlayer.getName() + " in " + worldName)), SingleShareWriter.of(inventories, targetPlayer, worldName, profileType, Sharables.OFF_HAND) .write(newOffHand, true) - .thenRun(() -> inventories.getLogger().fine("Saved off-hand for " + targetPlayer.getName() + " in " + worldName)) + .thenRun(() -> Logging.fine("Saved off-hand for " + targetPlayer.getName() + " in " + worldName)) ); } } From a856f3410ce02088a5dec40086278cfcf0e2333b Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 14:28:19 -0400 Subject: [PATCH 26/39] Make Helper more concise, fix bug where fillers don't always show in stored inventories --- .../commands/InventoryModifyCommand.java | 1 - .../commands/InventoryViewCommand.java | 1 - .../inventories/view/InventoryGUIHelper.java | 50 +++++++++---------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index fe62cbf2..58416ad2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -3,7 +3,6 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; import org.bukkit.ChatColor; -import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 427d7547..6760fd26 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -3,7 +3,6 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; import org.bukkit.ChatColor; -import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 2183b32a..af81d0f0 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -142,6 +142,21 @@ public ItemStack createFillerItemForSlot(int slot, boolean isModifiable) { }; } + + /** + * Helper method to get an item for a slot, returning a filler if the item is null or air. + * @param item The actual ItemStack from player data. Can be null. + * @param slot The slot number. + * @param isModifiable True if the inventory is modifiable. + * @return The actual item or a generated filler item. + */ + private ItemStack getOrFillItem(ItemStack item, int slot, boolean isModifiable) { + if (item == null || item.getType() == Material.AIR) { + return createFillerItemForSlot(slot, isModifiable); + } + return item; + } + /** * Populates the given custom inventory GUI with player inventory data and appropriate filler items. * @param inv The Inventory GUI to populate. @@ -165,40 +180,23 @@ public void populateInventoryGUI(@NotNull Inventory inv, // Minecraft Internal: armor[3]=Helmet, armor[2]=Chestplate, armor[1]=Leggings, armor[0]=Boots // Slot 36: Helmet - if (playerInventoryData.armor == null || playerInventoryData.armor[3] == null) { - inv.setItem(36, createFillerItemForSlot(36, isModifiable)); - } else { - inv.setItem(36, playerInventoryData.armor[3]); - } + inv.setItem(36, getOrFillItem((playerInventoryData.armor != null ? playerInventoryData.armor[3] : null), 36, isModifiable)); + // Slot 37: Chestplate - if (playerInventoryData.armor == null || playerInventoryData.armor[2] == null) { - inv.setItem(37, createFillerItemForSlot(37, isModifiable)); - } else { - inv.setItem(37, playerInventoryData.armor[2]); - } + inv.setItem(37, getOrFillItem((playerInventoryData.armor != null ? playerInventoryData.armor[2] : null), 37, isModifiable)); + // Slot 38: Leggings - if (playerInventoryData.armor == null || playerInventoryData.armor[1] == null) { - inv.setItem(38, createFillerItemForSlot(38, isModifiable)); - } else { - inv.setItem(38, playerInventoryData.armor[1]); - } + inv.setItem(38, getOrFillItem((playerInventoryData.armor != null ? playerInventoryData.armor[1] : null), 38, isModifiable)); + // Slot 39: Boots - if (playerInventoryData.armor == null || playerInventoryData.armor[0] == null) { - inv.setItem(39, createFillerItemForSlot(39, isModifiable)); - } else { - inv.setItem(39, playerInventoryData.armor[0]); - } + inv.setItem(39, getOrFillItem((playerInventoryData.armor != null ? playerInventoryData.armor[0] : null), 39, isModifiable)); // Off-hand slot (40) and add filler if empty - if (playerInventoryData.offHand == null || playerInventoryData.offHand.getType() == Material.AIR) { - inv.setItem(40, createFillerItemForSlot(40, isModifiable)); - } else { - inv.setItem(40, playerInventoryData.offHand); - } + inv.setItem(40, getOrFillItem(playerInventoryData.offHand, 40, isModifiable)); // Fill the remaining slots (41-44) with non-interactable filler items for (int i = 41; i <= 44; i++) { - inv.setItem(i, createFillerItemForSlot(i, isModifiable)); // Use createFillerItemForSlot for padding too + inv.setItem(i, createFillerItemForSlot(i, isModifiable)); } } } From f0d2daaec13acae1724dfb37347102496a983e99 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 14:46:00 -0400 Subject: [PATCH 27/39] Split into smaller private methods --- .../commands/InventoryModifyCommand.java | 82 ++++++++++++++----- .../commands/InventoryViewCommand.java | 53 ++++++++++-- 2 files changed, 105 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 58416ad2..07e7e5d7 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -67,38 +67,76 @@ void onInventoryModifyCommand( } String worldName = world.getName(); - // Asynchronously load data using InventoryDataProvider issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); + handleInventoryLoadAndDisplay(issuer, player, targetPlayer, worldName); + } + + /** + * Handles the asynchronous loading of player inventory data and the display of the GUI. + * + * @param issuer The command issuer. + * @param player The player who will view/modify the inventory. + * @param targetPlayer The offline or online player whose inventory data is being loaded. + * @param worldName The name of the world for which inventory data is loaded. + */ + private void handleInventoryLoadAndDisplay( + @NotNull MVCommandIssuer issuer, + @NotNull Player player, + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName + ) { inventoryDataProvider.loadPlayerInventoryData(targetPlayer, worldName) .thenAccept(playerInventoryData -> { // Ensure GUI operations run on the main thread Bukkit.getScheduler().runTask(inventories, () -> { - // Create inventory with ModifiableInventoryHolder - // Pass all necessary context to the holder for saving on close. - String title = "Modify " + targetPlayer.getName() + " @ " + worldName; - Inventory inv = Bukkit.createInventory( - new ModifiableInventoryHolder( - targetPlayer, - worldName, - playerInventoryData.profileTypeUsed // Use the determined profile type - ), - 45, - title - ); - - // Call the helper method to populate the GUI - inventoryGUIHelper.populateInventoryGUI(inv, playerInventoryData, true); - player.openInventory(inv); - issuer.sendInfo(playerInventoryData.status.getFormattedMessage(targetPlayer.getName(), worldName) + ". Changes will save on close."); - }); // End of Bukkit.getScheduler().runTask() + createAndOpenGUI(issuer, player, targetPlayer, worldName, playerInventoryData); + }); }) .exceptionally(throwable -> { // This block runs if an exception occurs during data loading - issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); - Logging.severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); + String errorMessage = throwable.getMessage(); + if (errorMessage == null || errorMessage.isEmpty()) { + errorMessage = "An unknown error occurred while loading inventory data."; + } + issuer.sendError(ChatColor.RED + errorMessage); + Logging.fine("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); throwable.printStackTrace(); - return null; // Must return null for CompletableFuture in exceptionally + return null; }); } + + /** + * Creates and opens the custom inventory GUI for modification. + * This method must be called on the main server thread. + * + * @param issuer The command issuer. + * @param player The player who will view/modify the inventory. + * @param targetPlayer The offline player whose inventory is being displayed. + * @param worldName The name of the world for which inventory data is displayed. + * @param playerInventoryData The loaded inventory data. + */ + private void createAndOpenGUI( + @NotNull MVCommandIssuer issuer, + @NotNull Player player, + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull InventoryDataProvider.PlayerInventoryData playerInventoryData + ) { + String title = "Modify " + targetPlayer.getName() + " @ " + worldName; + Inventory inv = Bukkit.createInventory( + new ModifiableInventoryHolder( + targetPlayer, + worldName, + playerInventoryData.profileTypeUsed // Use the determined profile type + ), + 45, // 5 rows for 36 main + 4 armor + 1 off-hand + 4 fillers + title + ); + + // Call the helper method to populate the GUI + inventoryGUIHelper.populateInventoryGUI(inv, playerInventoryData, true); + player.openInventory(inv); + issuer.sendInfo(ChatColor.GREEN + playerInventoryData.status.getFormattedMessage(targetPlayer.getName(), worldName) + ". Changes will save on close."); + } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 6760fd26..38e83c01 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -64,19 +64,27 @@ void onInventoryViewCommand( // Asynchronously load data using InventoryDataProvider issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); + } + /** + * Handles the asynchronous loading of player inventory data and the display of the GUI. + * + * @param issuer The command issuer. + * @param player The player who will view the inventory. + * @param targetPlayer The offline player whose inventory data is being loaded. + * @param worldName The name of the world for which inventory data is loaded. + */ + private void handleInventoryLoadAndDisplay( + @NotNull MVCommandIssuer issuer, + @NotNull Player player, + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName + ) { inventoryDataProvider.loadPlayerInventoryData(targetPlayer, worldName) .thenAccept(playerInventoryData -> { // Ensure GUI operations run on the main thread Bukkit.getScheduler().runTask(inventories, () -> { - // Create an inventory for viewing. - String title = targetPlayer.getName() + " @ " + worldName; - Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), 45, title); - - // Call the helper method to populate the GUI - inventoryGUIHelper.populateInventoryGUI(inv, playerInventoryData, false); - player.openInventory(inv); - issuer.sendInfo(playerInventoryData.status.getFormattedMessage(targetPlayer.getName(),worldName)); + createAndOpenGUI(issuer, player, targetPlayer, worldName, playerInventoryData); }); // End of Bukkit.getScheduler().runTask() }) .exceptionally(throwable -> { @@ -87,4 +95,33 @@ void onInventoryViewCommand( return null; // Must return null for CompletableFuture in exceptionally }); } + + /** + * Creates and opens the custom inventory GUI for viewing. + * This method must be called on the main server thread. + * + * @param issuer The command issuer. + * @param player The player who will view/modify the inventory. + * @param targetPlayer The offline player whose inventory is being displayed. + * @param worldName The name of the world for which inventory data is displayed. + * @param playerInventoryData The loaded inventory data. + */ + private void createAndOpenGUI( + @NotNull MVCommandIssuer issuer, + @NotNull Player player, + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull InventoryDataProvider.PlayerInventoryData playerInventoryData + ) { + String title = targetPlayer.getName() + " @ " + worldName; + Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), + 45, // 5 rows for 36 main + 4 armor + 1 off-hand + 4 fillers + title + ); + + // Call the helper method to populate the GUI + inventoryGUIHelper.populateInventoryGUI(inv, playerInventoryData, false); + player.openInventory(inv); + issuer.sendInfo(ChatColor.GREEN + playerInventoryData.status.getFormattedMessage(targetPlayer.getName(), worldName) + ". Changes will save on close."); + } } From e5b730b7d619d96b607e26f008ccb2258f060d8e Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 15:14:17 -0400 Subject: [PATCH 28/39] Players can modify their own stored inventory, but not their live inventory --- .../commands/InventoryModifyCommand.java | 13 +++++++++---- .../inventories/commands/InventoryViewCommand.java | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 07e7e5d7..dafaf5e8 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -61,10 +61,6 @@ void onInventoryModifyCommand( @Description("The world the player's inventory is in") MultiverseWorld world ) { - if (player.getUniqueId().equals(targetPlayer.getUniqueId())) { - issuer.sendError(ChatColor.RED + "You cannot modify your own inventory using this command. Use your regular inventory."); - return; - } String worldName = world.getName(); issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); @@ -90,9 +86,18 @@ private void handleInventoryLoadAndDisplay( .thenAccept(playerInventoryData -> { // Ensure GUI operations run on the main thread Bukkit.getScheduler().runTask(inventories, () -> { + + // If the player tries to modify their own live inventory, stop + if (player.getUniqueId().equals(targetPlayer.getUniqueId()) + && player.getWorld().getName().equalsIgnoreCase(worldName)) { + issuer.sendError(ChatColor.RED + "You cannot modify your own live inventory using this command. Use your regular inventory."); + return; // Stop here if it's a live self-inventory + } createAndOpenGUI(issuer, player, targetPlayer, worldName, playerInventoryData); }); }) + + .exceptionally(throwable -> { // This block runs if an exception occurs during data loading String errorMessage = throwable.getMessage(); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 38e83c01..fa6a3513 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -64,6 +64,7 @@ void onInventoryViewCommand( // Asynchronously load data using InventoryDataProvider issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); + handleInventoryLoadAndDisplay(issuer, player, targetPlayer, worldName); } /** From 05a0a91e375d93dab43e5a480459df892f7b848c Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 15:28:53 -0400 Subject: [PATCH 29/39] Fix bug where replacing an item in a filler slot makes the item disappear --- .../listeners/InventoryViewListener.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 329f73a0..28b32108 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -60,7 +60,6 @@ void onInventoryClick(InventoryClickEvent event) { int clickedSlot = event.getRawSlot(); ItemStack cursorItem = event.getCursor(); // Item held by the cursor ItemStack currentItem = event.getCurrentItem(); // Item in the clicked slot - Inventory customGUI = event.getInventory(); // The custom inventory Player player = (Player) event.getWhoClicked(); // The player who clicked // Define the special slots @@ -96,11 +95,17 @@ void onInventoryClick(InventoryClickEvent event) { if (cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { event.setCancelled(true); // Take full control of the event - // Place the new item from cursor into the clicked slot + // Place the new item from the cursor into the clicked slot event.getInventory().setItem(clickedSlot, cursorItem); - // Clear the player's cursor - player.setItemOnCursor(null); + // If the player tries to replace an item in the filler slot, check if the item is valid. + if (currentItem != null && currentItem.getType() != Material.AIR && !inventoryGUIHelper.isFillerItem(currentItem)) { + // If the original item was a real, non-filler item, put it on the player's cursor. + player.setItemOnCursor(currentItem); + } else { + // If the original item was null, air, or a filler, clear the player's cursor. + player.setItemOnCursor(null); + } player.updateInventory(); // Update client to reflect changes return; // Event handled From 77928ceafb04df3028bd1f88ee8c34052c3c6804 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 16:41:13 -0400 Subject: [PATCH 30/39] Implement slots for health, hunger, exp --- .../profile/InventoryDataProvider.java | 46 +++++++++++- .../inventories/view/InventoryGUIHelper.java | 71 ++++++++++++++++++- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 385567dc..3533a123 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -59,6 +59,13 @@ public static class PlayerInventoryData { public final InventoryStatus status; // To indicate if it's live or stored data public final ProfileType profileTypeUsed; // To pass back which profile type was used for stored data + // Non-inventory data + public final double health; + public final int level; + public final float exp; + public final int foodLevel; + public final float saturation; + /** * * @param contents @@ -70,12 +77,23 @@ public static class PlayerInventoryData { * @since 5.2 */ @ApiStatus.AvailableSince("5.2") - public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, ProfileType profileTypeUsed) { + public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, + ProfileType profileTypeUsed, double health, int level, float exp, int foodLevel, + float saturation) { this.contents = contents; this.armor = armor; this.offHand = offHand; this.status = status; this.profileTypeUsed = profileTypeUsed; + + // Non-inventory data + this.health = health; + this.level = level; + this.exp = exp; + this.foodLevel = foodLevel; + this.saturation = saturation; + + } } @@ -127,7 +145,12 @@ private CompletableFuture loadInventoryDataFromPlayer( onlineTarget.getInventory().getArmorContents(), onlineTarget.getInventory().getItemInOffHand(), InventoryStatus.LIVE_INVENTORY, - profileType + profileType, + onlineTarget.getHealth(), + onlineTarget.getLevel(), + onlineTarget.getExp(), + onlineTarget.getFoodLevel(), + onlineTarget.getSaturation() )); } @@ -152,12 +175,29 @@ private CompletableFuture loadInventoryDataFromProfileStora ItemStack[] armor = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.ARMOR).read().join(); ItemStack offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); + // Non-inventory data + double storedHealth = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.HEALTH) + .read().join(); + int storedLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LEVEL) + .read().join(); + float storedExp = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.EXPERIENCE) + .read().join(); + int storedFoodLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.FOOD_LEVEL) + .read().join(); + float storedSaturationLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.SATURATION) + .read().join(); + return new PlayerInventoryData( contents, armor, offHand, InventoryStatus.STORED_INVENTORY, - profileTypeToUse + profileTypeToUse, + storedHealth, + storedLevel, + storedExp, + storedFoodLevel, + storedSaturationLevel ); } catch (CompletionException e) { // Unwrap CompletionException to get the actual cause diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index af81d0f0..afba2fe4 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -15,6 +15,9 @@ import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; +import java.util.List; +import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.Collections; /** @@ -28,10 +31,12 @@ public final class InventoryGUIHelper { private final NamespacedKey IS_FILLER_KEY; // Key to mark filler items + private final NamespacedKey IS_DISPLAY_ITEM_KEY; // Key to mark stat display items @Inject InventoryGUIHelper(@NotNull MultiverseInventories inventories) { this.IS_FILLER_KEY = new NamespacedKey(inventories, "is_mvinv_filler"); + this.IS_DISPLAY_ITEM_KEY = new NamespacedKey(inventories, "is_mvinv_display"); } /** @@ -75,6 +80,21 @@ public boolean isFillerItem(@NotNull ItemStack item) { persistentDataContainer.getOrDefault(IS_FILLER_KEY, PersistentDataType.BYTE, (byte) 0) == (byte) 1; } + /** + * Checks if a given ItemStack is a general display item created by this helper. + * @param item The ItemStack to check. + * @return True if the item is a display item, false otherwise. + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public boolean isDisplayItem(@NotNull ItemStack item) { + if (!item.hasItemMeta()) { + return false; + } + return item.getItemMeta().getPersistentDataContainer().has(IS_DISPLAY_ITEM_KEY, PersistentDataType.BYTE) && + item.getItemMeta().getPersistentDataContainer().get(IS_DISPLAY_ITEM_KEY, PersistentDataType.BYTE) == (byte) 1; + } /** * Determines if an ItemStack is valid for a given special inventory slot in the custom GUI. * This method is used for both armor and off-hand slot validation. @@ -142,6 +162,45 @@ public ItemStack createFillerItemForSlot(int slot, boolean isModifiable) { }; } + /** + * Creates a generic display item for GUI slots (non-interactable, for showing stats). + * @param material The material of the display item. + * @param name The display name of the item. + * @param lore The lore text for the item. + * @return The created ItemStack. + */ + private ItemStack createDisplayItem(Material material, String name, List lore) { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(ChatColor.AQUA + name); + meta.setLore(lore); + meta.getPersistentDataContainer().set(IS_DISPLAY_ITEM_KEY, PersistentDataType.BYTE, (byte) 1); + item.setItemMeta(meta); + } + return item; + } + + private ItemStack createHealthDisplayItem(double health) { + List lore = new ArrayList<>(); + DecimalFormat df = new DecimalFormat("0.0"); + lore.add(ChatColor.WHITE + "Current: " + ChatColor.RED + df.format(health) + ChatColor.WHITE + " / " + ChatColor.RED + "20.0"); + return createDisplayItem(Material.RED_DYE, "Health", lore); + } + + private ItemStack createLevelDisplayItem(int level, float exp) { + List lore = new ArrayList<>(); + lore.add(ChatColor.WHITE + "Level: " + ChatColor.GREEN + level); + lore.add(ChatColor.WHITE + "Progress: " + ChatColor.AQUA + String.format("%.1f%%", exp * 100)); + return createDisplayItem(Material.EXPERIENCE_BOTTLE, "Experience", lore); + } + + private ItemStack createFoodDisplayItem(int foodLevel, float saturation) { + List lore = new ArrayList<>(); + lore.add(ChatColor.WHITE + "Food: " + ChatColor.GOLD + foodLevel + ChatColor.WHITE + " / " + ChatColor.GOLD + "20"); + lore.add(ChatColor.WHITE + "Saturation: " + ChatColor.LIGHT_PURPLE + String.format("%.1f", saturation)); + return createDisplayItem(Material.COOKED_BEEF, "Food & Saturation", lore); + } /** * Helper method to get an item for a slot, returning a filler if the item is null or air. @@ -194,9 +253,17 @@ public void populateInventoryGUI(@NotNull Inventory inv, // Off-hand slot (40) and add filler if empty inv.setItem(40, getOrFillItem(playerInventoryData.offHand, 40, isModifiable)); - // Fill the remaining slots (41-44) with non-interactable filler items + // These slots are always treated as read-only by the listener. + inv.setItem(41, createHealthDisplayItem(playerInventoryData.health)); + inv.setItem(42, createFoodDisplayItem(playerInventoryData.foodLevel, playerInventoryData.saturation)); + inv.setItem(43, createLevelDisplayItem(playerInventoryData.level, playerInventoryData.exp)); + + // Slot 44 will be a generic filler/padding item + inv.setItem(44, createFillerItemForSlot(44, isModifiable)); + + /*// Fill the remaining slots (41-44) with non-interactable filler items for (int i = 41; i <= 44; i++) { inv.setItem(i, createFillerItemForSlot(i, isModifiable)); - } + */ } } From 243247c7641eb45085b9ee4cb29ef2bc3581b167 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 16:47:06 -0400 Subject: [PATCH 31/39] Add description to javadocs --- .../profile/InventoryDataProvider.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 3533a123..5d91d977 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -68,11 +68,16 @@ public static class PlayerInventoryData { /** * - * @param contents - * @param armor - * @param offHand - * @param status - * @param profileTypeUsed + * @param contents The player's main inventory contents (slots 0-35). + * @param armor The player's armor contents (boots, leggings, chestplate, helmet). + * @param offHand The player's off-hand item. + * @param status The status of the inventory data load (e.g., LIVE, STORED, NO_DATA_FOUND). + * @param profileTypeUsed The profile type that was used to retrieve the data (e.g., SURVIVAL). + * @param health The player's current health. + * @param level The player's current experience level. + * @param exp The player's current experience progress towards the next level (0.0-1.0). + * @param foodLevel The player's current food level (0-20). + * @param saturation The player's current saturation level. * * @since 5.2 */ From 2d2455dd183e49a58b8ddc5dc04d46a5b4b0e48f Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 17:41:20 -0400 Subject: [PATCH 32/39] Begin to add player last location as a slot --- .../profile/InventoryDataProvider.java | 39 ++++++++++++++++--- .../inventories/view/InventoryGUIHelper.java | 12 ++++-- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 5d91d977..685b32e1 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -2,6 +2,7 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -65,6 +66,7 @@ public static class PlayerInventoryData { public final float exp; public final int foodLevel; public final float saturation; + public final String lastLocation; /** * @@ -78,13 +80,14 @@ public static class PlayerInventoryData { * @param exp The player's current experience progress towards the next level (0.0-1.0). * @param foodLevel The player's current food level (0-20). * @param saturation The player's current saturation level. + * @param lastLocation The player's last location. * * @since 5.2 */ @ApiStatus.AvailableSince("5.2") public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, ProfileType profileTypeUsed, double health, int level, float exp, int foodLevel, - float saturation) { + float saturation, String lastLocation) { this.contents = contents; this.armor = armor; this.offHand = offHand; @@ -97,8 +100,7 @@ public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack of this.exp = exp; this.foodLevel = foodLevel; this.saturation = saturation; - - + this.lastLocation = lastLocation; } } @@ -155,7 +157,12 @@ private CompletableFuture loadInventoryDataFromPlayer( onlineTarget.getLevel(), onlineTarget.getExp(), onlineTarget.getFoodLevel(), - onlineTarget.getSaturation() + onlineTarget.getSaturation(), + String.format("%s (%.1f, %.1f, %.1f)", + onlineTarget.getWorld().getName(), + onlineTarget.getLocation().getX(), + onlineTarget.getLocation().getY(), + onlineTarget.getLocation().getZ()) )); } @@ -191,6 +198,27 @@ private CompletableFuture loadInventoryDataFromProfileStora .read().join(); float storedSaturationLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.SATURATION) .read().join(); + Location storedLocationObject = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LAST_LOCATION) + .read().join(); + + // Note that this is here for debugging purposes + // If the player leaves a world or logs off, Sharables.LAST_LOCATION appears to read null + // storedLocationObject + String storedLastLocation; + if (storedLocationObject == null) { + storedLastLocation = "N/A (No Location Data)"; + } else if (storedLocationObject.getWorld() == null) { + storedLastLocation = String.format("N/A (World Null: %.1f, %.1f, %.1f)", + storedLocationObject.getX(), + storedLocationObject.getY(), + storedLocationObject.getZ()); + } else { + storedLastLocation = String.format("%s (%.1f, %.1f, %.1f)", + storedLocationObject.getWorld().getName(), + storedLocationObject.getX(), + storedLocationObject.getY(), + storedLocationObject.getZ()); + } return new PlayerInventoryData( contents, @@ -202,7 +230,8 @@ private CompletableFuture loadInventoryDataFromProfileStora storedLevel, storedExp, storedFoodLevel, - storedSaturationLevel + storedSaturationLevel, + storedLastLocation ); } catch (CompletionException e) { // Unwrap CompletionException to get the actual cause diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index afba2fe4..6b89c950 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -202,6 +202,14 @@ private ItemStack createFoodDisplayItem(int foodLevel, float saturation) { return createDisplayItem(Material.COOKED_BEEF, "Food & Saturation", lore); } + private ItemStack createLastLocationDisplayItem(@NotNull String locationString) { + List lore = new ArrayList<>(); + lore.add(ChatColor.WHITE + "Last Location:"); + // Split the location string if it's too long, or just add it directly + // Assuming locationString is already formatted like "world (x.x, y.y, z.z)" + lore.add(ChatColor.YELLOW + locationString); + return createDisplayItem(Material.COMPASS, "Last Location", lore); + } /** * Helper method to get an item for a slot, returning a filler if the item is null or air. * @param item The actual ItemStack from player data. Can be null. @@ -257,9 +265,7 @@ public void populateInventoryGUI(@NotNull Inventory inv, inv.setItem(41, createHealthDisplayItem(playerInventoryData.health)); inv.setItem(42, createFoodDisplayItem(playerInventoryData.foodLevel, playerInventoryData.saturation)); inv.setItem(43, createLevelDisplayItem(playerInventoryData.level, playerInventoryData.exp)); - - // Slot 44 will be a generic filler/padding item - inv.setItem(44, createFillerItemForSlot(44, isModifiable)); + inv.setItem(44, createLastLocationDisplayItem(playerInventoryData.lastLocation)); /*// Fill the remaining slots (41-44) with non-interactable filler items for (int i = 41; i <= 44; i++) { From 7da6de3ae9298825dbfe661c321e7c5beeee76f9 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Mon, 21 Jul 2025 17:51:53 -0400 Subject: [PATCH 33/39] Add maxHealth to the health slot --- .../inventories/profile/InventoryDataProvider.java | 10 +++++++++- .../inventories/view/InventoryGUIHelper.java | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 685b32e1..395b8c95 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -4,6 +4,7 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.OfflinePlayer; +import org.bukkit.attribute.Attribute; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.ApiStatus; @@ -62,6 +63,7 @@ public static class PlayerInventoryData { // Non-inventory data public final double health; + public final double maxHealth; public final int level; public final float exp; public final int foodLevel; @@ -76,6 +78,7 @@ public static class PlayerInventoryData { * @param status The status of the inventory data load (e.g., LIVE, STORED, NO_DATA_FOUND). * @param profileTypeUsed The profile type that was used to retrieve the data (e.g., SURVIVAL). * @param health The player's current health. + * @param maxHealth The player's maximum health. * @param level The player's current experience level. * @param exp The player's current experience progress towards the next level (0.0-1.0). * @param foodLevel The player's current food level (0-20). @@ -86,7 +89,7 @@ public static class PlayerInventoryData { */ @ApiStatus.AvailableSince("5.2") public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, - ProfileType profileTypeUsed, double health, int level, float exp, int foodLevel, + ProfileType profileTypeUsed, double health, double maxHealth, int level, float exp, int foodLevel, float saturation, String lastLocation) { this.contents = contents; this.armor = armor; @@ -96,6 +99,7 @@ public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack of // Non-inventory data this.health = health; + this.maxHealth = maxHealth; this.level = level; this.exp = exp; this.foodLevel = foodLevel; @@ -154,6 +158,7 @@ private CompletableFuture loadInventoryDataFromPlayer( InventoryStatus.LIVE_INVENTORY, profileType, onlineTarget.getHealth(), + onlineTarget.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue(), onlineTarget.getLevel(), onlineTarget.getExp(), onlineTarget.getFoodLevel(), @@ -190,6 +195,8 @@ private CompletableFuture loadInventoryDataFromProfileStora // Non-inventory data double storedHealth = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.HEALTH) .read().join(); + double storedMaxHealth = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.MAX_HEALTH) + .read().join(); int storedLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LEVEL) .read().join(); float storedExp = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.EXPERIENCE) @@ -227,6 +234,7 @@ private CompletableFuture loadInventoryDataFromProfileStora InventoryStatus.STORED_INVENTORY, profileTypeToUse, storedHealth, + storedMaxHealth, storedLevel, storedExp, storedFoodLevel, diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 6b89c950..0cf0efeb 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -181,10 +181,10 @@ private ItemStack createDisplayItem(Material material, String name, List return item; } - private ItemStack createHealthDisplayItem(double health) { + private ItemStack createHealthDisplayItem(double health, double maxHealth) { List lore = new ArrayList<>(); DecimalFormat df = new DecimalFormat("0.0"); - lore.add(ChatColor.WHITE + "Current: " + ChatColor.RED + df.format(health) + ChatColor.WHITE + " / " + ChatColor.RED + "20.0"); + lore.add(ChatColor.WHITE + "Current: " + ChatColor.RED + df.format(health) + ChatColor.WHITE + " / " + ChatColor.RED + df.format(maxHealth)); return createDisplayItem(Material.RED_DYE, "Health", lore); } @@ -262,7 +262,7 @@ public void populateInventoryGUI(@NotNull Inventory inv, inv.setItem(40, getOrFillItem(playerInventoryData.offHand, 40, isModifiable)); // These slots are always treated as read-only by the listener. - inv.setItem(41, createHealthDisplayItem(playerInventoryData.health)); + inv.setItem(41, createHealthDisplayItem(playerInventoryData.health, playerInventoryData.maxHealth)); inv.setItem(42, createFoodDisplayItem(playerInventoryData.foodLevel, playerInventoryData.saturation)); inv.setItem(43, createLevelDisplayItem(playerInventoryData.level, playerInventoryData.exp)); inv.setItem(44, createLastLocationDisplayItem(playerInventoryData.lastLocation)); From eedd72308e5224d9ada3ffe7801dfdc648a72a9f Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Tue, 22 Jul 2025 12:25:53 -0400 Subject: [PATCH 34/39] Remove unnecessary comments and fix indentation --- .../commands/InventoryViewCommand.java | 57 +++++++++---------- .../profile/InventoryDataProvider.java | 8 +-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index fa6a3513..20ac8769 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -67,35 +67,34 @@ void onInventoryViewCommand( handleInventoryLoadAndDisplay(issuer, player, targetPlayer, worldName); } - /** - * Handles the asynchronous loading of player inventory data and the display of the GUI. - * - * @param issuer The command issuer. - * @param player The player who will view the inventory. - * @param targetPlayer The offline player whose inventory data is being loaded. - * @param worldName The name of the world for which inventory data is loaded. - */ - private void handleInventoryLoadAndDisplay( - @NotNull MVCommandIssuer issuer, - @NotNull Player player, - @NotNull OfflinePlayer targetPlayer, - @NotNull String worldName - ) { - inventoryDataProvider.loadPlayerInventoryData(targetPlayer, worldName) - .thenAccept(playerInventoryData -> { - // Ensure GUI operations run on the main thread - Bukkit.getScheduler().runTask(inventories, () -> { - createAndOpenGUI(issuer, player, targetPlayer, worldName, playerInventoryData); - }); // End of Bukkit.getScheduler().runTask() - }) - .exceptionally(throwable -> { - // This block runs if an exception occurs during data loading - issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); - Logging.severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); - throwable.printStackTrace(); - return null; // Must return null for CompletableFuture in exceptionally - }); - } + /** + * Handles the asynchronous loading of player inventory data and the display of the GUI. + * + * @param issuer The command issuer. + * @param player The player who will view the inventory. + * @param targetPlayer The offline player whose inventory data is being loaded. + * @param worldName The name of the world for which inventory data is loaded. + */ + private void handleInventoryLoadAndDisplay( + @NotNull MVCommandIssuer issuer, + @NotNull Player player, + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName) { + inventoryDataProvider.loadPlayerInventoryData(targetPlayer, worldName) + .thenAccept(playerInventoryData -> { + // Ensure GUI operations run on the main thread + Bukkit.getScheduler().runTask(inventories, () -> { + createAndOpenGUI(issuer, player, targetPlayer, worldName, playerInventoryData); + }); // End of Bukkit.getScheduler().runTask() + }) + .exceptionally(throwable -> { + // This block runs if an exception occurs during data loading + issuer.sendError(ChatColor.RED + "Failed to load inventory data: " + throwable.getMessage()); + Logging.severe("Error loading inventory for " + targetPlayer.getName() + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; // Must return null for CompletableFuture in exceptionally + }); +} /** * Creates and opens the custom inventory GUI for viewing. diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 395b8c95..58d31418 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -23,8 +23,8 @@ import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; import org.mvplugins.multiverse.inventories.share.Sharables; -import java.util.concurrent.CompletableFuture; // Needed for async operations -import java.util.concurrent.CompletionException; // Needed for async error handling +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; /** * Provides methods for asynchronously loading player inventory data. @@ -58,8 +58,8 @@ public static class PlayerInventoryData { public final ItemStack[] contents; public final ItemStack[] armor; public final ItemStack offHand; - public final InventoryStatus status; // To indicate if it's live or stored data - public final ProfileType profileTypeUsed; // To pass back which profile type was used for stored data + public final InventoryStatus status; + public final ProfileType profileTypeUsed; // Non-inventory data public final double health; From a23903d12f215077cf661ca22cd56537c6683be6 Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Tue, 22 Jul 2025 13:14:20 -0400 Subject: [PATCH 35/39] Add private helper to get sharable value. Add check for config option for LAST_LOCATION --- .../profile/InventoryDataProvider.java | 83 ++++++++++++------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index 58d31418..e92f35f2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -13,6 +13,7 @@ import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.external.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainer; @@ -21,6 +22,7 @@ import org.mvplugins.multiverse.inventories.profile.key.ContainerType; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; +import org.mvplugins.multiverse.inventories.share.Sharable; import org.mvplugins.multiverse.inventories.share.Sharables; import java.util.concurrent.CompletableFuture; @@ -38,14 +40,17 @@ public final class InventoryDataProvider { private final ProfileContainerStoreProvider profileContainerStoreProvider; private final MultiverseInventories inventories; + private final InventoriesConfig inventoriesConfig; @Inject InventoryDataProvider( @NotNull ProfileContainerStoreProvider profileContainerStoreProvider, - @NotNull MultiverseInventories inventories + @NotNull MultiverseInventories inventories, + @NotNull InventoriesConfig inventoriesConfig ) { this.profileContainerStoreProvider = profileContainerStoreProvider; this.inventories = inventories; + this.inventoriesConfig = inventoriesConfig; } /** @@ -193,38 +198,28 @@ private CompletableFuture loadInventoryDataFromProfileStora ItemStack offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); // Non-inventory data - double storedHealth = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.HEALTH) - .read().join(); - double storedMaxHealth = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.MAX_HEALTH) - .read().join(); - int storedLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LEVEL) - .read().join(); - float storedExp = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.EXPERIENCE) - .read().join(); - int storedFoodLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.FOOD_LEVEL) - .read().join(); - float storedSaturationLevel = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.SATURATION) - .read().join(); - Location storedLocationObject = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LAST_LOCATION) - .read().join(); - - // Note that this is here for debugging purposes - // If the player leaves a world or logs off, Sharables.LAST_LOCATION appears to read null - // storedLocationObject + double storedHealth = getSharableValue(Sharables.HEALTH, targetPlayer, worldName, profileTypeToUse, 20.0); + double storedMaxHealth = getSharableValue(Sharables.MAX_HEALTH, targetPlayer, worldName, profileTypeToUse, 20.0); + int storedLevel = getSharableValue(Sharables.LEVEL, targetPlayer, worldName, profileTypeToUse, 0); + float storedExp = getSharableValue(Sharables.EXPERIENCE, targetPlayer, worldName, profileTypeToUse, 0.0f); + int storedFoodLevel = getSharableValue(Sharables.FOOD_LEVEL, targetPlayer, worldName, profileTypeToUse, 20); + float storedSaturationLevel = getSharableValue(Sharables.SATURATION, targetPlayer, worldName, profileTypeToUse, 5.0f); String storedLastLocation; - if (storedLocationObject == null) { - storedLastLocation = "N/A (No Location Data)"; - } else if (storedLocationObject.getWorld() == null) { - storedLastLocation = String.format("N/A (World Null: %.1f, %.1f, %.1f)", - storedLocationObject.getX(), - storedLocationObject.getY(), - storedLocationObject.getZ()); - } else { - storedLastLocation = String.format("%s (%.1f, %.1f, %.1f)", - storedLocationObject.getWorld().getName(), - storedLocationObject.getX(), - storedLocationObject.getY(), - storedLocationObject.getZ()); + + // Check if LAST_LOCATION is enabled in config + if (!inventoriesConfig.getActiveOptionalShares().contains(Sharables.LAST_LOCATION)) { + storedLastLocation = "Disabled in config"; + } else { // if the location is null or the world is null + Location storedLocationObject = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LAST_LOCATION).read().join(); + if (storedLocationObject == null || storedLocationObject.getWorld() == null) { + storedLastLocation = "N/A (No Location Data)"; + } else { // if the location is valid, print the coordinates and the world + storedLastLocation = String.format("%s (%.1f, %.1f, %.1f)", + storedLocationObject.getWorld().getName(), + storedLocationObject.getX(), + storedLocationObject.getY(), + storedLocationObject.getZ()); + } } return new PlayerInventoryData( @@ -264,6 +259,30 @@ private PlayerProfile loadMVInvPlayerProfile(ProfileContainer container, Offline return null; } + /** + * Helper method to safely read a sharable value, returning a default if disabled or not found. + * @param sharable The Sharable to read. + * @param targetPlayer The OfflinePlayer. + * @param worldName The world name. + * @param profileType The profile type. + * @param defaultValue The default value to return if sharable is disabled or data is null. + * @param The type of the sharable value. + * @return The sharable value or the default value. + */ + private T getSharableValue(@NotNull Sharable sharable, + @NotNull OfflinePlayer targetPlayer, + @NotNull String worldName, + @NotNull ProfileType profileType, + @NotNull T defaultValue) { + try { + T value = SingleShareReader.of(inventories, targetPlayer, worldName, profileType, sharable).read().join(); + return value != null ? value : defaultValue; + } catch (CompletionException e) { + Logging.warning("Failed to read sharable '" + sharable.getNames()[0] + "' for player " + targetPlayer.getName() + " in world " + worldName + ": " + e.getCause().getMessage()); + return defaultValue; + } + } + /** * Asynchronously saves a player's inventory data to their Multiverse-Inventories profile. * If the player is online and in the target world, their live inventory is also updated. From b675cb78da0e6fba4e86612cf3c4537f15a5832a Mon Sep 17 00:00:00 2001 From: Ryan Breeding Date: Tue, 22 Jul 2025 13:38:05 -0400 Subject: [PATCH 36/39] Check if sharables are null --- .../profile/InventoryDataProvider.java | 39 +++++++++---------- .../inventories/view/InventoryGUIHelper.java | 26 +++++++++---- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java index e92f35f2..3d15f696 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java @@ -2,6 +2,7 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.attribute.Attribute; @@ -67,12 +68,12 @@ public static class PlayerInventoryData { public final ProfileType profileTypeUsed; // Non-inventory data - public final double health; - public final double maxHealth; - public final int level; - public final float exp; - public final int foodLevel; - public final float saturation; + public final Double health; + public final Double maxHealth; + public final Integer level; + public final Float exp; + public final Integer foodLevel; + public final Float saturation; public final String lastLocation; /** @@ -94,8 +95,8 @@ public static class PlayerInventoryData { */ @ApiStatus.AvailableSince("5.2") public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, - ProfileType profileTypeUsed, double health, double maxHealth, int level, float exp, int foodLevel, - float saturation, String lastLocation) { + ProfileType profileTypeUsed, Double health, Double maxHealth, Integer level, Float exp, Integer foodLevel, + Float saturation, String lastLocation) { this.contents = contents; this.armor = armor; this.offHand = offHand; @@ -198,17 +199,17 @@ private CompletableFuture loadInventoryDataFromProfileStora ItemStack offHand = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.OFF_HAND).read().join(); // Non-inventory data - double storedHealth = getSharableValue(Sharables.HEALTH, targetPlayer, worldName, profileTypeToUse, 20.0); - double storedMaxHealth = getSharableValue(Sharables.MAX_HEALTH, targetPlayer, worldName, profileTypeToUse, 20.0); - int storedLevel = getSharableValue(Sharables.LEVEL, targetPlayer, worldName, profileTypeToUse, 0); - float storedExp = getSharableValue(Sharables.EXPERIENCE, targetPlayer, worldName, profileTypeToUse, 0.0f); - int storedFoodLevel = getSharableValue(Sharables.FOOD_LEVEL, targetPlayer, worldName, profileTypeToUse, 20); - float storedSaturationLevel = getSharableValue(Sharables.SATURATION, targetPlayer, worldName, profileTypeToUse, 5.0f); + Double storedHealth = getSharableValue(Sharables.HEALTH, targetPlayer, worldName, profileTypeToUse); + Double storedMaxHealth = getSharableValue(Sharables.MAX_HEALTH, targetPlayer, worldName, profileTypeToUse); + Integer storedLevel = getSharableValue(Sharables.LEVEL, targetPlayer, worldName, profileTypeToUse); + Float storedExp = getSharableValue(Sharables.EXPERIENCE, targetPlayer, worldName, profileTypeToUse); + Integer storedFoodLevel = getSharableValue(Sharables.FOOD_LEVEL, targetPlayer, worldName, profileTypeToUse); + Float storedSaturationLevel = getSharableValue(Sharables.SATURATION, targetPlayer, worldName, profileTypeToUse); String storedLastLocation; // Check if LAST_LOCATION is enabled in config if (!inventoriesConfig.getActiveOptionalShares().contains(Sharables.LAST_LOCATION)) { - storedLastLocation = "Disabled in config"; + storedLastLocation = ChatColor.RED + "Disabled in Config"; } else { // if the location is null or the world is null Location storedLocationObject = SingleShareReader.of(inventories, targetPlayer, worldName, profileTypeToUse, Sharables.LAST_LOCATION).read().join(); if (storedLocationObject == null || storedLocationObject.getWorld() == null) { @@ -265,21 +266,19 @@ private PlayerProfile loadMVInvPlayerProfile(ProfileContainer container, Offline * @param targetPlayer The OfflinePlayer. * @param worldName The world name. * @param profileType The profile type. - * @param defaultValue The default value to return if sharable is disabled or data is null. * @param The type of the sharable value. * @return The sharable value or the default value. */ private T getSharableValue(@NotNull Sharable sharable, @NotNull OfflinePlayer targetPlayer, @NotNull String worldName, - @NotNull ProfileType profileType, - @NotNull T defaultValue) { + @NotNull ProfileType profileType) { try { T value = SingleShareReader.of(inventories, targetPlayer, worldName, profileType, sharable).read().join(); - return value != null ? value : defaultValue; + return value; } catch (CompletionException e) { Logging.warning("Failed to read sharable '" + sharable.getNames()[0] + "' for player " + targetPlayer.getName() + " in world " + worldName + ": " + e.getCause().getMessage()); - return defaultValue; + return null; } } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index 0cf0efeb..a49323a3 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -10,6 +10,7 @@ import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.inventories.MultiverseInventories; @@ -181,22 +182,33 @@ private ItemStack createDisplayItem(Material material, String name, List return item; } - private ItemStack createHealthDisplayItem(double health, double maxHealth) { + private ItemStack createHealthDisplayItem(@Nullable Double health, @Nullable Double maxHealth) { List lore = new ArrayList<>(); - DecimalFormat df = new DecimalFormat("0.0"); - lore.add(ChatColor.WHITE + "Current: " + ChatColor.RED + df.format(health) + ChatColor.WHITE + " / " + ChatColor.RED + df.format(maxHealth)); + if (health == null) { + lore.add(ChatColor.RED + "N/A (No Data)"); + } else { + DecimalFormat df = new DecimalFormat("0.0"); + lore.add(ChatColor.WHITE + "Current: " + ChatColor.RED + df.format(health) + ChatColor.WHITE + " / " + ChatColor.RED + df.format(maxHealth)); + } return createDisplayItem(Material.RED_DYE, "Health", lore); } - private ItemStack createLevelDisplayItem(int level, float exp) { + private ItemStack createLevelDisplayItem(@Nullable Integer level, @Nullable Float exp) { List lore = new ArrayList<>(); - lore.add(ChatColor.WHITE + "Level: " + ChatColor.GREEN + level); - lore.add(ChatColor.WHITE + "Progress: " + ChatColor.AQUA + String.format("%.1f%%", exp * 100)); + if (level == null) { + lore.add(ChatColor.RED + "N/A (No Data)"); + } else { + lore.add(ChatColor.WHITE + "Level: " + ChatColor.GREEN + level); + lore.add(ChatColor.WHITE + "Progress: " + ChatColor.AQUA + String.format("%.1f%%", exp * 100)); + } return createDisplayItem(Material.EXPERIENCE_BOTTLE, "Experience", lore); } - private ItemStack createFoodDisplayItem(int foodLevel, float saturation) { + private ItemStack createFoodDisplayItem(@Nullable Integer foodLevel, @Nullable Float saturation) { List lore = new ArrayList<>(); + if (foodLevel == null) { + lore.add(ChatColor.RED + "N/A (No Data)"); + } lore.add(ChatColor.WHITE + "Food: " + ChatColor.GOLD + foodLevel + ChatColor.WHITE + " / " + ChatColor.GOLD + "20"); lore.add(ChatColor.WHITE + "Saturation: " + ChatColor.LIGHT_PURPLE + String.format("%.1f", saturation)); return createDisplayItem(Material.COOKED_BEEF, "Food & Saturation", lore); From 8c92f8ce843ca44d921db440ad54161ff804fe7e Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:12:10 +0800 Subject: [PATCH 37/39] Reduce code nesting within InventoryViewListener --- .../listeners/InventoryViewListener.java | 273 +++++++++--------- 1 file changed, 140 insertions(+), 133 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index 28b32108..c57453be 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -56,77 +56,81 @@ void onInventoryClick(InventoryClickEvent event) { } // If it's a modifiable inventory, apply specific restrictions for armor/off-hand slots. - if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder) { - int clickedSlot = event.getRawSlot(); - ItemStack cursorItem = event.getCursor(); // Item held by the cursor - ItemStack currentItem = event.getCurrentItem(); // Item in the clicked slot - Player player = (Player) event.getWhoClicked(); // The player who clicked + if (!(event.getInventory().getHolder() instanceof ModifiableInventoryHolder)) { + return; + } - // Define the special slots - boolean isSpecialSlot = (clickedSlot >= 36 && clickedSlot <= 40); // Armor (36-39) and Off-hand (40) + int clickedSlot = event.getRawSlot(); + ItemStack cursorItem = event.getCursor(); // Item held by the cursor + ItemStack currentItem = event.getCurrentItem(); // Item in the clicked slot + Player player = (Player) event.getWhoClicked(); // The player who clicked - // Determine if the slot is one of the padding slots (41-44) - boolean isPaddingSlot = (clickedSlot >= 41 && clickedSlot <= 44); + // Define the special slots + boolean isSpecialSlot = (clickedSlot >= 36 && clickedSlot <= 40); // Armor (36-39) and Off-hand (40) - if (isPaddingSlot) { - // Clicks on padding slots are always cancelled and do nothing else. - event.setCancelled(true); - return; - } + // Determine if the slot is one of the padding slots (41-44) + boolean isPaddingSlot = (clickedSlot >= 41 && clickedSlot <= 44); - // --- Logic for special slots (armor/off-hand) --- - if (isSpecialSlot) { - boolean currentItemIsFiller = currentItem != null && inventoryGUIHelper.isFillerItem(currentItem); + if (isPaddingSlot) { + // Clicks on padding slots are always cancelled and do nothing else. + event.setCancelled(true); + return; + } - // Scenario 1: Player tries to pick up a filler item (cursor is empty) - if (currentItemIsFiller && (cursorItem == null || cursorItem.getType() == Material.AIR)) { - event.setCancelled(true); - return; // Prevent pickup, filler stays in place - } + // --- Logic for special slots (armor/off-hand) --- + if (!isSpecialSlot) { + return; + } - // Scenario 2: Player tries to place an invalid item into a special slot - if (cursorItem != null && cursorItem.getType() != Material.AIR && !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { - event.setCancelled(true); - return; // Prevent invalid placement - } + boolean currentItemIsFiller = currentItem != null && inventoryGUIHelper.isFillerItem(currentItem); - // Scenario 3: Player places a valid item into a special slot that currently holds a filler or a valid item. - // Manually handle the swap to ensure fillers don't leave the GUI. - if (cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { - event.setCancelled(true); // Take full control of the event - - // Place the new item from the cursor into the clicked slot - event.getInventory().setItem(clickedSlot, cursorItem); - - // If the player tries to replace an item in the filler slot, check if the item is valid. - if (currentItem != null && currentItem.getType() != Material.AIR && !inventoryGUIHelper.isFillerItem(currentItem)) { - // If the original item was a real, non-filler item, put it on the player's cursor. - player.setItemOnCursor(currentItem); - } else { - // If the original item was null, air, or a filler, clear the player's cursor. - player.setItemOnCursor(null); - } - - player.updateInventory(); // Update client to reflect changes - return; // Event handled - } + // Scenario 1: Player tries to pick up a filler item (cursor is empty) + if (currentItemIsFiller && (cursorItem == null || cursorItem.getType() == Material.AIR)) { + event.setCancelled(true); + return; // Prevent pickup, filler stays in place + } - // Scenario 4: Player is shift-clicking a valid item from a special slot. - // Or picking up a valid item from a special slot. - // We need to ensure filler reappears if the slot becomes empty. - if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY || - event.getAction() == InventoryAction.PICKUP_ALL || - event.getAction() == InventoryAction.PICKUP_HALF || - event.getAction() == InventoryAction.PICKUP_ONE || - event.getAction() == InventoryAction.PICKUP_SOME) { - Bukkit.getScheduler().runTaskLater(inventories, () -> { - // After Bukkit processes the click, if the slot is now empty, put the filler back. - if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { - event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot, true)); - } - }, 1L); - } + // Scenario 2: Player tries to place an invalid item into a special slot + if (cursorItem != null && cursorItem.getType() != Material.AIR && !inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { + event.setCancelled(true); + return; // Prevent invalid placement + } + + // Scenario 3: Player places a valid item into a special slot that currently holds a filler or a valid item. + // Manually handle the swap to ensure fillers don't leave the GUI. + if (cursorItem != null && cursorItem.getType() != Material.AIR && inventoryGUIHelper.isValidItemForSlot(cursorItem, clickedSlot)) { + event.setCancelled(true); // Take full control of the event + + // Place the new item from the cursor into the clicked slot + event.getInventory().setItem(clickedSlot, cursorItem); + + // If the player tries to replace an item in the filler slot, check if the item is valid. + if (currentItem != null && currentItem.getType() != Material.AIR && !inventoryGUIHelper.isFillerItem(currentItem)) { + // If the original item was a real, non-filler item, put it on the player's cursor. + player.setItemOnCursor(currentItem); + } else { + // If the original item was null, air, or a filler, clear the player's cursor. + player.setItemOnCursor(null); } + + player.updateInventory(); // Update client to reflect changes + return; // Event handled + } + + // Scenario 4: Player is shift-clicking a valid item from a special slot. + // Or picking up a valid item from a special slot. + // We need to ensure filler reappears if the slot becomes empty. + if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY || + event.getAction() == InventoryAction.PICKUP_ALL || + event.getAction() == InventoryAction.PICKUP_HALF || + event.getAction() == InventoryAction.PICKUP_ONE || + event.getAction() == InventoryAction.PICKUP_SOME) { + Bukkit.getScheduler().runTaskLater(inventories, () -> { + // After Bukkit processes the click, if the slot is now empty, put the filler back. + if (event.getInventory().getItem(clickedSlot) == null || event.getInventory().getItem(clickedSlot).getType() == Material.AIR) { + event.getInventory().setItem(clickedSlot, inventoryGUIHelper.createFillerItemForSlot(clickedSlot, true)); + } + }, 1L); } } @@ -140,44 +144,45 @@ void onInventoryDrag(InventoryDragEvent event) { } // If it's a modifiable inventory, apply specific restrictions for armor/off-hand slots. - if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder) { - ItemStack draggedItem = event.getCursor(); // The item being dragged (after the drag operation) + if (!(event.getInventory().getHolder() instanceof ModifiableInventoryHolder)) { + return; + } - // If nothing is being dragged, or dragging air, no restriction needed. - if (draggedItem == null || draggedItem.getType() == Material.AIR) { - return; - } - for (int slot : event.getRawSlots()) { - // Define the special slots - boolean isSpecialSlot = (slot >= 36 && slot <= 40); + ItemStack draggedItem = event.getCursor(); // The item being dragged (after the drag operation) - // Determine if the slot is one of the padding slots (41-44) - boolean isPaddingSlot = (slot >= 41 && slot <= 44); + // If nothing is being dragged, or dragging air, no restriction needed. + if (draggedItem == null || draggedItem.getType() == Material.AIR) { + return; + } - if (isPaddingSlot) { - // Clicks on padding slots are always cancelled and do nothing else. - event.setCancelled(true); - return; - } + for (int slot : event.getRawSlots()) { + // Define the special slots + boolean isSpecialSlot = (slot >= 36 && slot <= 40); - if (isSpecialSlot) { - // Check if the dragged item is valid for this special slot - if (!inventoryGUIHelper.isValidItemForSlot(draggedItem, slot)) { // Use helper - event.setCancelled(true); - return; // Cancel the entire drag event - } - } + // Determine if the slot is one of the padding slots (41-44) + boolean isPaddingSlot = (slot >= 41 && slot <= 44); + + if (isPaddingSlot) { + // Clicks on padding slots are always cancelled and do nothing else. + event.setCancelled(true); + return; } - // After a drag, check if any special slots became empty and replace with filler - Bukkit.getScheduler().runTaskLater(inventories, () -> { - for (int slot : event.getRawSlots()) { - if ((slot >= 36 && slot <= 40) && (event.getInventory().getItem(slot) == null || event.getInventory().getItem(slot).getType() == Material.AIR)) { - event.getInventory().setItem(slot, inventoryGUIHelper.createFillerItemForSlot(slot, true)); // Use helper - } - } - }, 1L); // Run one tick later + // Check if the dragged item is valid for this special slot + if (isSpecialSlot && !inventoryGUIHelper.isValidItemForSlot(draggedItem, slot)) { + event.setCancelled(true); + return; // Cancel the entire drag event + } } + + // After a drag, check if any special slots became empty and replace with filler + Bukkit.getScheduler().runTaskLater(inventories, () -> { + for (int slot : event.getRawSlots()) { + if ((slot >= 36 && slot <= 40) && (event.getInventory().getItem(slot) == null || event.getInventory().getItem(slot).getType() == Material.AIR)) { + event.getInventory().setItem(slot, inventoryGUIHelper.createFillerItemForSlot(slot, true)); // Use helper + } + } + }, 1L); // Run one tick later } // Event handler for InventoryCloseEvent to save changes @@ -185,46 +190,48 @@ void onInventoryDrag(InventoryDragEvent event) { @DefaultEventPriority(EventPriority.NORMAL) void onInventoryClose(InventoryCloseEvent event) { // Check if the closed inventory has the custom ModifiableInventoryHolder class - if (event.getInventory().getHolder() instanceof ModifiableInventoryHolder holder) { - final OfflinePlayer targetPlayer = holder.getTargetPlayer(); - final String worldName = holder.getWorldName(); - final ProfileType profileType = holder.getProfileType(); - final Inventory closedInventory = event.getInventory(); - - // Extract contents: 0-35 for inventory, 36-39 for armor, 40 for off-hand - ItemStack[] newContents = Arrays.copyOfRange(closedInventory.getContents(), 0, 36); - ItemStack[] newArmor = new ItemStack[4]; - // Map GUI armor slots back to Multiverse-Inventories' expected order [helmet, chestplate, leggings, boots] - newArmor[3] = closedInventory.getItem(36); // Helmet - newArmor[2] = closedInventory.getItem(37); // Chestplate - newArmor[1] = closedInventory.getItem(38); // Leggings - newArmor[0] = closedInventory.getItem(39); // Boots - ItemStack newOffHand = closedInventory.getItem(40); - - // Before saving, ensure any filler items are removed from the actual data - for (int i = 0; i < newArmor.length; i++) { - if (newArmor[i] != null && inventoryGUIHelper.isFillerItem(newArmor[i])) { - newArmor[i] = null; // Replace filler with null for saving - } - } - if (newOffHand != null && inventoryGUIHelper.isFillerItem(newOffHand)) { - newOffHand = null; // Replace filler with null for saving - } + if (!(event.getInventory().getHolder() instanceof ModifiableInventoryHolder holder)) { + return; + } - // Delegate saving to InventoryDataProvider - inventoryDataProvider.savePlayerInventoryData( - targetPlayer, - worldName, - profileType, - newContents, - newArmor, - newOffHand - ).exceptionally(throwable -> { - // Error logging is now handled within InventoryDataProvider, but we can add a general one here too - Logging.severe("Error during inventory save process for " + targetPlayer.getName() + ": " + throwable.getMessage()); - throwable.printStackTrace(); - return null; - }); + final OfflinePlayer targetPlayer = holder.getTargetPlayer(); + final String worldName = holder.getWorldName(); + final ProfileType profileType = holder.getProfileType(); + final Inventory closedInventory = event.getInventory(); + + // Extract contents: 0-35 for inventory, 36-39 for armor, 40 for off-hand + ItemStack[] newContents = Arrays.copyOfRange(closedInventory.getContents(), 0, 36); + ItemStack[] newArmor = new ItemStack[4]; + // Map GUI armor slots back to Multiverse-Inventories' expected order [helmet, chestplate, leggings, boots] + newArmor[3] = closedInventory.getItem(36); // Helmet + newArmor[2] = closedInventory.getItem(37); // Chestplate + newArmor[1] = closedInventory.getItem(38); // Leggings + newArmor[0] = closedInventory.getItem(39); // Boots + ItemStack newOffHand = closedInventory.getItem(40); + + // Before saving, ensure any filler items are removed from the actual data + for (int i = 0; i < newArmor.length; i++) { + if (newArmor[i] != null && inventoryGUIHelper.isFillerItem(newArmor[i])) { + newArmor[i] = null; // Replace filler with null for saving + } + } + if (newOffHand != null && inventoryGUIHelper.isFillerItem(newOffHand)) { + newOffHand = null; // Replace filler with null for saving } + + // Delegate saving to InventoryDataProvider + inventoryDataProvider.savePlayerInventoryData( + targetPlayer, + worldName, + profileType, + newContents, + newArmor, + newOffHand + ).exceptionally(throwable -> { + // Error logging is now handled within InventoryDataProvider, but we can add a general one here too + Logging.severe("Error during inventory save process for " + targetPlayer.getName() + ": " + throwable.getMessage()); + throwable.printStackTrace(); + return null; + }); } -} \ No newline at end of file +} From 9c9cb3ac4bd3d838074f54823517e3659efb63b0 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:21:10 +0800 Subject: [PATCH 38/39] Refactor InventoryDataProvider and make all the related apis experimental --- .../commands/InventoryModifyCommand.java | 5 +- .../commands/InventoryViewCommand.java | 5 +- .../listeners/InventoryViewListener.java | 2 +- .../InventoryDataProvider.java | 63 +----------------- .../inventories/view/InventoryGUIHelper.java | 4 +- .../{profile => view}/InventoryStatus.java | 19 +++++- .../inventories/view/PlayerInventoryData.java | 65 +++++++++++++++++++ 7 files changed, 94 insertions(+), 69 deletions(-) rename src/main/java/org/mvplugins/multiverse/inventories/{profile => view}/InventoryDataProvider.java (86%) rename src/main/java/org/mvplugins/multiverse/inventories/{profile => view}/InventoryStatus.java (78%) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/view/PlayerInventoryData.java diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index dafaf5e8..5dc90c0a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -18,9 +18,10 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; +import org.mvplugins.multiverse.inventories.view.InventoryDataProvider; import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; +import org.mvplugins.multiverse.inventories.view.PlayerInventoryData; @Service final class InventoryModifyCommand extends InventoriesCommand { @@ -126,7 +127,7 @@ private void createAndOpenGUI( @NotNull Player player, @NotNull OfflinePlayer targetPlayer, @NotNull String worldName, - @NotNull InventoryDataProvider.PlayerInventoryData playerInventoryData + @NotNull PlayerInventoryData playerInventoryData ) { String title = "Modify " + targetPlayer.getName() + " @ " + worldName; Inventory inv = Bukkit.createInventory( diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 20ac8769..84dd0f1c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -19,8 +19,9 @@ import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; +import org.mvplugins.multiverse.inventories.view.PlayerInventoryData; import org.mvplugins.multiverse.inventories.view.ReadOnlyInventoryHolder; -import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; +import org.mvplugins.multiverse.inventories.view.InventoryDataProvider; @Service final class InventoryViewCommand extends InventoriesCommand { @@ -111,7 +112,7 @@ private void createAndOpenGUI( @NotNull Player player, @NotNull OfflinePlayer targetPlayer, @NotNull String worldName, - @NotNull InventoryDataProvider.PlayerInventoryData playerInventoryData + @NotNull PlayerInventoryData playerInventoryData ) { String title = targetPlayer.getName() + " @ " + worldName; Inventory inv = Bukkit.createInventory(new ReadOnlyInventoryHolder(), diff --git a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java index c57453be..123ca412 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/listeners/InventoryViewListener.java @@ -18,7 +18,7 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; +import org.mvplugins.multiverse.inventories.view.InventoryDataProvider; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import org.mvplugins.multiverse.inventories.view.InventoryGUIHelper; import org.mvplugins.multiverse.inventories.view.ModifiableInventoryHolder; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryDataProvider.java similarity index 86% rename from src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java rename to src/main/java/org/mvplugins/multiverse/inventories/view/InventoryDataProvider.java index 3d15f696..d47c1ce9 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryDataProvider.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryDataProvider.java @@ -1,4 +1,4 @@ -package org.mvplugins.multiverse.inventories.profile; +package org.mvplugins.multiverse.inventories.view; import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; @@ -35,6 +35,7 @@ * * @since 5.2 */ +@ApiStatus.Experimental @ApiStatus.AvailableSince("5.2") @Service public final class InventoryDataProvider { @@ -54,66 +55,6 @@ public final class InventoryDataProvider { this.inventoriesConfig = inventoriesConfig; } - /** - * Represents the loaded inventory data. - * - * @since 5.2 - */ - @ApiStatus.AvailableSince("5.2") - public static class PlayerInventoryData { - public final ItemStack[] contents; - public final ItemStack[] armor; - public final ItemStack offHand; - public final InventoryStatus status; - public final ProfileType profileTypeUsed; - - // Non-inventory data - public final Double health; - public final Double maxHealth; - public final Integer level; - public final Float exp; - public final Integer foodLevel; - public final Float saturation; - public final String lastLocation; - - /** - * - * @param contents The player's main inventory contents (slots 0-35). - * @param armor The player's armor contents (boots, leggings, chestplate, helmet). - * @param offHand The player's off-hand item. - * @param status The status of the inventory data load (e.g., LIVE, STORED, NO_DATA_FOUND). - * @param profileTypeUsed The profile type that was used to retrieve the data (e.g., SURVIVAL). - * @param health The player's current health. - * @param maxHealth The player's maximum health. - * @param level The player's current experience level. - * @param exp The player's current experience progress towards the next level (0.0-1.0). - * @param foodLevel The player's current food level (0-20). - * @param saturation The player's current saturation level. - * @param lastLocation The player's last location. - * - * @since 5.2 - */ - @ApiStatus.AvailableSince("5.2") - public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, - ProfileType profileTypeUsed, Double health, Double maxHealth, Integer level, Float exp, Integer foodLevel, - Float saturation, String lastLocation) { - this.contents = contents; - this.armor = armor; - this.offHand = offHand; - this.status = status; - this.profileTypeUsed = profileTypeUsed; - - // Non-inventory data - this.health = health; - this.maxHealth = maxHealth; - this.level = level; - this.exp = exp; - this.foodLevel = foodLevel; - this.saturation = saturation; - this.lastLocation = lastLocation; - } - } - /** * Asynchronously loads a player's inventory data. * If the player is online AND in the specified world, it attempts to get their live inventory. diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java index a49323a3..756c5a2c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryGUIHelper.java @@ -14,7 +14,6 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.inventories.MultiverseInventories; -import org.mvplugins.multiverse.inventories.profile.InventoryDataProvider; import java.util.List; import java.text.DecimalFormat; @@ -27,6 +26,7 @@ * * @since 5.2 */ +@ApiStatus.Experimental @ApiStatus.AvailableSince("5.2") @Service public final class InventoryGUIHelper { @@ -246,7 +246,7 @@ private ItemStack getOrFillItem(ItemStack item, int slot, boolean isModifiable) */ @ApiStatus.AvailableSince("5.2") public void populateInventoryGUI(@NotNull Inventory inv, - @NotNull InventoryDataProvider.PlayerInventoryData playerInventoryData, + @NotNull PlayerInventoryData playerInventoryData, boolean isModifiable) { // Fill main inventory slots (0–35) if (playerInventoryData.contents != null) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryStatus.java similarity index 78% rename from src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java rename to src/main/java/org/mvplugins/multiverse/inventories/view/InventoryStatus.java index d5cca0e1..69bcef36 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/InventoryStatus.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/InventoryStatus.java @@ -1,26 +1,40 @@ -package org.mvplugins.multiverse.inventories.profile; +package org.mvplugins.multiverse.inventories.view; import org.bukkit.ChatColor; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** * Represents the status of an inventory data load operation. * Provides a fixed set of states for clarity and type safety. + * + * @since 5.2 */ +@ApiStatus.Experimental +@ApiStatus.AvailableSince("5.2") public enum InventoryStatus { /** * Indicates that live inventory data from an online player was displayed. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") LIVE_INVENTORY(ChatColor.GREEN + "Displaying LIVE inventory"), /** * Indicates that stored inventory data from Multiverse-Inventories profiles was displayed. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") STORED_INVENTORY(ChatColor.GREEN + "Displaying STORED inventory"), /** * Indicates that no player data was found for the specified world/player. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") NO_DATA_FOUND(ChatColor.RED + "No player data found"); private final String message; @@ -35,7 +49,10 @@ public enum InventoryStatus { * @param playerName The name of the target player. * @param worldName The name of the target world. * @return The formatted status message. + * + * @since 5.2 */ + @ApiStatus.AvailableSince("5.2") public @NotNull String getFormattedMessage(@NotNull String playerName, @NotNull String worldName) { if (this == NO_DATA_FOUND) { return this.message + " for " + playerName + " in world" + worldName + ". Try checking a different world or ensure the player has played in this world."; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/view/PlayerInventoryData.java b/src/main/java/org/mvplugins/multiverse/inventories/view/PlayerInventoryData.java new file mode 100644 index 00000000..f49c3832 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/view/PlayerInventoryData.java @@ -0,0 +1,65 @@ +package org.mvplugins.multiverse.inventories.view; + +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; + +/** + * Represents the loaded inventory data. + * + * @since 5.2 + */ +@ApiStatus.Experimental +@ApiStatus.AvailableSince("5.2") +public final class PlayerInventoryData { + public final ItemStack[] contents; + public final ItemStack[] armor; + public final ItemStack offHand; + public final InventoryStatus status; + public final ProfileType profileTypeUsed; + + // Non-inventory data + public final Double health; + public final Double maxHealth; + public final Integer level; + public final Float exp; + public final Integer foodLevel; + public final Float saturation; + public final String lastLocation; + + /** + * + * @param contents The player's main inventory contents (slots 0-35). + * @param armor The player's armor contents (boots, leggings, chestplate, helmet). + * @param offHand The player's off-hand item. + * @param status The status of the inventory data load (e.g., LIVE, STORED, NO_DATA_FOUND). + * @param profileTypeUsed The profile type that was used to retrieve the data (e.g., SURVIVAL). + * @param health The player's current health. + * @param maxHealth The player's maximum health. + * @param level The player's current experience level. + * @param exp The player's current experience progress towards the next level (0.0-1.0). + * @param foodLevel The player's current food level (0-20). + * @param saturation The player's current saturation level. + * @param lastLocation The player's last location. + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public PlayerInventoryData(ItemStack[] contents, ItemStack[] armor, ItemStack offHand, InventoryStatus status, + ProfileType profileTypeUsed, Double health, Double maxHealth, Integer level, Float exp, Integer foodLevel, + Float saturation, String lastLocation) { + this.contents = contents; + this.armor = armor; + this.offHand = offHand; + this.status = status; + this.profileTypeUsed = profileTypeUsed; + + // Non-inventory data + this.health = health; + this.maxHealth = maxHealth; + this.level = level; + this.exp = exp; + this.foodLevel = foodLevel; + this.saturation = saturation; + this.lastLocation = lastLocation; + } +} From fd117a334b6ba344a70ef8fd62b720572c348fed Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:30:19 +0800 Subject: [PATCH 39/39] Cleanup some formatting --- .../multiverse/inventories/commands/InventoryModifyCommand.java | 2 -- .../multiverse/inventories/commands/InventoryViewCommand.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java index 5dc90c0a..33003fa3 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryModifyCommand.java @@ -62,11 +62,9 @@ void onInventoryModifyCommand( @Description("The world the player's inventory is in") MultiverseWorld world ) { - String worldName = world.getName(); issuer.sendInfo(ChatColor.YELLOW + "Loading inventory data for " + targetPlayer.getName() + "..."); handleInventoryLoadAndDisplay(issuer, player, targetPlayer, worldName); - } /** diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java index 84dd0f1c..27cc2a6e 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/InventoryViewCommand.java @@ -95,7 +95,7 @@ private void handleInventoryLoadAndDisplay( throwable.printStackTrace(); return null; // Must return null for CompletableFuture in exceptionally }); -} + } /** * Creates and opens the custom inventory GUI for viewing.