diff --git a/build.gradle b/build.gradle index 36dbc1146..fdbe71951 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ dependencies { } // PlaceholderAPI - externalPlugin 'me.clip:placeholderapi:2.11.6' + externalPlugin 'me.clip:placeholderapi:2.12.3' // Command Framework shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT' @@ -48,7 +48,7 @@ dependencies { } // Utils - shadowed 'io.vavr:vavr:0.10.7' + shadowed 'io.vavr:vavr:1.0.1' shadowed 'org.glassfish.hk2:hk2-locator:3.1.1' shadowed('org.glassfish.hk2:hk2-inhabitant-generator:3.1.1') { exclude group: 'org.apache.maven', module: 'maven-core' @@ -57,11 +57,11 @@ dependencies { exclude group: 'junit', module: 'junit' } shadowed 'de.themoep.idconverter:mappings:1.2-SNAPSHOT' - shadowed('org.bstats:bstats-bukkit:3.1.0') { + shadowed('org.bstats:bstats-bukkit:3.2.1') { exclude group: 'org.bukkit', module: 'bukkit' } - shadowed 'net.minidev:json-smart:2.5.2' - shadowed 'org.jetbrains:annotations:26.0.2' + shadowed 'net.minidev:json-smart:2.6.0' + shadowed 'org.jetbrains:annotations:26.1.0' shadowed 'io.papermc:paperlib:1.0.8' // Tests diff --git a/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java b/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java index cae726cbd..95e6262a4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java +++ b/src/main/java/org/mvplugins/multiverse/core/MultiverseCore.java @@ -102,11 +102,12 @@ public void onEnable() { SpawnCategoryMapper.buildSpawnCategoryMap(); // Initialize the worlds - worldManagerProvider.get().initAllWorlds().andThenTry(() -> { + Try.run(() -> { + setUpLocales(); + worldManagerProvider.get().initAllWorlds(); loadEconomist(); // Setup economy here so vault is loaded loadAnchors(); registerDynamicListeners(CoreListener.class); - setUpLocales(); registerCommands(CoreCommand.class); registerDestinations(); setupMetrics(); diff --git a/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java b/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java index 8b1de9362..cd0400713 100644 --- a/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java +++ b/src/main/java/org/mvplugins/multiverse/core/PlaceholderExpansionHook.java @@ -87,7 +87,8 @@ public boolean persist() { @Override public @Nullable String onRequest(OfflinePlayer offlinePlayer, @NotNull String params) { // Split string in to an Array with underscores - List paramsArray = Lists.newArrayList(REPatterns.UNDERSCORE.split(params)); + List paramsArray = Lists.newArrayList( + StringFormatter.parseQuotesInArgs(REPatterns.UNDERSCORE.split(params), "_")); // No placeholder defined if (paramsArray.isEmpty()) { diff --git a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandManager.java b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandManager.java index 3dabe4cb3..eafa2166e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandManager.java @@ -12,6 +12,8 @@ import co.aikar.commands.PaperCommandManager; import co.aikar.commands.RootCommand; import com.dumptruckman.minecraft.util.Logging; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import jakarta.inject.Inject; import jakarta.inject.Provider; import org.bukkit.Bukkit; @@ -24,6 +26,7 @@ import org.mvplugins.multiverse.core.command.queue.CommandQueueManager; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.locale.PluginLocales; +import org.mvplugins.multiverse.core.locale.message.LocalizedMessage; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.helpers.WorldNameChecker; @@ -68,6 +71,16 @@ public class MVCommandManager extends PaperCommandManager { this.setDefaultExceptionHandler(new MVDefaultExceptionHandler()); } + @PostConstruct + private void postConstruct() { + LocalizedMessage.setDefaultLocalesManager(getLocales()); + } + + @PreDestroy + private void preDestroy() { + LocalizedMessage.setDefaultLocalesManager(null); + } + /** * Registers a list of commands and handles {@link LegacyAliasCommand} based on config option. * @param commands The commands to register diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/CheckCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/CheckCommand.java index 3f2b3b334..4c3da572c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/CheckCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/CheckCommand.java @@ -61,7 +61,7 @@ void onCheckCommand( replace("{location}").with(destination.getLocation(player) .map(locationManipulation::locationToString) .map(Message::of) - .getOrElse(() -> Message.of(MVCorei18n.GENERIC_NULL, "Null!")))); + .getOrElse(() -> Message.of(MVCorei18n.GENERIC_NULL)))); // TODO: Show permission required for this particular destination } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/CloneCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/CloneCommand.java index 5a9ada784..9b2b0b105 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/CloneCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/CloneCommand.java @@ -51,7 +51,7 @@ void onCloneCommand( MultiverseWorld world, @Syntax("") - @Description("{@@mv-core.clone.newWorld.description}") + @Description("{@@mv-core.clone.newworld.description}") String newWorldName, @Optional diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java index 059086407..cfbd2b51f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java @@ -79,7 +79,7 @@ void onDeleteCommand( commandQueueManager.addToQueue(CommandQueuePayload .issuer(issuer) .action(() -> runDeleteCommand(issuer, world, parsedFlags)) - .prompt(Message.of(MVCorei18n.DELETE_PROMPT, "", + .prompt(Message.of(MVCorei18n.DELETE_PROMPT, Replace.WORLD.with(world.getName())))); } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java index b4cdd84ed..7acd67d8c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java @@ -89,7 +89,7 @@ void onRegenCommand( commandQueueManager.addToQueue(CommandQueuePayload .issuer(issuer) .action(() -> runRegenCommand(issuer, world, parsedFlags)) - .prompt(Message.of(MVCorei18n.REGEN_PROMPT, "", + .prompt(Message.of(MVCorei18n.REGEN_PROMPT, Replace.WORLD.with(world.getName())))); } diff --git a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java index 9aaf5fc4c..3140cb126 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java @@ -246,6 +246,16 @@ public DimensionFormat getEndWorldNameFormat() { return configHandle.get(configNodes.endWorldNameFormat); } + @ApiStatus.AvailableSince("5.8") + public Try setWarnAliasConflicts(boolean warnAliasConflicts) { + return configHandle.set(configNodes.warnAliasConflicts, warnAliasConflicts); + } + + @ApiStatus.AvailableSince("5.8") + public boolean getWarnAliasConflicts() { + return configHandle.get(configNodes.warnAliasConflicts); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java index 7a6b5ed5f..e40103841 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java @@ -200,6 +200,15 @@ private N node(N node) { .stringParser(DimensionFormatNodeStringParser.INSTANCE) .build()); + final ConfigNode warnAliasConflicts = node(ConfigNode.builder("world.warn-alias-conflicts", Boolean.class) + .comment("") + .comment("Sets whether Multiverse will warn about alias duplicates when adding worlds or modifying aliases.") + .comment("Although not enforced, it is highly recommended to not have multiple worlds with the same alias or") + .comment("alias matching another world's name as it can cause confusion during world listing and selection in commands.") + .defaultValue(true) + .name("warn-alias-conflicts") + .build()); + private final ConfigHeaderNode teleportHeader = node(ConfigHeaderNode.builder("teleport") .comment("") .comment("") diff --git a/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java index caab48f1d..837447bfa 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/node/ConfigNode.java @@ -492,6 +492,7 @@ protected Builder(@NotNull String path, @NotNull Class type) { * @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead. */ @Deprecated(since = "5.4", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") public @NotNull B onSetValue(@NotNull BiConsumer onSetValue) { return onLoadAndChange(onSetValue::accept); } diff --git a/src/main/java/org/mvplugins/multiverse/core/exceptions/MultiverseException.java b/src/main/java/org/mvplugins/multiverse/core/exceptions/MultiverseException.java index bb8fa88a7..033372f5e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/exceptions/MultiverseException.java +++ b/src/main/java/org/mvplugins/multiverse/core/exceptions/MultiverseException.java @@ -1,6 +1,5 @@ package org.mvplugins.multiverse.core.exceptions; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.core.locale.message.LocalizableMessage; diff --git a/src/main/java/org/mvplugins/multiverse/core/exceptions/command/MVInvalidCommandArgument.java b/src/main/java/org/mvplugins/multiverse/core/exceptions/command/MVInvalidCommandArgument.java index 10a46322f..d088a0880 100644 --- a/src/main/java/org/mvplugins/multiverse/core/exceptions/command/MVInvalidCommandArgument.java +++ b/src/main/java/org/mvplugins/multiverse/core/exceptions/command/MVInvalidCommandArgument.java @@ -2,6 +2,7 @@ import co.aikar.commands.InvalidCommandArgument; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.locale.message.LocalizableMessage; import org.mvplugins.multiverse.core.locale.message.LocalizedMessage; import org.mvplugins.multiverse.core.locale.message.Message; @@ -18,16 +19,20 @@ public static MVInvalidCommandArgument causeBy(Throwable throwable) { @ApiStatus.AvailableSince("5.7") public static MVInvalidCommandArgument causeBy(Throwable throwable, boolean showSyntax) { - return (throwable instanceof LocalizableMessage localizableMessage) - ? of(localizableMessage.getLocalizableMessage(), showSyntax) - : new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax); + if (throwable instanceof LocalizableMessage localizableMessage) { + Message message = localizableMessage.getLocalizableMessage(); + return message == null + ? new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax) + : of(message, showSyntax); + } + return new MVInvalidCommandArgument(throwable.getLocalizedMessage(), showSyntax); } - public static MVInvalidCommandArgument of(Message message) { + public static MVInvalidCommandArgument of(@NotNull Message message) { return of(message, true); } - public static MVInvalidCommandArgument of(Message message, boolean showSyntax) { + public static MVInvalidCommandArgument of(@NotNull Message message, boolean showSyntax) { return message instanceof LocalizedMessage ? new MVInvalidCommandArgument((LocalizedMessage) message, showSyntax) : new MVInvalidCommandArgument(message, showSyntax); @@ -42,6 +47,6 @@ private MVInvalidCommandArgument(Message message, boolean showSyntax) { } private MVInvalidCommandArgument(LocalizedMessage message, boolean showSyntax) { - super(message.getMessageKey(), showSyntax, message.getReplacements()); + super(message.getMessageKey(), showSyntax, message.getRawReplacements()); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java index dee0575db..4ec67906d 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java @@ -182,7 +182,7 @@ public enum MVCorei18n implements MessageKeyProvider { // /mv meta info META_INFO_DESCRIPTION, - META_INFO_WORLD, + META_INFO_WORLD_DESCRIPTION, META_INFO_HEADER, META_INFO_NOCONTENT, @@ -370,6 +370,11 @@ public enum MVCorei18n implements MessageKeyProvider { TELEPORTFAILUREREASON_TELEPORT_FAILED_EXCEPTION, TELEPORTFAILUREREASON_EVENT_CANCELLED, + // alias name conflict + ALIASNAMECONFLICT_DETECTED, + ALIASNAMECONFLICT_DUPLICATEALIAS, + ALIASNAMECONFLICT_DUPLICATEWORLDNAME, + // world manager result CLONEWORLD_INVALIDWORLDNAME, CLONEWORLD_WORLDEXISTFOLDER, diff --git a/src/main/java/org/mvplugins/multiverse/core/locale/message/LocalizedMessage.java b/src/main/java/org/mvplugins/multiverse/core/locale/message/LocalizedMessage.java index 49a4f575f..03169c167 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/message/LocalizedMessage.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/message/LocalizedMessage.java @@ -1,17 +1,23 @@ package org.mvplugins.multiverse.core.locale.message; -import java.util.Objects; - import co.aikar.commands.ACFUtil; import co.aikar.commands.CommandIssuer; import co.aikar.commands.Locales; import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class LocalizedMessage extends Message implements MessageKeyProvider { + private static @Nullable Locales locales; + + @ApiStatus.Internal + public static void setDefaultLocalesManager(@Nullable Locales locales) { + LocalizedMessage.locales = locales; + } + private final @NotNull MessageKeyProvider messageKeyProvider; LocalizedMessage( @@ -22,11 +28,33 @@ public final class LocalizedMessage extends Message implements MessageKeyProvide this.messageKeyProvider = messageKeyProvider; } + /** + * {@inheritDoc} + */ @Override public MessageKey getMessageKey() { return messageKeyProvider.getMessageKey(); } + /** + * {@inheritDoc} + */ + @Override + public @NotNull String[] getReplacements() { + return locales == null ? super.getReplacements() : getReplacements(locales, null); + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull String formatted() { + return locales == null ? super.formatted() : formatted(locales, null); + } + + /** + * {@inheritDoc} + */ @Override public @NotNull String formatted(@NotNull Locales locales, @Nullable CommandIssuer commandIssuer) { String[] parsedReplacements = getReplacements(locales, commandIssuer); diff --git a/src/main/java/org/mvplugins/multiverse/core/locale/message/Message.java b/src/main/java/org/mvplugins/multiverse/core/locale/message/Message.java index 78b76de76..e3697e91d 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/message/Message.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/message/Message.java @@ -6,9 +6,11 @@ import co.aikar.commands.CommandIssuer; import co.aikar.commands.Locales; import co.aikar.locales.MessageKeyProvider; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.mvplugins.multiverse.core.command.MVCommandManager; /** * A message that can be formatted with replacements and localized. @@ -81,17 +83,47 @@ protected Message(@NotNull String message, @NotNull MessageReplacement... replac } /** - * Gets the replacements for this message. + * Gets the replacements for this message. This is the raw, non-localized parsing of replacements. *
* This array is guaranteed to be of even length and suitable for use with * {@link ACFUtil#replaceStrings(String, String...)}. * * @return The replacements + * + * @since 5.8 */ - public @NotNull String[] getReplacements() { + @ApiStatus.AvailableSince("5.8") + public @NotNull String[] getRawReplacements() { return toReplacementsArray(replacements); } + /** + * Gets the replacements for this message with localization support when the plugin has loaded and injected its + * default {@link MVCommandManager#getLocales()} instance. + * Otherwise, it will fall back to use {@link getRawReplacements()}. + * + * @return The replacements + */ + public @NotNull String[] getReplacements() { + return getRawReplacements(); + } + + /** + * Gets the replacements for this message with localization support based on the command issuer's locale config. + *
+ * This array is guaranteed to be of even length and suitable for use with + * {@link ACFUtil#replaceStrings(String, String...)}. + * + * @param issuer The command issuer the message is for + * @return The replacements + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public @NotNull String[] getReplacements(@NotNull CommandIssuer issuer) { + return getReplacements(issuer.getManager().getLocales(), issuer); + } + /** * Gets the replacements for this message with localization support. *
@@ -116,20 +148,36 @@ protected Message(@NotNull String message, @NotNull MessageReplacement... replac } /** - * Gets the formatted message. + * Gets the raw formatted message. *
* This is the raw, non-localized message with replacements applied. + * This method will never use the locale message key. * * @return The formatted message + * + * @since 5.8 */ - public @NotNull String formatted() { - String[] parsedReplacements = getReplacements(); + @ApiStatus.AvailableSince("5.8") + public @NotNull String rawFormatted() { + String[] parsedReplacements = getRawReplacements(); if (parsedReplacements.length == 0) { return raw(); } return ACFUtil.replaceStrings(message, parsedReplacements); } + /** + * Gets the formatted message. + *
+ * This is the localized message with replacements applied when the plugin has loaded and injected its default + * {@link MVCommandManager#getLocales()} instance. Otherwise, it will fall back to use {@link rawFormatted()}. + * + * @return The formatted message + */ + public @NotNull String formatted() { + return rawFormatted(); + } + /** * Gets the formatted message from localization data. *
@@ -146,9 +194,9 @@ protected Message(@NotNull String message, @NotNull MessageReplacement... replac * Gets the formatted message from localization data. *
* This is the localized message with replacements applied. The message is localized using the locale of the given - * command issuer, if not null. + * command issuer. * - * @param commandIssuer The command issuer the message is for, or null for the console (default locale) + * @param commandIssuer The command issuer the message is for. * @return The formatted, localized message */ public @NotNull String formatted(@NotNull CommandIssuer commandIssuer) { @@ -178,7 +226,7 @@ private static String[] toReplacementsArray(@NotNull MessageReplacement... repla int i = 0; for (MessageReplacement replacement : replacements) { replacementsArray[i++] = replacement.getKey(); - replacementsArray[i++] = replacement.getReplacement().fold(s -> s, Message::formatted); + replacementsArray[i++] = replacement.getReplacement().fold(s -> s, Message::rawFormatted); } return replacementsArray; } diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/ReflectHelper.java b/src/main/java/org/mvplugins/multiverse/core/utils/ReflectHelper.java index 25cf52cd9..1ac7e95e2 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/ReflectHelper.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/ReflectHelper.java @@ -189,6 +189,7 @@ public static Try tryGetStaticFieldValue(@NotNull Field field, @NotNull C * the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static Class getClass(String classPath) { try { @@ -211,6 +212,7 @@ public static Class getClass(String classPath) { * used to handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static Method getMethod(Class clazz, String methodName, Class... parameterTypes) { try { @@ -235,6 +237,7 @@ public static Method getMethod(Class clazz, String methodName, Class.. * used to handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static Method getMethod(C classInstance, String methodName, Class... parameterTypes) { return getMethod(classInstance.getClass(), methodName, parameterTypes); @@ -254,6 +257,7 @@ public static Method getMethod(C classInstance, String methodName, Class. * be used to handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable @SuppressWarnings("unchecked") public static R invokeMethod(C classInstance, Method method, Object...parameters) { @@ -276,6 +280,7 @@ public static R invokeMethod(C classInstance, Method method, Object...par * handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static Field getField(Class clazz, String fieldName) { try { @@ -299,6 +304,7 @@ public static Field getField(Class clazz, String fieldName) { * handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static Field getField(C classInstance, String fieldName) { return getField(classInstance.getClass(), fieldName); @@ -318,6 +324,7 @@ public static Field getField(C classInstance, String fieldName) { * used to handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static V getFieldValue(C classInstance, @Nullable Field field, @NotNull Class fieldType) { try { @@ -345,6 +352,7 @@ public static V getFieldValue(C classInstance, @Nullable Field field, @No * which returns a {@link Try} that can be used to handle the failure case more explicitly. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") @Nullable public static V getFieldValue(C classInstance, @Nullable String fieldName, @NotNull Class fieldType) { return getFieldValue(classInstance, getField(classInstance, fieldName), fieldType); diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java b/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java index acfe5da65..4f7fa18e0 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/StringFormatter.java @@ -118,6 +118,20 @@ public static Collection addOnToCommaSeparated(@Nullable String input, @ * @return The parsed args */ public static @NotNull Collection parseQuotesInArgs(@NotNull String[] args) { + return parseQuotesInArgs(args, " "); + } + + /** + * Parse quotes in args into a single string. E.g. ["\"my", "string\""] -> ["my string"] + * + * @param args The args to parse + * @param separator The separator to use between args when joining them + * @return The parsed args + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public static @NotNull Collection parseQuotesInArgs(@NotNull String[] args, @NotNull String separator) { List result = new ArrayList<>(args.length); StringBuilder current = new StringBuilder(); boolean inQuotes = false; @@ -131,13 +145,13 @@ public static Collection addOnToCommaSeparated(@Nullable String input, @ quoteStartIndex = i; current.append(arg.substring(1)); } else if (inQuotes && arg.endsWith("\"")) { - current.append(" ").append(arg, 0, arg.length() - 1); + current.append(separator).append(arg, 0, arg.length() - 1); result.add(current.toString()); current.setLength(0); inQuotes = false; quoteStartIndex = -1; } else if (inQuotes) { - current.append(" ").append(arg); + current.append(separator).append(arg); } else if (arg.startsWith("\"") && arg.endsWith("\"") && arg.length() > 1) { // Fully quoted in one token result.add(arg.substring(1, arg.length() - 1)); diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java index 80d30fbc9..e4673d4a9 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/Attempt.java @@ -49,7 +49,7 @@ static Attempt.Success successRef(T value) { @ApiStatus.AvailableSince("5.7") static Attempt.Failure failureRef( F failureReason, MessageReplacement... messageReplacements) { - return new Failure<>(failureReason, Message.of(failureReason, "Failed!", messageReplacements)); + return new Failure<>(failureReason, Message.of(failureReason, messageReplacements)); } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/Result.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/Result.java index 8979b6d42..2d3516397 100644 --- a/src/main/java/org/mvplugins/multiverse/core/utils/result/Result.java +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/Result.java @@ -220,7 +220,7 @@ final class Success implements Success(S successReason, MessageReplacement[] replacements) { this.successReason = successReason; - this.message = Message.of(successReason, "Success!", replacements); + this.message = Message.of(successReason, replacements); } @Override @@ -273,7 +273,7 @@ final class Failure implements Failure(F failureReason, MessageReplacement[] replacements) { this.failureReason = failureReason; - this.message = Message.of(failureReason, "Failed!", replacements); + this.message = Message.of(failureReason, replacements); } @Override diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java index fca89d685..b31040a31 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java @@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.MultiverseCore; +import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.config.node.MapConfigNode; import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer; @@ -27,6 +28,7 @@ import org.mvplugins.multiverse.core.economy.MVEconomist; import org.mvplugins.multiverse.core.utils.MaterialConverter; import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; +import org.mvplugins.multiverse.core.world.helpers.AliasNameConflictChecker; import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler; import org.mvplugins.multiverse.core.world.key.WorldKeyOrName; import org.mvplugins.multiverse.core.world.location.NullSpawnLocation; @@ -43,6 +45,8 @@ final class WorldConfigNodes { private WorldManager worldManager; private EnforcementHandler enforcementHandler; private CoreConfig config; + private AliasNameConflictChecker aliasNameConflictChecker; + private MVCommandManager commandManager; private WorldKeyOrName keyOrName; private MultiverseWorld world = null; @@ -50,6 +54,8 @@ final class WorldConfigNodes { this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class); this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class); this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class); + this.aliasNameConflictChecker = multiverseCore.getServiceLocator().getService(AliasNameConflictChecker.class); + this.commandManager = multiverseCore.getServiceLocator().getService(MVCommandManager.class); this.keyOrName = keyOrName; } @@ -93,7 +99,7 @@ private ConfigNode node(ConfigNode.Builder nodeBuilder) { final ConfigNode alias = node(ConfigNode.builder("alias", String.class) .defaultValue("") - .onLoadAndChange((oldValue, newValue) -> { + .onLoadAndChange((sender, oldValue, newValue) -> { worldManager.getWorldStore().changeAlias( ChatTextFormatter.removeColor(oldValue), ChatTextFormatter.removeColor(newValue), @@ -101,6 +107,10 @@ private ConfigNode node(ConfigNode.Builder nodeBuilder) { ); if (world == null) return; world.updateColourlessAlias(); + if (config.getWarnAliasConflicts()) { + aliasNameConflictChecker.checkDuplicateFor(world) + .sendConflictMessage(commandManager.getCommandIssuer(sender)); + } })); final ConfigNode allowAdvancementGrant = node(ConfigNode.builder("allow-advancement-grant", Boolean.class) diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java index 9351147e4..2c070b4b5 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -154,10 +154,14 @@ public final class WorldManager { */ @ApiStatus.Internal public Try initAllWorlds() { - return updateWorldsFromConfig().andThenTry(() -> { - importExistingWorlds(); - autoLoadWorlds(); - }).flatMap(ignore -> saveWorldsConfig()); + return updateWorldsFromConfig() + .andThenTry(this::importExistingWorlds) + .andThenTry(this::autoLoadWorlds) + .flatMap(ignore -> saveWorldsConfig()) + .onFailure(ex -> { + Logging.severe("Failed to load worlds from config: %s", ex.getMessage()); + ex.printStackTrace(); + }); } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/world/helpers/AliasNameConflictChecker.java b/src/main/java/org/mvplugins/multiverse/core/world/helpers/AliasNameConflictChecker.java new file mode 100644 index 000000000..506631798 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/world/helpers/AliasNameConflictChecker.java @@ -0,0 +1,157 @@ +package org.mvplugins.multiverse.core.world.helpers; + +import com.google.common.base.Strings; +import jakarta.inject.Inject; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.locale.MVCorei18n; +import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; +import org.mvplugins.multiverse.core.world.MultiverseWorld; +import org.mvplugins.multiverse.core.world.WorldManager; + +import java.util.ArrayList; +import java.util.List; + +import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace; + +/** + * Checks world aliases for conflicts with other world aliases and names. + * + * @since 5.8 + */ +@ApiStatus.AvailableSince("5.8") +@Service +public class AliasNameConflictChecker { + + private final WorldManager worldManager; + + @Inject + private AliasNameConflictChecker(@NotNull WorldManager worldManager) { + this.worldManager = worldManager; + } + + /** + * Checks whether the target world's alias conflicts with another world's alias or name. + * Color formatting and letter case are ignored when aliases are compared. + * + * @param targetWorld The world whose alias should be checked. + * @return The detected alias conflicts. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public @NotNull AliasNameConflictResult checkDuplicateFor(MultiverseWorld targetWorld) { + AliasNameConflictResult aliasNameConflictResult = new AliasNameConflictResult(targetWorld); + String targetWorldAlias = ChatTextFormatter.removeColor(targetWorld.getAlias()); + if (Strings.isNullOrEmpty(targetWorldAlias)) { + return aliasNameConflictResult; + } + + for (MultiverseWorld otherWorld : worldManager.getWorlds()) { + if (otherWorld.getKey().equals(targetWorld.getKey())) { + continue; + } + if (targetWorldAlias.equalsIgnoreCase(ChatTextFormatter.removeColor(otherWorld.getAlias()))) { + aliasNameConflictResult.getDuplicateAliases().add(otherWorld); + } + if (targetWorldAlias.equalsIgnoreCase(otherWorld.getName())) { + aliasNameConflictResult.getDuplicateWorldNames().add(otherWorld); + } + } + return aliasNameConflictResult; + } + + /** + * Contains the alias conflicts detected for a world. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public static class AliasNameConflictResult { + private final MultiverseWorld targetWorld; + private final List duplicateAliases; + private final List duplicateWorldNames; + + private AliasNameConflictResult(@NotNull MultiverseWorld targetWorld) { + this.targetWorld = targetWorld; + duplicateAliases = new ArrayList<>(); + duplicateWorldNames = new ArrayList<>(); + } + + /** + * Sends localized messages describing the detected conflicts to the command issuer. + * No message is sent when there are no conflicts. + * + * @param issuer The command issuer to notify. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public void sendConflictMessage(MVCommandIssuer issuer) { + if (!hasConflict()) { + return; + } + issuer.sendError(MVCorei18n.ALIASNAMECONFLICT_DETECTED, + Replace.WORLD.with(targetWorld.getName()), + replace("{alias}").with(targetWorld.getColourlessAlias())); + duplicateAliases.forEach(conflictingWorld -> + issuer.sendError(MVCorei18n.ALIASNAMECONFLICT_DUPLICATEALIAS, + Replace.WORLD.with(conflictingWorld.getName()), + replace("{alias}").with(conflictingWorld.getColourlessAlias()))); + duplicateWorldNames.forEach(conflictingWorld -> + issuer.sendError(MVCorei18n.ALIASNAMECONFLICT_DUPLICATEWORLDNAME, + Replace.WORLD.with(conflictingWorld.getName()))); + } + + /** + * Gets whether any alias or world name conflicts were detected. + * + * @return {@code true} if at least one conflict was detected, otherwise {@code false}. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public boolean hasConflict() { + return !duplicateAliases.isEmpty() || !duplicateWorldNames.isEmpty(); + } + + /** + * Gets the world whose alias was checked. + * + * @return The checked world. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public @NotNull MultiverseWorld getTargetWorld() { + return targetWorld; + } + + /** + * Gets the worlds whose aliases match the target world's alias. + * + * @return The worlds with conflicting aliases. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public @NotNull List getDuplicateAliases() { + return duplicateAliases; + } + + /** + * Gets the worlds whose names match the target world's alias. + * + * @return The worlds with names that conflict with the target world's alias. + * + * @since 5.8 + */ + @ApiStatus.AvailableSince("5.8") + public @NotNull List getDuplicateWorldNames() { + return duplicateWorldNames; + } + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java index 3b4b1d518..5f96cd32a 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/CloneWorldOptions.java @@ -126,6 +126,8 @@ public LoadedMultiverseWorld world() { * Gets the name of the new world. * * @return The name of the new world. + * + * @deprecated Use {@link #newWorldKeyOrName()} instead. */ @Deprecated(forRemoval = true, since = "5.7") @ApiStatus.ScheduledForRemoval(inVersion = "6.0") diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java index 2cbcd57f7..8d0f5c3d4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java @@ -101,10 +101,13 @@ public final class CreateWorldOptions { * Gets the name of the world to create. * * @return The name of the world to create. + * + * @deprecated Use {@link #keyOrName()} instead. */ @Deprecated(forRemoval = true, since = "5.7") + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") public @NotNull String worldName() { - return keyOrName.fold(name -> name ,WorldKeyOrName::usableName); + return keyOrName.fold(name -> name, WorldKeyOrName::usableName); } /** diff --git a/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java b/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java index c362b2a0b..f0ad81ff5 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/options/ImportWorldOptions.java @@ -85,11 +85,13 @@ public final class ImportWorldOptions { * Gets the name of the world to create. * * @return The name of the world to create. + * + * @deprecated Use {@link #keyOrName()} instead. */ @Deprecated(forRemoval = true, since = "5.7") @ApiStatus.ScheduledForRemoval(inVersion = "6.0") public @NotNull String worldName() { - return keyOrName.fold(name -> name ,WorldKeyOrName::usableName); + return keyOrName.fold(name -> name, WorldKeyOrName::usableName); } /** diff --git a/src/main/resources/multiverse-core_en.properties b/src/main/resources/multiverse-core_en.properties index 2a8b1f454..b44852b70 100644 --- a/src/main/resources/multiverse-core_en.properties +++ b/src/main/resources/multiverse-core_en.properties @@ -32,7 +32,7 @@ mv-core.check.location=The destination's location is: &f{location} # /mv clone mv-core.clone.description=Clones a world. mv-core.clone.world.description=The target world to clone. -mv-core.clone.newWorld.description=The new cloned world name. +mv-core.clone.newworld.description=The new cloned world name. mv-core.clone.cloning=Cloning world '{world}' to '{newworld}'... mv-core.clone.success=&aWorld cloned to '{world}'! @@ -53,7 +53,7 @@ mv-core.coordinates.description=Simply sends your coordinates mv-core.coordinates.info.title=&b--- Location Information --- mv-core.coordinates.info.world=&bWorld: &f{world} mv-core.coordinates.info.alias=&bAlias: &f{alias} -mv-core.coordinates.info.worldScale=&bWorld Scale: &f{scale} +mv-core.coordinates.info.worldscale=&bWorld Scale: &f{scale} mv-core.coordinates.info.coordinates=&bCoordinates: &f{coordinates} mv-core.coordinates.info.direction=&bDirection: &f{direction} @@ -356,6 +356,11 @@ mv-core.teleportfailurereason.teleport.failed=The teleport was rejected. Please mv-core.teleportfailurereason.teleport.failed.exception=An error occurred during the teleport. Check server logs for more details. mv-core.teleportfailurereason.event.cancelled=The teleport was cancelled by another plugin. +# alias name conflict +mv-core.aliasnameconflict.detected=Alias conflict detected for world '{world}' with alias '{alias}'. This may cause world list and selection to be confusing! +mv-core.aliasnameconflict.duplicatealias= - Conflicts with '{world}' world's alias: '{alias}' +mv-core.aliasnameconflict.duplicateworldname= - Conflicts with world name: '{world}' + # world manager result mv-core.cloneworld.invalidworldname=World '{world}' contains invalid characters! mv-core.cloneworld.worldexistfolder=World '{world}' exists in server folders! You need to delete it first before cloning. @@ -379,14 +384,14 @@ mv-core.importworld.worldexistunloaded=World '{world}' already exists, but it's mv-core.importworld.worldexistloaded=World '{world}' already exists! mv-core.importworld.worldfolderinvalid=World '{world}' folder contents does not seem to be a valid world! Make sure it contains the correct world structure of data and region folders.\n&cIf the server software does something different with world folders, or you are very certain the world is valid, use '&3--skip-folder-check&c' flag. mv-core.importworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to import it with environment '{mvEnvironment}'. -mv-core.importworld.bukkitnamespacemismatch=Namespace mismatch detected!&f The world '{world}' is already loaded with namespace '{bukkitNamespace}', but Multiverse is trying to import it with namespace '{mvNamespace}'. +mv-core.importworld.bukkitnamespacedkeymismatch=Namespace mismatch detected!&f The world '{world}' is already loaded with namespace '{bukkitNamespace}', but Multiverse is trying to import it with namespace '{mvNamespace}'. mv-core.loadworld.worldalreadyloading=World '{world}' is already loading! Please wait... mv-core.loadworld.worldnonexistent=World '{world}' not found! Use '&a/mv create {world} &f' to create it. mv-core.loadworld.worldexistfolder=World '{world}' exists in server folders, but it's not known to Multiverse!&f Type '&a/mv import {world} &f' if you wish to import it. mv-core.loadworld.worldexistloaded=World '{world}' is already loaded! mv-core.loadworld.bukkitenvironmentmismatch=Environment mismatch detected!&f The world '{world}' is already loaded with environment '{bukkitEnvironment}', but Multiverse is trying to load it with environment '{mvEnvironment}'. -mv-core.loadworld.bukkitnamespacemismatch=Namespace mismatch detected!&f The world '{world}' is already loaded with namespace '{bukkitNamespace}', but Multiverse is trying to load it with namespace '{mvNamespace}'. +mv-core.loadworld.bukkitnamespacedkeymismatch=Namespace mismatch detected!&f The world '{world}' is already loaded with namespace '{bukkitNamespace}', but Multiverse is trying to load it with namespace '{mvNamespace}'. mv-core.removeworld.worldnonexistent=World '{world}' not found! diff --git a/src/main/resources/multiverse-core_es.properties b/src/main/resources/multiverse-core_es.properties index df8be2fa5..c34cf4aed 100644 --- a/src/main/resources/multiverse-core_es.properties +++ b/src/main/resources/multiverse-core_es.properties @@ -32,7 +32,7 @@ mv-core.check.location=La localización de su destino es: &f{location} # /mv clone mv-core.clone.description=Clona un mundo. mv-core.clone.world.description=El mundo a clonar. -mv-core.clone.newWorld.description=El nombre del nuevo mundo clonado. +mv-core.clone.newworld.description=El nombre del nuevo mundo clonado. mv-core.clone.cloning=Clonando mundo '{world}' a '{newworld}'... mv-core.clone.success=&a¡Mundo clonado a '{world}'! @@ -53,7 +53,7 @@ mv-core.coordinates.description=Simple y llanamente te muestra tus coordenadas. mv-core.coordinates.info.title=&b--- Información de la localización --- mv-core.coordinates.info.world=&bMundo: &f{world} mv-core.coordinates.info.alias=&bAlias: &f{alias} -mv-core.coordinates.info.worldScale=&bEscala del mundo: &f{scale} +mv-core.coordinates.info.worldscale=&bEscala del mundo: &f{scale} mv-core.coordinates.info.coordinates=&bCoordenadas: &f{coordinates} mv-core.coordinates.info.direction=&bDirección: &f{direction} @@ -330,6 +330,11 @@ mv-core.teleportfailurereason.teleport.failed=Algo ha evitado la teletransportac mv-core.teleportfailurereason.teleport.failed.exception=Un error ha ocurrido durante la teletransportación. Mira la consola para ver los detalles. mv-core.teleportfailurereason.event.cancelled=El teletransporte fue cancelado por otro plugin. +# alias name conflict +mv-core.aliasnameconflict.detected=Se ha detectado un conflicto de alias para el mundo '{world}' con alias '{alias}'. Esto puede hacer que la lista y selección de mundos sea confusa. +mv-core.aliasnameconflict.duplicatealias= - Entra en conflicto con el alias del mundo '{world}': '{alias}' +mv-core.aliasnameconflict.duplicateworldname= - Entra en conflicto con el nombre del mundo: '{world}' + # world manager result mv-core.cloneworld.invalidworldname=¡El mundo '{world}' contiene caracteres inválidos! mv-core.cloneworld.worldexistfolder=¡El mundo '{world}' ya existe en los archivos del servidor! Necesitas eliminarlo antes de clonarlo. diff --git a/src/main/resources/multiverse-core_pl.properties b/src/main/resources/multiverse-core_pl.properties index 3e46d564e..e9b8c7d67 100644 --- a/src/main/resources/multiverse-core_pl.properties +++ b/src/main/resources/multiverse-core_pl.properties @@ -32,7 +32,7 @@ mv-core.check.location=&fLokalizacja celu: &a{location} # /mv clone mv-core.clone.description=Klonuje świat. mv-core.clone.world.description=Świat do sklonowania. -mv-core.clone.newWorld.description=Nazwa nowego sklonowanego świata. +mv-core.clone.newworld.description=Nazwa nowego sklonowanego świata. mv-core.clone.cloning=&e&l! &fTrwa klonowanie świata '&a{world}&f' do '&a{newworld}&f'... mv-core.clone.success=&a&l✔ &fŚwiat został sklonowany jako '&a{world}&f'! @@ -56,7 +56,7 @@ mv-core.coordinates.description=Wysyła twoje koordynaty. mv-core.coordinates.info.title=&a&l--- Informacje o Lokalizacji --- mv-core.coordinates.info.world=&fŚwiat: &a{world} mv-core.coordinates.info.alias=&fAlias: &a{alias} -mv-core.coordinates.info.worldScale=&fSkala świata: &a{scale} +mv-core.coordinates.info.worldscale=&fSkala świata: &a{scale} mv-core.coordinates.info.coordinates=&fKoordynaty: &a{coordinates} mv-core.coordinates.info.direction=&fKierunek: &a{direction} @@ -365,6 +365,11 @@ mv-core.teleportfailurereason.teleport.failed=&c&l✘ &cTeleportacja została od mv-core.teleportfailurereason.teleport.failed.exception=&c&l✘ &cWystąpił błąd podczas teleportacji. Sprawdź logi serwera po więcej informacji. mv-core.teleportfailurereason.event.cancelled=&c&l✘ &cTeleportacja została anulowana przez inny plugin. +# alias name conflict +mv-core.aliasnameconflict.detected=&c&l✘ &cWykryto konflikt aliasu dla świata '&a{world}&c' z aliasem '&a{alias}&c'. Może to utrudnić czytelność listy światów i ich wybieranie! +mv-core.aliasnameconflict.duplicatealias= - Konflikt z aliasem świata '&a{world}&r': '&a{alias}&r' +mv-core.aliasnameconflict.duplicateworldname= - Konflikt z nazwą świata: '&a{world}&r' + # world manager result mv-core.cloneworld.invalidworldname=&c&l✘ &cŚwiat '&a{world}&c' zawiera niedozwolone znaki! mv-core.cloneworld.worldexistfolder=&c&l✘ &cŚwiat '&a{world}&c' istnieje już w folderach serwera! Musisz go najpierw usunąć przed klonowaniem. @@ -388,14 +393,14 @@ mv-core.importworld.worldexistunloaded=&e&l! &eŚwiat '&a{world}&e' już istniej mv-core.importworld.worldexistloaded=&c&l✘ &cŚwiat '&a{world}&c' już istnieje! mv-core.importworld.worldfolderinvalid=&c&l✘ &cFolder świata '&a{world}&c' nie wygląda na poprawny świat! Upewnij się, że zawiera poprawną strukturę folderów data oraz region.\n&eJeśli oprogramowanie serwera działa inaczej lub masz pewność, że świat jest poprawny, użyj flagi '&a--skip-folder-check&e'. mv-core.importworld.bukkitenvironmentmismatch=&c&l✘ &cWykryto niezgodność environment! &fŚwiat '&a{world}&f' jest już załadowany z environment '&a{bukkitEnvironment}&f', ale Multiverse próbuje zaimportować go jako '&a{mvEnvironment}&f'. -mv-core.importworld.bukkitnamespacemismatch=&c&l✘ &cWykryto niezgodność namespace! &fŚwiat '&a{world}&f' jest już załadowany z namespace '&a{bukkitNamespace}&f', ale Multiverse próbuje zaimportować go jako '&a{mvNamespace}&f'. +mv-core.importworld.bukkitnamespacedkeymismatch=&c&l✘ &cWykryto niezgodność namespace! &fŚwiat '&a{world}&f' jest już załadowany z namespace '&a{bukkitNamespace}&f', ale Multiverse próbuje zaimportować go jako '&a{mvNamespace}&f'. mv-core.loadworld.worldalreadyloading=&e&l! &eŚwiat '&a{world}&e' jest już ładowany! Poczekaj... mv-core.loadworld.worldnonexistent=&c&l✘ &cŚwiat '&a{world}&c' nie istnieje! Użyj '&a/mv create {world} &f', aby go stworzyć. mv-core.loadworld.worldexistfolder=&e&l! &eŚwiat '&a{world}&e' istnieje w folderach serwera, ale nie jest znany Multiverse! &fWpisz '&a/mv import {world} &f', jeśli chcesz go zaimportować. mv-core.loadworld.worldexistloaded=&e&l! &eŚwiat '&a{world}&e' jest już załadowany! mv-core.loadworld.bukkitenvironmentmismatch=&c&l✘ &cWykryto niezgodność environment! &fŚwiat '&a{world}&f' jest już załadowany z environment '&a{bukkitEnvironment}&f', ale Multiverse próbuje załadować go jako '&a{mvEnvironment}&f'. -mv-core.loadworld.bukkitnamespacemismatch=&c&l✘ &cWykryto niezgodność namespace! &fŚwiat '&a{world}&f' jest już załadowany z namespace '&a{bukkitNamespace}&f', ale Multiverse próbuje załadować go jako '&a{mvNamespace}&f'. +mv-core.loadworld.bukkitnamespacedkeymismatch=&c&l✘ &cWykryto niezgodność namespace! &fŚwiat '&a{world}&f' jest już załadowany z namespace '&a{bukkitNamespace}&f', ale Multiverse próbuje załadować go jako '&a{mvNamespace}&f'. mv-core.removeworld.worldnonexistent=&c&l✘ &cNie znaleziono świata '&a{world}&c'! diff --git a/src/main/resources/multiverse-core_ru.properties b/src/main/resources/multiverse-core_ru.properties index 1d63e66dc..c08d7c30e 100644 --- a/src/main/resources/multiverse-core_ru.properties +++ b/src/main/resources/multiverse-core_ru.properties @@ -32,7 +32,7 @@ mv-core.check.location=Местоположение пункта назначе # /mv clone mv-core.clone.description=Клонирует мир. mv-core.clone.world.description=Целевой мир для клонирования. -mv-core.clone.newWorld.description=Название нового клонированного мира. +mv-core.clone.newworld.description=Название нового клонированного мира. mv-core.clone.cloning=Клонирование мира '{world}' в '{newworld}'... mv-core.clone.success=&aМир клонирован в '{world}'! @@ -53,7 +53,7 @@ mv-core.coordinates.description=Просто отправляет ваши ко mv-core.coordinates.info.title=&b--- Информация о местоположении --- mv-core.coordinates.info.world=&bМир: &f{world} mv-core.coordinates.info.alias=&bПсевдоним: &f{alias} -mv-core.coordinates.info.worldScale=&bМасштаб мира: &f{scale} +mv-core.coordinates.info.worldscale=&bМасштаб мира: &f{scale} mv-core.coordinates.info.coordinates=&bКоординаты: &f{coordinates} mv-core.coordinates.info.direction=&bНаправление: &f{direction} @@ -325,6 +325,11 @@ mv-core.teleportfailurereason.teleport.failed=Что-то отклонило т mv-core.teleportfailurereason.teleport.failed.exception=Произошла ошибка во время телепортации. См. консоль для подробностей. mv-core.teleportfailurereason.event.cancelled=Телепортация была отменена другим плагином. +# alias name conflict +mv-core.aliasnameconflict.detected=Обнаружен конфликт псевдонима для мира '{world}' с псевдонимом '{alias}'. Это может затруднить понимание списка миров и их выбор! +mv-core.aliasnameconflict.duplicatealias= - Конфликтует с псевдонимом мира '{world}': '{alias}' +mv-core.aliasnameconflict.duplicateworldname= - Конфликтует с названием мира: '{world}' + # world manager result mv-core.cloneworld.invalidworldname=Мир '{world}' содержит недопустимые символы! mv-core.cloneworld.worldexistfolder=Мир '{world}' существует в папках сервера! Вам нужно удалить его перед клонированием. diff --git a/src/main/resources/multiverse-core_tr.properties b/src/main/resources/multiverse-core_tr.properties index 61ec30496..ba49620fb 100644 --- a/src/main/resources/multiverse-core_tr.properties +++ b/src/main/resources/multiverse-core_tr.properties @@ -32,7 +32,7 @@ mv-core.check.location=Hedefin konumu: &f{location} # /mv clone mv-core.clone.description=Bir dünyayı kopyalar. mv-core.clone.world.description=Kopyalanacak hedef dünya. -mv-core.clone.newWorld.description=Yeni kopyalanan dünyanın adı. +mv-core.clone.newworld.description=Yeni kopyalanan dünyanın adı. mv-core.clone.cloning='{world}' dünyası '{newworld}' olarak kopyalanıyor... mv-core.clone.success=&aDünya '{world}' olarak kopyalandı! @@ -53,7 +53,7 @@ mv-core.coordinates.description=Koordinatlarınızı gösterir mv-core.coordinates.info.title=&b--- Konum Bilgisi --- mv-core.coordinates.info.world=&bDünya: &f{world} mv-core.coordinates.info.alias=&bTakma Ad: &f{alias} -mv-core.coordinates.info.worldScale=&bDünya Ölçeği: &f{scale} +mv-core.coordinates.info.worldscale=&bDünya Ölçeği: &f{scale} mv-core.coordinates.info.coordinates=&bKoordinatlar: &f{coordinates} mv-core.coordinates.info.direction=&bYön: &f{direction} @@ -356,6 +356,11 @@ mv-core.teleportfailurereason.teleport.failed=Işınlama reddedildi. Başka bir mv-core.teleportfailurereason.teleport.failed.exception=Işınlama sırasında bir hata oluştu. Daha fazla ayrıntı için sunucu günlüklerini kontrol edin. mv-core.teleportfailurereason.event.cancelled=Işınlama başka bir eklenti tarafından iptal edildi. +# alias name conflict +mv-core.aliasnameconflict.detected='{alias}' takma adına sahip '{world}' dünyası için takma ad çakışması algılandı. Bu, dünya listesini ve seçimini kafa karıştırıcı hale getirebilir! +mv-core.aliasnameconflict.duplicatealias= - '{world}' dünyasının takma adıyla çakışıyor: '{alias}' +mv-core.aliasnameconflict.duplicateworldname= - Dünya adıyla çakışıyor: '{world}' + # world manager result mv-core.cloneworld.invalidworldname='{world}' dünyası geçersiz karakterler içeriyor! mv-core.cloneworld.worldexistfolder='{world}' dünyası sunucu klasörlerinde mevcut! Kopyalamadan önce silmeniz gerekiyor. @@ -379,14 +384,14 @@ mv-core.importworld.worldexistunloaded='{world}' dünyası zaten mevcut, ancak y mv-core.importworld.worldexistloaded='{world}' dünyası zaten mevcut! mv-core.importworld.worldfolderinvalid='{world}' dünya klasörünün içeriği geçerli bir dünya gibi görünmüyor! Doğru veri ve bölge klasörleri içerdiğinden emin olun.\n&cSunucu yazılımı dünya klasörleriyle farklı bir şey yapıyorsa veya dünyanın geçerli olduğundan çok eminseniz, '&3--skip-folder-check&c' bayrağını kullanın. mv-core.importworld.bukkitenvironmentmismatch=Ortam uyuşmazlığı tespit edildi!&f '{world}' dünyası '{bukkitEnvironment}' ortamıyla zaten yüklü, ancak Multiverse onu '{mvEnvironment}' ortamıyla içe aktarmaya çalışıyor. -mv-core.importworld.bukkitnamespacemismatch=Ad alanı uyuşmazlığı tespit edildi!&f '{world}' dünyası '{bukkitNamespace}' ad alanıyla zaten yüklü, ancak Multiverse onu '{mvNamespace}' ad alanıyla içe aktarmaya çalışıyor. +mv-core.importworld.bukkitnamespacedkeymismatch=Ad alanı uyuşmazlığı tespit edildi!&f '{world}' dünyası '{bukkitNamespace}' ad alanıyla zaten yüklü, ancak Multiverse onu '{mvNamespace}' ad alanıyla içe aktarmaya çalışıyor. mv-core.loadworld.worldalreadyloading='{world}' dünyası zaten yükleniyor! Lütfen bekleyin... mv-core.loadworld.worldnonexistent='{world}' dünyası bulunamadı! Oluşturmak için '&a/mv create {world} &f' kullanın. mv-core.loadworld.worldexistfolder='{world}' dünyası sunucu klasörlerinde mevcut, ancak Multiverse tarafından bilinmiyor!&f İçe aktarmak istiyorsanız '&a/mv import {world} &f' yazın. mv-core.loadworld.worldexistloaded='{world}' dünyası zaten yüklü! mv-core.loadworld.bukkitenvironmentmismatch=Ortam uyuşmazlığı tespit edildi!&f '{world}' dünyası '{bukkitEnvironment}' ortamıyla zaten yüklü, ancak Multiverse onu '{mvEnvironment}' ortamıyla yüklemeye çalışıyor. -mv-core.loadworld.bukkitnamespacemismatch=Ad alanı uyuşmazlığı tespit edildi!&f '{world}' dünyası '{bukkitNamespace}' ad alanıyla zaten yüklü, ancak Multiverse onu '{mvNamespace}' ad alanıyla yüklemeye çalışıyor. +mv-core.loadworld.bukkitnamespacedkeymismatch=Ad alanı uyuşmazlığı tespit edildi!&f '{world}' dünyası '{bukkitNamespace}' ad alanıyla zaten yüklü, ancak Multiverse onu '{mvNamespace}' ad alanıyla yüklemeye çalışıyor. mv-core.removeworld.worldnonexistent='{world}' dünyası bulunamadı! @@ -441,4 +446,4 @@ mv-core.generic.error.details=Hata: {error} mv-core.generic.null=Null! mv-core.generic.you=sen mv-core.generic.playercount={count} oyuncu -mv-core.generic.teleportplayers.failed=Bir veya daha fazla oyuncu dünyadan ışınlanamadı! \ No newline at end of file +mv-core.generic.teleportplayers.failed=Bir veya daha fazla oyuncu dünyadan ışınlanamadı! diff --git a/src/main/resources/multiverse-core_zh.properties b/src/main/resources/multiverse-core_zh.properties index c2297cc80..1ba08a5c7 100644 --- a/src/main/resources/multiverse-core_zh.properties +++ b/src/main/resources/multiverse-core_zh.properties @@ -32,7 +32,7 @@ mv-core.check.location=目标的地址是: &f{location} # /mv clone mv-core.clone.description=复制一个世界。 mv-core.clone.world.description=需要复制的原世界。 -mv-core.clone.newWorld.description=新世界的名称。 +mv-core.clone.newworld.description=新世界的名称。 mv-core.clone.cloning=正在复制 '{world}' 到 '{newworld}'…… mv-core.clone.success=&a已将世界复制到 '{world}'! @@ -53,7 +53,7 @@ mv-core.coordinates.description=直接发送你的坐标 mv-core.coordinates.info.title=&b--- 位置信息 --- mv-core.coordinates.info.world=&b世界: &f{world} mv-core.coordinates.info.alias=&b别名: &f{alias} -mv-core.coordinates.info.worldScale=&b世界比例: &f{scale} +mv-core.coordinates.info.worldscale=&b世界比例: &f{scale} mv-core.coordinates.info.coordinates=&b坐标: &f{coordinates} mv-core.coordinates.info.direction=&b方向: &f{direction} @@ -331,6 +331,11 @@ mv-core.teleportfailurereason.teleport.failed=传送被拒绝。请确保其他 mv-core.teleportfailurereason.teleport.failed.exception=在传送过程中出现了一个错误。查看服务器日志获取更多信息。 mv-core.teleportfailurereason.event.cancelled=传送被另一个插件取消了。 +# alias name conflict +mv-core.aliasnameconflict.detected=检测到世界 '{world}' 的别名 '{alias}' 存在冲突。这可能会让世界列表和选择变得混乱! +mv-core.aliasnameconflict.duplicatealias= - 与世界 '{world}' 的别名冲突:'{alias}' +mv-core.aliasnameconflict.duplicateworldname= - 与世界名称冲突:'{world}' + # world manager result mv-core.cloneworld.invalidworldname=世界 '{world}' 包含无效的字符! mv-core.cloneworld.worldexistfolder=世界 '{world}' 已经存在于服务器的文件中!在复制前你需要先删除它。 diff --git a/src/test/java/org/mvplugins/multiverse/core/command/LocalizationTest.kt b/src/test/java/org/mvplugins/multiverse/core/command/LocalizationTest.kt index 6a7476c76..6c076e980 100644 --- a/src/test/java/org/mvplugins/multiverse/core/command/LocalizationTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/command/LocalizationTest.kt @@ -18,6 +18,7 @@ import org.mvplugins.multiverse.core.locale.PluginLocales import org.mvplugins.multiverse.core.locale.message.Message import org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace import java.util.Locale +import java.util.Properties import kotlin.test.* class LocalizationTest : TestWithMockBukkit() { @@ -31,6 +32,18 @@ class LocalizationTest : TestWithMockBukkit() { locales = commandManager.locales } + @Test + fun `Default locale bundle contains every message key`() { + val properties = Properties() + assertNotNull(javaClass.getResourceAsStream("/multiverse-core_en.properties")).use { + properties.load(it) + } + + MVCorei18n.entries.forEach { message -> + assertTrue(properties.containsKey(message.messageKey.key), "Missing locale key: ${message.messageKey.key}") + } + } + @Nested @DisplayName("Given a Message with only a non-localized message") inner class BasicMessage { @@ -45,7 +58,7 @@ class LocalizationTest : TestWithMockBukkit() { @Test fun `The formatted message should be the same as the original`() { - assertEquals(messageString, message.formatted()) + assertEquals(messageString, message.rawFormatted()) } @Test @@ -97,7 +110,7 @@ class LocalizationTest : TestWithMockBukkit() { @Test fun `The formatted message should be the replaced message string`() { - assertEquals(replacedMessageString, message.formatted()) + assertEquals(replacedMessageString, message.rawFormatted()) } @Test @@ -156,7 +169,7 @@ class LocalizationTest : TestWithMockBukkit() { @Test fun `The formatted message should be the replaced message string`() { - assertEquals(replacedMessageString, message.formatted()) + assertEquals(replacedMessageString, message.rawFormatted()) } @Test @@ -209,8 +222,13 @@ class LocalizationTest : TestWithMockBukkit() { } @Test - fun `The formatted message should be the replaced original string`() { - assertEquals(replacedMessageString, message.formatted()) + fun `The raw formatted message should be the replaced original string`() { + assertEquals(replacedMessageString, message.rawFormatted()) + } + + @Test + fun `The formatted message should be different from the replaced original string`() { + assertNotEquals(replacedMessageString, message.formatted()) } @Test @@ -271,7 +289,7 @@ class LocalizationTest : TestWithMockBukkit() { @Test fun `The formatted message should be the replaced original string`() { - assertEquals(replacedMessageString, message.formatted()) + assertEquals(replacedMessageString, message.rawFormatted()) } @Test diff --git a/src/test/java/org/mvplugins/multiverse/core/world/helper/AliasNameConflictCheckerTest.kt b/src/test/java/org/mvplugins/multiverse/core/world/helper/AliasNameConflictCheckerTest.kt new file mode 100644 index 000000000..80210197f --- /dev/null +++ b/src/test/java/org/mvplugins/multiverse/core/world/helper/AliasNameConflictCheckerTest.kt @@ -0,0 +1,50 @@ +package org.mvplugins.multiverse.core.world.helper + +import org.mvplugins.multiverse.core.TestWithMockBukkit +import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld +import org.mvplugins.multiverse.core.world.WorldManager +import org.mvplugins.multiverse.core.world.helpers.AliasNameConflictChecker +import org.mvplugins.multiverse.core.world.options.CreateWorldOptions +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AliasNameConflictCheckerTest : TestWithMockBukkit() { + + private lateinit var conflictChecker: AliasNameConflictChecker + private lateinit var firstWorld: LoadedMultiverseWorld + private lateinit var secondWorld: LoadedMultiverseWorld + + @BeforeTest + fun setUp() { + val worldManager = requireNotNull(serviceLocator.getActiveService(WorldManager::class.java)) + conflictChecker = requireNotNull(serviceLocator.getService(AliasNameConflictChecker::class.java)) + + assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("FirstWorld")).isSuccess) + assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("SecondWorld")).isSuccess) + firstWorld = worldManager.getLoadedWorld("FirstWorld").get() + secondWorld = worldManager.getLoadedWorld("SecondWorld").get() + } + + @Test + fun `Aliases differing only by case conflict`() { + assertTrue(firstWorld.setAlias("SharedAlias").isSuccess) + assertTrue(secondWorld.setAlias("sharedalias").isSuccess) + + val result = conflictChecker.checkDuplicateFor(secondWorld) + + assertEquals(listOf(firstWorld), result.duplicateAliases) + assertTrue(result.duplicateWorldNames.isEmpty()) + } + + @Test + fun `Alias and world name differing only by case conflict`() { + assertTrue(firstWorld.setAlias("SECONDWORLD").isSuccess) + + val result = conflictChecker.checkDuplicateFor(firstWorld) + + assertTrue(result.duplicateAliases.isEmpty()) + assertEquals(listOf(secondWorld), result.duplicateWorldNames) + } +} diff --git a/src/test/resources/configs/fresh_config.yml b/src/test/resources/configs/fresh_config.yml index 7fc906999..c63c10261 100644 --- a/src/test/resources/configs/fresh_config.yml +++ b/src/test/resources/configs/fresh_config.yml @@ -11,6 +11,7 @@ world: world-name-format: nether: '%overworld%_nether' end: '%overworld%_the_end' + warn-alias-conflicts: true teleport: use-finer-teleport-permissions: true