diff --git a/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java b/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java index cad3582e..15c6e231 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java @@ -8,6 +8,7 @@ import org.mvplugins.multiverse.core.module.MultiverseModule; import org.mvplugins.multiverse.core.utils.StringFormatter; import org.mvplugins.multiverse.inventories.command.MVInvCommandConditions; +import org.mvplugins.multiverse.inventories.command.MVInvCommandPermissions; import org.mvplugins.multiverse.inventories.commands.InventoriesCommand; import org.mvplugins.multiverse.inventories.command.MVInvCommandCompletion; import org.mvplugins.multiverse.inventories.command.MVInvCommandContexts; @@ -73,6 +74,8 @@ public class MultiverseInventories extends MultiverseModule { private Provider mvInvCommandContexts; @Inject private Provider mvInvCommandConditions; + @Inject + private Provider mvInvCommandPermissions; private InventoriesDupingPatch dupingPatch; private boolean usingSpawnChangeEvent = false; @@ -167,6 +170,7 @@ private void registerCommands() { mvInvCommandCompletion.get(); mvInvCommandContexts.get(); mvInvCommandConditions.get(); + mvInvCommandPermissions.get(); }).onFailure(e -> { Logging.warning("Failed to register command completers: %s", e.getMessage()); }); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java index 5ea0aadf..7f18bd1f 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java @@ -15,6 +15,8 @@ import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +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.Arrays; @@ -53,6 +55,7 @@ private MVInvCommandCompletion( commandCompletions.registerStaticCompletion("mvinvconfigs", inventoriesConfig.getStringPropertyHandle().getAllPropertyNames()); commandCompletions.registerAsyncCompletion("mvinvconfigvalues", this::suggestConfigValues); commandCompletions.registerAsyncCompletion("mvinvplayernames", this::suggestPlayerNames); + commandCompletions.registerAsyncCompletion("mvinvprofiletypes", this::suggestProfileTypes); commandCompletions.registerAsyncCompletion("sharables", this::suggestSharables); commandCompletions.registerAsyncCompletion("shares", this::suggestShares); commandCompletions.registerAsyncCompletion("worldGroups", this::suggestWorldGroups); @@ -89,6 +92,29 @@ private List getPlayerNames() { .collect(Collectors.toList()); } + private Collection suggestProfileTypes(BukkitCommandCompletionContext context) { + if (!context.hasConfig("multiple")) { + return ProfileTypes.getTypes().stream() + .map(ProfileType::getName) + .map(String::toLowerCase) + .toList(); + } + + if (Objects.equals(context.getInput(), "@all")) { + return Collections.emptyList(); + } + List profileTypes = ProfileTypes.getTypes() + .stream() + .map(ProfileType::getName) + .map(String::toLowerCase) + .collect(Collectors.toList()); + if (context.getInput().indexOf(',') == -1) { + profileTypes.add("@all"); + return profileTypes; + } + return StringFormatter.addonToCommaSeperated(context.getInput(), profileTypes); + } + private Collection suggestSharables(BukkitCommandCompletionContext context) { String scope = context.getConfig("scope", "enabled"); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java index 763d9f25..dbe0a7ef 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java @@ -1,7 +1,6 @@ package org.mvplugins.multiverse.inventories.command; import com.google.common.base.Strings; -import org.bukkit.Bukkit; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.utils.REPatterns; @@ -11,47 +10,102 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.external.vavr.control.Option; +import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.profile.PlayerNamesMapper; +import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; +import org.mvplugins.multiverse.inventories.profile.key.ContainerKey; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +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 org.mvplugins.multiverse.inventories.share.Shares; import org.mvplugins.multiverse.inventories.util.MVInvi18n; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Objects; +import java.util.Collections; +import java.util.List; @Service public final class MVInvCommandContexts { private final WorldGroupManager worldGroupManager; private final PlayerNamesMapper playerNamesMapper; + private final InventoriesConfig config; + private final ProfileDataSource profileDataSource; @Inject private MVInvCommandContexts( @NotNull MVCommandManager commandManager, @NotNull WorldGroupManager worldGroupManager, - @NotNull PlayerNamesMapper playerNamesMapper + @NotNull PlayerNamesMapper playerNamesMapper, + @NotNull InventoriesConfig config, + @NotNull ProfileDataSource profileDataSource ) { this.worldGroupManager = worldGroupManager; this.playerNamesMapper = playerNamesMapper; + this.config = config; + this.profileDataSource = profileDataSource; CommandContexts commandContexts = commandManager.getCommandContexts(); - commandContexts.registerContext(GlobalProfileKey[].class, this::parseGlobalProfileKeys); + commandContexts.registerContext(ContainerKey[].class, this::parseContainerKeyArray); + commandContexts.registerContext(GlobalProfileKey[].class, this::parseGlobalProfileKeyArray); + commandContexts.registerIssuerAwareContext(ProfileType.class, this::parseProfileType); + commandContexts.registerIssuerAwareContext(ProfileType[].class, this::parseProfileTypeArray); commandContexts.registerContext(Sharable.class, this::parseSharable); commandContexts.registerContext(Shares.class, this::parseShares); commandContexts.registerContext(WorldGroup.class, this::parseWorldGroup); } - private GlobalProfileKey[] parseGlobalProfileKeys(BukkitCommandExecutionContext context) { - String profileStrings = context.popFirstArg(); - if (profileStrings.equals("@all")) { - return playerNamesMapper.getKeys().toArray(GlobalProfileKey[]::new); + private ProfileType parseProfileType(BukkitCommandExecutionContext context) { + if (!config.getEnableGamemodeShareHandling()) { + return ProfileTypes.getDefault(); } + String profileType = context.popFirstArg(); + return ProfileTypes.forName(profileType) + .getOrElseThrow(() -> new InvalidCommandArgument("Invalid profile type: " + profileType)); + } + + private ContainerKey[] parseContainerKeyArray(BukkitCommandExecutionContext context) { + String keyStrings = context.popFirstArg(); + if (keyStrings.equals("@all")) { + return Arrays.stream(ContainerType.values()).flatMap(containerType -> + profileDataSource.listContainerDataNames(containerType) + .stream() + .map(containerName -> ContainerKey.create(containerType, containerName))) + .toArray(ContainerKey[]::new); + } + List containerKeys = new ArrayList<>(); + String[] typesSplit = REPatterns.SEMICOLON.split(keyStrings); + for (String typeSplit : typesSplit) { + String[] keyValueSplit = REPatterns.EQUALS.split(typeSplit, 2); + if (keyValueSplit.length != 2) { + // todo: Probably error invalid format + continue; + } + ContainerType containerType = ContainerType.valueOf(keyValueSplit[0].toUpperCase()); + String[] dataNameSplit = REPatterns.COMMA.split(keyValueSplit[1]); + List availableDataNames = profileDataSource.listContainerDataNames(containerType); + for (String dataName : dataNameSplit) { + if (availableDataNames.contains(dataName)) { + containerKeys.add(ContainerKey.create(containerType, dataName)); + } + } + } + return containerKeys.toArray(new ContainerKey[0]); + } - String[] profileNames = REPatterns.COMMA.split(profileStrings); + private GlobalProfileKey[] parseGlobalProfileKeyArray(BukkitCommandExecutionContext context) { + String keyStrings = context.popFirstArg(); + if (keyStrings.equals("@all")) { + return playerNamesMapper.getKeys().toArray(GlobalProfileKey[]::new); + } + // todo: UUID parsing + String[] profileNames = REPatterns.COMMA.split(keyStrings); return Arrays.stream(profileNames) .map(playerNamesMapper::getKey) .filter(Option::isDefined) @@ -59,6 +113,19 @@ private GlobalProfileKey[] parseGlobalProfileKeys(BukkitCommandExecutionContext .toArray(GlobalProfileKey[]::new); } + private ProfileType[] parseProfileTypeArray(BukkitCommandExecutionContext context) { + String keyStrings = context.popFirstArg(); + if (keyStrings.equals("@all")) { + return ProfileTypes.getTypes().toArray(ProfileType[]::new); + } + String[] profileNames = REPatterns.COMMA.split(keyStrings); + return Arrays.stream(profileNames) + .map(ProfileTypes::forName) + .filter(Option::isDefined) + .map(Option::get) + .toArray(ProfileType[]::new); + } + private Sharable parseSharable(BukkitCommandExecutionContext context) { String sharableName = context.popFirstArg(); Sharable targetSharable = Sharables.all().stream() diff --git a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandPermissions.java b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandPermissions.java new file mode 100644 index 00000000..5a88faa9 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandPermissions.java @@ -0,0 +1,20 @@ +package org.mvplugins.multiverse.inventories.command; + +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.MVCommandManager; +import org.mvplugins.multiverse.core.command.MVCommandPermissions; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.config.InventoriesConfig; + +@Service +public class MVInvCommandPermissions { + + @Inject + MVInvCommandPermissions(@NotNull MVCommandManager commandManager, @NotNull InventoriesConfig config) { + + MVCommandPermissions commandPermissions = commandManager.getCommandPermissions(); + commandPermissions.registerPermissionChecker("mvinv-gamemode-profile-true", commandIssuer -> config.getEnableGamemodeShareHandling()); + commandPermissions.registerPermissionChecker("mvinv-gamemode-profile-false", commandIssuer -> !config.getEnableGamemodeShareHandling()); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/GiveCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/GiveCommand.java index 13073621..f0954753 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/GiveCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/GiveCommand.java @@ -2,18 +2,16 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandIssuer; -import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.core.world.MultiverseWorld; -import org.mvplugins.multiverse.external.acf.commands.annotation.CommandAlias; 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; @@ -22,7 +20,6 @@ import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.vavr.control.Try; 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.ProfileDataSource; @@ -30,6 +27,7 @@ import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; import org.mvplugins.multiverse.inventories.share.Sharables; +import org.mvplugins.multiverse.inventories.util.PlayerStats; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -49,17 +47,30 @@ final class GiveCommand extends InventoriesCommand { this.profileDataSource = profileDataSource; } - // TODO Support custom gamemode when gamemode profile is enabled // TODO Better offline player parsing @Subcommand("give") @CommandPermission("multiverse.inventories.give") - @CommandCompletion("@players @mvworlds:scope=both @materials @range:64") - @Syntax(" [amount]") + @CommandCompletion("@players " + + "@mvworlds:scope=both " + + "@mvinvprofiletypes:checkPermissions=@mvinv-gamemode-profile-true|@materials:checkPermissions=@mvinv-gamemode-profile-false " + + "@materials:checkPermissions=@mvinv-gamemode-profile-true|@range:64,checkPermissions=@mvinv-gamemode-profile-false " + + "@range:64,checkPermissions=@mvinv-gamemode-profile-true|@empty " + + "@empty") + @Syntax(" [gamemode] [amount]") @Description("World and Group Information") void onGiveCommand( MVCommandIssuer issuer, + + @Syntax("") OfflinePlayer player, + + @Syntax("") MultiverseWorld world, + + @Syntax("[gamemode]") + ProfileType profileType, + + @Syntax(" [amount]") String item ) { ItemStack itemStack = parseItemFromString(issuer, item); @@ -69,16 +80,16 @@ void onGiveCommand( Logging.finer("Giving player " + player.getName() + " item: " + itemStack); // Giving online player in same world - // TODO check for gamemode as well if gamemode-profile is enabled. Player onlinePlayer = player.getPlayer(); - if (onlinePlayer != null && world.getName().equals(onlinePlayer.getWorld().getName())) { + if (onlinePlayer != null + && world.getName().equals(onlinePlayer.getWorld().getName()) + && ProfileTypes.forPlayer(onlinePlayer).equals(profileType)) { onlinePlayer.getInventory().addItem(itemStack); issuer.sendInfo("Gave player %s %s %s in world %s." .formatted(player.getName(), itemStack.getAmount(), itemStack, world.getName())); return; } - ProfileType profileType = ProfileTypes.getDefault(); SingleShareReader.of(inventories, player, world.getName(), profileType, Sharables.INVENTORY) .read() .thenCompose(inventory -> updatePlayerInventory(issuer, player, world, profileType, inventory, itemStack)) @@ -88,7 +99,7 @@ void onGiveCommand( }); } - private ItemStack parseItemFromString(MVCommandIssuer issuer, String item) { + private @Nullable ItemStack parseItemFromString(MVCommandIssuer issuer, String item) { // Get amount int amount = 1; AtomicBoolean endIsAmount = new AtomicBoolean(false); @@ -137,8 +148,8 @@ private CompletableFuture updatePlayerInventory( OfflinePlayer player, MultiverseWorld world, ProfileType profileType, - ItemStack[] inventory, - ItemStack itemStack + @Nullable ItemStack[] inventory, + @NotNull ItemStack itemStack ) { putItemInInventory(inventory, itemStack); return SingleShareWriter.of(inventories, player, world.getName(), profileType, Sharables.INVENTORY) @@ -150,7 +161,10 @@ private CompletableFuture updatePlayerInventory( .formatted(player.getName(), itemStack.getAmount(), itemStack.getI18NDisplayName(), world.getName()))); } - private void putItemInInventory(ItemStack[] inventory, ItemStack itemStack) { + private void putItemInInventory(@Nullable ItemStack[] inventory, @NotNull ItemStack itemStack) { + if (inventory == null) { + inventory = new ItemStack[PlayerStats.INVENTORY_SIZE]; + } int amountLeft = itemStack.getAmount(); for (int i = 0; i < inventory.length; i++) { if (inventory[i] == null || inventory[i].getType() == Material.AIR) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/BulkEditCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/BulkEditCommand.java new file mode 100644 index 00000000..8bfe8a54 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/BulkEditCommand.java @@ -0,0 +1,35 @@ +package org.mvplugins.multiverse.inventories.commands.bulkedit; + +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.utils.StringFormatter; +import org.mvplugins.multiverse.inventories.commands.InventoriesCommand; +import org.mvplugins.multiverse.inventories.profile.bulkedit.action.BulkEditAction; +import org.mvplugins.multiverse.inventories.profile.bulkedit.action.BulkEditResult; + +@ApiStatus.Internal +public abstract class BulkEditCommand extends InventoriesCommand { + + protected void outputActionSummary(MVCommandIssuer issuer, BulkEditAction bulkEditAction) { + issuer.sendMessage("Summary of affected profiles:"); + bulkEditAction.getActionSummary().forEach((key, value) -> + issuer.sendMessage(" %s: %s".formatted(key, value.size() > 10 + ? value.size() + : StringFormatter.join(value, ", ")))); + + } + + protected void runBulkEditAction(MVCommandIssuer issuer, BulkEditAction bulkEditAction) { + issuer.sendMessage("Starting bulk edit action..."); + bulkEditAction.execute() + .thenAccept(result -> outputResult(issuer, result)); + } + + protected void outputResult(MVCommandIssuer issuer, BulkEditResult bulkEditResult) { + issuer.sendMessage("Successfully processed %d profiles!".formatted(bulkEditResult.getSuccessCount())); + if (bulkEditResult.getFailureCount() > 0) { + issuer.sendError("Failed to process %d profiles! See log for details.".formatted(bulkEditResult.getFailureCount())); + } + issuer.sendMessage("Bulk edit action completed in %.4f ms.".formatted(bulkEditResult.getTimeTaken())); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java index 24c8a73c..c737315a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java @@ -61,6 +61,7 @@ void onCommand( } private void doClear(MVCommandIssuer issuer, GlobalProfileKey[] globalProfileKeys, boolean clearPlayerProfile) { + //TODO: Check lastWorld and online CompletableFuture[] futures = Arrays.stream(globalProfileKeys) .map(globalProfileKey -> profileDataSource.deleteGlobalProfile(globalProfileKey, clearPlayerProfile)) .toArray(CompletableFuture[]::new); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClearCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClearCommand.java new file mode 100644 index 00000000..19978982 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClearCommand.java @@ -0,0 +1,70 @@ +package org.mvplugins.multiverse.inventories.commands.bulkedit.playerprofile; + +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags; +import org.mvplugins.multiverse.core.command.queue.CommandQueueManager; +import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload; +import org.mvplugins.multiverse.core.locale.message.Message; +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.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.commands.bulkedit.BulkEditCommand; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesPayload; +import org.mvplugins.multiverse.inventories.profile.bulkedit.action.PlayerProfileClearAction; +import org.mvplugins.multiverse.inventories.profile.key.ContainerKey; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; + +@Service +final class ClearCommand extends BulkEditCommand { + + private final MultiverseInventories inventories; + private final CommandQueueManager commandQueueManager; + private final IncludeGroupsWorldsFlag flags; + + @Inject + ClearCommand( + @NotNull MultiverseInventories inventories, + @NotNull CommandQueueManager commandQueueManager, + @NotNull IncludeGroupsWorldsFlag flags + ) { + this.inventories = inventories; + this.commandQueueManager = commandQueueManager; + this.flags = flags; + } + + @Subcommand("bulkedit playerprofile clear") + @CommandPermission("multiverse.inventories.bulkedit") + @CommandCompletion("@mvinvplayernames @empty @mvinvprofiletypes:multiple @flags:groupName=" + IncludeGroupsWorldsFlag.NAME) + @Syntax(" [profile-type] [--include-groups-worlds]") + void onCommand( + MVCommandIssuer issuer, + GlobalProfileKey[] globalProfileKeys, + ContainerKey[] containerKeys, + ProfileType[] profileTypes, + String[] flagArray + ) { + ParsedCommandFlags parsedFlags = flags.parse(flagArray); + + PlayerProfileClearAction bulkEditAction = new PlayerProfileClearAction( + inventories, + new BulkProfilesPayload( + globalProfileKeys, + containerKeys, + profileTypes, + parsedFlags.hasFlag(flags.includeGroupsWorlds) + ) + ); + + outputActionSummary(issuer, bulkEditAction); + + commandQueueManager.addToQueue(CommandQueuePayload.issuer(issuer) + .prompt(Message.of("Are you sure you want to clear the selected profiles?")) + .action(() -> runBulkEditAction(issuer, bulkEditAction))); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/DeleteCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/DeleteCommand.java new file mode 100644 index 00000000..276fa4b7 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/DeleteCommand.java @@ -0,0 +1,76 @@ +package org.mvplugins.multiverse.inventories.commands.bulkedit.playerprofile; + +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags; +import org.mvplugins.multiverse.core.command.queue.CommandQueueManager; +import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.StringFormatter; +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.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.commands.bulkedit.BulkEditCommand; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesPayload; +import org.mvplugins.multiverse.inventories.profile.bulkedit.action.PlayerProfileDeleteAction; +import org.mvplugins.multiverse.inventories.profile.key.ContainerKey; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.share.Sharable; + +@Service +final class DeleteCommand extends BulkEditCommand { + + private final MultiverseInventories inventories; + private final CommandQueueManager commandQueueManager; + private final IncludeGroupsWorldsFlag flags; + + @Inject + DeleteCommand( + @NotNull MultiverseInventories inventories, + @NotNull CommandQueueManager commandQueueManager, + @NotNull IncludeGroupsWorldsFlag flags + ) { + this.inventories = inventories; + this.commandQueueManager = commandQueueManager; + this.flags = flags; + } + + @Subcommand("bulkedit playerprofile delete") + @CommandPermission("multiverse.inventories.bulkedit") + @CommandCompletion("@shares @mvinvplayernames @empty @mvinvprofiletypes:multiple @flags:groupName=" + IncludeGroupsWorldsFlag.NAME) + @Syntax(" [profile-type] [--include-groups-worlds]") + void onCommand( + MVCommandIssuer issuer, + Sharable sharable, + GlobalProfileKey[] globalProfileKeys, + ContainerKey[] containerKeys, + ProfileType[] profileTypes, + String[] flagArray + ) { + ParsedCommandFlags parsedFlags = flags.parse(flagArray); + + PlayerProfileDeleteAction bulkEditAction = new PlayerProfileDeleteAction( + inventories, + sharable, + new BulkProfilesPayload( + globalProfileKeys, + containerKeys, + profileTypes, + parsedFlags.hasFlag(flags.includeGroupsWorlds) + ) + ); + + issuer.sendMessage("Summary of affected profiles:"); + bulkEditAction.getActionSummary().forEach((key, value) -> + issuer.sendMessage(" %s: %s".formatted(key, value.size() > 10 ? value.size() : StringFormatter.join(value, ", ")))); + + commandQueueManager.addToQueue(CommandQueuePayload.issuer(issuer) + .prompt(Message.of("Are you sure you want to delete %s from the selected profiles?".formatted(sharable.getNames()[0]))) + .action(() -> runBulkEditAction(issuer, bulkEditAction))); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/IncludeGroupsWorldsFlag.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/IncludeGroupsWorldsFlag.java new file mode 100644 index 00000000..b344cb48 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/IncludeGroupsWorldsFlag.java @@ -0,0 +1,21 @@ +package org.mvplugins.multiverse.inventories.commands.bulkedit.playerprofile; + +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.flag.CommandFlag; +import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager; +import org.mvplugins.multiverse.core.command.flag.FlagBuilder; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; + +@Service +final class IncludeGroupsWorldsFlag extends FlagBuilder { + static final String NAME = "mvinvincludegroupsworlds"; + + @Inject + private IncludeGroupsWorldsFlag(CommandFlagsManager flagsManager) { + super(NAME, flagsManager); + } + + final CommandFlag includeGroupsWorlds = flag(CommandFlag.builder("--include-groups-worlds") + .addAlias("-i") + .build()); +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigrateInventorySerializationCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigrateInventorySerializationCommand.java index 5b497e24..bf9a9aca 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigrateInventorySerializationCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigrateInventorySerializationCommand.java @@ -71,7 +71,7 @@ private void doMigration(MVCommandIssuer issuer, boolean useByteSerialization) { AtomicLong profileCounter = new AtomicLong(0); CompletableFuture.allOf(profileDataSource.listGlobalProfileUUIDs() .stream() - .map(playerUUID -> profileDataSource.getGlobalProfile(GlobalProfileKey.create(playerUUID)) + .map(playerUUID -> profileDataSource.getGlobalProfile(GlobalProfileKey.create(playerUUID, "")) .thenCompose(profile -> run(profile, profileCounter)) .exceptionally(throwable -> { issuer.sendMessage("Error updating player " + playerUUID + ": " + throwable.getMessage()); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ReadOnlyShareHandler.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ReadOnlyShareHandler.java index 3cdf04c1..272dff8a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ReadOnlyShareHandler.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/ReadOnlyShareHandler.java @@ -10,8 +10,9 @@ import java.util.List; -final class ReadOnlyShareHandler extends ShareHandler { - ReadOnlyShareHandler(MultiverseInventories inventories, Player player) { +public final class ReadOnlyShareHandler extends ShareHandler { + + public ReadOnlyShareHandler(MultiverseInventories inventories, Player player) { super(inventories, player); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareReader.java b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareReader.java index 394051a5..c4908d5f 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareReader.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/handleshare/SingleShareReader.java @@ -1,12 +1,14 @@ package org.mvplugins.multiverse.inventories.handleshare; import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import org.mvplugins.multiverse.inventories.profile.key.ContainerType; import org.mvplugins.multiverse.inventories.profile.container.ProfileContainerStoreProvider; import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; +import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; import org.mvplugins.multiverse.inventories.share.Sharable; import java.util.List; @@ -14,6 +16,10 @@ public final class SingleShareReader { + public static SingleShareReader of(MultiverseInventories inventories, Player player, Sharable sharable) { + return new SingleShareReader<>(inventories, player, player.getWorld().getName(), ProfileTypes.forPlayer(player), sharable); + } + public static SingleShareReader of(MultiverseInventories inventories, OfflinePlayer player, String worldName, ProfileType profileType, Sharable sharable) { return new SingleShareReader<>(inventories, player, worldName, profileType, sharable); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java index f9afe7d5..4e418881 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java @@ -11,6 +11,7 @@ import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; import org.mvplugins.multiverse.inventories.profile.key.ProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; import org.mvplugins.multiverse.inventories.profile.key.ContainerType; import org.bukkit.configuration.ConfigurationSection; @@ -19,6 +20,7 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Array; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -60,7 +62,7 @@ private FileConfiguration loadFileToJsonConfiguration(File file) { return jsonConfiguration; } - private FileConfiguration getOrLoadPlayerProfileFile(ProfileKey profileKey, File playerFile) { + private FileConfiguration getOrLoadPlayerProfileFile(ProfileFileKey profileKey, File playerFile) { ProfileKey fileProfileKey = profileKey.forProfileType(null); return Try.of(() -> profileCacheManager.getOrLoadPlayerFile(fileProfileKey, (key) -> playerFile.exists() @@ -176,13 +178,31 @@ private void savePlayerProfileToDisk(ProfileKey profileKey, File playerFile, Pla public CompletableFuture deletePlayerProfile(ProfileKey profileKey) { File playerFile = profileFilesLocator.getPlayerProfileFile(profileKey); profileCacheManager.getCachedPlayerProfile(profileKey).peek(profile -> profile.getData().clear()); - return asyncFileIO.queueFileAction(playerFile, () -> deletePlayerProfileFromDisk(profileKey, playerFile)); + return asyncFileIO.queueFileAction(playerFile, () -> + deletePlayerProfileFromDisk(profileKey, playerFile, new ProfileType[]{profileKey.getProfileType()})); } - private void deletePlayerProfileFromDisk(ProfileKey profileKey, File playerFile) { + @Override + public CompletableFuture deletePlayerProfiles(ProfileFileKey profileKey, ProfileType[] profileTypes) { + if (ProfileTypes.isAll(profileTypes)) { + Logging.finer("Deleting profile: " + profileKey + " for all profile-types"); + return deletePlayerFile(profileKey); + } + for (var profileType : profileTypes) { + profileCacheManager.getCachedPlayerProfile(profileKey.forProfileType(profileType)) + .peek(profile -> profile.getData().clear()); + } + File playerFile = profileFilesLocator.getPlayerProfileFile(profileKey); + return asyncFileIO.queueFileAction(playerFile, () -> + deletePlayerProfileFromDisk(profileKey, playerFile, profileTypes)); + } + + private void deletePlayerProfileFromDisk(ProfileFileKey profileKey, File playerFile, ProfileType[] profileTypes) { try { FileConfiguration playerData = getOrLoadPlayerProfileFile(profileKey, playerFile); - playerData.set(profileKey.getProfileType().getName(), null); + for (var profileType : profileTypes) { + playerData.set(profileType.getName(), null); + } playerData.save(playerFile); } catch (IOException e) { Logging.severe("Could not delete data for player: " + profileKey.getPlayerName() @@ -202,7 +222,7 @@ public CompletableFuture deletePlayerFile(ProfileFileKey profileKey) { } File playerFile = profileFilesLocator.getPlayerProfileFile(profileKey); if (!playerFile.exists()) { - Logging.warning("Attempted to delete file that did not exist for player " + profileKey.getPlayerName() + Logging.finer("Attempted to delete file that did not exist for player " + profileKey.getPlayerName() + " in " + profileKey.getContainerType() + " " + profileKey.getDataName()); return CompletableFuture.completedFuture(null); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java index c316ef16..5072f537 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java @@ -86,7 +86,7 @@ private void buildPlayerNamesMap() { ProfileDataSource profileDataSource = profileDataSourceProvider.get(); CompletableFuture[] futures = profileDataSource.listGlobalProfileUUIDs().stream() - .map(uuid -> profileDataSource.getGlobalProfile(GlobalProfileKey.create(uuid)) + .map(uuid -> profileDataSource.getGlobalProfile(GlobalProfileKey.create(uuid, "")) .thenAccept(globalProfile -> setPlayerName(uuid, globalProfile.getLastKnownName()))) .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures).thenRun(this::savePlayerNames).join(); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java index 2c188273..ac17c3f2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java @@ -1,15 +1,14 @@ package org.mvplugins.multiverse.inventories.profile; -import org.bukkit.OfflinePlayer; import org.jvnet.hk2.annotations.Contract; import org.mvplugins.multiverse.external.vavr.control.Option; import org.mvplugins.multiverse.inventories.profile.key.ContainerType; import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; import org.mvplugins.multiverse.inventories.profile.key.ProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; import java.io.IOException; -import java.util.Collection; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -38,6 +37,8 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { CompletableFuture deletePlayerProfile(ProfileKey profileKey); + CompletableFuture deletePlayerProfiles(ProfileFileKey profileKey, ProfileType[] profileTypes); + CompletableFuture deletePlayerFile(ProfileFileKey profileKey); /** diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkProfilesAggregator.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkProfilesAggregator.java new file mode 100644 index 00000000..cd14f66b --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkProfilesAggregator.java @@ -0,0 +1,91 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit; + +import com.google.common.collect.Sets; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; +import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; +import org.mvplugins.multiverse.inventories.profile.key.ContainerKey; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@ApiStatus.Experimental +@Service +public final class BulkProfilesAggregator { + + private final WorldGroupManager worldGroupManager; + + @Inject + BulkProfilesAggregator(WorldGroupManager worldGroupManager) { + this.worldGroupManager = worldGroupManager; + } + + public List getProfileFileKeys(BulkProfilesPayload payload) { + var containerKeys = payload.includeGroupsWorlds() + ? includeGroupsWorlds(payload.containerKeys()) + : payload.containerKeys(); + + List profileFileKeys = new ArrayList<>( + payload.globalProfileKeys().length * containerKeys.length); + + for (GlobalProfileKey globalProfileKey : payload.globalProfileKeys()) { + for (ContainerKey containerKey : containerKeys) { + profileFileKeys.add(ProfileFileKey.create( + containerKey.getContainerType(), + containerKey.getDataName(), + globalProfileKey.getPlayerUUID(), + globalProfileKey.getPlayerName())); + } + } + return profileFileKeys; + } + + public List getPlayerProfileKeys(BulkProfilesPayload payload) { + var containerKeys = payload.includeGroupsWorlds() + ? includeGroupsWorlds(payload.containerKeys()) + : payload.containerKeys(); + + List profileKeys = new ArrayList<>( + payload.globalProfileKeys().length * containerKeys.length * payload.profileTypes().length); + + for (GlobalProfileKey globalProfileKey : payload.globalProfileKeys()) { + for (ContainerKey containerKey : containerKeys) { + for (ProfileType profileType : payload.profileTypes()) { + profileKeys.add(ProfileKey.create( + containerKey.getContainerType(), + containerKey.getDataName(), + profileType, + globalProfileKey.getPlayerUUID(), + globalProfileKey.getPlayerName())); + } + } + } + return profileKeys; + } + + private ContainerKey[] includeGroupsWorlds(ContainerKey[] containerKeys) { + Set containerKeyList = Sets.newHashSet(containerKeys); + for (ContainerKey containerKey : containerKeys) { + if (containerKey.getContainerType() != ContainerType.GROUP) { + continue; + } + WorldGroup group = worldGroupManager.getGroup(containerKey.getDataName()); + if (group == null) { + continue; + } + containerKeyList.addAll(group.getWorlds().stream() + .map(worldName -> ContainerKey.create(ContainerType.WORLD, worldName)) + .toList()); + } + return containerKeyList.toArray(ContainerKey[]::new); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkProfilesPayload.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkProfilesPayload.java new file mode 100644 index 00000000..6773c10b --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkProfilesPayload.java @@ -0,0 +1,39 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.inventories.profile.key.ContainerKey; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@ApiStatus.Experimental +public record BulkProfilesPayload(@NotNull GlobalProfileKey[] globalProfileKeys, + @NotNull ContainerKey[] containerKeys, + @NotNull ProfileType[] profileTypes, + boolean includeGroupsWorlds) { + + public Map> getSummary() { + return Map.of( + "Players", Arrays.stream(globalProfileKeys) + .map(GlobalProfileKey::getPlayerName) + .toList(), + "Worlds", Arrays.stream(containerKeys) + .filter(c -> c.getContainerType() == ContainerType.WORLD) + .map(ContainerKey::getDataName) + .toList(), + "Groups", Arrays.stream(containerKeys) + .filter(c -> c.getContainerType() == ContainerType.GROUP) + .map(ContainerKey::getDataName) + .toList(), + "Profile Types", Arrays.stream(profileTypes) + .map(ProfileType::getName) + .map(String::toLowerCase) + .toList() + ); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/BulkEditAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/BulkEditAction.java new file mode 100644 index 00000000..be435ba4 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/BulkEditAction.java @@ -0,0 +1,70 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@ApiStatus.Experimental +public abstract class BulkEditAction { + + protected final MultiverseInventories inventories; + protected final ProfileDataSource profileDataSource; + protected final GlobalProfileKey[] globalProfileKeys; + + BulkEditAction(MultiverseInventories inventories, GlobalProfileKey[] globalProfileKeys) { + this.inventories = inventories; + this.profileDataSource = inventories.getServiceLocator().getService(ProfileDataSource.class); + this.globalProfileKeys = globalProfileKeys; + } + + public CompletableFuture execute() { + BulkEditResult bulkEditResult = new BulkEditResult(); + List targetKeys = aggregateKeys(); + Set onlinePlayers = new HashSet<>(); + return CompletableFuture.allOf(targetKeys.stream() + .map(key -> { + Player player = key.getOnlinePlayer(); + if (player != null && isOnlinePlayerAffected(key, player)) { + onlinePlayers.add(player); + } + return performAction(key) + .thenRun(bulkEditResult::incrementSuccess) + .exceptionally(throwable -> { + bulkEditResult.incrementFailure(); + throwable.printStackTrace(); + return null; + }); + }) + .toArray(CompletableFuture[]::new)) + .thenRun(() -> + Bukkit.getScheduler().runTask(inventories, () -> + onlinePlayers.forEach(this::updateOnlinePlayerNow))) + .thenApply(ignore -> bulkEditResult); + } + + protected abstract List aggregateKeys(); + + protected abstract CompletableFuture performAction(K key); + + protected boolean isOnlinePlayerAffected(K key, Player player) { + return key.getPlayerUUID().equals(player.getUniqueId()); + } + + protected abstract void updateOnlinePlayerNow(Player player); + + public Map> getActionSummary() { + return Map.of("Players", Arrays.stream(globalProfileKeys) + .map(GlobalProfileKey::getPlayerName) + .toList()); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/BulkEditResult.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/BulkEditResult.java new file mode 100644 index 00000000..97bdf270 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/BulkEditResult.java @@ -0,0 +1,38 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.concurrent.atomic.AtomicInteger; + +@ApiStatus.Experimental +public final class BulkEditResult { + + private final long startTime = System.nanoTime(); + private final AtomicInteger successCount = new AtomicInteger(0); + private final AtomicInteger failureCount = new AtomicInteger(0); + + void incrementSuccess() { + successCount.incrementAndGet(); + } + + void incrementFailure() { + failureCount.incrementAndGet(); + } + + public int getSuccessCount() { + return successCount.get(); + } + + public int getFailureCount() { + return failureCount.get(); + } + + /** + * In milliseconds + * + * @return Gets the time taken + */ + public double getTimeTaken() { + return (double) (System.nanoTime() - startTime) / 1_000_000.0; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerFileAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerFileAction.java new file mode 100644 index 00000000..c4499ff4 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerFileAction.java @@ -0,0 +1,33 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesAggregator; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesPayload; +import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; + +import java.util.List; +import java.util.Map; + +@ApiStatus.Experimental +public abstract class PlayerFileAction extends BulkEditAction { + + private final BulkProfilesAggregator profilesAggregator; + protected final BulkProfilesPayload bulkProfilesPayload; + + PlayerFileAction(MultiverseInventories inventories, BulkProfilesPayload bulkProfilesPayload) { + super(inventories, bulkProfilesPayload.globalProfileKeys()); + this.profilesAggregator = inventories.getServiceLocator().getService(BulkProfilesAggregator.class); + this.bulkProfilesPayload = bulkProfilesPayload; + } + + @Override + protected List aggregateKeys() { + return profilesAggregator.getProfileFileKeys(bulkProfilesPayload); + } + + @Override + public Map> getActionSummary() { + return bulkProfilesPayload.getSummary(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileAction.java new file mode 100644 index 00000000..156d8526 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileAction.java @@ -0,0 +1,36 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesAggregator; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesPayload; +import org.mvplugins.multiverse.inventories.profile.key.ProfileKey; + +import java.util.List; +import java.util.Map; + +@ApiStatus.Experimental +public abstract class PlayerProfileAction extends BulkEditAction { + + private final BulkProfilesAggregator profilesAggregator; + private final BulkProfilesPayload bulkProfilesPayload; + + protected PlayerProfileAction( + MultiverseInventories inventories, + BulkProfilesPayload bulkProfilesPayload + ) { + super(inventories, bulkProfilesPayload.globalProfileKeys()); + this.profilesAggregator = inventories.getServiceLocator().getService(BulkProfilesAggregator.class); + this.bulkProfilesPayload = bulkProfilesPayload; + } + + @Override + protected List aggregateKeys() { + return profilesAggregator.getPlayerProfileKeys(bulkProfilesPayload); + } + + @Override + public Map> getActionSummary() { + return bulkProfilesPayload.getSummary(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileClearAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileClearAction.java new file mode 100644 index 00000000..feef9357 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileClearAction.java @@ -0,0 +1,70 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.ReadOnlyShareHandler; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesPayload; +import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; +import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; +import org.mvplugins.multiverse.inventories.share.Sharables; +import org.mvplugins.multiverse.inventories.share.Shares; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@ApiStatus.Experimental +public final class PlayerProfileClearAction extends PlayerFileAction { + + private final WorldGroupManager worldGroupManager; + private final Set profileTypesSet; + + public PlayerProfileClearAction(MultiverseInventories inventories, BulkProfilesPayload bulkProfilesPayload) { + super(inventories, bulkProfilesPayload); + this.worldGroupManager = inventories.getServiceLocator().getService(WorldGroupManager.class); + this.profileTypesSet = Set.of(bulkProfilesPayload.profileTypes()); + } + + @Override + protected CompletableFuture performAction(ProfileFileKey key) { + return profileDataSource.deletePlayerProfiles(key, bulkProfilesPayload.profileTypes()) + .thenCompose(ignore -> profileDataSource.modifyGlobalProfile( + key, + profile -> profile.setLoadOnLogin(true) + )); + } + + @Override + protected boolean isOnlinePlayerAffected(ProfileFileKey key, Player player) { + if (!profileTypesSet.contains(ProfileTypes.forPlayer(player))) { + return false; + } + + // Gets groups that share this sharable + List groups = worldGroupManager.getGroupsForWorld(player.getWorld().getName()); + + Shares unhandledSharables = Sharables.enabledOf(); + for (WorldGroup worldGroup : groups) { + unhandledSharables.removeAll(worldGroup.getApplicableShares()); + } + + if (!unhandledSharables.isEmpty()) { + return key.getContainerType() == ContainerType.WORLD && player.getWorld().getName().equals(key.getDataName()); + } + + // Using group for sharable + return key.getContainerType() == ContainerType.GROUP && groups.stream() + .anyMatch(group -> group.getName().equals(key.getDataName())); + } + + @Override + protected void updateOnlinePlayerNow(Player player) { + new ReadOnlyShareHandler(inventories, player).handleSharing(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileDeleteAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileDeleteAction.java new file mode 100644 index 00000000..74285ce9 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/PlayerProfileDeleteAction.java @@ -0,0 +1,76 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.SingleShareReader; +import org.mvplugins.multiverse.inventories.profile.ProfileDataSnapshot; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkProfilesPayload; +import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; +import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; +import org.mvplugins.multiverse.inventories.profile.key.ContainerType; +import org.mvplugins.multiverse.inventories.profile.key.ProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes; +import org.mvplugins.multiverse.inventories.share.Sharable; +import org.mvplugins.multiverse.inventories.util.FutureNow; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@ApiStatus.Experimental +public final class PlayerProfileDeleteAction extends PlayerProfileAction { + + + private final WorldGroupManager worldGroupManager; + private final Sharable sharable; + + public PlayerProfileDeleteAction( + MultiverseInventories inventories, + Sharable sharable, + BulkProfilesPayload bulkProfilesPayload + ) { + super(inventories, bulkProfilesPayload); + this.worldGroupManager = inventories.getServiceLocator().getService(WorldGroupManager.class); + this.sharable = sharable; + } + + @Override + protected CompletableFuture performAction(ProfileKey key) { + return profileDataSource.getPlayerProfile(key) + .thenCompose(playerProfile -> { + playerProfile.set(sharable, null); + return profileDataSource.updatePlayerProfile(playerProfile); + }) + .thenCompose(ignore -> profileDataSource.modifyGlobalProfile(key, profile -> { + profile.setLoadOnLogin(true); + })); + } + + @Override + protected boolean isOnlinePlayerAffected(ProfileKey key, Player player) { + if (!ProfileTypes.forPlayer(player).equals(key.getProfileType())) { + return false; + } + + // Gets groups that share this sharable + List groups = worldGroupManager.getGroupsForWorld(player.getWorld().getName()).stream() + .filter(group -> group.isSharing(sharable)) + .toList(); + + if (groups.isEmpty()) { + // Using world itself for sharable + return key.getContainerType() == ContainerType.WORLD && player.getWorld().getName().equals(key.getDataName()); + } + + // Using group for sharable + return key.getContainerType() == ContainerType.GROUP && groups.stream() + .anyMatch(group -> group.getName().equals(key.getDataName())); + } + + @Override + protected void updateOnlinePlayerNow(Player player) { + ProfileDataSnapshot profileDataSnapshot = new ProfileDataSnapshot(); + profileDataSnapshot.set(sharable, FutureNow.get(SingleShareReader.of(inventories, player, sharable).read())); + sharable.getHandler().updatePlayer(player, profileDataSnapshot); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/package-info.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/package-info.java new file mode 100644 index 00000000..d17b3875 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/action/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Experimental +package org.mvplugins.multiverse.inventories.profile.bulkedit.action; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/package-info.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/package-info.java new file mode 100644 index 00000000..bae46c1a --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Experimental +package org.mvplugins.multiverse.inventories.profile.bulkedit; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java index 727f0697..d0829c4e 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/group/WorldGroup.java @@ -180,7 +180,7 @@ public Set getWorlds() { * @return true is the sharable is shared for this group. */ public boolean isSharing(Sharable sharable) { - return getShares().isSharing(sharable); + return getApplicableShares().isSharing(sharable); } /** diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ContainerKey.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ContainerKey.java new file mode 100644 index 00000000..1903bc83 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ContainerKey.java @@ -0,0 +1,46 @@ +package org.mvplugins.multiverse.inventories.profile.key; + +import java.util.Objects; + +public final class ContainerKey { + + public static ContainerKey create(ContainerType containerType, String dataName) { + return new ContainerKey(containerType, dataName); + } + + private final ContainerType containerType; + private final String dataName; + + private ContainerKey(ContainerType containerType, String dataName) { + this.containerType = containerType; + this.dataName = dataName; + } + + public ContainerType getContainerType() { + return containerType; + } + + public String getDataName() { + return dataName; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ContainerKey that = (ContainerKey) o; + return containerType == that.containerType && Objects.equals(dataName, that.dataName); + } + + @Override + public int hashCode() { + return Objects.hash(containerType, dataName); + } + + @Override + public String toString() { + return "ContainerKey{" + + "containerType=" + containerType + + ", dataName='" + dataName + '\'' + + '}'; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/GlobalProfileKey.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/GlobalProfileKey.java index 976b1897..058d9cfc 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/GlobalProfileKey.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/GlobalProfileKey.java @@ -2,16 +2,14 @@ import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.external.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.UUID; -public class GlobalProfileKey { - - public static GlobalProfileKey create(UUID playerUUID) { - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerUUID); - return create(offlinePlayer); - } +public sealed class GlobalProfileKey permits ProfileFileKey { public static GlobalProfileKey create(OfflinePlayer offlinePlayer) { return create(offlinePlayer.getUniqueId(), offlinePlayer.getName()); @@ -21,10 +19,10 @@ public static GlobalProfileKey create(UUID playerUUID, String playerName) { return new GlobalProfileKey(playerUUID, playerName); } - private final UUID playerUUID; - private final String playerName; + protected final UUID playerUUID; + protected final String playerName; - private GlobalProfileKey(UUID playerUUID, String playerName) { + protected GlobalProfileKey(UUID playerUUID, String playerName) { this.playerUUID = playerUUID; this.playerName = playerName; } @@ -37,6 +35,14 @@ public String getPlayerName() { return playerName; } + public @NotNull OfflinePlayer getOfflinePlayer() { + return Bukkit.getOfflinePlayer(playerUUID); + } + + public @Nullable Player getOnlinePlayer() { + return Bukkit.getPlayer(playerUUID); + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileFileKey.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileFileKey.java index 254d99f7..e8e9d79c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileFileKey.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileFileKey.java @@ -9,7 +9,7 @@ import java.util.UUID; -public class ProfileFileKey { +public sealed class ProfileFileKey extends GlobalProfileKey permits ProfileKey { public static ProfileFileKey create( ContainerType containerType, @@ -26,13 +26,6 @@ public static ProfileFileKey create( return new ProfileFileKey(containerType, dataName, offlinePlayer.getUniqueId(), offlinePlayer.getName()); } - public static ProfileFileKey create( - ContainerType containerType, - String dataName, - UUID playerUUID) { - return new ProfileFileKey(containerType, dataName, playerUUID, Bukkit.getOfflinePlayer(playerUUID).getName()); - } - public static ProfileFileKey fromPlayerProfile(PlayerProfile profile) { return new ProfileFileKey( profile.getContainerType(), @@ -44,8 +37,6 @@ public static ProfileFileKey fromPlayerProfile(PlayerProfile profile) { protected final ContainerType containerType; protected final String dataName; - protected final String playerName; - protected final UUID playerUUID; protected final int hashCode; private ProfileFileKey(ContainerType containerType, String dataName, UUID playerUUID, String playerName) { @@ -57,10 +48,9 @@ private ProfileFileKey(ContainerType containerType, String dataName, UUID player } protected ProfileFileKey(ContainerType containerType, String dataName, UUID playerUUID, String playerName, int hashCode) { + super(playerUUID, playerName); this.containerType = containerType; this.dataName = dataName; - this.playerUUID = playerUUID; - this.playerName = playerName; this.hashCode = hashCode; } @@ -80,14 +70,6 @@ public String getDataName() { return dataName; } - public String getPlayerName() { - return playerName; - } - - public UUID getPlayerUUID() { - return playerUUID; - } - public boolean isSameFile(ProfileFileKey other) { return Objects.equal(getContainerType(), other.getContainerType()) && Objects.equal(getDataName(), other.getDataName()) && diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileType.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileType.java index 4b129810..a837c839 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileType.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileType.java @@ -23,13 +23,13 @@ public String getName() { } @Override - public final boolean equals(Object o) { - return o instanceof ProfileType && ((ProfileType) o).getName().equals(this.getName()); + public boolean equals(Object o) { + return o instanceof ProfileType otherType && otherType.getName().equals(this.getName()); } @Override - public final int hashCode() { - return getName().hashCode(); + public int hashCode() { + return name.hashCode(); } @Override diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileTypes.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileTypes.java index d1bb7b08..1640f4fa 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileTypes.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileTypes.java @@ -1,19 +1,25 @@ package org.mvplugins.multiverse.inventories.profile.key; +import com.google.common.collect.Sets; import org.bukkit.GameMode; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; +import org.mvplugins.multiverse.external.vavr.control.Option; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Static class for profile type lookup and protected registration. */ public final class ProfileTypes { - private static final List types = new ArrayList<>(); + private static final Set types = new HashSet<>(); private static InventoriesConfig config; public static void init(MultiverseInventories plugin) { @@ -26,10 +32,6 @@ private static ProfileType createProfileType(String name) { return type; } - public static List getTypes() { - return types; - } - /** * The profile type for the SURVIVAL Game Mode. */ @@ -50,6 +52,17 @@ public static List getTypes() { */ public static final ProfileType SPECTATOR = createProfileType("SPECTATOR"); + public static Collection getTypes() { + return types; + } + + public static Collection getApplicableTypes() { + if (config != null && config.getEnableGamemodeShareHandling()) { + return types; + } + return List.of(getDefault()); + } + public static ProfileType getDefault() { return SURVIVAL; } @@ -61,6 +74,12 @@ public static ProfileType forPlayer(Player player) { return getDefault(); } + public static Option forName(String name) { + return Option.ofOptional(types.stream() + .filter(type -> type.getName().equalsIgnoreCase(name)) + .findFirst()); + } + /** * Returns the appropriate ProfileType for the given game mode. * @@ -77,7 +96,11 @@ public static ProfileType forGameMode(GameMode mode) { }; } + public static boolean isAll(ProfileType[] otherTypes) { + return Set.of(otherTypes).equals(types); + } + private ProfileTypes() { - throw new AssertionError(); + throw new IllegalStateException(); } } diff --git a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt index 8e406ba8..36d7c644 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(26, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) + assertEquals(28, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) } @Test diff --git a/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt index 1ce4407f..84826cf6 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt @@ -52,7 +52,7 @@ class FilePerformanceTest : TestWithMockBukkit() { val startTime = System.nanoTime() val futures = ArrayList>(1000) for (i in 0..1000) { - futures.add(profileDataSource.modifyGlobalProfile(GlobalProfileKey.create(UUID.randomUUID()), { globalProfile -> + futures.add(profileDataSource.modifyGlobalProfile(GlobalProfileKey.create(UUID.randomUUID(), ""), { globalProfile -> globalProfile.setLoadOnLogin(true) })) }