From 06aeeec921c0689e5b2197a1b6ead6891501831f Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:27:02 +0800 Subject: [PATCH 1/3] Refactor bulkedit commands to remove "bulkedit" prefix and add clone functionality --- .../command/MVInvCommandCompletion.java | 31 ++++++- .../command/MVInvCommandContexts.java | 50 ++++++++-- .../commands/bulkedit/BulkEditCommand.java | 2 + .../bulkedit/globalprofile/ClearCommand.java | 2 +- .../bulkedit/globalprofile/ModifyCommand.java | 2 +- .../bulkedit/playerprofile/ClearCommand.java | 14 ++- .../playerprofile/ClonePlayerCommand.java | 82 +++++++++++++++++ .../playerprofile/CloneWorldGroupCommand.java | 92 +++++++++++++++++++ .../bulkedit/playerprofile/DeleteCommand.java | 19 +++- .../MigrateInventorySerializationCommand.java | 4 +- .../MigratePlayerNameCommand.java | 2 +- .../inventories/profile/AsyncFileIO.java | 19 ++++ .../profile/FlatFileProfileDataSource.java | 81 +++++++++++++++- .../profile/ProfileCacheManager.java | 29 +++++- .../profile/ProfileDataSource.java | 28 ++++++ .../bulkedit/AllSharesPlayerFileAction.java | 60 ++++++++++++ .../profile/bulkedit/BulkEditCreator.java | 11 +++ .../profile/bulkedit/PlayerFileAction.java | 4 +- .../bulkedit/PlayerProfileClearAction.java | 47 +--------- .../PlayerProfileClonePlayerAction.java | 29 ++++++ .../PlayerProfileCloneWorldGroupAction.java | 29 ++++++ .../profile/key/ProfileFileKey.java | 13 +++ .../inventories/profile/key/ProfileKey.java | 6 ++ 23 files changed, 580 insertions(+), 76 deletions(-) create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClonePlayerCommand.java create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/CloneWorldGroupCommand.java create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/AllSharesPlayerFileAction.java create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClonePlayerAction.java create mode 100644 src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileCloneWorldGroupAction.java 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 455789f4..f17a89e9 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java @@ -1,5 +1,6 @@ package org.mvplugins.multiverse.inventories.command; +import com.google.common.collect.Streams; import org.bukkit.Bukkit; import org.bukkit.generator.WorldInfo; import org.jetbrains.annotations.NotNull; @@ -14,8 +15,10 @@ import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.dataimport.DataImportManager; 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.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; @@ -26,6 +29,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -39,6 +43,7 @@ public final class MVInvCommandCompletion { private final WorldGroupManager worldGroupManager; private final DataImportManager dataImportManager; private final PlayerNamesMapper playerNamesMapper; + private final ProfileDataSource profileDataSource; @Inject private MVInvCommandCompletion( @@ -46,18 +51,23 @@ private MVInvCommandCompletion( @NotNull WorldGroupManager worldGroupManager, @NotNull DataImportManager dataImportManager, @NotNull MVCommandManager mvCommandManager, - @NotNull PlayerNamesMapper playerNamesMapper + @NotNull PlayerNamesMapper playerNamesMapper, + @NotNull ProfileDataSource profileDataSource ) { this.inventoriesConfig = inventoriesConfig; this.worldGroupManager = worldGroupManager; this.dataImportManager = dataImportManager; this.playerNamesMapper = playerNamesMapper; + this.profileDataSource = profileDataSource; MVCommandCompletions commandCompletions = mvCommandManager.getCommandCompletions(); commandCompletions.registerAsyncCompletion("dataimporters", this::suggestDataImporters); commandCompletions.registerStaticCompletion("mvinvconfigs", inventoriesConfig.getStringPropertyHandle().getAllPropertyNames()); commandCompletions.registerAsyncCompletion("mvinvconfigvalues", this::suggestConfigValues); + commandCompletions.registerAsyncCompletion("mvinvplayername", this::suggestPlayerName); commandCompletions.registerAsyncCompletion("mvinvplayernames", this::suggestPlayerNames); + commandCompletions.registerAsyncCompletion("mvinvcontainerkey", this::suggestContainerKey); + commandCompletions.registerAsyncCompletion("mvinvcontainerkeys", this::suggestContainerKeys); commandCompletions.registerAsyncCompletion("mvinvprofiletypes", this::suggestProfileTypes); commandCompletions.registerAsyncCompletion("sharables", this::suggestSharables); commandCompletions.registerAsyncCompletion("shares", this::suggestShares); @@ -77,6 +87,10 @@ private Collection suggestConfigValues(BukkitCommandCompletionContext co .getOrElse(Collections.emptyList()); } + private Collection suggestPlayerName(BukkitCommandCompletionContext context) { + return getPlayerNames(); + } + private Collection suggestPlayerNames(BukkitCommandCompletionContext context) { if (Objects.equals(context.getInput(), "@all")) { return Collections.emptyList(); @@ -96,6 +110,21 @@ private List getPlayerNames() { .collect(Collectors.toList()); } + private Collection suggestContainerKey(BukkitCommandCompletionContext context) { + return Arrays.stream(ContainerType.values()) + .flatMap(containerType -> profileDataSource.listContainerDataNames(containerType) + .stream() + .map(dataName -> containerType.name().toLowerCase(Locale.ENGLISH) + "=" + dataName)) + .collect(Collectors.toList()); + } + + private Collection suggestContainerKeys(BukkitCommandCompletionContext context) { + //todo handle multiple worlds and/or groups + Collection strings = suggestContainerKey(context); + strings.add("@all"); + return strings; + } + private Collection suggestProfileTypes(BukkitCommandCompletionContext context) { if (!context.hasConfig("multiple")) { return ProfileTypes.getTypes().stream() 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 dbe0a7ef..dac90d3b 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java @@ -10,6 +10,7 @@ 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.external.vavr.control.Try; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.profile.PlayerNamesMapper; import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; @@ -29,6 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; @Service public final class MVInvCommandContexts { @@ -52,7 +54,9 @@ private MVInvCommandContexts( this.profileDataSource = profileDataSource; CommandContexts commandContexts = commandManager.getCommandContexts(); + commandContexts.registerContext(ContainerKey.class, this::parseContainerKey); commandContexts.registerContext(ContainerKey[].class, this::parseContainerKeyArray); + commandContexts.registerContext(GlobalProfileKey.class, this::parseGlobalProfileKey); commandContexts.registerContext(GlobalProfileKey[].class, this::parseGlobalProfileKeyArray); commandContexts.registerIssuerAwareContext(ProfileType.class, this::parseProfileType); commandContexts.registerIssuerAwareContext(ProfileType[].class, this::parseProfileTypeArray); @@ -70,6 +74,22 @@ private ProfileType parseProfileType(BukkitCommandExecutionContext context) { .getOrElseThrow(() -> new InvalidCommandArgument("Invalid profile type: " + profileType)); } + private ContainerKey parseContainerKey(BukkitCommandExecutionContext context) { + String input = context.popFirstArg(); + String[] keyValueSplit = REPatterns.EQUALS.split(input, 2); + if (keyValueSplit.length != 2) { + throw new InvalidCommandArgument("Invalid world/group format: " + input + ". Expected format: type=name"); + } + ContainerType containerType = Try.of(() -> ContainerType.valueOf(keyValueSplit[0].toUpperCase())) + .getOrElseThrow(() -> new InvalidCommandArgument("Unknown container type: " + keyValueSplit[0])); + String dataName = keyValueSplit[1]; + List availableDataNames = profileDataSource.listContainerDataNames(containerType); + if (!availableDataNames.contains(dataName)) { + throw new InvalidCommandArgument("The " + keyValueSplit[0] + " name " + dataName + " does not have any data."); + } + return ContainerKey.create(containerType, dataName); + } + private ContainerKey[] parseContainerKeyArray(BukkitCommandExecutionContext context) { String keyStrings = context.popFirstArg(); if (keyStrings.equals("@all")) { @@ -84,8 +104,7 @@ private ContainerKey[] parseContainerKeyArray(BukkitCommandExecutionContext cont for (String typeSplit : typesSplit) { String[] keyValueSplit = REPatterns.EQUALS.split(typeSplit, 2); if (keyValueSplit.length != 2) { - // todo: Probably error invalid format - continue; + throw new InvalidCommandArgument("Invalid worlds/groups format: " + typeSplit + ". Expected format: type=name1,name2;type2=name3"); } ContainerType containerType = ContainerType.valueOf(keyValueSplit[0].toUpperCase()); String[] dataNameSplit = REPatterns.COMMA.split(keyValueSplit[1]); @@ -99,9 +118,16 @@ private ContainerKey[] parseContainerKeyArray(BukkitCommandExecutionContext cont return containerKeys.toArray(new ContainerKey[0]); } + private GlobalProfileKey parseGlobalProfileKey(BukkitCommandExecutionContext context) { + String keyString = context.popFirstArg(); + // todo: UUID parsing + return playerNamesMapper.getKey(keyString) + .getOrElseThrow(() -> new InvalidCommandArgument("Unknown player name: " + keyString)); + } + private GlobalProfileKey[] parseGlobalProfileKeyArray(BukkitCommandExecutionContext context) { String keyStrings = context.popFirstArg(); - if (keyStrings.equals("@all")) { + if (Objects.equals(keyStrings, "@all")) { return playerNamesMapper.getKeys().toArray(GlobalProfileKey[]::new); } // todo: UUID parsing @@ -114,16 +140,26 @@ private GlobalProfileKey[] parseGlobalProfileKeyArray(BukkitCommandExecutionCont } private ProfileType[] parseProfileTypeArray(BukkitCommandExecutionContext context) { - String keyStrings = context.popFirstArg(); - if (keyStrings.equals("@all")) { + String keyStrings = context.getFirstArg(); + if (keyStrings == null) { + return ProfileTypes.getTypes().toArray(ProfileType[]::new); + } + if (Objects.equals(keyStrings, "@all")) { + context.popFirstArg(); return ProfileTypes.getTypes().toArray(ProfileType[]::new); } String[] profileNames = REPatterns.COMMA.split(keyStrings); - return Arrays.stream(profileNames) + List list = Arrays.stream(profileNames) .map(ProfileTypes::forName) .filter(Option::isDefined) .map(Option::get) - .toArray(ProfileType[]::new); + .toList(); + if (list.isEmpty()) { + return ProfileTypes.getTypes().toArray(ProfileType[]::new); + } + + context.popFirstArg(); + return list.toArray(new ProfileType[0]); } private Sharable parseSharable(BukkitCommandExecutionContext context) { 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 index e071d9ed..d6cfb863 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/BulkEditCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/BulkEditCommand.java @@ -4,6 +4,7 @@ import org.jvnet.hk2.annotations.Contract; import org.mvplugins.multiverse.core.command.MVCommandIssuer; import org.mvplugins.multiverse.core.utils.StringFormatter; +import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.inventories.commands.InventoriesCommand; import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkEditAction; @@ -12,6 +13,7 @@ @Contract @ApiStatus.Internal +@Subcommand("bulkedit") public abstract class BulkEditCommand extends InventoriesCommand { protected final BulkEditCreator bulkEditCreator; 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 7be2c960..20daca13 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 @@ -42,7 +42,7 @@ final class ClearCommand extends BulkEditCommand { this.flags = flags; } - @Subcommand("bulkedit globalprofile clear") + @Subcommand("globalprofile clear") @CommandPermission("multiverse.inventories.bulkedit") @CommandCompletion("@mvinvplayernames @flags:groupName=" + Flags.NAME) @Syntax(" [--clear-all-player-profiles]") diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java index bd419200..d5e129a2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java @@ -31,7 +31,7 @@ final class ModifyCommand extends InventoriesCommand { this.profileDataSource = profileDataSource; } - @Subcommand("bulkedit globalprofile modify") + @Subcommand("globalprofile modify") @CommandPermission("multiverse.inventories.bulkedit") @CommandCompletion("load-on-login|last-world @empty @mvinvplayernames") @Syntax(" ") 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 index ee32445e..2d163f07 100644 --- 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 @@ -37,15 +37,23 @@ final class ClearCommand extends BulkEditCommand { this.flags = flags; } - @Subcommand("bulkedit playerprofile clear") + @Subcommand("playerprofile clear") @CommandPermission("multiverse.inventories.bulkedit") - @CommandCompletion("@mvinvplayernames @empty @mvinvprofiletypes:multiple @flags:groupName=" + IncludeGroupsWorldsFlag.NAME) - @Syntax(" [profile-type] [--include-groups-worlds]") + @CommandCompletion("@mvinvplayernames @mvinvcontainerkeys @mvinvprofiletypes:multiple @flags:groupName=" + IncludeGroupsWorldsFlag.NAME) + @Syntax(" [profile-type] [--include-groups-worlds]") void onCommand( MVCommandIssuer issuer, + + @Syntax("") GlobalProfileKey[] globalProfileKeys, + + @Syntax("") ContainerKey[] containerKeys, + + @Syntax("[profile-types]") ProfileType[] profileTypes, + + @Syntax("[--include-groups-worlds]") String[] flagArray ) { ParsedCommandFlags parsedFlags = flags.parse(flagArray); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClonePlayerCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClonePlayerCommand.java new file mode 100644 index 00000000..fa14bc71 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/ClonePlayerCommand.java @@ -0,0 +1,82 @@ +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.commands.bulkedit.BulkEditCommand; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkEditAction; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkEditCreator; +import org.mvplugins.multiverse.inventories.profile.bulkedit.PlayerProfilesPayload; +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 +public class ClonePlayerCommand extends BulkEditCommand { + + private final @NotNull CommandQueueManager commandQueueManager; + private final @NotNull IncludeGroupsWorldsFlag flags; + + @Inject + ClonePlayerCommand(@NotNull BulkEditCreator bulkEditCreator, + @NotNull CommandQueueManager commandQueueManager, + @NotNull IncludeGroupsWorldsFlag flags + ) { + super(bulkEditCreator); + this.commandQueueManager = commandQueueManager; + this.flags = flags; + } + + @Subcommand("playerprofile clone-player") + @CommandPermission("multiverse.inventories.bulkedit") + @CommandCompletion("@mvinvplayername @mvinvplayername @mvinvcontainerkeys @mvinvprofiletypes:multiple " + + "@flags:groupName=" + IncludeGroupsWorldsFlag.NAME) + @Syntax(" [profile-types] [--include-groups-worlds]") + void onCommand( + MVCommandIssuer issuer, + + @Syntax("") + GlobalProfileKey fromPlayer, + + @Syntax("") + GlobalProfileKey[] toPlayers, + + @Syntax("") + ContainerKey[] containerKeys, + + @Syntax("[profile-types]") + ProfileType[] profileTypes, + + @Syntax("[--include-groups-worlds]") + String[] flagArray + ) { + ParsedCommandFlags parsedFlags = flags.parse(flagArray); + + BulkEditAction bulkEditAction = bulkEditCreator.playerProfileClonePlayer( + fromPlayer, + new PlayerProfilesPayload( + toPlayers, + containerKeys, + profileTypes, + parsedFlags.hasFlag(flags.includeGroupsWorlds) + ) + ); + + outputActionSummary(issuer, bulkEditAction); + + commandQueueManager.addToQueue(CommandQueuePayload.issuer(issuer) + .prompt(Message.of("Are you sure you want to clone profiles from %s to %s for the selected groups/worlds?" + .formatted(fromPlayer.getPlayerName(), + toPlayers.length == 1 ? toPlayers[0].getPlayerName() : toPlayers.length + " players"))) + .action(() -> runBulkEditAction(issuer, bulkEditAction))); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/CloneWorldGroupCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/CloneWorldGroupCommand.java new file mode 100644 index 00000000..ab2f1de1 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/CloneWorldGroupCommand.java @@ -0,0 +1,92 @@ +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.external.vavr.collection.Array; +import org.mvplugins.multiverse.inventories.commands.bulkedit.BulkEditCommand; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkEditAction; +import org.mvplugins.multiverse.inventories.profile.bulkedit.BulkEditCreator; +import org.mvplugins.multiverse.inventories.profile.bulkedit.PlayerProfilesPayload; +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 java.util.Objects; + +@Service +public class CloneWorldGroupCommand extends BulkEditCommand { + + private final CommandQueueManager commandQueueManager; + private final IncludeGroupsWorldsFlag flags; + + @Inject + CloneWorldGroupCommand(@NotNull BulkEditCreator bulkEditCreator, + @NotNull CommandQueueManager commandQueueManager, + @NotNull IncludeGroupsWorldsFlag flags + ) { + super(bulkEditCreator); + this.commandQueueManager = commandQueueManager; + this.flags = flags; + } + + @Subcommand("playerprofile clone-world-group") + @CommandPermission("multiverse.inventories.bulkedit") + @CommandCompletion("@mvinvplayernames @mvinvcontainerkey @mvinvcontainerkeys @mvinvprofiletypes:multiple " + + "@flags:groupName=" + IncludeGroupsWorldsFlag.NAME) + @Syntax(" [profile-type]") + void onCommand( + MVCommandIssuer issuer, + + @Syntax("") + GlobalProfileKey[] globalProfileKeys, + + @Syntax("") + ContainerKey fromContainerKey, + + @Syntax("") + ContainerKey[] toContainerKeys, + + @Syntax("[profile-types]") + ProfileType[] profileTypes, + + @Syntax("[--include-groups-worlds]") + String[] flagArray + ) { + if (Array.of(toContainerKeys) + .find(toKey -> Objects.equals(fromContainerKey, toKey)) + .peek(toKey -> issuer.sendError("Cannot copy profiles to the same " + + toKey.getContainerType() + ": " + toKey.getDataName())) + .isDefined()) { + return; + } + + ParsedCommandFlags parsedFlags = flags.parse(flagArray); + + BulkEditAction bulkEditAction = bulkEditCreator.playerProfileCloneWorldGroup( + fromContainerKey, + new PlayerProfilesPayload( + globalProfileKeys, + toContainerKeys, + profileTypes, + parsedFlags.hasFlag(flags.includeGroupsWorlds) + ) + ); + + outputActionSummary(issuer, bulkEditAction); + + commandQueueManager.addToQueue(CommandQueuePayload.issuer(issuer) + .prompt(Message.of("Are you sure you want to clone profiles from %s %s to the selected groups/worlds?" + .formatted(fromContainerKey.getContainerType(), fromContainerKey.getDataName()))) + .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 index 7308e7be..7f151aba 100644 --- 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 @@ -6,7 +6,6 @@ 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; @@ -39,16 +38,26 @@ final class DeleteCommand extends BulkEditCommand { this.flags = flags; } - @Subcommand("bulkedit playerprofile delete") + @Subcommand("playerprofile delete") @CommandPermission("multiverse.inventories.bulkedit") - @CommandCompletion("@shares @mvinvplayernames @empty @mvinvprofiletypes:multiple @flags:groupName=" + IncludeGroupsWorldsFlag.NAME) - @Syntax(" [profile-type] [--include-groups-worlds]") + @CommandCompletion("@shares @mvinvplayernames @mvinvcontainerkeys @mvinvprofiletypes:multiple @flags:groupName=" + IncludeGroupsWorldsFlag.NAME) + @Syntax(" [profile-type] [--include-groups-worlds]") void onCommand( MVCommandIssuer issuer, - Sharable sharable, + + @Syntax("") + Sharable sharable, + + @Syntax("") GlobalProfileKey[] globalProfileKeys, + + @Syntax("") ContainerKey[] containerKeys, + + @Syntax("[profile-types]") ProfileType[] profileTypes, + + @Syntax("[--include-groups-worlds]") String[] flagArray ) { ParsedCommandFlags parsedFlags = flags.parse(flagArray); 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 593695ad..f249629b 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 @@ -40,7 +40,7 @@ final class MigrateInventorySerializationCommand extends InventoriesCommand { this.inventoriesConfig = inventoriesConfig; } - @Subcommand("bulkedit migrate inventory-serialization nbt") + @Subcommand("migrate inventory-serialization nbt") @CommandPermission("multiverse.inventories.bulkedit") void onNbtCommand(MVCommandIssuer issuer) { commandQueueManager.addToQueue(CommandQueuePayload.issuer(issuer) @@ -48,7 +48,7 @@ void onNbtCommand(MVCommandIssuer issuer) { .action(() -> doMigration(issuer, true))); } - @Subcommand("bulkedit migrate inventory-serialization bukkit") + @Subcommand("migrate inventory-serialization bukkit") @CommandPermission("multiverse.inventories.bulkedit") void onBukkitCommand(MVCommandIssuer issuer) { commandQueueManager.addToQueue(CommandQueuePayload.issuer(issuer) diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigratePlayerNameCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigratePlayerNameCommand.java index e4aec8ad..649c13f2 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigratePlayerNameCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/playerprofile/MigratePlayerNameCommand.java @@ -25,7 +25,7 @@ final class MigratePlayerNameCommand extends InventoriesCommand { this.profileDataSource = profileDataSource; } - @Subcommand("bulkedit migrate player-name") + @Subcommand("migrate player-name") @CommandPermission("multiverse.inventories.bulkedit") @Syntax(" ") @Description("Only use this if automatic migration failed for some reason.") diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/AsyncFileIO.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/AsyncFileIO.java index 267aab66..fe3d1e0a 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/AsyncFileIO.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/AsyncFileIO.java @@ -43,6 +43,25 @@ CompletableFuture queueCallable(Supplier supplier) { return future; } + CompletableFuture queueFilesAction(File[] files, Runnable action) { + CompletableFuture future = new CompletableFuture<>(); + fileIOExecutorService.submit(() -> { + for (File file : files) { + CountDownLatch toWaitLatch = fileLocks.put(file, new CountDownLatch(1)); + waitForLock(file, toWaitLatch); + } + Try tryResult = Try.runRunnable(action); + for (File file : files) { + CountDownLatch thisLatch = fileLocks.remove(file); + if (thisLatch != null) { + thisLatch.countDown(); + } + } + tryResult.onFailure(future::completeExceptionally).onSuccess(ignore -> future.complete(null)); + }); + return future; + } + CompletableFuture queueFileAction(File file, Runnable action) { CountDownLatch thisLatch = new CountDownLatch(1); CountDownLatch toWaitLatch = fileLocks.put(file, thisLatch); 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 c3ccccc2..ea17a4c1 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java @@ -21,6 +21,9 @@ import java.io.File; import java.io.IOException; +import java.nio.file.CopyOption; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -64,13 +67,12 @@ private FileConfiguration loadFileToJsonConfiguration(File file) { } private FileConfiguration getOrLoadPlayerProfileFile(ProfileFileKey profileKey, File playerFile) { - ProfileKey fileProfileKey = profileKey.forProfileType(null); return Try.of(() -> - profileCacheManager.getOrLoadPlayerFile(fileProfileKey, (key) -> playerFile.exists() + profileCacheManager.getOrLoadPlayerFile(profileKey, (key) -> playerFile.exists() ? loadFileToJsonConfiguration(playerFile) : new JsonConfiguration()) ).getOrElseThrow(e -> { - Logging.severe("Could not load profile data for player: " + fileProfileKey); + Logging.severe("Could not load profile data for player: " + profileKey); return new RuntimeException(e); }); } @@ -222,6 +224,79 @@ public CompletableFuture deletePlayerFile(ProfileFileKey profileKey) { }); } + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture clonePlayerProfiles(ProfileFileKey fromProfileKey, ProfileFileKey toProfileKey, ProfileType[] profileTypes) { + if (Strings.isNullOrEmpty(fromProfileKey.getPlayerName())) { + return CompletableFuture.failedFuture(new IllegalArgumentException("Player name cannot be null or empty. " + fromProfileKey)); + } + if (Strings.isNullOrEmpty(toProfileKey.getPlayerName())) { + return CompletableFuture.failedFuture(new IllegalArgumentException("Player name cannot be null or empty. " + toProfileKey)); + } + if (ProfileTypes.isAll(profileTypes)) { + Logging.finer("Clone profile from " + fromProfileKey + " to " + toProfileKey + " for all profile-types"); + return clonePlayerFile(fromProfileKey, toProfileKey); + } + + profileCacheManager.clearCacheForFile(toProfileKey); + + File fromPlayerFile = profileFilesLocator.getPlayerProfileFile(fromProfileKey); + File toPlayerFile = profileFilesLocator.getPlayerProfileFile(toProfileKey); + return asyncFileIO.queueFilesAction(new File[]{fromPlayerFile, toPlayerFile}, () -> + cloneSpecificProfiles(fromProfileKey, fromPlayerFile, toProfileKey, toPlayerFile, profileTypes)); + } + + private void cloneSpecificProfiles(ProfileFileKey fromProfileKey, + File fromPlayerFile, + ProfileFileKey toProfileKey, + File toPlayerFile, + ProfileType[] profileTypes) { + FileConfiguration fromPlayerData = getOrLoadPlayerProfileFile(fromProfileKey, fromPlayerFile); + FileConfiguration toPlayerData = getOrLoadPlayerProfileFile(toProfileKey, toPlayerFile); + + for (var profileType : profileTypes) { + ConfigurationSection fromSection = fromPlayerData.getConfigurationSection(profileType.getName()); + if (fromSection != null && !fromSection.getKeys(false).isEmpty()) { + toPlayerData.set(profileType.getName(), fromSection); + } else { + toPlayerData.set(profileType.getName(), null); + } + } + + Try.run(() -> toPlayerData.save(toPlayerFile)).onFailure(e -> { + Logging.severe("Could not clone specific profiles from player " + fromProfileKey.getPlayerName() + + " to " + toProfileKey.getPlayerName() + + " in " + fromProfileKey.getContainerType() + " " + fromProfileKey.getDataName()); + Logging.severe(e.getMessage()); + }); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture clonePlayerFile(ProfileFileKey fromProfileKey, ProfileFileKey toProfileKey) { + File fromPlayerFile = profileFilesLocator.getPlayerProfileFile(fromProfileKey); + File toPlayerFile = profileFilesLocator.getPlayerProfileFile(toProfileKey); + if (!fromPlayerFile.exists()) { + Logging.finer("Attempted to clone file that did not exist for player " + fromProfileKey.getPlayerName() + + " in " + fromProfileKey.getContainerType() + " " + fromProfileKey.getDataName()); + return CompletableFuture.completedFuture(null); + } + + profileCacheManager.clearCacheForFile(toProfileKey); + + return asyncFileIO.queueFilesAction(new File[]{fromPlayerFile, toPlayerFile}, () -> { + Try.run(() -> Files.copy(fromPlayerFile.toPath(), toPlayerFile.toPath(), StandardCopyOption.REPLACE_EXISTING)) + .onFailure(e -> { + Logging.severe("Could not clone file " + fromPlayerFile + " to " + toPlayerFile); + e.printStackTrace(); + }); + }); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileCacheManager.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileCacheManager.java index 1c2b5814..584d0f9d 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileCacheManager.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileCacheManager.java @@ -12,7 +12,10 @@ import org.mvplugins.multiverse.external.vavr.control.Option; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; +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 java.util.HashMap; import java.util.Map; @@ -27,7 +30,7 @@ @Service public final class ProfileCacheManager { - private final Cache playerFileCache; + private final Cache playerFileCache; private final AsyncCache playerProfileCache; private final AsyncCache globalProfileCache; @@ -54,8 +57,8 @@ public final class ProfileCacheManager { .buildAsync(); } - FileConfiguration getOrLoadPlayerFile(ProfileKey key, Function mappingFunction) { - return playerFileCache.get(key, mappingFunction); + FileConfiguration getOrLoadPlayerFile(ProfileFileKey key, Function mappingFunction) { + return playerFileCache.get(ProfileFileKey.copyOf(key), mappingFunction); } CompletableFuture getOrLoadPlayerProfile(ProfileKey key, BiFunction> mappingFunction) { @@ -70,6 +73,24 @@ Option getCachedPlayerProfile(ProfileKey key) { return Option.of(playerProfileCache.synchronous().getIfPresent(key)); } + void clearCacheForProfile(ProfileKey profileKey) { + playerFileCache.invalidate(profileKey); + playerProfileCache.synchronous().invalidate(profileKey); + } + + void clearCacheForFile(ProfileFileKey profileKey) { + ProfileFileKey profileFileKeyCopy = ProfileFileKey.copyOf(profileKey); + playerFileCache.invalidate(profileFileKeyCopy); + for (ProfileType profileType : ProfileTypes.getTypes()) { + playerProfileCache.synchronous().invalidate(profileFileKeyCopy.forProfileType(profileType)); + } + } + + void clearProfileFileCache(Predicate predicate) { + playerFileCache.invalidateAll(Sets.filter(playerFileCache.asMap().keySet(), predicate::test)); + playerProfileCache.synchronous().invalidateAll(Sets.filter(playerProfileCache.asMap().keySet(), predicate::test)); + } + public void clearPlayerCache(String playerName) { clearPlayerProfileCache(key -> key.getPlayerName().equals(playerName)); } @@ -80,7 +101,7 @@ public void clearPlayerCache(UUID playerUUID) { } public void clearPlayerProfileCache(Predicate predicate) { - playerFileCache.invalidateAll(Sets.filter(playerFileCache.asMap().keySet(), predicate::test)); + playerFileCache.invalidateAll(Sets.filter(playerFileCache.asMap().keySet(), key -> predicate.test(key.forProfileType(null)))); playerProfileCache.synchronous().invalidateAll(Sets.filter(playerProfileCache.asMap().keySet(), predicate::test)); } 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 c77bbe1d..d0676cbe 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/ProfileDataSource.java @@ -1,8 +1,10 @@ package org.mvplugins.multiverse.inventories.profile; +import org.jetbrains.annotations.ApiStatus; import org.jvnet.hk2.annotations.Contract; import org.mvplugins.multiverse.external.vavr.control.Option; import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; +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; @@ -70,6 +72,32 @@ public sealed interface ProfileDataSource permits FlatFileProfileDataSource { */ CompletableFuture deletePlayerFile(ProfileFileKey profileKey); + /** + * Clones all data of selected {@link ProfileType} from one player profile to another. + * + * @param fromProfileKey The key of the profile to clone from. + * @param toProfileKey The key of the profile to clone to. + * @param profileTypes The list of profile types to clone. + * @return Future that completes when the profile has been cloned. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + CompletableFuture clonePlayerProfiles(ProfileFileKey fromProfileKey, ProfileFileKey toProfileKey, ProfileType[] profileTypes); + + /** + * Clones all data from one player profile to another for all profile types. + * Effectively same as copying the entire profile file. + * + * @param fromProfileKey The key of the profile to clone from. + * @param toProfileKey The key of the profile to clone to. + * @return Future that completes when the profile has been cloned. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + CompletableFuture clonePlayerFile(ProfileFileKey fromProfileKey, ProfileFileKey toProfileKey); + /** * Copies all the data belonging to oldName to newName and removes the old data. * diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/AllSharesPlayerFileAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/AllSharesPlayerFileAction.java new file mode 100644 index 00000000..406fbbc7 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/AllSharesPlayerFileAction.java @@ -0,0 +1,60 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit; + +import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.entity.Player; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.handleshare.ReadOnlyShareHandler; +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.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; + +abstract sealed class AllSharesPlayerFileAction extends PlayerFileAction + permits PlayerProfileClearAction, PlayerProfileCloneWorldGroupAction, PlayerProfileClonePlayerAction { + + private final WorldGroupManager worldGroupManager; + private final Set profileTypesSet; + + AllSharesPlayerFileAction(MultiverseInventories inventories, PlayerProfilesPayload bulkProfilesPayload) { + super(inventories, bulkProfilesPayload); + this.worldGroupManager = inventories.getServiceLocator().getService(WorldGroupManager.class); + this.profileTypesSet = Set.of(bulkProfilesPayload.profileTypes()); + } + + @Override + protected boolean isOnlinePlayerAffected(ProfileFileKey key, Player player) { + if (!profileTypesSet.contains(ProfileTypes.forPlayer(player))) { + return false; + } + + // Gets groups that share this sharable + String playerWorldName = player.getWorld().getName(); + List groups = worldGroupManager.getGroupsForWorld(playerWorldName); + + Shares unhandledSharables = Sharables.enabledOf(); + for (WorldGroup worldGroup : groups) { + unhandledSharables.removeAll(worldGroup.getApplicableShares()); + } + + if (!unhandledSharables.isEmpty()) { + return key.getContainerType() == ContainerType.WORLD && playerWorldName.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) { + Logging.finer("Updating online player after bulkedit: " + player.getName()); + new ReadOnlyShareHandler(inventories, player).handleSharing(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkEditCreator.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkEditCreator.java index 851bd8af..11c96fe9 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkEditCreator.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/BulkEditCreator.java @@ -5,6 +5,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.key.ContainerKey; import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; import org.mvplugins.multiverse.inventories.share.Sharable; @@ -23,6 +24,16 @@ public BulkEditAction globalProfileClear(GlobalProfileKey[] globalProfileKeys return new GlobalProfileClearAction(inventories, globalProfileKeys, clearPlayerProfiles); } + @ApiStatus.AvailableSince("5.3") + public BulkEditAction playerProfileCloneWorldGroup(ContainerKey fromContainerKey, PlayerProfilesPayload toProfiles) { + return new PlayerProfileCloneWorldGroupAction(inventories, toProfiles, fromContainerKey); + } + + @ApiStatus.AvailableSince("5.3") + public BulkEditAction playerProfileClonePlayer(GlobalProfileKey fromPlayer, PlayerProfilesPayload toProfiles) { + return new PlayerProfileClonePlayerAction(inventories, toProfiles, fromPlayer); + } + public BulkEditAction playerProfileClear(PlayerProfilesPayload bulkProfilesPayload) { return new PlayerProfileClearAction(inventories, bulkProfilesPayload); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerFileAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerFileAction.java index 94a24e18..f19be39e 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerFileAction.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerFileAction.java @@ -1,13 +1,13 @@ package org.mvplugins.multiverse.inventories.profile.bulkedit; -import org.jetbrains.annotations.ApiStatus; import org.mvplugins.multiverse.inventories.MultiverseInventories; import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; import java.util.List; import java.util.Map; -abstract sealed class PlayerFileAction extends BulkEditAction permits PlayerProfileClearAction { +abstract sealed class PlayerFileAction extends BulkEditAction + permits AllSharesPlayerFileAction { private final PlayerProfilesAggregator profilesAggregator; protected final PlayerProfilesPayload bulkProfilesPayload; diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClearAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClearAction.java index 45d44acb..5812f33b 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClearAction.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClearAction.java @@ -1,31 +1,14 @@ package org.mvplugins.multiverse.inventories.profile.bulkedit; -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.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.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; -final class PlayerProfileClearAction extends PlayerFileAction { - - private final WorldGroupManager worldGroupManager; - private final Set profileTypesSet; +final class PlayerProfileClearAction extends AllSharesPlayerFileAction { public PlayerProfileClearAction(MultiverseInventories inventories, PlayerProfilesPayload bulkProfilesPayload) { super(inventories, bulkProfilesPayload); - this.worldGroupManager = inventories.getServiceLocator().getService(WorldGroupManager.class); - this.profileTypesSet = Set.of(bulkProfilesPayload.profileTypes()); } @Override @@ -36,32 +19,4 @@ protected CompletableFuture performAction(ProfileFileKey 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/PlayerProfileClonePlayerAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClonePlayerAction.java new file mode 100644 index 00000000..5749844a --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileClonePlayerAction.java @@ -0,0 +1,29 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit; + +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; + +import java.util.concurrent.CompletableFuture; + +final class PlayerProfileClonePlayerAction extends AllSharesPlayerFileAction { + + private final GlobalProfileKey fromPlayer; + + PlayerProfileClonePlayerAction(MultiverseInventories inventories, + PlayerProfilesPayload bulkProfilesPayload, + GlobalProfileKey fromPlayer) { + super(inventories, bulkProfilesPayload); + this.fromPlayer = fromPlayer; + } + + @Override + protected CompletableFuture performAction(ProfileFileKey key) { + ProfileFileKey fromKey = ProfileFileKey.of(key.getContainerType(), key.getDataName(), fromPlayer); + return profileDataSource.clonePlayerProfiles(fromKey, key, bulkProfilesPayload.profileTypes()) + .thenCompose(ignore -> profileDataSource.modifyGlobalProfile( + key, + profile -> profile.setLoadOnLogin(true) + )); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileCloneWorldGroupAction.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileCloneWorldGroupAction.java new file mode 100644 index 00000000..3d62c37d --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/bulkedit/PlayerProfileCloneWorldGroupAction.java @@ -0,0 +1,29 @@ +package org.mvplugins.multiverse.inventories.profile.bulkedit; + +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.key.ContainerKey; +import org.mvplugins.multiverse.inventories.profile.key.ProfileFileKey; + +import java.util.concurrent.CompletableFuture; + +final class PlayerProfileCloneWorldGroupAction extends AllSharesPlayerFileAction { + + private final ContainerKey fromContainerKey; + + PlayerProfileCloneWorldGroupAction(MultiverseInventories inventories, + PlayerProfilesPayload bulkProfilesPayload, + ContainerKey fromContainerKey) { + super(inventories, bulkProfilesPayload); + this.fromContainerKey = fromContainerKey; + } + + @Override + protected CompletableFuture performAction(ProfileFileKey key) { + ProfileFileKey fromKey = key.forContainer(fromContainerKey); + return profileDataSource.clonePlayerProfiles(fromKey, key, bulkProfilesPayload.profileTypes()) + .thenCompose(ignore -> profileDataSource.modifyGlobalProfile( + key, + profile -> profile.setLoadOnLogin(true) + )); + } +} 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 47537f8a..1b9c4973 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 @@ -19,6 +19,15 @@ public static ProfileFileKey fromPlayerProfile(PlayerProfile profile) { ); } + public static ProfileFileKey copyOf(ProfileFileKey key) { + return of( + key.getContainerType(), + key.getDataName(), + key.getPlayerUUID(), + key.getPlayerName() + ); + } + public static ProfileFileKey of( ContainerType containerType, String dataName, @@ -64,6 +73,10 @@ public ProfileKey forProfileType(@Nullable ProfileType profileType) { return ProfileKey.of(containerType, dataName, profileType, playerUUID, playerName); } + public ProfileFileKey forContainer(@NotNull ContainerKey containerKey) { + return new ProfileFileKey(containerKey.getContainerType(), containerKey.getDataName(), playerUUID, playerName); + } + public ProfileFileKey forContainerType(@NotNull ContainerType containerType) { return new ProfileFileKey(containerType, dataName, playerUUID, playerName); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java index 557f02dc..4a7a171b 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java @@ -65,6 +65,12 @@ private ProfileKey( this.profileType = profileType; } + @Override + public ProfileKey forContainer(@NotNull ContainerKey containerKey) { + return new ProfileKey(containerKey.getContainerType(), containerKey.getDataName(), + profileType, playerUUID, playerName); + } + @Override public ProfileKey forContainerType(@NotNull ContainerType containerType) { return new ProfileKey(containerType, dataName, profileType, playerUUID, playerName); From a939d6dc40f36ac7a96f13a135bb5fe6eacc12b2 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:15:07 +0800 Subject: [PATCH 2/3] Fix command count test --- .../java/org/mvplugins/multiverse/inventories/InjectionTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/InjectionTest.kt index 6466267e..aa61c4a9 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(32, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) + assertEquals(34, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size) } @Test From a67b569c7bd7b7bc7bc1a9041799cf22c054d5db Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:17:25 +0800 Subject: [PATCH 3/3] Add ApiStatus annotations to ProfileKey and ProfileFileKey for versioning --- .../multiverse/inventories/profile/key/ProfileFileKey.java | 3 +++ .../multiverse/inventories/profile/key/ProfileKey.java | 2 ++ 2 files changed, 5 insertions(+) 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 1b9c4973..3bf83d00 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 @@ -2,6 +2,7 @@ import com.google.common.base.Objects; import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; @@ -19,6 +20,7 @@ public static ProfileFileKey fromPlayerProfile(PlayerProfile profile) { ); } + @ApiStatus.AvailableSince("5.3") public static ProfileFileKey copyOf(ProfileFileKey key) { return of( key.getContainerType(), @@ -73,6 +75,7 @@ public ProfileKey forProfileType(@Nullable ProfileType profileType) { return ProfileKey.of(containerType, dataName, profileType, playerUUID, playerName); } + @ApiStatus.AvailableSince("5.3") public ProfileFileKey forContainer(@NotNull ContainerKey containerKey) { return new ProfileFileKey(containerKey.getContainerType(), containerKey.getDataName(), playerUUID, playerName); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java index 4a7a171b..eef5c828 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/key/ProfileKey.java @@ -2,6 +2,7 @@ import com.google.common.base.Objects; import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.inventories.profile.data.PlayerProfile; @@ -65,6 +66,7 @@ private ProfileKey( this.profileType = profileType; } + @ApiStatus.AvailableSince("5.3") @Override public ProfileKey forContainer(@NotNull ContainerKey containerKey) { return new ProfileKey(containerKey.getContainerType(), containerKey.getDataName(),