Skip to content
Open

V5.4 #3361

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c068225
Implement ConcurrentPlayerWorldTracker to allow for async online play…
benwoo1110 Oct 1, 2025
58ac0f5
Make tab completion for players a non-async call
benwoo1110 Oct 12, 2025
ce3b6ab
Make getOnlinePlayers unmodifiable and getPlayerWorld return option
benwoo1110 Oct 12, 2025
8d06b92
Implement display message for destination instances to improve telepo…
benwoo1110 Oct 12, 2025
29cc974
Add @since 5.4 annotations to ConcurrentPlayerWorldTracker methods
benwoo1110 Oct 13, 2025
55d1347
Merge pull request #3360 from Multiverse/feat/destination-message
benwoo1110 Oct 13, 2025
fdd9257
Merge pull request #3359 from Multiverse/fix/async-online-players
benwoo1110 Oct 13, 2025
79a71fa
Add a silent flag to turn off teleport output message
benwoo1110 Oct 15, 2025
8bffffd
Merge pull request #3364 from Multiverse/feat/silent-teleport
benwoo1110 Oct 16, 2025
4ef8390
Minor perf improvement to suggestDestinationsWithPerms
benwoo1110 Oct 26, 2025
3e4ded5
Merge branch 'main' into v5.4
benwoo1110 Oct 26, 2025
c4e4e31
Merge pull request #3368 from Multiverse/perf/suggest-destination
benwoo1110 Oct 26, 2025
ff2135c
Add simplified destination tab completion configuration option
benwoo1110 Oct 26, 2025
b52d657
Fix config test
benwoo1110 Oct 26, 2025
0e0e4e0
Register permission with its wildcards explicitly
benwoo1110 Oct 30, 2025
f7d8688
Merge pull request #3369 from Multiverse/feat/simplified-destination
benwoo1110 Oct 30, 2025
147564c
Merge pull request #3372 from Multiverse/fix/perm-register
benwoo1110 Nov 13, 2025
eed3353
Refactor Attempt interface to remove default method implementations a…
benwoo1110 Nov 18, 2025
3629bab
Remove unnecessary blank line in CloneWorldOptions.java
benwoo1110 Nov 18, 2025
82799cd
Open up PlayerFinder::isSelector method
benwoo1110 Nov 18, 2025
7207d93
Refactor IssuerAwareContextBuilder and add maxArgForAware option
benwoo1110 Nov 18, 2025
f181bb7
Better error message for teleport command when no players match selec…
benwoo1110 Nov 18, 2025
e207eb5
Merge branch 'main' into v5.4
benwoo1110 Nov 18, 2025
4bcebd5
Merge pull request #3381 from Multiverse/chore/whitespace
benwoo1110 Nov 18, 2025
2bb6010
Merge pull request #3382 from Multiverse/refactor/attempt
benwoo1110 Nov 18, 2025
6dcb40f
Merge pull request #3380 from Multiverse/fix/issuer-aware-argument
benwoo1110 Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.NotNull;
Expand All @@ -43,9 +44,9 @@
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.StringFormatter;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
Expand Down Expand Up @@ -100,7 +101,7 @@ public class MVCommandCompletions extends PaperCommandCompletions {
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
registerAsyncCompletion("mvworldpropsname", this::suggestMVWorldPropsName);
registerAsyncCompletion("mvworldpropsvalue", this::suggestMVWorldPropsValue);
registerAsyncCompletion("playersarray", this::suggestPlayersArray);
registerCompletion("playersarray", this::suggestPlayersArray); // getting online players cannot be async
registerStaticCompletion("propsmodifyaction", suggestEnums(PropertyModifyAction.class));
registerStaticCompletion("spawncategories", suggestEnums(SpawnCategory.class));
registerAsyncCompletion("spawncategorypropsname", this::suggestSpawnCategoryPropsName);
Expand Down Expand Up @@ -224,18 +225,22 @@ private Collection<String> suggestDestinations(BukkitCommandCompletionContext co
// Most likely console did not specify a player
return Collections.<String>emptyList();
}
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(context.getIssuer().getIssuer()))) {
CommandSender sender = context.getIssuer().getIssuer();
if (context.hasConfig("othersOnly") && (players.length == 1 && players[0].equals(sender))) {
return Collections.<String>emptyList();
}
return suggestDestinationsWithPerms(context.getIssuer().getIssuer(), players, context.getInput());
return suggestDestinationsWithPerms(sender, Arrays.asList(players), context.getInput());
})
.getOrElse(Collections.emptyList());
}

private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, Player[] players, String deststring) {
private Collection<String> suggestDestinationsWithPerms(CommandSender teleporter, List<Entity> teleportees, String deststring) {
return destinationsProvider.suggestDestinations(teleporter, deststring).stream()
.filter(packet -> !config.getSimplifiedDestinationTabCompletion()
|| packet.destination() instanceof WorldDestination
|| deststring.startsWith(packet.destination().getIdentifier() + ":"))
.filter(packet -> corePermissionsChecker
.checkDestinationPacketPermission(teleporter, Arrays.asList(players), packet))
.checkDestinationPacketPermission(teleporter, teleportees, packet))
.map(DestinationSuggestionPacket::parsableString)
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,12 @@ private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
})
.issuerOnlyFailMessage((context) -> Message.of("This command can only be used by a player."))
.issuerAwareInputFailMessage((context, input) -> Message.of("Invalid player: " + input + ". Either specify an online player or use this command as a player."))
.inputOnlyFailMessage((context, input) -> Message.of("Player " + input + " not found."));
.inputOnlyFailMessage((context, input) -> {
if (PlayerFinder.isSelector(input)) {
return Message.of("No player(s) matched selector: " + input + ".");
}
return Message.of("Player(s) " + input + " not found.");
});
}

private PlayerLocation parsePlayerLocation(BukkitCommandExecutionContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,48 +123,8 @@
*/
@ApiStatus.AvailableSince("5.1")
public IssuerAwareContextResolver<T, BukkitCommandExecutionContext> generateContext() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);

return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");

if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return result;
}
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}

String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return result;
}

if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return result;
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}

throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
validateRequiredVariables();
return context -> resolveValue(context, GenericIssuerAwareValue::new).value;
}

/**
Expand All @@ -178,49 +138,66 @@
*/
@ApiStatus.AvailableSince("5.1")
public <I extends IssuerAwareValue> IssuerAwareContextResolver<I, BukkitCommandExecutionContext> generateContext(BiFunction<Boolean, T, I> createValue) {
// todo: This is a copy and paste from above
validateRequiredVariables();
return context -> resolveValue(context, createValue);
}

private void validateRequiredVariables() {
Objects.requireNonNull(fromPlayer);
Objects.requireNonNull(fromInput);
Objects.requireNonNull(issuerOnlyFailMessage);
Objects.requireNonNull(issuerAwarePlayerFailMessage);
Objects.requireNonNull(issuerAwareInputFailMessage);
Objects.requireNonNull(inputOnlyFailMessage);
}

return context -> {
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");
private <I extends IssuerAwareValue> I resolveValue(BukkitCommandExecutionContext context, BiFunction<Boolean, T, I> createValue) {

Check warning on line 154 in src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Cyclomatic Complexity is 10 (max allowed is 7). Raw Output: /github/workspace/./src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java:154:5: warning: Cyclomatic Complexity is 10 (max allowed is 7). (com.puppycrawl.tools.checkstyle.checks.metrics.CyclomaticComplexityCheck)

Check warning on line 154 in src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Return count is 3 (max allowed for non-void methods/lambdas is 2). Raw Output: /github/workspace/./src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java:154:5: info: Return count is 3 (max allowed for non-void methods/lambdas is 2). (com.puppycrawl.tools.checkstyle.checks.coding.ReturnCountCheck)

Check warning on line 154 in src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Line is longer than 120 characters (found 135). Raw Output: /github/workspace/./src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java:154:0: warning: Line is longer than 120 characters (found 135). (com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck)
BukkitCommandIssuer issuer = context.getIssuer();
String resolve = context.getFlagValue("resolve", "");

if (resolve.equals("issuerOnly")) {
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
if (resolve.equals("issuerOnly")) {

Check warning on line 158 in src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 String literal expressions should be on the left side of an equals comparison. Raw Output: /github/workspace/./src/main/java/org/mvplugins/multiverse/core/command/context/issueraware/IssuerAwareContextBuilder.java:158:27: warning: String literal expressions should be on the left side of an equals comparison. (com.puppycrawl.tools.checkstyle.checks.coding.EqualsAvoidNullCheck)
if (issuer.isPlayer()) {
T result = fromPlayer.apply(context, issuer.getPlayer());
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}

String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}

if (resolve.equals("issuerAware")) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
throw new InvalidCommandArgument(issuerOnlyFailMessage.apply(context).formatted(issuer));
}

String input = context.getFirstArg();
T result = fromInput.apply(context, input);
if (result != null) {
context.popFirstArg();
return createValue.apply(false, result);
}

int maxArgForAware = context.getFlagValue("maxArgForAware", Integer.MAX_VALUE);
long argLengthWithoutFlags = context.getArgs().stream()
.takeWhile(value -> !value.startsWith("--") && !value.isEmpty())
.count();

if (resolve.equals("issuerAware") && argLengthWithoutFlags <= maxArgForAware) {
if (issuer.isPlayer()) {
Player player = issuer.getPlayer();
result = fromPlayer.apply(context, player);
if (result != null) {
return createValue.apply(true, result);
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
throw new InvalidCommandArgument(issuerAwarePlayerFailMessage.apply(context, player).formatted(issuer));
}
throw new InvalidCommandArgument(issuerAwareInputFailMessage.apply(context, input).formatted(issuer));
}

throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
}

private static class GenericIssuerAwareValue<T> extends IssuerAwareValue {
private final T value;

throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
};
public GenericIssuerAwareValue(boolean byIssuer, T value) {
super(byIssuer);
this.value = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@

import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.core.command.context.issueraware.PlayerArrayValue;
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
import org.mvplugins.multiverse.core.command.flag.ParsedCommandFlags;
import org.mvplugins.multiverse.core.command.flags.PageFilterFlags;

Check warning on line 25 in src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Unused import - org.mvplugins.multiverse.core.command.flags.PageFilterFlags. Raw Output: /github/workspace/./src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java:25:8: warning: Unused import - org.mvplugins.multiverse.core.command.flags.PageFilterFlags. (com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck)
import org.mvplugins.multiverse.core.command.flags.UnsafeFlags;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
Expand All @@ -35,14 +38,14 @@
private final CoreConfig config;
private final CorePermissionsChecker permissionsChecker;
private final AsyncSafetyTeleporter safetyTeleporter;
private final UnsafeFlags flags;
private final Flags flags;

@Inject
TeleportCommand(
@NotNull CoreConfig config,
@NotNull CorePermissionsChecker permissionsChecker,
@NotNull AsyncSafetyTeleporter safetyTeleporter,
@NotNull UnsafeFlags flags
@NotNull Flags flags
) {
this.config = config;
this.permissionsChecker = permissionsChecker;
Expand All @@ -54,14 +57,14 @@
@Subcommand("teleport|tp")
@CommandPermission("@mvteleport")
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + UnsafeFlags.NAME)
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME)
@Syntax("[player] <destination> [--unsafe]")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
MVCommandIssuer issuer,

@Flags("resolve=issuerAware")
@co.aikar.commands.annotation.Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[player]")
@Description("{@@mv-core.teleport.player.description}")
PlayerArrayValue playersValue,
Expand Down Expand Up @@ -103,16 +106,21 @@
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleportSingle(player)
.onSuccess(() -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.toString())))
.onSuccess(() -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(getYouOrName(issuer, player)),
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(player.getName()),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
Expand All @@ -136,18 +144,38 @@
.checkSafety(!parsedFlags.hasFlag(flags.unsafe) && destination.checkTeleportSafety())
.passengerMode(config.getPassengerMode())
.teleport(List.of(players))
.onSuccessCount(successCount -> issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),
Replace.DESTINATION.with(destination.toString())))
.onSuccessCount(successCount -> {
if (parsedFlags.hasFlag(flags.silent)) {
return;
}
issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS,
Replace.PLAYER.with(successCount + " players"),

Check warning on line 152 in src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 The String " players" appears 2 times in the file. Raw Output: /github/workspace/./src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java:152:64: warning: The String " players" appears 2 times in the file. (com.puppycrawl.tools.checkstyle.checks.coding.MultipleStringLiteralsCheck)
Replace.DESTINATION.with(destination.getDisplayMessage()));
})
.onFailureCount(reasonsCountMap -> {
for (var entry : reasonsCountMap.entrySet()) {
Logging.finer("Failed to teleport %s players to %s: %s",
entry.getValue(), destination, entry.getKey());
issuer.sendError(MVCorei18n.TELEPORT_FAILED,
Replace.PLAYER.with(entry.getValue() + " players"),
Replace.DESTINATION.with(destination.toString()),
Replace.DESTINATION.with(destination.getDisplayMessage()),
Replace.REASON.with(Message.of(entry.getKey())));
}
});
}

@Service
private static final class Flags extends UnsafeFlags {

private static final String NAME = "mvteleport";

@Inject
private Flags(@NotNull CommandFlagsManager flagsManager) {
super(NAME, flagsManager);
}

private final CommandFlag silent = flag(CommandFlag.builder("--silent")
.addAlias("-s")
.build());
}
}
14 changes: 10 additions & 4 deletions src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;

import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.command.MVCommandManager;
import org.mvplugins.multiverse.core.command.queue.ConfirmMode;
import org.mvplugins.multiverse.core.config.handle.CommentedConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
Expand All @@ -28,7 +25,6 @@
import org.mvplugins.multiverse.core.config.migration.action.MoveMigratorAction;
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
import org.mvplugins.multiverse.core.config.migration.action.SetMigratorAction;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.teleportation.PassengerMode;
import org.mvplugins.multiverse.core.teleportation.PassengerModes;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
Expand Down Expand Up @@ -539,6 +535,16 @@ public boolean getResolveAliasName() {
return configHandle.get(configNodes.resolveAliasName);
}

@ApiStatus.AvailableSince("5.4")
public Try<Void> setSimplifiedDestinationTabCompletion(boolean simplifiedDestinationTabCompletion) {
return configHandle.set(configNodes.simplifiedDestinationTabCompletion, simplifiedDestinationTabCompletion);
}

@ApiStatus.AvailableSince("5.4")
public boolean getSimplifiedDestinationTabCompletion() {
return configHandle.get(configNodes.simplifiedDestinationTabCompletion);
}

/**
* {@inheritDoc}
*/
Expand Down
Loading
Loading