Compare commits

...
74 Commits
Author SHA1 Message Date
Ben Woo 72ee0d5bed Merge pull request #3402 from Multiverse/fix/on-change-node
Always fire onChange for node even if old and new values may be equal
2025-12-12 12:01:31 +08:00
Ben Woo 1afecd40ea Merge pull request #3401 from Multiverse/chore/1.21.11
Update platforms supported version for 1.21.11
2025-12-12 11:57:24 +08:00
Ben Woo 00944ed3b2 Always fire onChange for node even if old and new values may be equal 2025-12-12 11:56:50 +08:00
Ben Woo 5f659a80d8 Update platforms supported version for 1.21.11 2025-12-11 14:24:39 +08:00
Ben Woo 5f9b0af8d1 Merge pull request #3400 from Multiverse/feat/notify-restart
Add restart message for config options that only applies after a server restart
2025-12-10 10:44:34 +08:00
Ben Woo 5c44c05f6f Add restart message for config options that only applies after a server restart 2025-12-09 20:48:04 +08:00
Ben Woo 1699a47011 Merge pull request #3396 from Multiverse/feat/unset-entity-rate
Default spawn rate and limit to "unset" instead of server wide's default
2025-12-09 17:16:27 +08:00
Ben Woo 96deee719b Merge pull request #3397 from Multiverse/fix/config-reset
Remove config saving on plugin disable
2025-12-09 17:15:33 +08:00
Ben Woo 8a60761098 Merge pull request #3398 from Multiverse/fix/arg-handling
Add maxArgForAware flag to gamerule commands for improved argument handling
2025-12-09 17:14:35 +08:00
Ben Woo a38b4e03b4 Merge pull request #3399 from Multiverse/fix/localise-replacements
Properly localise message replacement even if message itself is not localised
2025-12-09 17:14:18 +08:00
Ben Woo db9b725e10 Properly localise message replacement even if message itself is not localised 2025-12-09 16:17:43 +08:00
Ben Woo d43b23e7ea Add maxArgForAware flag to gamerule commands for improved argument handling 2025-12-09 15:59:57 +08:00
Ben Woo 86d77ae796 Remove config saving on plugin disable 2025-12-09 15:50:38 +08:00
Ben Woo 86bf785c84 Improve config saving error checking 2025-12-09 15:50:19 +08:00
Ben Woo 626537290d Default spawn rate and limit to "unset" instead of server wide's default 2025-12-09 14:41:01 +08:00
Ben Woo 9fa47727f4 Merge pull request #3395 from Multiverse/feat/node-load-change
Refactor configuration node on set callbacks to split up load and change events
2025-12-09 12:34:12 +08:00
Ben Woo ae3bc6d201 Rename NodeLoadCallback to NodeValueCallback to extend it potential use case 2025-12-09 11:28:02 +08:00
Ben Woo b1d126397e Refactor configuration node on set callbacks to split up load and change events 2025-12-08 21:18:10 +08:00
Ben Woo 8ab9993e97 Merge pull request #3394 from Multiverse/feat/case-insensitive
Make world name matching case-insensitive to follow papermc behvaiour
2025-12-08 21:03:28 +08:00
Ben Woo 13b949c692 Make world name matching case-insensitive to follow papermc behvaiour 2025-12-08 17:39:10 +08:00
Ben Woo 2b41c1367e Merge pull request #3393 from Multiverse/feat/more-patterns
Add more useful common regex patterns
2025-12-08 17:21:25 +08:00
Ben Woo fde3f388bd Add more useful common regex patterns 2025-12-07 20:56:00 +08:00
Ben Woo eb3968b7d6 Merge pull request #3391 from Multiverse/feat/sort-plugin-list
Sort plugin list alphabetically for dumps
2025-12-04 10:20:19 +08:00
Ben Woo fed1fdcbea Sort plugin list alphabetically for dumps 2025-12-03 10:34:05 +08:00
Ben Woo bfb6fea5f5 Merge pull request #3390 from Multiverse/feat/placeholder-playercount
Add player count placeholder
2025-12-02 23:13:13 +08:00
Ben Woo 5b02452a3d Add player count placeholder 2025-12-02 23:03:30 +08:00
Ben Woo 4a2bfc74fa Merge pull request #3361 from Multiverse/v5.4
V5.4
2025-12-01 17:06:01 +08:00
Ben Woo f0539441cf Merge pull request #3388 from Multiverse/chore/purpur-loader
Add purpur as a supported loader tag on modrinth
2025-12-01 17:04:51 +08:00
Ben Woo efe490d8c4 Add purpur as a supported loader tag on modrinth 2025-12-01 16:55:41 +08:00
Ben Woo 3074cfab17 Fix typo in maxArgForAware for who command 2025-12-01 16:51:16 +08:00
Ben Woo 7a5dd44feb Merge pull request #3383 from Multiverse/refactor/using-option
Refactor world manager methods with better use of streams and option
2025-12-01 16:41:21 +08:00
Ben Woo 88a9ee5b6d Merge pull request #3387 from Multiverse/fix/selectors-error-spam
Fix selectors spamming stacktrace and improve invalid format message
2025-12-01 16:41:02 +08:00
Ben Woo 46f67240e8 Merge pull request #3384 from Multiverse/fix/issuer-aware
Fix ambiguous error message with issue aware args is not found by using maxArgForAware
2025-12-01 16:40:45 +08:00
Ben Woo 89e765b2e8 Fix selectors spamming stacktrace and improve invalid format message 2025-11-29 11:15:04 +08:00
Ben Woo b8bdec22d3 Fix ambiguous error message with issue aware args is not found by using maxArgForAware 2025-11-19 15:32:26 +08:00
Ben Woo 792913cff7 Refactor world manager methods with better use of streams and option 2025-11-19 15:00:39 +08:00
Ben Woo 6dcb40fdcb Merge pull request #3380 from Multiverse/fix/issuer-aware-argument
Better error message for teleport command when no players match selectors
2025-11-19 11:25:46 +08:00
Ben Woo 2bb601098a Merge pull request #3382 from Multiverse/refactor/attempt
Refactor Attempt interface to remove default method implementations and enforce contract
2025-11-18 16:40:22 +08:00
Ben Woo 4bcebd51f5 Merge pull request #3381 from Multiverse/chore/whitespace
Remove unnecessary blank line in CloneWorldOptions.java
2025-11-18 16:40:01 +08:00
Ben Woo e207eb5d0d Merge branch 'main' into v5.4 2025-11-18 16:37:44 +08:00
Ben Woo f181bb7ade Better error message for teleport command when no players match selectors. 2025-11-18 16:34:38 +08:00
Ben Woo 7207d9303f Refactor IssuerAwareContextBuilder and add maxArgForAware option 2025-11-18 16:34:00 +08:00
Ben Woo 82799cdbe8 Open up PlayerFinder::isSelector method 2025-11-18 16:31:12 +08:00
Ben Woo 3629babdbc Remove unnecessary blank line in CloneWorldOptions.java 2025-11-18 15:34:49 +08:00
Ben Woo eed3353e2b Refactor Attempt interface to remove default method implementations and enforce contract 2025-11-18 15:34:28 +08:00
Ben Woo 147564caab Merge pull request #3372 from Multiverse/fix/perm-register
Register permission with its wildcards explicitly
2025-11-13 10:52:04 +08:00
Ben Woo 30e50866bd Merge pull request #3376 from Multiverse/fix/java-17-issue
Fix compatibility with Java 17
2025-11-05 17:29:46 +08:00
Ben Woo 5dde7608f2 Merge pull request #3377 from Multiverse/fix/checkstyle-action
Bump checkstyle action version
2025-11-05 17:29:11 +08:00
Ben Woo 2f9915c62e Bump checkstyle action version 2025-11-05 17:28:36 +08:00
Ben Woo e62e37831b Fix compatibility with Java 17 2025-11-05 10:09:28 +08:00
Ben Woo ef9934f3c6 Merge pull request #3373 from riku6460/fix-set-spawn
Fix spawn location inconsistency
2025-11-04 11:20:19 +08:00
Ben Woo 691bcd0ffa Merge pull request #3375 from BenTechDev/patch-1
✏️ fix typo in UnloadWorldOptions
2025-11-04 11:10:00 +08:00
BenTech 148644970d ✏️ fix typo in UnloadWorldOptions 2025-11-03 18:34:42 +00:00
riku6460 ad20ec94c5 Fix spawn location inconsistency
CraftWorld.setSpawnLocation(Location) requires the world in location.
https://github.com/PaperMC/Paper/blob/e8c6ba5068287049c890688534963041208c4213/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java#L338

See #3321
2025-11-03 03:33:06 +09:00
Ben Woo f7d86884b4 Merge pull request #3369 from Multiverse/feat/simplified-destination
Add simplified destination tab completion configuration option
2025-10-30 11:30:12 +08:00
Ben Woo 0e0e4e0434 Register permission with its wildcards explicitly 2025-10-30 11:29:12 +08:00
Ben Woo 8e878b1ed0 Merge pull request #3370 from Multiverse/fix/passenger-mode
Update default passenger mode description for papermc 1.21.10 changes
2025-10-26 23:21:33 +08:00
Ben Woo e1c6af2f38 Update default passenger mode description for papermc 1.21.10 changes 2025-10-26 13:37:58 +08:00
Ben Woo b52d65757a Fix config test 2025-10-26 13:27:55 +08:00
Ben Woo ff2135c775 Add simplified destination tab completion configuration option 2025-10-26 12:09:06 +08:00
Ben Woo c4e4e311e2 Merge pull request #3368 from Multiverse/perf/suggest-destination
Minor perf improvement to suggestDestinationsWithPerms
2025-10-26 11:59:39 +08:00
Ben Woo 3e4ded52e3 Merge branch 'main' into v5.4 2025-10-26 11:45:23 +08:00
Ben Woo 4ef8390ea3 Minor perf improvement to suggestDestinationsWithPerms 2025-10-26 11:44:57 +08:00
Ben Woo bca71e1acc Merge pull request #3367 from Multiverse/fix/tab-complete-quotes
Quote multi-word strings for all tab-completion
2025-10-24 14:43:14 +08:00
Ben Woo 343da8c301 Quote multi-word strings for all tab-completion 2025-10-24 14:29:07 +08:00
Ben Woo 8bffffdc65 Merge pull request #3364 from Multiverse/feat/silent-teleport
Add a silent flag to turn off teleport output message
2025-10-16 11:21:03 +08:00
Ben Woo 79a71fa58f Add a silent flag to turn off teleport output message 2025-10-15 22:43:22 +08:00
Ben Woo fdd9257e2c Merge pull request #3359 from Multiverse/fix/async-online-players
Implement ConcurrentPlayerWorldTracker to allow for async online player names and worlds lookup
2025-10-13 17:41:37 +08:00
Ben Woo 55d1347d4f Merge pull request #3360 from Multiverse/feat/destination-message
Implement display message for destination instances to improve teleport command output
2025-10-13 17:38:45 +08:00
Ben Woo 29cc97486b Add @since 5.4 annotations to ConcurrentPlayerWorldTracker methods 2025-10-13 17:37:41 +08:00
Ben Woo 8d06b92edd Implement display message for destination instances to improve teleport command output 2025-10-12 23:27:12 +08:00
Ben Woo ce3b6ab700 Make getOnlinePlayers unmodifiable and getPlayerWorld return option 2025-10-12 23:00:01 +08:00
Ben Woo 58ac0f505c Make tab completion for players a non-async call 2025-10-12 22:58:50 +08:00
Ben Woo c068225950 Implement ConcurrentPlayerWorldTracker to allow for async online player names and worlds lookup 2025-10-01 21:45:27 +08:00
70 changed files with 2003 additions and 592 deletions

No files matched your search

+2 -2
View File
@@ -11,9 +11,9 @@ jobs:
pull-requests: write
continue-on-error: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- uses: dbelyaev/action-checkstyle@v0.8.4
- uses: dbelyaev/action-checkstyle@v3
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -107,9 +107,9 @@ jobs:
files: '["${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar"]'
name: ${{ steps.release-info.outputs.tag_name }}
changelog: ${{ steps.release-artifact.outputs.body }}
game_versions: 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
version_type: ${{ steps.parse-release-type.outputs.release_type }}
loaders: bukkit, spigot, paper
loaders: bukkit, spigot, paper, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
- name: Upload to DBO
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
game_versions: 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2
release_type: ${{ steps.parse-release-type.outputs.release_type }}
project_relations: ${{ inputs.dbo_project_relations }}
file_path: ${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar
@@ -136,5 +136,5 @@ jobs:
channel: ${{ steps.parse-release-type.outputs.release_type }}
files: '[{"path": "${{ github.workspace }}/${{ inputs.plugin_name }}-${{ steps.release-info.outputs.tag_name }}.jar", "platforms": ["PAPER"]}]'
description: ${{ steps.release-artifact.outputs.body }}
platform_dependencies: '{"PAPER": ["1.18.2-1.21.10"]}'
platform_dependencies: '{"PAPER": ["1.18.2-1.21.11"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
@@ -124,7 +124,6 @@ public class MultiverseCore extends MultiverseModule {
public void onDisable() {
super.onDisable();
MultiverseCoreApi.shutdown();
saveAllConfigs();
shutdownDependencyInjection();
PluginServiceLocatorFactory.get().shutdown();
Logging.shutdown();
@@ -215,11 +214,11 @@ public class MultiverseCore extends MultiverseModule {
*/
private Try<Void> saveAllConfigs() {
return configProvider.get().save()
.flatMap(ignore -> worldManagerProvider.get().saveWorldsConfig())
.flatMap(ignore ->anchorManagerProvider.get().saveAllAnchors())
.onFailure(e -> {
Logging.severe("Failed to save configs, things may not work as expected.");
});
.andThenTry(() -> worldManagerProvider.get().saveWorldsConfig())
.andThenTry(() -> anchorManagerProvider.get().saveAllAnchors())
.onFailure(e ->
Logging.severe("Failed to save all configs, things may not work as expected. %s",
e.getLocalizedMessage()));
}
/**
@@ -80,7 +80,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return null;
}
final var placeholder = paramsArray.removeFirst();
final var placeholder = paramsArray.remove(0);
String worldName = parseWorldName(offlinePlayer, paramsArray);
if (worldName == null) return null;
@@ -103,9 +103,9 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
}
// Try get from params
String paramWorldName = paramsArray.getLast();
String paramWorldName = paramsArray.get(paramsArray.size() - 1);
if (worldManager.isLoadedWorld(paramWorldName)) {
paramsArray.removeLast();
paramsArray.remove(paramsArray.size() - 1);
return paramWorldName;
}
@@ -164,6 +164,12 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
case "name" -> {
return world.getName();
}
case "playercount" -> {
return String.valueOf(world.getBukkitWorld()
.map(World::getPlayers)
.map(List::size)
.getOrElse(-1));
}
case "playerlimit" -> {
return String.valueOf(world.getPlayerLimit());
}
@@ -177,7 +183,7 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
return String.valueOf(world.getSeed());
}
case "time" -> {
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.getFirst() : "";
String timeFormat = !placeholderParams.isEmpty() ? placeholderParams.get(0) : "";
long time = world.getBukkitWorld().map(World::getTime).getOrElse(0L);
switch (timeFormat) {
case "" -> {
@@ -27,6 +27,7 @@ import org.bukkit.GameRule;
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;
@@ -43,9 +44,9 @@ import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction;
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;
@@ -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);
@@ -224,18 +225,22 @@ public class MVCommandCompletions extends PaperCommandCompletions {
// 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();
}
@@ -279,19 +284,16 @@ public class MVCommandCompletions extends PaperCommandCompletions {
case "both" -> {
return worldManager.getWorlds().stream()
.map(MultiverseWorld::getTabCompleteName)
.map(StringFormatter::quoteMultiWordString)
.toList();
}
case "loaded" -> {
return worldManager.getLoadedWorlds().stream()
.map(MultiverseWorld::getTabCompleteName)
.map(StringFormatter::quoteMultiWordString)
.toList();
}
case "unloaded" -> {
return worldManager.getUnloadedWorlds().stream()
.map(MultiverseWorld::getTabCompleteName)
.map(StringFormatter::quoteMultiWordString)
.toList();
}
case "potential" -> {
@@ -11,6 +11,7 @@ import co.aikar.commands.MinecraftMessageKeys;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import org.bukkit.Bukkit;
@@ -272,13 +273,25 @@ public class MVCommandContexts extends PaperCommandContexts {
private IssuerAwareContextBuilder<Player[]> playerArrayContextBuilder() {
return new IssuerAwareContextBuilder<Player[]>()
.fromPlayer((context, player) -> new Player[]{player})
.fromInput((context, input) -> {
Player[] players = PlayerFinder.getMulti(input, context.getSender()).toArray(new Player[0]);
return (players.length == 0) ? null : players;
})
.fromInput((context, input) -> PlayerFinder
.tryGetMulti(input, context.getSender())
.map(list -> list.toArray(new Player[0]))
.map(arr -> (arr.length == 0) ? null : arr)
.getOrElseThrow(failure -> {
if (failure instanceof MVInvalidCommandArgument mvFailure) {
throw mvFailure;
}
throw new InvalidCommandArgument(failure.getLocalizedMessage() + " "
+ Option.of(failure.getCause()).map(Throwable::getLocalizedMessage).getOrElse(""));
}))
.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) {
@@ -112,9 +112,9 @@ public class MVCommandIssuer extends BukkitCommandIssuer {
} else {
var formatter = getManager().getFormat(messageType);
if (formatter != null) {
sendMessage(formatter.format(message.formatted()));
sendMessage(formatter.format(message.formatted(getManager().getLocales(), this)));
} else {
sendMessage(message.formatted());
sendMessage(message.formatted(getManager().getLocales(), this));
}
}
}
@@ -22,6 +22,9 @@ public class MVRootCommand extends BukkitRootCommand {
@Override
public List<String> getTabCompletions(CommandIssuer sender, String alias, String[] args, boolean commandsOnly, boolean isAsync) {
String[] quoteFormatedArgs = StringFormatter.parseQuotesInArgs(args).toArray(String[]::new);
return super.getTabCompletions(sender, alias, quoteFormatedArgs, commandsOnly, isAsync);
return super.getTabCompletions(sender, alias, quoteFormatedArgs, commandsOnly, isAsync)
.stream()
.map(StringFormatter::quoteMultiWordString)
.toList();
}
}
@@ -123,48 +123,8 @@ public final class IssuerAwareContextBuilder<T> {
*/
@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;
}
/**
@@ -178,49 +138,66 @@ public final class IssuerAwareContextBuilder<T> {
*/
@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) {
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")) {
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));
}
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);
}
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));
int maxArgForAware = context.getFlagValue("maxArgForAware", Integer.MAX_VALUE);
long argLengthWithoutFlags = context.getArgs().stream()
.takeWhile(value -> !value.startsWith("-") && !value.isEmpty()) // ignore the flags
.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));
};
throw new InvalidCommandArgument(inputOnlyFailMessage.apply(context, input).formatted(issuer));
}
private static class GenericIssuerAwareValue<T> extends IssuerAwareValue {
private final T value;
public GenericIssuerAwareValue(boolean byIssuer, T value) {
super(byIssuer);
this.value = value;
}
}
}
@@ -67,12 +67,10 @@ class ConfigCommand extends CoreCommand {
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
var stringPropertyHandle = config.getStringPropertyHandle();
stringPropertyHandle.setPropertyString(issuer.getIssuer(), name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull()));
})
.andThenTry(config::save)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.CONFIG_SET_SUCCESS,
Replace.NAME.with(name),
Replace.VALUE.with(stringPropertyHandle.getProperty(name).getOrNull())))
.onFailure(e -> issuer.sendMessage(MVCorei18n.CONFIG_SET_ERROR,
Replace.NAME.with(name),
Replace.VALUE.with(value),
@@ -5,6 +5,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
@@ -136,6 +137,9 @@ final class DumpsService {
}
private String getPluginList() {
return " - " + StringUtils.join(plugin.getServer().getPluginManager().getPlugins(), "\n - ");
return " - " + StringUtils.join(Arrays.stream(plugin.getServer().getPluginManager().getPlugins())
.map(Object::toString)
.sorted()
.toList(), "\n - ");
}
}
@@ -52,7 +52,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
MultiverseWorld world,
@@ -91,7 +91,7 @@ final class EntitySpawnConfigCommand extends CoreCommand {
void onModifyCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=4")
@Syntax("[world]")
MultiverseWorld world,
@@ -113,11 +113,10 @@ final class EntitySpawnConfigCommand extends CoreCommand {
.getSpawnCategoryConfig(spawnCategory)
.getStringPropertyHandle()
.modifyPropertyString(property, value, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendInfo("Successfully set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName()))
.onFailure(e -> issuer.sendError("Unable to set " + property + " to " + value
+ " for " + spawnCategory.name() + " in " + world.getName() + ": " + e.getMessage()));
worldManager.saveWorldsConfig();
}
}
@@ -67,7 +67,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=2")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -114,7 +114,7 @@ class GameruleCommand extends CoreCommand {
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[World or *]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
@@ -152,7 +152,7 @@ class GameruleCommand extends CoreCommand {
void onGameruleListCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("<world>")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -61,8 +61,8 @@ class InfoCommand extends CoreCommand {
public void onInfoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.info.description.world}")
LoadedMultiverseWorld world,
@@ -49,12 +49,12 @@ class ModifyCommand extends CoreCommand {
"@propsmodifyaction:notByIssuerForArg=arg1|@mvworldpropsname:byIssuerForArg=arg1 " +
"@mvworldpropsname:notByIssuerForArg=arg1|@mvworldpropsvalue:byIssuerForArg=arg1 " +
"@mvworldpropsvalue:notByIssuerForArg=arg1")
@Syntax("[world] <set|add|remove|reset> <property> <value>")
@Syntax("[world] <set|add|remove|reset> <property> [value]")
@Description("{@@mv-core.modify.description}")
void onModifyCommand(// SUPPRESS CHECKSTYLE: ParameterNumber
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=3")
@Syntax("[world]")
@Description("{@@mv-core.modify.world.description}")
@NotNull MultiverseWorldValue worldValue,
@@ -94,28 +94,28 @@ class ModifyCommand extends CoreCommand {
}
StringPropertyHandle worldPropertyHandle = world.getStringPropertyHandle();
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action).onSuccess(ignore -> {
issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
worldPropertyHandle.modifyPropertyString(issuer.getIssuer(), propertyName, propertyValue, action)
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.MODIFY_SUCCESS,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(worldPropertyHandle.getProperty(propertyName).getOrNull()),
Replace.WORLD.with(world.getName()));
worldManager.saveWorldsConfig();
}).onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
Replace.WORLD.with(world.getName())))
.onFailure(exception -> {
if (propertyValue == null) {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE_NOVALUE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
} else {
issuer.sendMessage(MVCorei18n.MODIFY_FAILURE,
replace("{action}").with(action.name().toLowerCase()),
replace("{property}").with(propertyName),
Replace.VALUE.with(propertyValue),
Replace.WORLD.with(world.getName()),
Replace.ERROR.with(exception.getMessage()));
}
});
}
@@ -33,7 +33,7 @@ final class PurgeAllEntitiesCommand extends CoreCommand {
void onPurgeAllEntitiesCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[world]")
LoadedMultiverseWorld world,
@@ -28,7 +28,7 @@ final class PurgeEntitiesCommand extends CoreCommand {
void onPurgeEntityCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -63,7 +63,7 @@ class SetSpawnCommand extends CoreCommand {
worldManager.getLoadedWorld(location.getWorld())
.peek(mvWorld -> mvWorld.setSpawnLocation(location)
.flatMap(ignore -> worldManager.saveWorldsConfig())
.andThenTry(worldManager::saveWorldsConfig)
.onSuccess(ignore -> issuer.sendMessage(MVCorei18n.SETSPAWN_SUCCESS,
Replace.WORLD.with(mvWorld.getName()),
Replace.LOCATION.with(prettyLocation(location))))
@@ -119,7 +119,7 @@ final class SpawnCommand extends CoreCommand {
? Message.of(MVCorei18n.GENERIC_YOU)
: Message.of(entity.getName())),
Replace.WORLD.with(mvWorld.getName()),
Replace.REASON.with(failure.getFirst().getFailureMessage())));
Replace.REASON.with(failure.get(0).getFailureMessage())));
}
private void handleMultiTeleport(MVCommandIssuer issuer, LoadedMultiverseWorld mvWorld,
@@ -19,7 +19,10 @@ import org.jvnet.hk2.annotations.Service;
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;
import org.mvplugins.multiverse.core.command.flags.UnsafeFlags;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
@@ -35,14 +38,14 @@ final class TeleportCommand extends CoreCommand {
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;
@@ -54,14 +57,14 @@ final class TeleportCommand extends CoreCommand {
@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,
@@ -103,16 +106,21 @@ final class TeleportCommand extends CoreCommand {
.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())));
}
});
@@ -136,18 +144,38 @@ final class TeleportCommand extends CoreCommand {
.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"),
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());
}
}
@@ -74,12 +74,12 @@ class WhoCommand extends CoreCommand {
@Subcommand("who")
@CommandPermission("multiverse.core.list.who")
@CommandCompletion("@mvworlds:scope=both @flags:groupName=" + PageFilterFlags.NAME)
@Syntax("<world> [--page <page>] [--filter <filter>]")
@Syntax("[world] [--page <page>] [--filter <filter>]")
@Description("{@@mv-core.who.description}")
void onWhoCommand(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Syntax("<world>")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
@Description("{@@mv-core.who.world.description}")
LoadedMultiverseWorld inputtedWorld,
@@ -35,7 +35,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[time]")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -54,7 +54,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[z]")
double z,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -79,7 +79,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<damage>")
double damage,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -103,7 +103,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
double distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -124,7 +124,7 @@ final class WorldBorderCommand extends CoreCommand {
void onWorldBorderGet(
MVCommandIssuer issuer,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -147,7 +147,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("[time]")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -179,7 +179,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<distance>")
int distance,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -203,7 +203,7 @@ final class WorldBorderCommand extends CoreCommand {
@Syntax("<time>")
int time,
@Flags("resolve=issuerAware")
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("[world]")
LoadedMultiverseWorld world
) {
@@ -8,16 +8,13 @@ import java.util.Locale;
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;
@@ -28,7 +25,6 @@ import org.mvplugins.multiverse.core.config.migration.action.InvertBoolMigratorA
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;
@@ -539,6 +535,16 @@ public final class CoreConfig {
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}
*/
@@ -4,6 +4,7 @@ import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventPriority;
import org.bukkit.plugin.PluginManager;
@@ -139,8 +140,12 @@ final class CoreConfigNodes {
.comment("Sets whether Multiverse will apply the world's entity `tick-rate` config in worlds.yml.")
.comment("If disabled, the `tick-rate` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity spawn rate per world.")
.comment("!!!NOTE: This will only apply after a server restart!")
.defaultValue(true)
.name("apply-entity-spawn-rate")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'apply-entity-spawn-rate' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> applyEntitySpawnLimit = node(ConfigNode.builder("world.apply-entity-spawn-limit", Boolean.class)
@@ -148,8 +153,12 @@ final class CoreConfigNodes {
.comment("Sets whether Multiverse will apply the world's entity `spawn-limit` config when a world is loaded.")
.comment("If disabled, the `spawn-limit` config in worlds.yml will be ignored.")
.comment("Disable this if you want paper-world.yml or another plugin to handle entity limits per world.")
.comment("!!!NOTE: This will only apply after a server restart!")
.defaultValue(true)
.name("apply-entity-spawn-limit")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'apply-entity-spawn-limit' config option will only take" +
" full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoPurgeEntities = node(ConfigNode.builder("world.auto-purge-entities", Boolean.class)
@@ -202,7 +211,8 @@ final class CoreConfigNodes {
final ConfigNode<PassengerModes> passengerMode = node(ConfigNode.builder("teleport.passenger-mode", PassengerModes.class)
.comment("")
.comment("Configures how passengers and vehicles are handled when an entity is teleported.")
.comment(" default: Server will handle passengers and vehicles, this usually means entities will not be teleported to a different world if they have passengers.")
.comment(" default: Server will handle passengers and vehicles. On papermc 1.21.10+, all will be retained by default.")
.comment(" On older versions, all will be dismounted if world changes.")
.comment(" dismount_passengers: Passengers will be removed from the parent entity before the teleport.")
.comment(" dismount_vehicle: Vehicle will be removed and from the parent entity before the teleport.")
.comment(" dismount_all: All passengers and vehicles will be removed from the parent entity before the teleport.")
@@ -385,7 +395,7 @@ final class CoreConfigNodes {
.comment("This config option defines the default language Multiverse should use.")
.defaultValue(Locale.ENGLISH)
.name("default-locale")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().getLocales().setDefaultLocale(newValue))
.build());
@@ -395,9 +405,8 @@ final class CoreConfigNodes {
.comment("If the player's language does not have a translation, it will use the default language set above instead.")
.defaultValue(true)
.name("per-player-locale")
.onSetValue((oldValue, newValue) -> {
commandManager.get().usePerIssuerLocale(newValue);
})
.onLoadAndChange((oldValue, newValue) ->
commandManager.get().usePerIssuerLocale(newValue))
.build());
private final ConfigHeaderNode commandHeader = node(ConfigHeaderNode.builder("command")
@@ -413,6 +422,15 @@ final class CoreConfigNodes {
.name("resolve-alias-name")
.build());
final ConfigNode<Boolean> simplifiedDestinationTabCompletion = node(ConfigNode.builder("command.simplified-destination-tab-completion", Boolean.class)
.comment("")
.comment("If this is set to true, Multiverse will only suggest simple destination formats in tab completion.")
.comment("This means only world names will be suggested without other destination type such as `e:worldname:x,y,z` or `p:playername`.")
.comment("Note: This DOES NOT prevent players from using the destinations, as that is controlled by permissions.")
.defaultValue(false)
.name("simplified-destination-tab-completion")
.build());
final ConfigNode<ConfirmMode> confirmMode = node(ConfigNode.builder("command.confirm-mode", ConfirmMode.class)
.comment("")
.comment("This config option defines whether `/mv confirm` is needed before running a DANGEROUS action.")
@@ -467,8 +485,11 @@ final class CoreConfigNodes {
.comment("")
.comment("This config option defines the priority for the PlayerPortalEvent.")
.name("event-priority-player-portal")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-portal", newValue))
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-portal' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerRespawn = node(ConfigNode.builder("event-priority.player-respawn", EventPriority.class)
@@ -476,16 +497,23 @@ final class CoreConfigNodes {
.name("event-priority-player-respawn")
.comment("")
.comment("This config option defines the priority for the PlayerRespawnEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-respawn", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-respawn' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerSpawnLocation = node(ConfigNode.builder("event-priority.player-spawn-location", EventPriority.class)
.defaultValue(EventPriority.NORMAL)
.comment("")
.comment("This config option defines the priority for the PlayerSpawnLocationEvent.")
.name("event-priority-player-spawn-location").onSetValue((oldValue, newValue) ->
.name("event-priority-player-spawn-location")
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-spawn-location", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-spawn-location' config option will only take " +
"full effect after a server restart."))
.build());
final ConfigNode<EventPriority> eventPriorityPlayerTeleport = node(ConfigNode.builder("event-priority.player-teleport", EventPriority.class)
@@ -493,8 +521,11 @@ final class CoreConfigNodes {
.name("event-priority-player-teleport")
.comment("")
.comment("This config option defines the priority for the PlayerTeleportEvent.")
.onSetValue((oldValue, newValue) ->
.onLoadAndChange((oldValue, newValue) ->
eventPriorityMapper.get().setPriority("mvcore-player-teleport", newValue))
.onChange((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'event-priority.player-teleport' config option will only take " +
"full effect after a server restart."))
.build());
private final ConfigHeaderNode miscHeader = node(ConfigHeaderNode.builder("misc")
@@ -507,6 +538,9 @@ final class CoreConfigNodes {
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("bukkit.yml")
.name("bukkit-yml-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'bukkit-yml-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<String> serverPropertiesPath = node(ConfigNode.builder("misc.server-properties-path", String.class)
@@ -515,6 +549,9 @@ final class CoreConfigNodes {
.comment("Note: this config option needs a server restart to take effect.")
.defaultValue("server.properties")
.name("server-properties-path")
.onChange(((sender, oldValue, newValue) ->
sender.sendMessage(ChatColor.YELLOW + "'server-properties-path' config option will only take " +
"full effect after a server restart.")))
.build());
final ConfigNode<Boolean> autoDetectGeneratorPlugins = node(ConfigNode.builder("misc.auto-detect-generator-plugins", Boolean.class)
@@ -541,7 +578,7 @@ final class CoreConfigNodes {
.validator(value -> (value < 0 || value > 3)
? Try.failure(new MultiverseException("Debug level must be between 0 and 3."))
: Try.success(null))
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (newValue != Logging.getDebugLevel()) {
Logging.setDebugLevel(newValue);
pluginManager.callEvent(new MVDebugModeEvent(newValue));
@@ -555,7 +592,7 @@ final class CoreConfigNodes {
.comment("This will only work if the above 'global-debug' is set to 1 or more.")
.defaultValue(false)
.name("debug-permissions")
.onSetValue((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.onLoadAndChange((oldValue, newValue) -> PermissionUtils.setDebugPermissions(newValue))
.build());
final ConfigNode<Boolean> silentStart = node(ConfigNode.builder("misc.silent-start", Boolean.class)
@@ -563,7 +600,7 @@ final class CoreConfigNodes {
.comment("If true, the startup console messages will no longer show.")
.defaultValue(false)
.name("silent-start")
.onSetValue((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.onLoadAndChange((oldValue, newValue) -> Logging.setShowingConfig(!newValue))
.build());
final ConfigNode<Boolean> showDonationMessage = node(ConfigNode.builder("misc.show-donation-message", Boolean.class)
@@ -3,19 +3,22 @@ package org.mvplugins.multiverse.core.config.handle;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.config.node.ListValueNode;
import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.ValueNode;
@@ -84,7 +87,8 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
});
nodeValueMap.forEach((valueNode, value) -> {
valueNode.onSetValue(value, value);
valueNode.onLoad(value);
valueNode.onLoadAndChange(Bukkit.getConsoleSender(), null, value);
});
}
@@ -156,10 +160,27 @@ public abstract class BaseConfigurationHandle<C extends ConfigurationSection> {
* @return Empty try if the value was set, try containing an error otherwise.
*/
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
return set(Bukkit.getConsoleSender(), node, value);
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param sender The sender who triggered the change.
* @param node The node to set the value of.
* @param value The value to set.
* @param <T> The type of the node value.
* @return Empty try if the value was set, try containing an error otherwise.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public <T> Try<Void> set(@NotNull CommandSender sender, @NotNull ValueNode<T> node, T value) {
return node.validate(value).map(ignore -> {
T oldValue = get(node);
nodeValueMap.put(node, value);
node.onSetValue(oldValue, get(node));
node.onLoadAndChange(sender, oldValue, value);
node.onChange(sender, oldValue, value);
return null;
});
}
@@ -226,7 +226,7 @@ public class StringPropertyHandle {
public Try<Void> setPropertyString(@NotNull CommandSender sender, @Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(sender, value)
.flatMap(parsedValue -> handle.set(node, parsedValue)));
.flatMap(parsedValue -> handle.set(sender, node, parsedValue)));
}
/**
@@ -14,6 +14,9 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeSuggester;
import org.mvplugins.multiverse.core.config.node.serializer.DefaultSerializerProvider;
@@ -53,7 +56,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
protected ConfigNode(
@NotNull String path,
@@ -66,7 +71,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Nullable NodeStringParser<T> stringParser,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue) {
@Nullable NodeValueCallback<T> onLoad,
@Nullable NodeChangeCallback<T> onLoadAndChange,
@Nullable NodeChangeCallback<T> onChange) {
super(path, comments);
this.name = name;
this.type = type;
@@ -82,7 +89,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
? serializer
: DefaultSerializerProvider.getDefaultSerializer(type);
this.validator = validator;
this.onSetValue = onSetValue;
this.onLoad = onLoad;
this.onLoadAndChange = onLoadAndChange;
this.onChange = onChange;
}
/**
@@ -186,9 +195,29 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* {@inheritDoc}
*/
@Override
public void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
if (onSetValue != null) {
onSetValue.accept(oldValue, newValue);
public void onLoad(@Nullable T value) {
if (onLoad != null) {
onLoad.run(value);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onLoadAndChange != null) {
onLoadAndChange.run(sender, oldValue, newValue);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue) {
if (onChange != null) {
onChange.run(sender, oldValue, newValue);
}
}
@@ -208,7 +237,9 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
protected @Nullable NodeValueCallback<T> onLoad;
protected @Nullable NodeChangeCallback<T> onLoadAndChange;
protected @Nullable NodeChangeCallback<T> onChange;
/**
* Creates a new builder.
@@ -368,15 +399,83 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the action to be performed when the value is loaded.
*
* @param onLoad The action to be performed.
* @return This builder.
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoad(@NotNull NodeValueCallback<T> onLoad) {
this.onLoad = this.onLoad == null ? onLoad : this.onLoad.then(onLoad);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull NodeChangeCallback<T> onLoadAndChange) {
this.onLoadAndChange = this.onLoadAndChange == null ? onLoadAndChange : this.onLoadAndChange.then(onLoadAndChange);
return self();
}
/**
* Sets the action to be performed when the value is loaded and changed.
*
* @param onLoadAndChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onLoadAndChange(@NotNull SenderNodeChangeCallback<T> onLoadAndChange) {
return onLoadAndChange((NodeChangeCallback<T>) onLoadAndChange);
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull NodeChangeCallback<T> onChange) {
this.onChange = this.onChange == null ? onChange : this.onChange.then(onChange);
return self();
}
/**
* Sets the action to be performed when the value is changed.
*
* @param onChange The action to be performed.
* @return This builder.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public @NotNull B onChange(@NotNull SenderNodeChangeCallback<T> onChange) {
return onChange((NodeChangeCallback<T>) onChange);
}
/**
* Sets the action to be performed when the value is set.
*
* @param onSetValue The action to be performed.
* @return This builder.
*
* @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
this.onSetValue = this.onSetValue == null ? onSetValue : this.onSetValue.andThen(onSetValue);
return self();
return onLoadAndChange(onSetValue::accept);
}
/**
@@ -385,7 +484,8 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]),
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
name, type, aliases, defaultValue, suggester, stringParser, serializer, validator,
onLoad, onLoadAndChange, onChange);
}
}
}
@@ -19,6 +19,8 @@ import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.node.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.config.node.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.config.node.functions.NodeChangeCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeValueCallback;
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.functions.NodeSuggester;
import org.mvplugins.multiverse.core.config.node.functions.SenderNodeStringParser;
@@ -67,7 +69,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable NodeStringParser<List<I>> stringParser,
@Nullable NodeSerializer<List<I>> serializer,
@Nullable Function<List<I>, Try<Void>> validator,
@Nullable BiConsumer<List<I>, List<I>> onSetValue,
@Nullable NodeValueCallback<List<I>> onLoad,
@Nullable NodeChangeCallback<List<I>> onLoadAndChange,
@Nullable NodeChangeCallback<List<I>> onChange,
@NotNull Class<I> itemType,
@Nullable NodeSuggester itemSuggester,
@Nullable NodeStringParser<I> itemStringParser,
@@ -75,7 +79,7 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
@Nullable Function<I, Try<Void>> itemValidator,
@Nullable BiConsumer<I, I> onSetItemValue) {
super(path, comments, name, type, aliases, defaultValueSupplier, suggester, stringParser, serializer,
validator, onSetValue);
validator, onLoad, onLoadAndChange, onChange);
this.itemType = itemType;
this.itemSuggester = itemSuggester != null
? itemSuggester
@@ -105,8 +109,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
if (this.itemSerializer != null && this.serializer == null) {
setDefaultSerialiser();
}
if (this.onSetItemValue != null && this.onSetValue == null) {
setDefaultOnSetValue();
if (this.onSetItemValue != null && this.onLoadAndChange == null) {
setDefaultOnLoadAndChange();
}
if (this.defaultValue == null) {
this.defaultValue = ArrayList::new;
@@ -168,8 +172,8 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
};
}
private void setDefaultOnSetValue() {
this.onSetValue = (oldValue, newValue) -> {
private void setDefaultOnLoadAndChange() {
this.onLoadAndChange = (oldValue, newValue) -> {
if (oldValue != null) {
oldValue.stream()
.filter(value -> !newValue.contains(value))
@@ -381,7 +385,9 @@ public class ListConfigNode<I> extends ConfigNode<List<I>> implements ListValueN
stringParser,
serializer,
validator,
onSetValue,
onLoad,
onLoadAndChange,
onChange,
itemType,
itemSuggester,
itemStringParser,
@@ -5,11 +5,13 @@ import java.util.Collection;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.apache.logging.log4j.util.Strings;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.handle.BaseConfigurationHandle;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
public interface ValueNode<T> extends Node {
@@ -108,11 +110,51 @@ public interface ValueNode<T> extends Node {
*/
Try<Void> validate(@Nullable T value);
/**
* Called when the value of this node is loaded by {@link BaseConfigurationHandle#load()}.
*
* @param value The loaded value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoad(@Nullable T value);
/**
* Called when the value of this node is loaded or changed.
*
* @param sender The sender who triggered the change. If triggered by loading or no target sender is specified,
* it will be the console sender.
* @param oldValue The old value, will always be null on load.
* @param newValue The new value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onLoadAndChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue);
/**
* Called when the value of this node is changed by {@link BaseConfigurationHandle#set(ValueNode, Object)}.
*
* @param sender The sender who changed the value, or console sender if no target sender specified.
* @param oldValue The old value.
* @param newValue The new value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void onChange(@NotNull CommandSender sender, @Nullable T oldValue, @Nullable T newValue);
/**
* Called when the value of this node is set.
*
* @param oldValue The old value.
* @param newValue The new value.
*
* @deprecated Use {@link #onLoadAndChange(CommandSender, Object, Object)} instead.
*/
void onSetValue(@Nullable T oldValue, @Nullable T newValue);
@Deprecated(since = "5.4", forRemoval = true)
default void onSetValue(@Nullable T oldValue, @Nullable T newValue) {
onLoadAndChange(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeChangeCallback<T> {
/**
* Called when the value of a node changes.
*
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T oldValue, T newValue);
/**
* Called when the value of a node changes, with the sender of the change.
*
* @param sender The sender of the change.
* @param oldValue The old value of the node.
* @param newValue The new value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default void run(CommandSender sender, T oldValue, T newValue) {
run(oldValue, newValue);
}
/**
* Chains another {@link NodeChangeCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeChangeCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeChangeCallback<T> then(NodeChangeCallback<T> after) {
return new SenderNodeChangeCallback<T>() {
@Override
public void run(CommandSender sender, T oldValue, T newValue) {
NodeChangeCallback.this.run(sender, oldValue, newValue);
after.run(sender, oldValue, newValue);
}
@Override
public void run(T oldValue, T newValue) {
NodeChangeCallback.this.run(oldValue, newValue);
after.run(oldValue, newValue);
}
};
}
}
@@ -0,0 +1,38 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.jetbrains.annotations.ApiStatus;
/**
* Handler called when an action is performed on a node such as loading.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface NodeValueCallback<T> {
/**
* Called when an action is performed on a node.
*
* @param value The value of the node.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
void run(T value);
/**
* Chains another {@link NodeValueCallback} to be executed after this one.
*
* @param after The callback to execute after this one.
* @return A new {@link NodeValueCallback} that executes both callbacks in order.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
default NodeValueCallback<T> then(NodeValueCallback<T> after) {
return (T value) -> {
this.run(value);
after.run(value);
};
}
}
@@ -0,0 +1,31 @@
package org.mvplugins.multiverse.core.config.node.functions;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.ApiStatus;
/**
* A callback interface for handling changes to configuration node values with contextual information from the sender.
*
* @param <T> The type of the node value.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@FunctionalInterface
public interface SenderNodeChangeCallback<T> extends NodeChangeCallback<T> {
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
void run(CommandSender sender, T oldValue, T newValue);
/**
* {@inheritDoc}
*/
@ApiStatus.AvailableSince("5.4")
@Override
default void run(T oldValue, T newValue) {
run(Bukkit.getConsoleSender(), oldValue, newValue);
}
}
@@ -11,8 +11,11 @@ import org.jetbrains.annotations.Nullable;
* A function that parses a string into a node value object of type {@link T} with contextual information from the sender.
*
* @param <T> The type of the object to parse.
*
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
@FunctionalInterface
public interface SenderNodeStringParser<T> extends NodeStringParser<T> {
/**
* Parses a string into a node value object of type {@link T} with contextual information from the sender.
@@ -4,8 +4,9 @@ import io.vavr.control.Option;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
/**
@@ -79,6 +80,20 @@ public abstract class DestinationInstance<I extends DestinationInstance<I, T>,
*/
public abstract @NotNull Option<String> getFinerPermissionSuffix();
/**
* Gets a user-friendly display text for this destination instance. This is used when displaying the destination
* to the player. By default, this returns the same as {@link #toString()}. Override this method to provide a more
* user-friendly display text with colors, formatting and localization support.
*
* @return The display message.
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Message getDisplayMessage() {
return Message.of(this.toString());
}
/**
* Serialises the destination instance to a savable string.
*
@@ -7,6 +7,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link AnchorDestination}.
@@ -63,6 +64,15 @@ public final class AnchorDestinationInstance extends DestinationInstance<AnchorD
return Option.of(anchorName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of("anchor '" + anchorName + "'");
}
/**
* {@inheritDoc}
*/
@@ -6,7 +6,7 @@ import java.util.stream.Collectors;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -20,8 +20,7 @@ import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for beds.
@@ -30,7 +29,11 @@ import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.re
public final class BedDestination implements Destination<BedDestination, BedDestinationInstance, BedDestination.InstanceFailureReason> {
static final String OWN_BED_STRING = "playerbed";
BedDestination() {
private final ConcurrentPlayerWorldTracker worldTracker;
@Inject
BedDestination(@NotNull ConcurrentPlayerWorldTracker worldTracker) {
this.worldTracker = worldTracker;
}
/**
@@ -62,8 +65,8 @@ public final class BedDestination implements Destination<BedDestination, BedDest
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
List<DestinationSuggestionPacket> collect = Bukkit.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player.getName(), player.getName()))
List<DestinationSuggestionPacket> collect = worldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.collect(Collectors.toList());
if (sender instanceof Player) {
collect.add(new DestinationSuggestionPacket(this, OWN_BED_STRING, OWN_BED_STRING));
@@ -9,12 +9,13 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
/**
* Destination instance implementation for the {@link BedDestination}.
*/
public final class BedDestinationInstance extends DestinationInstance<BedDestinationInstance, BedDestination> {
private final Player player;
private final @Nullable Player player;
/**
* Constructor.
@@ -64,6 +65,15 @@ public final class BedDestinationInstance extends DestinationInstance<BedDestina
return Option.of(player != null ? player.getName() : BedDestination.OWN_BED_STRING);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
//TODO Localize
return Message.of(player == null ? "your bed/respawn point" : player.getName() + "'s bed/respawn point");
}
/**
* {@inheritDoc}
*/
@@ -23,14 +23,11 @@ import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseExce
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.utils.REPatterns;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.PositionNumber;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.*;
@@ -68,6 +65,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) {
return new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
);
@@ -88,6 +86,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
.map(location -> Attempt.<ExactDestinationInstance, InstanceFailureReason>success(
new ExactDestinationInstance(
this,
worldManager,
location.getWorld().getName(),
EntityPosition.ofLocation(location)
)
@@ -112,7 +111,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
return Attempt.success(new ExactDestinationInstance(this, worldName, position));
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
}
//TODO: Extract to a world finder class
@@ -9,14 +9,16 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.utils.position.EntityPosition;
import org.mvplugins.multiverse.core.utils.position.VectorPosition;
import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import org.mvplugins.multiverse.core.world.WorldManager;
/**
* Destination instance implementation for the {@link ExactDestination}.
*/
public final class ExactDestinationInstance extends DestinationInstance<ExactDestinationInstance, ExactDestination> {
private final WorldManager worldManager;
private final String worldName;
private final EntityPosition position;
@@ -28,9 +30,11 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
* @param position The position in the world.
*/
ExactDestinationInstance(@NotNull ExactDestination destination,
@NotNull WorldManager worldManager,
@NotNull String worldName,
@NotNull EntityPosition position) {
super(destination);
this.worldManager = worldManager;
this.worldName = worldName;
this.position = position;
}
@@ -73,6 +77,17 @@ public final class ExactDestinationInstance extends DestinationInstance<ExactDes
return Option.of(worldName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
String displayWorldName = worldManager.getWorld(worldName)
.map(MultiverseWorld::getAliasOrName)
.getOrElse(worldName);
return Message.of(displayWorldName + " at " + position.toString());
}
/**
* {@inheritDoc}
*/
@@ -4,7 +4,7 @@ import java.util.Collection;
import co.aikar.locales.MessageKey;
import co.aikar.locales.MessageKeyProvider;
import org.bukkit.Bukkit;
import jakarta.inject.Inject;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -14,21 +14,26 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement;
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
import org.mvplugins.multiverse.core.utils.PlayerFinder;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* {@link Destination} implementation for players.s
*/
@Service
public final class PlayerDestination implements Destination<PlayerDestination, PlayerDestinationInstance, PlayerDestination.InstanceFailureReason> {
private final ConcurrentPlayerWorldTracker playerWorldTracker;
/**
* Creates a new instance of the PlayerDestination.
*/
PlayerDestination() {
@Inject
PlayerDestination(@NotNull ConcurrentPlayerWorldTracker playerWorldTracker) {
this.playerWorldTracker = playerWorldTracker;
}
/**
@@ -60,8 +65,8 @@ public final class PlayerDestination implements Destination<PlayerDestination, P
@Override
public @NotNull Collection<DestinationSuggestionPacket> suggestDestinations(
@NotNull CommandSender sender, @Nullable String destinationParams) {
return Bukkit.getOnlinePlayers().stream()
.map(p -> new DestinationSuggestionPacket(this, p.getName(), p.getName()))
return playerWorldTracker.getOnlinePlayers().stream()
.map(player -> new DestinationSuggestionPacket(this, player, player))
.toList();
}
@@ -9,6 +9,7 @@ import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.locale.message.Message;
import java.util.UUID;
@@ -66,6 +67,14 @@ public final class PlayerDestinationInstance extends DestinationInstance<PlayerD
return Option.of(playerName);
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(playerName);
}
/**
* {@inheritDoc}
*/
@@ -8,12 +8,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.destination.DestinationInstance;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
/**
* Destination instance implementation for the {@link WorldDestination}.
*/
@@ -87,6 +84,14 @@ public final class WorldDestinationInstance extends DestinationInstance<WorldDes
return Option.of(world.getName());
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Message getDisplayMessage() {
return Message.of(world.getAliasOrName());
}
/**
* {@inheritDoc}
*/
@@ -16,6 +16,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.IgnoreIfCancell
import org.mvplugins.multiverse.core.dynamiclistener.annotations.SkipIfEventExist;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.helpers.ConcurrentPlayerWorldTracker;
/**
* Multiverse's Listener for players.
@@ -24,16 +25,17 @@ import org.mvplugins.multiverse.core.world.WorldManager;
final class MVChatListener implements CoreListener {
private final CoreConfig config;
private final WorldManager worldManager;
private final MVPlayerListener playerListener;
private final ConcurrentPlayerWorldTracker playerWorldTracker;
@Inject
MVChatListener(
CoreConfig config,
WorldManager worldManager,
MVPlayerListener playerListener) {
ConcurrentPlayerWorldTracker playerWorldTracker
) {
this.config = config;
this.worldManager = worldManager;
this.playerListener = playerListener;
this.playerWorldTracker = playerWorldTracker;
}
@EventClass("io.papermc.paper.event.player.AsyncChatEvent")
@@ -94,12 +96,9 @@ final class MVChatListener implements CoreListener {
}
private String getWorldName(Player player) {
String world = playerListener.getPlayerWorld().get(player.getName());
if (world == null) {
world = player.getWorld().getName();
playerListener.getPlayerWorld().put(player.getName(), world);
}
return this.worldManager.getLoadedWorld(world)
String worldName = playerWorldTracker.getPlayerWorld(player.getName())
.getOrElse(() -> player.getWorld().getName());
return this.worldManager.getLoadedWorld(worldName)
.map(mvworld -> mvworld.isHidden() ? "" : mvworld.getAliasOrName())
.getOrElse("");
}
@@ -74,8 +74,6 @@ final class MVPlayerListener implements CoreListener {
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
@Inject
MVPlayerListener(
MultiverseCore plugin,
@@ -120,15 +118,6 @@ final class MVPlayerListener implements CoreListener {
return getCommandManager().getLocales();
}
/**
* Gets the map of player and the world name they are in.
*
* @return the playerWorld-map
*/
Map<String, String> getPlayerWorld() {
return playerWorld;
}
/**
* This method is called when a player respawns.
*
@@ -260,7 +249,7 @@ final class MVPlayerListener implements CoreListener {
.onSuccess(result -> Logging.fine("Player %s has been teleported on join",
player.getName()))
.onFailure(failure -> Logging.warning("Failed to teleport player %s on join: %s",
player.getName(), failure.getFirst()));
player.getName(), failure.get(0)));
}
/**
@@ -272,7 +261,6 @@ final class MVPlayerListener implements CoreListener {
void playerChangedWorld(PlayerChangedWorldEvent event) {
// Permissions now determine whether or not to handle a gamemode.
this.handleGameModeAndFlight(event.getPlayer(), event.getPlayer().getWorld());
playerWorld.put(event.getPlayer().getName(), event.getPlayer().getWorld().getName());
}
/**
@@ -27,39 +27,12 @@ public final class LocalizedMessage extends Message implements MessageKeyProvide
return messageKeyProvider.getMessageKey();
}
@Override
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return toReplacementsArray(locales, commandIssuer, replacements);
}
@Override
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
@Override
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
Objects.requireNonNull(locales, "locales must not be null");
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return locales.getMessage(commandIssuer, getMessageKey());
}
return ACFUtil.replaceStrings(locales.getMessage(commandIssuer, getMessageKey()), parsedReplacements);
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -103,7 +103,7 @@ public sealed class Message permits LocalizedMessage {
* @return The replacements
*/
public @NotNull String[] getReplacements(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return getReplacements();
return toReplacementsArray(locales, commandIssuer, replacements);
}
/**
@@ -152,7 +152,7 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) {
return formatted();
return formatted(commandIssuer.getManager().getLocales(), commandIssuer);
}
/**
@@ -166,7 +166,11 @@ public sealed class Message permits LocalizedMessage {
* @return The formatted, localized message
*/
public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) {
return formatted();
String[] parsedReplacements = getReplacements(locales, commandIssuer);
if (parsedReplacements.length == 0) {
return raw();
}
return ACFUtil.replaceStrings(message, parsedReplacements);
}
private static String[] toReplacementsArray(@NotNull MessageReplacement... replacements) {
@@ -178,4 +182,19 @@ public sealed class Message permits LocalizedMessage {
}
return replacementsArray;
}
private static String[] toReplacementsArray(
@NotNull Locales locales,
@Nullable CommandIssuer commandIssuer,
@NotNull MessageReplacement... replacements) {
String[] replacementsArray = new String[replacements.length * 2];
int i = 0;
for (MessageReplacement replacement : replacements) {
replacementsArray[i++] = replacement.getKey();
replacementsArray[i++] = replacement.getReplacement().fold(
str -> str,
message -> message.formatted(locales, commandIssuer));
}
return replacementsArray;
}
}
@@ -13,6 +13,7 @@ import org.mvplugins.multiverse.core.destination.Destination;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.concatPermission;
import static org.mvplugins.multiverse.core.permissions.PermissionUtils.registerPermissionWithWildcards;
@Service
public final class CorePermissions {
@@ -60,28 +61,34 @@ public final class CorePermissions {
@PostConstruct
void registerBasePermissions() {
pluginManager.addPermission(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
Logging.fine("Successfully registered base permissions");
Try.run(() -> {
registerPermissionWithWildcards(new Permission(JOINLOCATION_BYPASS, PermissionDefault.FALSE));
}).onSuccess(ignore -> {
Logging.fine("Successfully registered base permissions");
}).onFailure(e -> {
Logging.fine("Failed to register base permissions: %s", e.getMessage());
});
}
public Try<Void> addWorldPermissions(@NotNull MultiverseWorld world) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_ACCESS, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(WORLD_EXEMPT, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(GAMEMODE_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(PLAYERLIMIT_BYPASS, world.getName()), PermissionDefault.FALSE));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "self", world.getName()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(SPAWN, "other", world.getName()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> removeWorldPermissions(@NotNull MultiverseWorld world) {
@@ -90,20 +97,23 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(WORLD_EXEMPT, world.getName()));
pluginManager.removePermission(concatPermission(GAMEMODE_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(PLAYERLIMIT_BYPASS, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "self", world.getName()));
pluginManager.removePermission(concatPermission(SPAWN, "other", world.getName()));
Logging.fine("Successfully removed permissions for world %s", world.getName());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for world %s: %s",
world.getName(), e.getMessage()));
}
public Try<Void> addDestinationPermissions(@NotNull Destination destination) {
return Try.run(() -> {
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "self", destination.getIdentifier()), PermissionDefault.OP));
pluginManager.addPermission(new Permission(
registerPermissionWithWildcards(new Permission(
concatPermission(TELEPORT, "other", destination.getIdentifier()), PermissionDefault.OP));
Logging.fine("Successfully registered permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to register permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
public Try<Void> removeDestinationPermissions(@NotNull Destination destination) {
@@ -111,6 +121,7 @@ public final class CorePermissions {
pluginManager.removePermission(concatPermission(TELEPORT, "self", destination.getIdentifier()));
pluginManager.removePermission(concatPermission(TELEPORT, "other", destination.getIdentifier()));
Logging.fine("Successfully removed permissions for destination %s", destination.getIdentifier());
});
}).onFailure(e -> Logging.fine("Failed to remove permissions for destination %s: %s",
destination.getIdentifier(), e.getMessage()));
}
}
@@ -1,8 +1,14 @@
package org.mvplugins.multiverse.core.permissions;
import com.dumptruckman.minecraft.util.Logging;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.jetbrains.annotations.ApiStatus;
import java.util.Arrays;
public final class PermissionUtils {
@@ -20,6 +26,37 @@ public final class PermissionUtils {
PermissionUtils.debugPermissions = debugPermissions;
}
/**
* Registers a permission along with all its wildcard parents.
* <br />
* For example, registering "mv.bypass.joinlocation" will also register "mv.*" and "mv.bypass.*" as parents.
*
* @param permission The permission to register.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static void registerPermissionWithWildcards(Permission permission) {
Bukkit.getServer().getPluginManager().addPermission(permission);
String[] split = permission.getName().split("\\.");
StringBuilder prefix = new StringBuilder();
// Skip the last element since it's the actual permission
Arrays.stream(Arrays.copyOfRange(split, 0, split.length - 1)).forEach(s -> {
prefix.append(s).append(".");
Permission perm = getOrAddPermission(prefix + "*");
permission.addParent(perm, true);
});
}
private static Permission getOrAddPermission(String permission) {
Permission perm = Bukkit.getServer().getPluginManager().getPermission(permission);
if (perm == null) {
perm = new Permission(permission, PermissionDefault.FALSE);
Bukkit.getServer().getPluginManager().addPermission(perm);
}
return perm;
}
/**
* Joins permissions with a dot.
*
@@ -1,16 +1,18 @@
package org.mvplugins.multiverse.core.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.base.Strings;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -76,22 +78,33 @@ public final class PlayerFinder {
public static @NotNull List<Player> getMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
List<Player> playerResults = new ArrayList<>();
return tryGetMulti(playerIdentifiers, sender)
.getOrElse(Collections.emptyList());
}
/**
* Get multiple {@link Player} based on many identifiers of name UUID or selector.
*
* @param playerIdentifiers An identifier of multiple names, UUIDs or selectors, separated by comma.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMulti(@Nullable String playerIdentifiers,
@NotNull CommandSender sender
) {
if (playerIdentifiers == null || Strings.isNullOrEmpty(playerIdentifiers)) {
return playerResults;
return Try.success(Collections.emptyList());
}
// TODO: Currently just assume entire string is a selector. Add support for comma seperated mixture of names, uuids and selectors
if (isSelector(playerIdentifiers)) {
Logging.finer("Using selector: %s", playerIdentifiers);
List<Player> targetPlayers = getMultiBySelector(playerIdentifiers, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
}
return playerResults;
return tryGetMultiBySelector(playerIdentifiers, sender);
}
List<Player> playerResults = new ArrayList<>();
String[] playerIdentifierArray = REPatterns.COMMA.split(playerIdentifiers);
for (String playerIdentifier : playerIdentifierArray) {
Player targetPlayer = getByName(playerIdentifier);
@@ -104,15 +117,25 @@ public final class PlayerFinder {
playerResults.add(targetPlayer);
continue;
}
List<Player> targetPlayers = getMultiBySelector(playerIdentifier, sender);
if (targetPlayers != null) {
playerResults.addAll(targetPlayers);
Try<@NotNull List<Player>> selectorParseResult = tryGetMultiBySelector(playerIdentifier, sender);
if (selectorParseResult.isFailure()) {
return Try.failure(selectorParseResult.getCause());
}
playerResults.addAll(selectorParseResult.getOrElse(Collections.emptyList()));
}
return playerResults;
return Try.success(playerResults);
}
private static boolean isSelector(@NotNull String playerIdentifier) {
/**
* Check if the player identifier is a selector.
*
* @param playerIdentifier An identifier of name, UUID or selector.
* @return True if the identifier is a selector, else false.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static boolean isSelector(@NotNull String playerIdentifier) {
return VANILLA_SELECTORS.stream().anyMatch(playerIdentifier::startsWith);
}
@@ -167,7 +190,7 @@ public final class PlayerFinder {
@NotNull CommandSender sender
) {
List<Player> matchedPlayers = getMultiBySelector(playerSelector, sender);
if (matchedPlayers == null || matchedPlayers.isEmpty()) {
if (matchedPlayers.isEmpty()) {
Logging.fine("No player found with selector '%s' for %s.", playerSelector, sender.getName());
return null;
}
@@ -186,22 +209,36 @@ public final class PlayerFinder {
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*/
public static @Nullable List<Player> getMultiBySelector(@NotNull String playerSelector,
public static @NotNull List<Player> getMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
return tryGetMultiBySelector(playerSelector, sender)
.onFailure(throwable -> Logging.warning(
"Error selecting entities with selector '%s' for %s: %s",
playerSelector, sender.getName(), throwable.getMessage()
))
.getOrElse(Collections::emptyList);
}
/**
* Get multiple {@link Player} based on <a href="https://minecraft.gamepedia.com/Commands#Target_selectors">vanilla selectors</a>.
*
* @param playerSelector A target selector, usually starts with an '@'.
* @param sender Target sender for selector.
* @return A list of all the {@link Player} found.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
public static @NotNull Try<@NotNull List<Player>> tryGetMultiBySelector(@NotNull String playerSelector,
@NotNull CommandSender sender
) {
if (playerSelector.charAt(0) != '@') {
return null;
}
try {
return Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList());
} catch (IllegalArgumentException e) {
Logging.warning("An error occurred while parsing selector '%s' for %s. Is it is the correct format?",
playerSelector, sender.getName());
e.printStackTrace();
return null;
return Try.success(Collections.emptyList());
}
return Try.of(() -> Bukkit.selectEntities(sender, playerSelector).stream()
.filter(e -> e instanceof Player)
.map(e -> ((Player) e))
.collect(Collectors.toList()));
}
}
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.utils;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@@ -16,7 +18,17 @@ public class REPatterns {
public static final Pattern COMMA = get(",");
public static final Pattern DOT = get("\\.");
public static final Pattern EQUALS = get("=");
@ApiStatus.AvailableSince("5.4")
public static final Pattern HYPHEN = get("-");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE = get("^[a-z0-9_.-]+$");
@ApiStatus.AvailableSince("5.4")
public static final Pattern NAMESPACE_KEY = get("^[a-z0-9/_.-]+$");
public static final Pattern SEMICOLON = get(";");
@ApiStatus.AvailableSince("5.4")
public static final Pattern SPACE = get(" ");
public static final Pattern UNDERSCORE = get("_");
public static final Pattern UUID = get("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
@ApiStatus.AvailableSince("5.4")
public static final Pattern YAML_KEY = get("^[a-zA-Z0-9\\[\\]_-]+$");
}
@@ -107,18 +107,14 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
*
* @return Whether this attempt is a success.
*/
default boolean isSuccess() {
return this instanceof Success;
}
boolean isSuccess();
/**
* Returns whether this attempt is a failure.
*
* @return Whether this attempt is a failure.
*/
default boolean isFailure() {
return this instanceof Failure;
}
boolean isFailure();
/**
* Converts this {@link Attempt} instance to an equivalent {@link Try} representation. Defaults to a
@@ -143,19 +139,25 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
@ApiStatus.AvailableSince("5.1")
Try<T> toTry(Function<Failure<T, F>, Throwable> throwableFunction);
/**
* Runs the given runnable after this attempt regardless of success or failure.
*
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> thenRun(Runnable runnable) {
runnable.run();
return this;
}
default Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
if (this instanceof Success) {
consumer.accept(Either.left(get()));
} else {
consumer.accept(Either.right(getFailureReason()));
}
return this;
}
/**
* Accepts either the value or the failure reason depending on the result type.
* This will run regardless of success or failure.
*
* @param consumer The consumer with either the value or the failure reason.
* @return This attempt.
*/
Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer);
/**
* Peeks at the value if this is a success attempt.
@@ -163,12 +165,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the value.
* @return This attempt.
*/
default Attempt<T, F> peek(Consumer<T> consumer) {
if (this instanceof Success) {
consumer.accept(get());
}
return this;
}
Attempt<T, F> peek(Consumer<T> consumer);
/**
* Maps the value to another value if this is a success attempt.
@@ -177,13 +174,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
if (this instanceof Success) {
return new Success<>(mapper.apply(get()));
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> map(Function<? super T, ? extends U> mapper);
/**
* Maps the value to another attempt if this is a success attempt.
@@ -192,13 +183,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
if (this instanceof Success) {
return new Success<>(mapper.get());
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> map(Supplier<? extends U> mapper);
/**
* Maps the value to another attempt with the same fail reason if this is a success attempt.
@@ -207,13 +192,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
if (this instanceof Success) {
return mapper.apply(get());
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper);
/**
* Maps the value to another attempt with the same fail reason if this is a success attempt.
@@ -222,13 +201,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <U> The type of the new value.
* @return The new attempt.
*/
default <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
if (this instanceof Success) {
return mapper.get();
} else {
return new Failure<>((Failure<T, F>) this);
}
}
<U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper);
/**
* Maps to another attempt with a different fail reason.
@@ -237,13 +210,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <UF> The type of the new fail reason.
* @return The new attempt.
*/
default <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
if (this instanceof Success) {
return new Success<>(get());
} else {
return new Failure<>(failureReason, getFailureMessage(), (Failure<T, F>) this);
}
}
<UF extends FailureReason> Attempt<T, UF> transform(UF failureReason);
/**
* Maps attempt result to another value.
@@ -256,13 +223,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @since 5.1
*/
@ApiStatus.AvailableSince("5.1")
default <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
if (this instanceof Success) {
return successMapper.apply(get());
} else {
return failureMapper.apply(getFailureReason());
}
}
<U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper);
/**
* Calls either the failure or success function depending on the result type.
@@ -272,13 +233,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param <N> The type of the new value.
* @return The result of the function.
*/
default <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
if (this instanceof Success) {
return successMapper.apply(get());
} else {
return failureMapper.apply((Failure<T, F>) this);
}
}
<N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper);
/**
* Calls the given runnable if this is a success attempt.
@@ -286,12 +241,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> onSuccess(Runnable runnable) {
if (this instanceof Success) {
runnable.run();
}
return this;
}
Attempt<T, F> onSuccess(Runnable runnable);
/**
* Calls the given consumer if this is a success attempt.
@@ -299,12 +249,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the value.
* @return This attempt.
*/
default Attempt<T, F> onSuccess(Consumer<T> consumer) {
if (this instanceof Success) {
consumer.accept(get());
}
return this;
}
Attempt<T, F> onSuccess(Consumer<T> consumer);
/**
* Calls the given consumer if this is a failure attempt.
@@ -312,12 +257,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param runnable The runnable.
* @return This attempt.
*/
default Attempt<T, F> onFailure(Runnable runnable) {
if (this instanceof Failure) {
runnable.run();
}
return this;
}
Attempt<T, F> onFailure(Runnable runnable);
/**
* Calls the given consumer if this is a failure attempt.
@@ -325,12 +265,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the failure instance.
* @return This attempt.
*/
default Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
if (this instanceof Failure) {
consumer.accept((Failure<T, F>) this);
}
return this;
}
Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer);
/**
* Calls the given runnable if this is a failure attempt.
@@ -338,12 +273,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
* @param consumer The consumer with the failure reason.
* @return This attempt.
*/
default Attempt<T, F> onFailureReason(Consumer<F> consumer) {
if (this instanceof Failure) {
consumer.accept(getFailureReason());
}
return this;
}
Attempt<T, F> onFailureReason(Consumer<F> consumer);
/**
* Represents a successful attempt with a value.
@@ -378,6 +308,26 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
return value;
}
@Override
public F getFailureReason() {
throw new UnsupportedOperationException("No failure reason as attempt is a success");
}
@Override
public Message getFailureMessage() {
throw new UnsupportedOperationException("No failure message as attempt is a success");
}
@Override
public boolean isSuccess() {
return true;
}
@Override
public boolean isFailure() {
return false;
}
@Override
public Try<T> toTry() {
return Try.success(value);
@@ -389,13 +339,83 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
}
@Override
public F getFailureReason() {
throw new UnsupportedOperationException("No failure reason as attempt is a success");
public Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
consumer.accept(Either.left(value));
return this;
}
@Override
public Message getFailureMessage() {
throw new UnsupportedOperationException("No failure message as attempt is a success");
public Attempt<T, F> peek(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
return new Success<>(mapper.apply(value));
}
@Override
public <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
return new Success<>(mapper.get());
}
@Override
public <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return mapper.apply(value);
}
@Override
public <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
return mapper.get();
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return changeFailureType();
}
@Override
public <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
return successMapper.apply(value);
}
@Override
public <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
return successMapper.apply(value);
}
@Override
public Attempt<T, F> onSuccess(Runnable runnable) {
runnable.run();
return this;
}
@Override
public Attempt<T, F> onSuccess(Consumer<T> consumer) {
consumer.accept(value);
return this;
}
@Override
public Attempt<T, F> onFailure(Runnable runnable) {
return this;
}
@Override
public Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
return this;
}
@Override
public Attempt<T, F> onFailureReason(Consumer<F> consumer) {
return this;
}
private <UF extends FailureReason> Attempt<T, UF> changeFailureType() {
@SuppressWarnings("unchecked")
Attempt<T, UF> mappedSuccess = (Attempt<T, UF>) this;
return mappedSuccess;
}
@Override
@@ -451,6 +471,26 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
throw exceptionSupplier.apply(this);
}
@Override
public F getFailureReason() {
return failureReason;
}
@Override
public Message getFailureMessage() {
return message;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public boolean isFailure() {
return true;
}
@Override
public Try<T> toTry() {
return Try.failure(new MultiverseException(message));
@@ -462,13 +502,83 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
}
@Override
public F getFailureReason() {
return failureReason;
public Attempt<T, F> thenAccept(Consumer<Either<T, F>> consumer) {
consumer.accept(Either.right(failureReason));
return this;
}
@Override
public Message getFailureMessage() {
return message;
public Attempt<T, F> peek(Consumer<T> consumer) {
return this;
}
@Override
public <U> Attempt<U, F> map(Function<? super T, ? extends U> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> map(Supplier<? extends U> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> mapAttempt(Function<? super T, Attempt<U, F>> mapper) {
return changeValueType();
}
@Override
public <U> Attempt<U, F> mapAttempt(Supplier<Attempt<U, F>> mapper) {
return changeValueType();
}
@Override
public <UF extends FailureReason> Attempt<T, UF> transform(UF failureReason) {
return new Failure<>(failureReason, getFailureMessage(), this);
}
@Override
public <U> U transform(Function<T, U> successMapper, Function<F, U> failureMapper) {
return failureMapper.apply(failureReason);
}
@Override
public <N> N fold(Function<Failure<T, F>, N> failureMapper, Function<T, N> successMapper) {
return failureMapper.apply(this);
}
@Override
public Attempt<T, F> onSuccess(Runnable runnable) {
return this;
}
@Override
public Attempt<T, F> onSuccess(Consumer<T> consumer) {
return this;
}
@Override
public Attempt<T, F> onFailure(Runnable runnable) {
runnable.run();
return this;
}
@Override
public Attempt<T, F> onFailure(Consumer<Failure<T, F>> consumer) {
consumer.accept(this);
return this;
}
@Override
public Attempt<T, F> onFailureReason(Consumer<F> consumer) {
consumer.accept(failureReason);
return this;
}
private <U> Attempt<U, F> changeValueType() {
@SuppressWarnings("unchecked")
Attempt<U, F> mappedFailure = (Attempt<U, F>) this;
return mappedFailure;
}
@Override
@@ -130,7 +130,9 @@ final class WorldConfig {
}
Try<Void> save() {
return configHandle.save();
return configHandle.save().onFailure(ex ->
Logging.warning("Failed to save world config for world '%s'. %s",
worldName, ex.getLocalizedMessage()));
}
ConfigurationSection getConfigurationSection() {
@@ -60,11 +60,15 @@ final class WorldConfigNodes {
}
private <T> ConfigNode<T> node(ConfigNode.Builder<T, ?> nodeBuilder) {
nodeBuilder.onSetValue((oldValue, newValue) -> {
nodeBuilder.onChange((oldValue, newValue) -> {
if (Objects.equals(oldValue, newValue)) return;
if (world == null) return;
MVWorldPropertyChangedEvent<?> mvWorldPropertyChangeEvent = new MVWorldPropertyChangedEvent<>(
world, Option.of(nodeBuilder.name()).getOrElse(nodeBuilder.path()), oldValue, newValue);
world,
Option.of(nodeBuilder.name()).getOrElse(nodeBuilder.path()),
oldValue,
newValue
);
Bukkit.getPluginManager().callEvent(mvWorldPropertyChangeEvent);
Logging.finer("MVWorldPropertyChangeEvent fired for world '%s' with name '%s' and value '%s'",
world.getName(), nodeBuilder.path(), newValue);
@@ -83,7 +87,7 @@ final class WorldConfigNodes {
final ConfigNode<String> alias = node(ConfigNode.builder("alias", String.class)
.defaultValue("")
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (world == null) return;
world.updateColourlessAlias();
}));
@@ -93,14 +97,14 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> allowFlight = node(ConfigNode.builder("allow-flight", Boolean.class)
.defaultValue(false)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
enforcementHandler.handleAllFlightEnforcement(loadedWorld);
}));
final ConfigNode<Boolean> allowWeather = node(ConfigNode.builder("allow-weather", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(world -> {
if (!world.isClearWeather() && !newValue) {
@@ -128,7 +132,7 @@ final class WorldConfigNodes {
final ConfigNode<Difficulty> difficulty = node(ConfigNode.builder("difficulty", Difficulty.class)
.defaultValue(Difficulty.NORMAL)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setDifficulty(newValue));
}));
@@ -175,7 +179,7 @@ final class WorldConfigNodes {
final ConfigNode<GameMode> gamemode = node(ConfigNode.builder("gamemode", GameMode.class)
.defaultValue(GameMode.SURVIVAL)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
enforcementHandler.handleAllGameModeEnforcement(loadedWorld);
}));
@@ -194,7 +198,7 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> keepSpawnInMemory = node(ConfigNode
.builder("keep-spawn-in-memory", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setKeepSpawnInMemory(newValue));
}));
@@ -208,7 +212,7 @@ final class WorldConfigNodes {
final ConfigNode<Boolean> pvp = node(ConfigNode.builder("pvp", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.setPVP(newValue));
}));
@@ -237,12 +241,12 @@ final class WorldConfigNodes {
final ConfigNode<SpawnLocation> spawnLocation = node(ConfigNode.builder("spawn-location", SpawnLocation.class)
.defaultValue(NullSpawnLocation.get())
.hidden()
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
if (!(world instanceof LoadedMultiverseWorld loadedWorld)) return;
if (newValue == null || newValue instanceof NullSpawnLocation) return;
loadedWorld.getBukkitWorld().peek(bukkitWorld -> {
bukkitWorld.setSpawnLocation(newValue);
newValue.setWorld(bukkitWorld);
bukkitWorld.setSpawnLocation(newValue);
});
}));
@@ -263,7 +267,7 @@ final class WorldConfigNodes {
return object.toSection();
}
})
.onSetValue((oldValue, newValue) -> {
.onLoadAndChange((oldValue, newValue) -> {
newValue.setWorldRef(world);
newValue.applyConfigToWorld();
}));
@@ -8,6 +8,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@@ -174,12 +175,12 @@ public final class WorldManager {
private void removeWorldsNotInConfigs(Collection<String> removedWorlds) {
removedWorlds.forEach(worldName -> getWorld(worldName)
.fold(
() -> Attempt.failure(FailureReason.GENERIC, Message.of("world already removed")),
world -> removeWorld(RemoveWorldOptions.world(world))
)
.onFailure(failure -> Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success -> Logging.fine("Unloaded world %s as it was removed from config", worldName)));
.map(world -> removeWorld(RemoveWorldOptions.world(world)))
.getOrElse(() -> worldActionResult(RemoveFailureReason.WORLD_NON_EXISTENT, worldName))
.onFailure(failure ->
Logging.severe("Failed to unload world %s: %s", worldName, failure))
.onSuccess(success ->
Logging.fine("Unloaded world %s as it was removed from config", worldName)));
}
/**
@@ -208,30 +209,29 @@ public final class WorldManager {
}
private void importExistingBukkitWorld(World bukkitWorld) {
if (bukkitWorld == null || isWorld(bukkitWorld.getName())) {
return;
}
importWorld(ImportWorldOptions.worldName(bukkitWorld.getName())
.environment(bukkitWorld.getEnvironment())
.generator(generatorProvider.getDefaultGeneratorForWorld(bukkitWorld.getName())))
.onFailure(failure ->
Logging.severe("Failed to import world %s: %s", bukkitWorld.getName(), failure))
.onSuccess(success ->
Logging.fine("Imported existing world %s", bukkitWorld.getName()));
Option.of(bukkitWorld)
.filter(world -> !isWorld(world.getName()))
.map(world -> importWorld(
ImportWorldOptions.worldName(world.getName())
.environment(world.getEnvironment())
.generator(generatorProvider.getDefaultGeneratorForWorld(world.getName())))
.onFailure(failure ->
Logging.severe("Failed to import world %s: %s", world.getName(), failure))
.onSuccess(newMVWorld ->
Logging.fine("Imported existing world %s", newMVWorld.getName())));
}
/**
* Loads all worlds that are set to autoload.
*/
private void autoLoadWorlds() {
getWorlds().forEach(world -> {
if (isLoadedWorld(world) || !world.isAutoLoad()) {
return;
}
loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to load world %s: %s", world.getName(), failure));
});
getWorlds().stream()
.filter(world -> !isLoadedWorld(world) && world.isAutoLoad())
.forEach(world -> loadWorld(LoadWorldOptions.world(world))
.onFailure(failure ->
Logging.severe("Failed to autoload world %s: %s", world.getName(), failure))
.onSuccess(newMVWorld ->
Logging.fine("Autoloaded world %s", newMVWorld.getName())));
}
/**
@@ -272,15 +272,12 @@ public final class WorldManager {
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(CreateFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> {
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld));
return loadedWorld;
});
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldCreatedEvent(loadedWorld)));
}
/**
@@ -296,14 +293,9 @@ public final class WorldManager {
} else if (isWorld(worldName)) {
return worldActionResult(ImportFailureReason.WORLD_EXIST_UNLOADED, worldName);
}
World bukkitWorld = Bukkit.getWorld(worldName);
if (bukkitWorld != null) {
// World is already loaded, maybe by another plugin
return doImportBukkitWorld(options, bukkitWorld);
}
return validateImportWorldOptions(options).mapAttempt(this::doImportWorld);
return Option.of(Bukkit.getWorld(worldName))
.map(bukkitWorld -> doImportBukkitWorld(options, bukkitWorld))
.getOrElse(() -> validateImportWorldOptions(options).mapAttempt(this::doImportWorld));
}
private Attempt<ImportWorldOptions, ImportFailureReason> validateImportWorldOptions(ImportWorldOptions options) {
@@ -326,15 +318,12 @@ public final class WorldManager {
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(ImportFailureReason.WORLD_CREATOR_FAILED)
.map(bukkitWorld -> {
LoadedMultiverseWorld loadedWorld = newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust());
pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld));
return loadedWorld;
});
.map(bukkitWorld -> newLoadedMultiverseWorld(
bukkitWorld,
generatorString,
options.biome(),
options.useSpawnAdjust()))
.peek(loadedWorld -> pluginManager.callEvent(new MVWorldImportedEvent(loadedWorld)));
}
private Attempt<LoadedMultiverseWorld, ImportFailureReason> doImportBukkitWorld(ImportWorldOptions options, World bukkitWorld) {
@@ -356,7 +345,7 @@ public final class WorldManager {
private MultiverseWorld newMultiverseWorld(String worldName, WorldConfig worldConfig) {
MultiverseWorld mvWorld = new MultiverseWorld(worldName, worldConfig, config);
worldsMap.put(mvWorld.getName(), mvWorld);
worldsMap.put(mvWorld.getName().toLowerCase(Locale.ENGLISH), mvWorld);
corePermissions.addWorldPermissions(mvWorld);
return mvWorld;
}
@@ -393,7 +382,7 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return loadedWorld;
@@ -512,7 +501,7 @@ public final class WorldManager {
locationManipulation,
entityPurger
);
loadedWorldsMap.put(loadedWorld.getName(), loadedWorld);
loadedWorldsMap.put(loadedWorld.getName().toLowerCase(Locale.ENGLISH), loadedWorld);
saveWorldsConfig();
pluginManager.callEvent(new MVWorldLoadedEvent(loadedWorld));
return Attempt.success(loadedWorld);
@@ -701,12 +690,13 @@ public final class WorldManager {
private Attempt<File, DeleteFailureReason> validateWorldToDelete(
@NotNull LoadedMultiverseWorld world) {
File worldFolder = world.getBukkitWorld().map(World::getWorldFolder).getOrNull();
if (worldFolder == null || !worldNameChecker.isValidWorldFolder(worldFolder)) {
Logging.severe("Failed to get world folder for world: " + world.getName());
return worldActionResult(DeleteFailureReason.WORLD_FOLDER_NOT_FOUND, world.getName());
}
return worldActionResult(worldFolder);
return world.getBukkitWorld().map(World::getWorldFolder)
.filter(worldNameChecker::isValidWorldFolder)
.map(this::<File, DeleteFailureReason>worldActionResult)
.getOrElse(() -> {
Logging.severe("World folder does not exist for world: " + world.getName());
return worldActionResult(DeleteFailureReason.WORLD_FOLDER_NOT_FOUND, world.getName());
});
}
/**
@@ -978,7 +968,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getWorld(@Nullable String worldName) {
return getLoadedWorld(worldName).fold(() -> getUnloadedWorld(worldName), Option::of);
return getLoadedWorld(worldName)
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorld(worldName));
}
/**
@@ -990,7 +982,8 @@ public final class WorldManager {
*/
public Option<MultiverseWorld> getWorldByNameOrAlias(@Nullable String worldNameOrAlias) {
return getLoadedWorldByNameOrAlias(worldNameOrAlias)
.fold(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias), Option::of);
.map(world -> (MultiverseWorld) world)
.orElse(() -> getUnloadedWorldByNameOrAlias(worldNameOrAlias));
}
/**
@@ -1005,7 +998,8 @@ public final class WorldManager {
public Collection<MultiverseWorld> getWorlds() {
return worldsMap.values().stream()
.map(world -> getLoadedWorld(world)
.fold(() -> world, loadedWorld -> loadedWorld))
.map(loadedWorld -> (MultiverseWorld) loadedWorld)
.getOrElse(world))
.toList();
}
@@ -1016,7 +1010,7 @@ public final class WorldManager {
* @return True if the world is a world is known to multiverse, but may or may not be loaded.
*/
public boolean isWorld(@Nullable String worldName) {
return worldsMap.containsKey(worldName);
return worldName != null && worldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
}
/**
@@ -1026,7 +1020,9 @@ public final class WorldManager {
* @return The world if it exists.
*/
public Option<MultiverseWorld> getUnloadedWorld(@Nullable String worldName) {
return isLoadedWorld(worldName) ? Option.none() : Option.of(worldsMap.get(worldName));
return isLoadedWorld(worldName)
? Option.none()
: Option.of(worldName).flatMap(name -> Option.of(worldsMap.get(name.toLowerCase(Locale.ENGLISH))));
}
/**
@@ -1078,7 +1074,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable World world) {
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName()));
}
/**
@@ -1088,7 +1084,7 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable MultiverseWorld world) {
return world == null ? Option.none() : Option.of(loadedWorldsMap.get(world.getName()));
return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName()));
}
/**
@@ -1098,7 +1094,8 @@ public final class WorldManager {
* @return The multiverse world if it exists.
*/
public Option<LoadedMultiverseWorld> getLoadedWorld(@Nullable String worldName) {
return Option.of(loadedWorldsMap.get(worldName));
return Option.of(worldName)
.flatMap(name -> Option.of(loadedWorldsMap.get(name.toLowerCase(Locale.ENGLISH))));
}
/**
@@ -1159,7 +1156,7 @@ public final class WorldManager {
* @return True if the world is a multiverse world that is loaded.
*/
public boolean isLoadedWorld(@Nullable String worldName) {
return loadedWorldsMap.containsKey(worldName);
return worldName != null && loadedWorldsMap.containsKey(worldName.toLowerCase(Locale.ENGLISH));
}
/**
@@ -38,8 +38,9 @@ public final class EntitySpawnConfig {
}
public void applyConfigToWorld() {
spawnCategoriesConfig.values()
.forEach(SpawnCategoryConfig::applyConfigToWorld);
for (SpawnCategory category : SpawnCategory.values()) {
getSpawnCategoryConfig(category).applyConfigToWorld();
}
}
@Override
@@ -61,14 +62,18 @@ public final class EntitySpawnConfig {
@ApiStatus.Internal
public static EntitySpawnConfig fromSection(CoreConfig config, ConfigurationSection section) {
Map<SpawnCategory, SpawnCategoryConfig> spawnCategoriesConfig = new LinkedHashMap<>();
section.getValues(false).forEach((key, value) -> {
if (!(value instanceof ConfigurationSection sectionPart)) {
Logging.warning("Invalid spawn category config for " + key + ": " + value);
return;
Map<String, Object> existingCategories = section.getValues(false);
for (SpawnCategory category : SpawnCategory.values()) {
Object value = existingCategories.get(category.toString().toLowerCase(Locale.ENGLISH));
if (!(value instanceof ConfigurationSection)) {
if (value != null) {
Logging.warning("Invalid spawn category config for " + category + ": " + value);
}
value = section.createSection(category.toString());
}
SpawnCategory spawnCategory = SpawnCategory.valueOf(key.toUpperCase(Locale.ENGLISH));
spawnCategoriesConfig.put(spawnCategory, new SpawnCategoryConfig(config, spawnCategory, sectionPart));
});
ConfigurationSection sectionPart = (ConfigurationSection) value;
spawnCategoriesConfig.put(category, new SpawnCategoryConfig(config, category, sectionPart));
}
return new EntitySpawnConfig(config, spawnCategoriesConfig);
}
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.world.entity;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Entity;
@@ -14,6 +15,7 @@ import org.mvplugins.multiverse.core.config.node.ConfigNode;
import org.mvplugins.multiverse.core.config.node.ListConfigNode;
import org.mvplugins.multiverse.core.config.node.Node;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
@@ -70,18 +72,17 @@ public final class SpawnCategoryConfig {
Logging.finer("World %s %s skipping setTicksPerSpawns due to core config", world.getName(), spawnCategory);
return;
}
if (!isSpawn()) {
if (getExceptions().isEmpty()) {
Logging.finer("World %s %s setTicksPerSpawns: 0", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, 0);
} else {
Logging.finer("World %s %s setTicksPerSpawns: -1", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, -1);
}
} else {
Logging.finer("World %s %s setTicksPerSpawns: %d", world.getName(), spawnCategory, getTickRate());
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
if (!isSpawn() && getExceptions().isEmpty()) {
Logging.finer("World %s %s setTicksPerSpawns: 0", world.getName(), spawnCategory);
bukkitWorld.setTicksPerSpawns(spawnCategory, 0);
return;
}
if (getTickRate() == -2) {
Logging.finer("World %s %s skipping setTicksPerSpawns as tick-rate is UNSET", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setTicksPerSpawns: %d", world.getName(), spawnCategory, getTickRate());
bukkitWorld.setTicksPerSpawns(spawnCategory, getTickRate());
}
private void applySpawnLimit(World bukkitWorld) {
@@ -89,6 +90,10 @@ public final class SpawnCategoryConfig {
Logging.finer("Skipping World %s %s setSpawnLimit due to core config", world.getName(), spawnCategory);
return;
}
if (getSpawnLimit() == -2) {
Logging.finer("World %s %s skipping setSpawnLimit as spawn-limit is UNSET", world.getName(), spawnCategory);
return;
}
Logging.finer("World %s %s setSpawnLimit: %d", world.getName(), spawnCategory, getSpawnLimit());
bukkitWorld.setSpawnLimit(spawnCategory, getSpawnLimit());
}
@@ -161,19 +166,39 @@ public final class SpawnCategoryConfig {
final ConfigNode<Boolean> spawn = node(ConfigNode.builder("spawn", Boolean.class)
.defaultValue(true)
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.build());
final ConfigNode<Integer> tickRate = node(ConfigNode.builder("tick-rate", Integer.class)
.defaultValue(-1)
.suggester(input -> List.of("-1", "10", "100", "400", "1000"))
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
if (!config.getApplyEntitySpawnRate()) {
sender.sendMessage(ChatColor.RED + "Warning: Changing tick rates has no effect because " +
"'apply-entity-spawn-rate' is disabled in the core config.");
} else if (newValue == -2) {
sender.sendMessage(ChatColor.YELLOW + "Note: Setting tick-rate to '@unset' may not reset to " +
"the server default until the world is reloaded or server is restarted.");
}
})
.build());
final ConfigNode<Integer> spawnLimit = node(ConfigNode.builder("spawn-limit", Integer.class)
.defaultValue(-1)
.suggester(input -> List.of("-1", "10", "100", "400", "1000"))
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
if (!config.getApplyEntitySpawnLimit()) {
sender.sendMessage(ChatColor.RED + "Warning: Changing spawn limits has no effect because " +
"'apply-entity-spawn-limit' is disabled in the core config.");
} else if (newValue == -2) {
sender.sendMessage(ChatColor.YELLOW + "Note: Setting spawn-limit to '@unset' may not reset to " +
"the server default until the world is reloaded or server is restarted.");
}
})
.build());
final ListConfigNode<EntityType> exceptions = node(ListConfigNode.listBuilder("exceptions", EntityType.class)
@@ -181,7 +206,32 @@ public final class SpawnCategoryConfig {
.itemSuggester(input -> SpawnCategoryMapper.getEntityTypes(spawnCategory).stream()
.map(EntityType::name)
.toList())
.onSetValue((oldValue, newValue) -> applyConfigToWorld())
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.build());
}
private static class SpawnValueSerializer implements NodeSerializer<Integer> {
private static final SpawnValueSerializer INSTANCE = new SpawnValueSerializer();
@Override
public Integer deserialize(Object object, Class<Integer> type) {
String str = String.valueOf(object);
if (str.equalsIgnoreCase("@unset")) {
return -2;
}
if (str.equalsIgnoreCase("@bukkit")) {
return -1;
}
return Integer.parseInt(str);
}
@Override
public Object serialize(Integer object, Class<Integer> type) {
return switch (object) {
case -2 -> "@unset";
case -1 -> "@bukkit";
default -> object;
};
}
}
}
@@ -0,0 +1,90 @@
package org.mvplugins.multiverse.core.world.helpers;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnmodifiableView;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Tracks which players are in which worlds, using a thread-safe map.
* This allows async access to online players list and the world they are in.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@Service
public final class ConcurrentPlayerWorldTracker implements Listener {
private final Map<String, String> playerWorldMap;
@Inject
ConcurrentPlayerWorldTracker(@NotNull MultiverseCore plugin) {
this.playerWorldMap = new ConcurrentHashMap<>();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Get an unmodifiable collection of all online player names on the server.
*
* @return Unmodifiable collection of online player names.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
@UnmodifiableView
public Collection<String> getOnlinePlayers() {
return Collections.unmodifiableCollection(playerWorldMap.keySet());
}
/**
* Get the world name a player is currently in.
*
* @param playerName Name of the player.
* @return World name the player is in, or null if the player is not online.
*
* @since 5.4
*/
@ApiStatus.AvailableSince("5.4")
@NotNull
public Option<String> getPlayerWorld(String playerName) {
return Option.of(playerWorldMap.get(playerName));
}
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerJoin(PlayerJoinEvent event) {
setPlayerWorld(event.getPlayer());
}
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerChangedWorld(@NotNull PlayerChangedWorldEvent event) {
setPlayerWorld(event.getPlayer());
}
private void setPlayerWorld(Player player) {
String playerName = player.getName();
String worldName = player.getWorld().getName();
playerWorldMap.put(playerName, worldName);
}
@EventHandler
private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
String playerName = event.getPlayer().getName();
playerWorldMap.remove(playerName);
}
}
@@ -3,13 +3,13 @@ package org.mvplugins.multiverse.core.world.helpers;
import java.io.File;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import io.vavr.control.Option;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.utils.REPatterns;
/**
* <p>Utility class in helping to check the status of a world name and it's associated world folder.</p>
@@ -20,11 +20,11 @@ import org.jvnet.hk2.annotations.Service;
@Service
public final class WorldNameChecker {
private static final Pattern WORLD_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9/._-]+");
private static final Set<String> BLACKLIST_NAMES = Set.of(
"cache",
"config",
"crash-reports",
"libraries",
"logs",
"plugins",
"versions");
@@ -47,18 +47,21 @@ public final class WorldNameChecker {
*/
@NotNull
public NameStatus checkName(@Nullable String worldName) {
return Option.of(worldName).map(name -> {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!WORLD_NAME_PATTERN.matcher(name).matches()) {
return NameStatus.INVALID_CHARS;
}
return NameStatus.VALID;
}).getOrElse(NameStatus.EMPTY);
return Option.of(worldName)
.map(name -> name.toLowerCase(Locale.ENGLISH))
.map(name -> {
if (name.isEmpty()) {
return NameStatus.EMPTY;
}
if (BLACKLIST_NAMES.contains(name)) {
return NameStatus.BLACKLISTED;
}
if (!REPatterns.NAMESPACE_KEY.matcher(name).matches()) {
return NameStatus.INVALID_CHARS;
}
return NameStatus.VALID;
})
.getOrElse(NameStatus.EMPTY);
}
/**
@@ -94,7 +94,6 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
return keepWorldConfig;
}
/**
* Sets whether to keep the world border of the world during cloning.
*
@@ -38,7 +38,7 @@ public final class UnloadWorldOptions {
* Sets whether to unload the bukkit world.
* <br />
* By setting this to false, multiverse will essentially untrack the world, but the world itself is still loaded.
* This should be only used in edge cases where the world is used by other plugins byt you don't want multiverse
* This should be only used in edge cases where the world is used by other plugins but you don't want multiverse
* to handle it.
*
* @param unloadBukkitWorldInput Whether to unload the bukkit world.
@@ -5,6 +5,7 @@ import org.bukkit.World
import org.bukkit.WorldType
import org.hamcrest.MatcherAssert.assertThat
import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasFiredEventInstance
import org.mockbukkit.mockbukkit.matcher.plugin.PluginManagerFiredEventClassMatcher.hasNotFiredEventInstance
import org.mvplugins.multiverse.core.TestWithMockBukkit
import org.mvplugins.multiverse.core.event.world.*
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions
@@ -43,7 +44,7 @@ class WorldManagerTest : TestWithMockBukkit() {
@Test
fun `Create world with custom options`() {
assertTrue(worldManager.createWorld(
CreateWorldOptions.worldName("world_nether")
CreateWorldOptions.worldName("Section/my-nether_world")
.environment(World.Environment.NETHER)
.generateStructures(false)
.seed(1234L)
@@ -51,11 +52,11 @@ class WorldManagerTest : TestWithMockBukkit() {
.worldType(WorldType.FLAT)
).isSuccess)
val getWorld = worldManager.getLoadedWorld("world_nether")
val getWorld = worldManager.getLoadedWorld("Section/my-nether_world")
assertTrue(getWorld.isDefined)
val world = getWorld.get()
assertNotNull(world)
assertEquals("world_nether", world.name)
assertEquals("Section/my-nether_world", world.name)
assertEquals(World.Environment.NETHER, world.environment)
assertFalse(world.canGenerateStructures().get())
assertEquals(1234L, world.seed)
@@ -84,6 +85,18 @@ class WorldManagerTest : TestWithMockBukkit() {
)
}
@Test
fun `Create world failed - world exists and loaded (case insensitive)`() {
assertEquals(
CreateFailureReason.WORLD_EXIST_LOADED,
worldManager.createWorld(CreateWorldOptions.worldName("WoRld2")).failureReason
)
assertEquals(
CreateFailureReason.WORLD_EXIST_LOADED,
worldManager.createWorld(CreateWorldOptions.worldName("WORLD2")).failureReason
)
}
@Test
fun `Create world failed - world exists but unloaded`() {
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world)).isSuccess)
@@ -255,4 +268,60 @@ class WorldManagerTest : TestWithMockBukkit() {
assertEquals(world, worldManager.getWorldByNameOrAlias("testalias").orNull)
assertNull(worldManager.getUnloadedWorldByNameOrAlias("testalias").orNull)
}
@Test
fun `Get world`() {
assertEquals(world, worldManager.getLoadedWorld("world").orNull)
assertNull(worldManager.getUnloadedWorld("world").orNull)
assertEquals(world, worldManager.getWorld("world").orNull)
assertTrue(worldManager.isLoadedWorld("world"))
assertFalse(worldManager.isUnloadedWorld("world"))
assertTrue(worldManager.isWorld("world"))
val unloadedWorld = worldManager.unloadWorld(UnloadWorldOptions.world(world)).get()
assertNull(worldManager.getLoadedWorld("world").orNull)
assertEquals(unloadedWorld, worldManager.getUnloadedWorld("world").orNull)
assertEquals(unloadedWorld, worldManager.getWorld("world").orNull)
assertFalse(worldManager.isLoadedWorld("world"))
assertTrue(worldManager.isUnloadedWorld("world"))
assertTrue(worldManager.isWorld("world"))
}
@Test
fun `Get world is case insensitive`() {
assertEquals(world2, worldManager.getLoadedWorld("WORLD2").orNull)
assertNull(worldManager.getUnloadedWorld("wOrld2").orNull)
assertEquals(world2, worldManager.getWorld("wOrld2").orNull)
assertTrue(worldManager.isLoadedWorld("WoRlD2"))
assertFalse(worldManager.isUnloadedWorld("WoRlD2"))
assertTrue(worldManager.isWorld("wORLD2"))
val unloadedWorld2 = worldManager.unloadWorld(UnloadWorldOptions.world(world2)).get()
assertNull(worldManager.getLoadedWorld("wOrld2").orNull)
assertEquals(unloadedWorld2, worldManager.getUnloadedWorld("wOrld2").orNull)
assertEquals(unloadedWorld2, worldManager.getWorld("wORLD2").orNull)
assertFalse(worldManager.isLoadedWorld("WoRlD2"))
assertTrue(worldManager.isUnloadedWorld("WoRlD2"))
assertTrue(worldManager.isWorld("wORLD2"))
}
@Test
fun `Set different value - MVWorldPropertyChangedEvent event is fired`() {
server.pluginManager.clearEvents()
world.scale = 8.0
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldPropertyChangedEvent::class.java))
}
@Test
fun `Set equal value - MVWorldPropertyChangedEvent event is not fired`() {
server.pluginManager.clearEvents()
world.scale = 1.0
assertThat(server.pluginManager, hasNotFiredEventInstance(MVWorldPropertyChangedEvent::class.java))
}
}
@@ -38,6 +38,7 @@ class WorldNameCheckerTest : TestWithMockBukkit() {
fun `Blacklisted world name`() {
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("logs"))
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("plugins"))
assertEquals(WorldNameChecker.NameStatus.BLACKLISTED, worldNameChecker.checkName("CACHE"))
}
@Test
@@ -41,6 +41,7 @@ messaging:
command:
resolve-alias-name: true
simplified-destination-tab-completion: false
confirm-mode: enable
use-confirm-otp: true
confirm-timeout: 30
+82 -2
View File
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -71,6 +111,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+41 -1
View File
@@ -33,6 +33,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+78 -3
View File
@@ -34,11 +34,46 @@ world:
pitch: 0.0
yaw: 0.0
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
spawn: falSe
tick-rate: 123
spawn-limit: -1
exceptions: [cow]
spawn-limit: '@unset'
exceptions: [ cow ]
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: # should be parsed as list of string
- a
- 1
@@ -78,6 +113,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+111 -21
View File
@@ -34,16 +34,46 @@ world_the_end:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world[dot]a[dot]b:
@@ -82,16 +112,46 @@ world[dot]a[dot]b:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist:
- test
version: 1.2
@@ -131,16 +191,46 @@ world[dot]a[dot]c:
pitch: 0.0
yaw: 0.0
spawning:
animal:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
monster:
exceptions: [ ]
spawn: true
spawn-limit: -1
tick-rate: -1
spawn-limit: '@unset'
tick-rate: '@bukkit'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@bukkit'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist:
- test
version: 1.2
+123 -3
View File
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -71,7 +111,47 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
new[dot]world:
@@ -104,6 +184,46 @@ new[dot]world:
seed: -9223372036854775808
spawn-location:
==: MVNullLocation (It's a bug if you see this in your config file)
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
@@ -33,7 +33,47 @@ world:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
world_nether:
@@ -66,6 +106,46 @@ world_nether:
seed: -9223372036854775808
spawn-location:
==: MVNullLocation (It's a bug if you see this in your config file)
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2
+74 -4
View File
@@ -34,17 +34,47 @@ world:
pitch: 0.0
yaw: 0.0
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
spawn: true
tick-rate: 111
spawn-limit: -1
spawn-limit: '@unset'
exceptions:
- cow
- pig
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
spawn: false
tick-rate: -1
spawn-limit: -1
tick-rate: '@unset'
spawn-limit: '@unset'
exceptions: [ ]
world-blacklist: []
version: 1.2
@@ -83,6 +113,46 @@ world_nether:
z: 48.0
pitch: 0.0
yaw: 0.0
spawning: {}
spawning:
monster:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_animal:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
water_underground_creature:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
ambient:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
axolotl:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
misc:
exceptions: [ ]
spawn: true
spawn-limit: '@unset'
tick-rate: '@unset'
world-blacklist: []
version: 1.2