Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
24b408c
Add Chinese translation file to the project
xiaohuang2004 Jul 9, 2025
55cd262
Merge pull request #609 from xiaohuang2004/main
benwoo1110 Jul 18, 2025
afb9f84
Implement view inventory command for online players only
Woodstop Jul 19, 2025
ecdbc1a
Add logic for offline players and command completion. Enforce read-on…
Woodstop Jul 19, 2025
e844df9
Remove old code
Woodstop Jul 19, 2025
fa50a1f
Added modify inventory functionality
Woodstop Jul 19, 2025
55ca31c
Show live inventory update if the player is online
Woodstop Jul 19, 2025
f16d4c2
Implement InventoryDataProvider for business logic
Woodstop Jul 20, 2025
06f469d
Create new package for ModifiableInventoryHolder and ReadOnlyInventor…
Woodstop Jul 20, 2025
0a95cf7
Minor update, remove unused code
Woodstop Jul 20, 2025
9e78737
Implement helper class to insert filler armor/off-hand items in the i…
Woodstop Jul 20, 2025
f24dd31
Fix bugs associated with the item filler. Remove ability for players …
Woodstop Jul 20, 2025
10a69ff
Simplify scenario 3 and remove import
Woodstop Jul 20, 2025
9ab5c10
Make custom inventory have fewer slots. Empty slots replaced with bar…
Woodstop Jul 20, 2025
a9ae11e
Change public void to void
Woodstop Jul 20, 2025
4d49bcf
Add api version to javadocs and fix minor formating
benwoo1110 Jul 21, 2025
131cd51
Make constructor of services non-public
benwoo1110 Jul 21, 2025
e3d7e54
Refactor InventoryDataProvider and InventoryGUIHelper to improve read…
benwoo1110 Jul 21, 2025
a09f33e
Reduce need for validation checks in view and modify commands
benwoo1110 Jul 21, 2025
6f8e81e
Remove adventure text API
Woodstop Jul 21, 2025
83bb05e
Add "No [Item] Here" for the read-only case
Woodstop Jul 21, 2025
d5be772
Remove inventories field
Woodstop Jul 21, 2025
b03482e
Remove old references to the inventories field
Woodstop Jul 21, 2025
b69e2b4
Change status message to enum type
Woodstop Jul 21, 2025
893ac2f
Move duplicate logic in the view and modify command to populateInvent…
Woodstop Jul 21, 2025
1a05c0e
Change inventories.getLogger() to Logging
Woodstop Jul 21, 2025
a856f34
Make Helper more concise, fix bug where fillers don't always show in …
Woodstop Jul 21, 2025
f0d2daa
Split into smaller private methods
Woodstop Jul 21, 2025
e5b730b
Players can modify their own stored inventory, but not their live inv…
Woodstop Jul 21, 2025
05a0a91
Fix bug where replacing an item in a filler slot makes the item disap…
Woodstop Jul 21, 2025
77928ce
Implement slots for health, hunger, exp
Woodstop Jul 21, 2025
243247c
Add description to javadocs
Woodstop Jul 21, 2025
2d2455d
Begin to add player last location as a slot
Woodstop Jul 21, 2025
7da6de3
Add maxHealth to the health slot
Woodstop Jul 21, 2025
eedd723
Remove unnecessary comments and fix indentation
Woodstop Jul 22, 2025
a23903d
Add private helper to get sharable value. Add check for config option…
Woodstop Jul 22, 2025
b675cb7
Check if sharables are null
Woodstop Jul 22, 2025
8c92f8c
Reduce code nesting within InventoryViewListener
benwoo1110 Jul 28, 2025
9c9cb3a
Refactor InventoryDataProvider and make all the related apis experime…
benwoo1110 Jul 28, 2025
fd117a3
Cleanup some formatting
benwoo1110 Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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("<player> <world>")
@Description("Modify a player's inventory in a specific world.")
void onInventoryModifyCommand(
@NotNull MVCommandIssuer issuer,
@Syntax("<player>")
@Description("Online or offline player")
OfflinePlayer target,

@Syntax("<world>")
@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.");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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 InventoryViewCommand extends InventoriesCommand {

private final ProfileContainerStoreProvider profileContainerStoreProvider;
private final MultiverseInventories inventories;

@Inject
InventoryViewCommand(
@NotNull ProfileContainerStoreProvider profileContainerStoreProvider,
@NotNull MultiverseInventories inventories
) {
this.profileContainerStoreProvider = profileContainerStoreProvider;
this.inventories = inventories;
}

@Subcommand("view")
@CommandPermission("multiverse.inventories.view")
@CommandCompletion("@mvinvplayernames @mvworlds")
@Syntax("<player> <world>")
@Description("View a player's inventory in a specific world.")
void onInventoryViewCommand(
@NotNull MVCommandIssuer issuer,

@Syntax("<player>")
@Description("Online or offline player")
//Player targetPlayer,
OfflinePlayer targetPlayer,

@Syntax("<world>")
@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.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();
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;
}
}
}
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);
}
}
Loading