Compare commits

...
40 Commits
Author SHA1 Message Date
Ben Woo 34185b90a9 Merge pull request #3524 from Multiverse/chore/ignore-kolin
Add Kolin build cache folder to gitignore
2026-10-19 13:17:52 +08:00
Ben Woo 5d57bbfda9 Add Kolin build cache folder to gitignore 2026-10-19 11:14:11 +08:00
Ben Woo a78459ab77 Merge pull request #3523 from Multiverse/fix/update-legacy-name
Update legacy world name on world load
2026-10-19 11:06:04 +08:00
Ben Woo 0ad795b27f Update legacy world name on world load
This should elevate any issues of legacy world name being desynced with actual world name.
2026-10-19 10:57:18 +08:00
Ben Woo 2765cf6f3e Merge pull request #3522 from Multiverse/fix/string-parse
Fix string parse for special `@` values
2026-10-19 10:47:51 +08:00
Ben Woo 7a9b21f9a0 Fix command input not allowing @vault-economy currency type 2026-10-19 10:43:25 +08:00
Ben Woo f55ceda42c Fix command input not allowing @unset, @bukkit spawn values 2026-10-19 10:42:42 +08:00
Ben Woo 893c69a6e1 Merge pull request #3521 from Multiverse/chore/async-fail
Clearer message that fail async teleport is not a Multiverse bug
2026-10-09 14:09:39 +08:00
Ben Woo 6bb93766d4 Clearer message that fail async teleport is not a Multiverse bug 2026-10-09 14:03:06 +08:00
Ben Woo fe49efe276 Merge pull request #3520 from Multiverse/chore/simplify-pr-comment
Simplify artifact PR comment message
2026-09-27 23:26:18 +08:00
Ben Woo 8de11b59a1 Simplify artifact PR comment message 2026-09-27 23:22:25 +08:00
Ben Woo 744450cbbb Merge pull request #3517 from Multiverse/chore/artifact-comment
Improve artifact PR comment with better message and supports forks
2026-09-27 16:23:48 +08:00
Ben Woo ecf107563d Improve artifact PR comment with better message and supports forks 2026-09-27 16:20:31 +08:00
Ben Woo f7ea31d949 Merge pull request #3516 from Multiverse/chore/mc-26.3
Mark builds at compatible with 26.3
2026-09-21 15:52:44 +08:00
Ben Woo 1bf4b99ecf Mark builds at compatible with 26.3 2026-09-21 15:41:15 +08:00
Ben Woo 4de4e5a68e Merge pull request #3515 from Multiverse/fix/level-name-change
Fix level-name changes causing mismatched name and key identities
2026-08-25 15:15:49 +08:00
Ben Woo f6a8a6b3b4 Better error handling for invalid/mismatch world keys 2026-08-25 15:03:10 +08:00
Ben Woo d485238c56 Fix level-name changes causing mismatched name and key identities 2026-08-25 14:15:23 +08:00
Ben Woo 5234445746 Merge pull request #3514 from Multiverse/fix/spawn-reason-chunk-load
Fix spawn reason check not done for chunk load entity pruning
2026-08-24 13:08:54 +08:00
Ben Woo 0b626d9eaf Fix spawn reason check not done for chunk load entity pruning 2026-08-21 22:48:27 +08:00
Ben Woo 2ff321e475 Merge pull request #3512 from Multiverse/fix/command-syntax
Fix various missing parameters and typos in command syntax
2026-08-15 23:49:29 +08:00
Ben Woo 58dbe55be4 Merge pull request #3513 from Multiverse/fix/apply-spawn-rate
Fix disabling spawn limit also disabled tick rate
2026-08-15 21:06:07 +08:00
Ben Woo a2045b52bd Fix disabling spawn limit also disabled tick rate 2026-08-15 20:03:09 +08:00
Ben Woo 396ece4d27 Fix various missing parameters and typos in command syntax 2026-08-09 23:32:06 +08:00
Ben Woo e25ac0e531 Merge pull request #3500 from Multiverse/v5.8
V5.8
2026-08-09 10:51:40 +08:00
Ben Woo 7cf53ee7a8 Checking for alias name conflict should be case-insensitive 2026-08-09 00:22:45 +08:00
Ben Woo 4a46c13754 Add missing deprecated reason javadoc 2026-08-09 00:09:00 +08:00
Ben Woo 5449ade6f3 Merge pull request #3511 from Multiverse/fix/locale-keys
Fix various incorrectly named locale keys
2026-08-08 23:47:10 +08:00
Ben Woo a9f8deeb01 Merge pull request #3510 from Multiverse/fix/underscore-worlds
Fix unable to select worlds with underscore in placeholders
2026-08-08 23:46:47 +08:00
Ben Woo 1ce7eb6d3e Fix various incorrectly named locale keys 2026-08-08 23:43:45 +08:00
Ben Woo d11d664020 Fix unable to select worlds with underscore in placeholders 2026-08-08 23:34:11 +08:00
Ben Woo abd86f81e5 Merge pull request #3509 from Multiverse/chore/for-removal
Add missing ScheduledForRemoval annotations
2026-08-08 22:44:00 +08:00
Ben Woo 0faec79250 Add missing ScheduledForRemoval annotations 2026-08-08 22:37:27 +08:00
Ben Woo 9264f5e2ce Merge branch 'main' into v5.8
# Conflicts:
#	src/main/java/org/mvplugins/multiverse/core/world/WorldConfigNodes.java
2026-07-23 22:14:29 +08:00
Ben Woo 28240c3649 Merge pull request #3501 from Multiverse/feat/inject-locales
Inject default locales instance to Message
2026-07-16 20:41:02 +08:00
Ben Woo c8e4c3239a Merge pull request #3497 from Multiverse/feat/alias-conflict
Add checks for alias name conflict
2026-07-16 15:11:23 +08:00
Ben Woo b23735dc72 Inject default locales instance to Message 2026-07-16 15:09:21 +08:00
Ben Woo 1ccfa2b24b Merge pull request #3496 from Multiverse/chore/deps
Bump various dependencies
2026-07-16 12:44:54 +08:00
Ben Woo 68bbb6ab90 Add checks for alias name conflict 2026-07-14 16:14:51 +08:00
Ben Woo 16738f181a Bump various dependencies 2026-07-14 14:51:02 +08:00
58 changed files with 872 additions and 139 deletions

No files matched your search

@@ -0,0 +1,77 @@
name: 'Generic: Artifact Comment'
on:
workflow_call:
inputs:
plugin_name:
description: 'Plugin name used by Generic: Test'
required: true
type: string
jobs:
comment-artifact:
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
pull-requests: write
steps:
# Run separately so fork builds can post comments without a write token.
# Do not check out or execute code from the pull request in this workflow.
- name: Find pull request and artifact
id: artifact
uses: actions/github-script@v7
env:
PLUGIN_NAME: ${{ inputs.plugin_name }}
with:
script: |
const run = context.payload.workflow_run;
// Fork runs can have an empty pull_requests array in the event.
const pulls = await github.paginate(github.rest.pulls.list, {
...context.repo,
state: 'open',
head: `${run.head_repository.owner.login}:${run.head_branch}`,
per_page: 100,
});
const pull = pulls.find(pr =>
pr.head.repo?.id === run.head_repository.id &&
pr.head.ref === run.head_branch &&
pr.head.sha === run.head_sha
);
if (!pull) {
core.info('No open pull request matches this build; skipping comment.');
return;
}
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
...context.repo,
run_id: run.id,
per_page: 100,
});
const artifact = artifacts.find(item =>
item.name === `${process.env.PLUGIN_NAME}-pr${pull.number}` && !item.expired
);
if (!artifact) {
core.info('No build artifact is available; skipping comment.');
return;
}
core.setOutput('number', pull.number);
core.setOutput('url', `${run.html_url}/artifacts/${artifact.id}`);
- name: Comment artifact
if: steps.artifact.outputs.number != ''
uses: marocchino/sticky-pull-request-comment@v3
with:
number: ${{ steps.artifact.outputs.number }}
header: build-artifact-${{ inputs.plugin_name }}
message: |
### ✅ `${{ inputs.plugin_name }}` test build ready
Last updated for: [${{ github.event.workflow_run.head_sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.workflow_run.head_sha }})
**Download:** [${{ inputs.plugin_name }}-pr${{ steps.artifact.outputs.number }}.zip](${{ steps.artifact.outputs.url }})
View [CI build and checks](${{ github.event.workflow_run.html_url }}) for more details.
@@ -107,7 +107,7 @@ 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: 26.2, 26.1.2, 26.1.1, 26.1, 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
game_versions: 26.3, 26.2, 26.1.2, 26.1.1, 26.1, 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, purpur
dependencies: ${{ inputs.modrinth_dependencies }}
@@ -121,7 +121,7 @@ jobs:
changelog: ${{ steps.release-artifact.outputs.body }}
changelog_type: markdown
display_name: ${{ steps.release-info.outputs.tag_name }}
game_versions: 26.2, 26.1.2, 26.1.1, 26.1, 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
game_versions: 26.3, 26.2, 26.1.2, 26.1.1, 26.1, 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-26.2"]}'
platform_dependencies: '{"PAPER": ["1.18.2-26.3"]}'
plugin_dependencies: ${{ inputs.hangar_plugin_dependencies }}
-10
View File
@@ -36,13 +36,3 @@ jobs:
with:
name: ${{ inputs.plugin_name }}-pr${{ github.event.pull_request.number }}
path: build/libs/${{ inputs.plugin_name }}-pr${{ github.event.pull_request.number }}.jar
comment-artifact:
needs: test-and-artifact
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: benwoo1110/artifact-comment-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
+15
View File
@@ -0,0 +1,15 @@
name: 'PR: Artifact Comment'
on:
workflow_run:
workflows: ['PR: Test']
types: [completed]
jobs:
comment-artifact:
permissions:
actions: read
pull-requests: write
uses: ./.github/workflows/generic.artifact_comment.yml
with:
plugin_name: multiverse-core
+3
View File
@@ -46,3 +46,6 @@ debug.txt
# Gradle
.gradle
# Kotlin generated files
/.kotlin/
+5 -5
View File
@@ -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
@@ -102,11 +102,12 @@ public class MultiverseCore extends MultiverseModule {
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();
@@ -87,7 +87,8 @@ final class PlaceholderExpansionHook extends PlaceholderExpansion {
@Override
public @Nullable String onRequest(OfflinePlayer offlinePlayer, @NotNull String params) {
// Split string in to an Array with underscores
List<String> paramsArray = Lists.newArrayList(REPatterns.UNDERSCORE.split(params));
List<String> paramsArray = Lists.newArrayList(
StringFormatter.parseQuotesInArgs(REPatterns.UNDERSCORE.split(params), "_"));
// No placeholder defined
if (paramsArray.isEmpty()) {
@@ -12,6 +12,8 @@ import co.aikar.commands.LogLevel;
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.flag.CommandFlagsManager;
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
@@ -61,7 +61,7 @@ class CheckCommand extends CoreCommand {
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
}
@@ -41,7 +41,7 @@ class CloneCommand extends CoreCommand {
@Subcommand("clone")
@CommandPermission("multiverse.core.clone")
@CommandCompletion("@mvworlds:scope=loaded @empty @flags:groupName=" + Flags.NAME)
@Syntax("<world> <new-world-name> [--reset-world-config --reset-gamerules --reset-world-border]")
@Syntax("<world> <new-world-name> [--reset-world-config --reset-gamerules --reset-world-border --no-save]")
@Description("{@@mv-core.clone.description}")
void onCloneCommand(
MVCommandIssuer issuer,
@@ -51,11 +51,11 @@ class CloneCommand extends CoreCommand {
MultiverseWorld world,
@Syntax("<new-world-name>")
@Description("{@@mv-core.clone.newWorld.description}")
@Description("{@@mv-core.clone.newworld.description}")
String newWorldName,
@Optional
@Syntax("[--reset-world-config --reset-gamerules --reset-world-border]")
@Syntax("[--reset-world-config --reset-gamerules --reset-world-border --no-save]")
@Description("{@@mv-core.regen.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -56,7 +56,7 @@ class CreateCommand extends CoreCommand {
@CommandPermission("multiverse.core.create")
@CommandCompletion("@empty @environments @flags:groupName=" + Flags.NAME)
@Syntax("<name> <environment> [--seed <seed> --generator <generator[:id]> --world-type <worldtype> " +
"--adjust-spawn --no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> " +
"--no-adjust-spawn --no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> " +
"--biome <biome> --generator-settings <json-settings> --properties <prop1=value1,prop2=value2,...>]")
@Description("{@@mv-core.create.description}")
void onCreateCommand(
@@ -71,7 +71,7 @@ class CreateCommand extends CoreCommand {
World.Environment environment,
@Optional
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn " +
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --no-adjust-spawn " +
"--no-structures --generate-bonus-chest --force-spawn-position <x,y,z:pitch:yaw> --biome <biome> " +
"--generator-settings <json-settings> --properties <prop1=value1,prop2=value2,...>]")
@Description("{@@mv-core.create.flags.description}")
@@ -79,7 +79,7 @@ class DeleteCommand extends CoreCommand {
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()))));
}
@@ -54,21 +54,21 @@ class GameruleCommand extends CoreCommand {
@Subcommand("gamerule|rule set")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @gamerulesvalues @mvworlds:multiple|*")
@Syntax("<Gamerule> <Gamerule value> [World or *]")
@Syntax("<gamerule> <value> [worlds|*]")
@Description("{@@mv-core.gamerule.set.description}")
void onGameruleSetCommand(
MVCommandIssuer issuer,
@Syntax("<Gamerule>")
@Syntax("<gamerule>")
@Description("{@@mv-core.gamerule.set.gamerule.description}")
GameRule gamerule,
@Syntax("<Value>")
@Syntax("<value>")
@Description("{@@mv-core.gamerule.set.value.description}")
GameRuleValue gameRuleValue,
@Flags("resolve=issuerAware,maxArgForAware=2")
@Syntax("[World or *]")
@Syntax("[worlds|*]")
@Description("{@@mv-core.gamerule.set.world.description}")
LoadedMultiverseWorld[] worlds) {
Object value = gameRuleValue.value();
@@ -105,17 +105,17 @@ class GameruleCommand extends CoreCommand {
@Subcommand("gamerule|rule reset")
@CommandPermission("multiverse.core.gamerule.set")
@CommandCompletion("@gamerules @mvworlds:multiple|*")
@Syntax("<Gamerule> [World or *]")
@Syntax("<gamerule> [worlds|*]")
@Description("{@@mv-core.gamerule.reset.description}")
void onGameruleResetCommand(
MVCommandIssuer issuer,
@Syntax("<Gamerule>")
@Syntax("<gamerule>")
@Description("{@@mv-core.gamerule.reset.gamerule.description}")
GameRule gamerule,
@Flags("resolve=issuerAware,maxArgForAware=1")
@Syntax("[World or *]")
@Syntax("[worlds|*]")
@Description("{@@mv-core.gamerule.reset.world.description}")
LoadedMultiverseWorld[] worlds) {
AtomicBoolean success = new AtomicBoolean(true);
@@ -153,7 +153,7 @@ class GameruleCommand extends CoreCommand {
MVCommandIssuer issuer,
@Flags("resolve=issuerAware,maxArgForAware=0")
@Syntax("<world>")
@Syntax("[world]")
@Description("{@@mv-core.gamerule.list.description.world}")
LoadedMultiverseWorld world,
@@ -43,7 +43,8 @@ class ImportCommand extends CoreCommand {
@Subcommand("import")
@CommandPermission("multiverse.core.import")
@CommandCompletion("@mvworlds:scope=potential @environments @flags:groupName=" + Flags.NAME)
@Syntax("<name> <environment> [--generator <generator[:id]> --adjust-spawn --biome <biome>]")
@Syntax("<name> <environment> [--generator <generator[:id]> --no-adjust-spawn --biome <biome> " +
"--skip-folder-check --generator-settings <json-settings>]")
@Description("{@@mv-core.import.description}")
void onImportCommand(
MVCommandIssuer issuer,
@@ -53,12 +54,13 @@ class ImportCommand extends CoreCommand {
@Description("{@@mv-core.import.name.description}")
String worldName,
@Syntax("<env>")
@Syntax("<environment>")
@Description("{@@mv-core.import.env.description}")
World.Environment environment,
@Optional
@Syntax("[--generator <generator[:id]> --adjust-spawn --biome <biome>]")
@Syntax("[--generator <generator[:id]> --no-adjust-spawn --biome <biome> " +
"--skip-folder-check --generator-settings <json-settings>]")
@Description("{@@mv-core.import.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -54,12 +54,12 @@ class ListCommand extends CoreCommand {
@Subcommand("list")
@CommandPermission("multiverse.core.list.worlds")
@CommandCompletion("@flags:groupName=" + Flags.NAME)
@Syntax("--filter [filter] --page [page] --raw")
@Syntax("[--filter <filter>] [--page <page>] [--raw]")
@Description("{{@mv-core.list.description}}")
public void onListCommand(
MVCommandIssuer issuer,
@Syntax("[--filter <filter>] [--page <page>]")
@Syntax("[--filter <filter>] [--page <page>] [--raw]")
@Description("Filters the list of worlds by the given regex and displays the given page.")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -39,7 +39,7 @@ class LoadCommand extends CoreCommand {
@Subcommand("load")
@CommandPermission("multiverse.core.load")
@CommandCompletion("@mvworlds:scope=unloaded @flags:groupName=" + Flags.NAME)
@Syntax("<world>")
@Syntax("<world> [--skip-folder-check]")
@Description("{@@mv-core.load.description}")
void onLoadCommand(
MVCommandIssuer issuer,
@@ -70,7 +70,7 @@ class RegenCommand extends CoreCommand {
@CommandPermission("multiverse.core.regen")
@CommandCompletion("@mvworlds:scope=loaded @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--seed [seed]] [--reset-world-config] [--reset-gamerules] [--reset-world-border] " +
"[--remove-players [destination]]")
"[--keep-files <files>] [--remove-players [destination]]")
@Description("{@@mv-core.regen.description}")
void onRegenCommand(
MVCommandIssuer issuer,
@@ -81,7 +81,7 @@ class RegenCommand extends CoreCommand {
@Optional
@Syntax("[--seed [seed]] [--reset-world-config] [--reset-gamerules] [--reset-world-border] " +
"[--remove-players [destination]]")
"[--keep-files <files>] [--remove-players [destination]]")
@Description("{@@mv-core.regen.other.description}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -89,7 +89,7 @@ class RegenCommand extends CoreCommand {
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()))));
}
@@ -40,12 +40,12 @@ class SetSpawnCommand extends CoreCommand {
@Subcommand("setspawn")
@CommandPermission("multiverse.core.spawn.set")
@CommandCompletion("@flags:groupName=" + UnsafeFlags.NAME + " @flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME)
@Syntax("[worldname:x,y,z[,pitch,yaw]] [--unsafe]")
@Syntax("[worldname:x,y,z[,yaw,pitch]] [--unsafe]")
@Description("{@@mv-core.setspawn.description}")
void onSetSpawnCommand(
MVCommandIssuer issuer,
@Syntax("[worldname:x,y,z[,pitch,yaw]]")
@Syntax("[worldname:x,y,z[,yaw,pitch]]")
@Description("{@@mv-core.setspawn.location.description}")
PlayerLocation playerLocation,
@@ -62,7 +62,7 @@ final class SpawnCommand extends CoreCommand {
@CommandPermission("@mvspawn")
@CommandCompletion("@playersarray:checkPermissions=@mvspawnother|@flags:resolveUntil=arg1,groupName=" + UnsafeFlags.NAME
+ " @flags:groupName=" + UnsafeFlags.NAME)
@Syntax("[player]")
@Syntax("[player] [--unsafe]")
@Description("{@@mv-core.spawn.description}")
void onSpawnTpCommand(
MVCommandIssuer issuer,
@@ -59,7 +59,7 @@ final class TeleportCommand extends CoreCommand {
@CommandCompletion("@playersarray:checkPermissions=@mvteleportother|@destinations:byIssuerForArg=arg1 "
+ "@destinations:notByIssuerForArg=arg1|@flags:byIssuerForArg=arg1,groupName=" + Flags.NAME + " "
+ "@flags:notByIssuerForArg=arg1,groupName=" + Flags.NAME)
@Syntax("[player] <destination> [--unsafe]")
@Syntax("[player] <destination> [--unsafe --silent]")
@Description("{@@mv-core.teleport.description}")
void onTeleportCommand(
MVCommandIssuer issuer,
@@ -74,7 +74,7 @@ final class TeleportCommand extends CoreCommand {
DestinationInstance<?, ?> destination,
@Optional
@Syntax("[--unsafe]")
@Syntax("[--unsafe --silent]")
@Description("")
String[] flagArray) {
Player[] players = playersValue.value();
@@ -52,7 +52,7 @@ class UnloadCommand extends CoreCommand {
@Subcommand("unload")
@CommandPermission("multiverse.core.unload")
@CommandCompletion("@mvworlds @flags:groupName=" + Flags.NAME)
@Syntax("<world> [--remove-players [destination]] [--no-save]")
@Syntax("<world> [--remove-players [destination]] [--no-unload-bukkit-world] [--no-save]")
@Description("{@@mv-core.unload.description}")
void onUnloadCommand(
MVCommandIssuer issuer,
@@ -62,7 +62,7 @@ class UnloadCommand extends CoreCommand {
LoadedMultiverseWorld world,
@Optional
@Syntax("[--remove-players [destination]] [--no-save]")
@Syntax("[--remove-players [destination]] [--no-unload-bukkit-world] [--no-save]")
@Description("{@@mv-core.gamerules.description.page}")
String[] flagArray) {
ParsedCommandFlags parsedFlags = flags.parse(flagArray);
@@ -50,10 +50,10 @@ final class WorldBorderCommand extends CoreCommand {
void onWorldBorderCenter(
MVCommandIssuer issuer,
@Syntax("[x]")
@Syntax("<x>")
double x,
@Syntax("[z]")
@Syntax("<z>")
double z,
@Flags("resolve=issuerAware,maxArgForAware=0")
@@ -246,6 +246,16 @@ public final class CoreConfig {
return configHandle.get(configNodes.endWorldNameFormat);
}
@ApiStatus.AvailableSince("5.8")
public Try<Void> setWarnAliasConflicts(boolean warnAliasConflicts) {
return configHandle.set(configNodes.warnAliasConflicts, warnAliasConflicts);
}
@ApiStatus.AvailableSince("5.8")
public boolean getWarnAliasConflicts() {
return configHandle.get(configNodes.warnAliasConflicts);
}
/**
* {@inheritDoc}
*/
@@ -200,6 +200,15 @@ final class CoreConfigNodes {
.stringParser(DimensionFormatNodeStringParser.INSTANCE)
.build());
final ConfigNode<Boolean> 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("")
@@ -492,6 +492,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @deprecated Use {@link #onLoadAndChange(NodeChangeCallback)} instead.
*/
@Deprecated(since = "5.4", forRemoval = true)
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
public @NotNull B onSetValue(@NotNull BiConsumer<T, T> onSetValue) {
return onLoadAndChange(onSetValue::accept);
}
@@ -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;
@@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.exceptions.command;
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 class MVInvalidCommandArgument extends InvalidCommandArgument {
@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 @@ public class MVInvalidCommandArgument extends InvalidCommandArgument {
}
private MVInvalidCommandArgument(LocalizedMessage message, boolean showSyntax) {
super(message.getMessageKey(), showSyntax, message.getReplacements());
super(message.getMessageKey(), showSyntax, message.getRawReplacements());
}
}
@@ -22,11 +22,13 @@ import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.dynamiclistener.EventRunnable;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.utils.compatibility.EntityCompatibility;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.Arrays;
@@ -96,10 +98,7 @@ final class MVEntityListener implements CoreListener {
@Override
public void onEvent(PreCreatureSpawnEvent event) {
// Always allow custom command and plugins to spawn creatures
if (event.getReason() == SpawnReason.CUSTOM
|| event.getReason() == SpawnReason.COMMAND
|| event.getReason() == SpawnReason.BREEDING
|| event.getReason() == SpawnReason.SPAWNER_EGG) {
if (isAlwaysAllowedSpawnReason(event.getReason())) {
return;
}
@@ -127,10 +126,7 @@ final class MVEntityListener implements CoreListener {
}
// Always allow custom command and plugins to spawn creatures
if (event.getSpawnReason() == SpawnReason.CUSTOM
|| event.getSpawnReason() == SpawnReason.COMMAND
|| event.getSpawnReason() == SpawnReason.BREEDING
|| event.getSpawnReason() == SpawnReason.SPAWNER_EGG) {
if (isAlwaysAllowedSpawnReason(event.getSpawnReason())) {
return;
}
@@ -168,6 +164,7 @@ final class MVEntityListener implements CoreListener {
.peek(world -> {
long count = Arrays.stream(event.getChunk().getEntities())
.filter(entity -> !(entity instanceof Player))
.filter(entity -> !isAlwaysAllowedSpawnReason(EntityCompatibility.getEntitySpawnReason(entity)))
.filter(entity -> !world.getEntitySpawnConfig().shouldAllowSpawn(entity))
.peek(Entity::remove)
.count();
@@ -177,4 +174,11 @@ final class MVEntityListener implements CoreListener {
}
});
}
private boolean isAlwaysAllowedSpawnReason(@Nullable SpawnReason reason) {
return reason == SpawnReason.CUSTOM
|| reason == SpawnReason.COMMAND
|| reason == SpawnReason.BREEDING
|| reason == SpawnReason.SPAWNER_EGG;
}
}
@@ -26,6 +26,7 @@ public enum MVCorei18n implements MessageKeyProvider {
CONFIG_STRING_PARSER_INVALIDDOUBLE,
CONFIG_STRING_PARSER_INVALIDFLOAT,
CONFIG_STRING_PARSER_INVALIDLONG,
CONFIG_STRING_PARSER_INVALIDMATERIAL,
// core config
CORE_CONFIG_CUSTOMPORTALSEARCHRADIUS_NONNEGATIVE,
@@ -182,7 +183,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 +371,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,
@@ -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);
@@ -6,9 +6,11 @@ import co.aikar.commands.ACFUtil;
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,15 +83,45 @@ public sealed class Message permits LocalizedMessage {
}
/**
* Gets the replacements for this message.
* Gets the replacements for this message. This is the raw, non-localized parsing of replacements.
* <br/>
* 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
*/
@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 toReplacementsArray(replacements);
return getRawReplacements();
}
/**
* Gets the replacements for this message with localization support based on the command issuer's locale config.
* <br/>
* 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);
}
/**
@@ -116,20 +148,36 @@ public sealed class Message permits LocalizedMessage {
}
/**
* Gets the formatted message.
* Gets the raw formatted message.
* <br/>
* 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.
* <br/>
* 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.
* <br/>
@@ -146,9 +194,9 @@ public sealed class Message permits LocalizedMessage {
* Gets the formatted message from localization data.
* <br/>
* 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 @@ public sealed class Message permits LocalizedMessage {
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;
}
@@ -264,7 +264,8 @@ public final class AsyncSafetyTeleporterAction {
Logging.finer("Teleported async %s to %s", teleportee.getName(), location);
return Attempt.success(null);
}
Logging.warning("Failed to async teleport %s to %s", teleportee.getName(), location);
Logging.warning("Failed to async teleport %s to %s. This is usually caused by another " +
"plugin canceling the teleport event and not a bug with Multiverse.", teleportee.getName(), location);
return Attempt.failure(TeleportFailureReason.TELEPORT_FAILED);
});
}
@@ -189,6 +189,7 @@ public final class ReflectHelper {
* 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 final class ReflectHelper {
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Method getMethod(Class<C> clazz, String methodName, Class<?>... parameterTypes) {
try {
@@ -235,6 +237,7 @@ public final class ReflectHelper {
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Method getMethod(C classInstance, String methodName, Class<?>... parameterTypes) {
return getMethod(classInstance.getClass(), methodName, parameterTypes);
@@ -254,6 +257,7 @@ public final class ReflectHelper {
* 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 <C, R> R invokeMethod(C classInstance, Method method, Object...parameters) {
@@ -276,6 +280,7 @@ public final class ReflectHelper {
* handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Field getField(Class<C> clazz, String fieldName) {
try {
@@ -299,6 +304,7 @@ public final class ReflectHelper {
* handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C> Field getField(C classInstance, String fieldName) {
return getField(classInstance.getClass(), fieldName);
@@ -318,6 +324,7 @@ public final class ReflectHelper {
* used to handle the failure case more explicitly.
*/
@Deprecated(forRemoval = true, since = "5.7")
@ApiStatus.ScheduledForRemoval(inVersion = "6.0")
@Nullable
public static <C, V> V getFieldValue(C classInstance, @Nullable Field field, @NotNull Class<V> fieldType) {
try {
@@ -345,6 +352,7 @@ public final class ReflectHelper {
* 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 <C, V> V getFieldValue(C classInstance, @Nullable String fieldName, @NotNull Class<V> fieldType) {
return getFieldValue(classInstance, getField(classInstance, fieldName), fieldType);
@@ -118,6 +118,20 @@ public final class StringFormatter {
* @return The parsed args
*/
public static @NotNull Collection<String> 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<String> parseQuotesInArgs(@NotNull String[] args, @NotNull String separator) {
List<String> result = new ArrayList<>(args.length);
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
@@ -131,13 +145,13 @@ public final class StringFormatter {
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));
@@ -0,0 +1,40 @@
package org.mvplugins.multiverse.core.utils.compatibility;
import org.bukkit.entity.Entity;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.utils.ReflectHelper;
/**
* Compatibility class used to handle API changes in {@link Entity} class.
*/
// TODO: Consider making this part of the public API in v5.9.
@ApiStatus.Internal
public final class EntityCompatibility {
private static final boolean HAS_GET_ENTITY_SPAWN_REASON_METHOD;
static {
HAS_GET_ENTITY_SPAWN_REASON_METHOD = ReflectHelper.hasMethod(Entity.class, "getEntitySpawnReason");
}
/**
* Gets the reason that initially spawned the entity when supported by the server.
*
* @param entity The entity to query.
* @return The entity's spawn reason, or null when the API is unavailable.
*/
@Nullable
public static SpawnReason getEntitySpawnReason(@NotNull Entity entity) {
if (HAS_GET_ENTITY_SPAWN_REASON_METHOD) {
return entity.getEntitySpawnReason();
}
return null;
}
private EntityCompatibility() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}
@@ -49,7 +49,7 @@ public sealed interface Attempt<T, F extends FailureReason> permits Attempt.Succ
@ApiStatus.AvailableSince("5.7")
static <T, F extends FailureReason> Attempt.Failure<T, F> failureRef(
F failureReason, MessageReplacement... messageReplacements) {
return new Failure<>(failureReason, Message.of(failureReason, "Failed!", messageReplacements));
return new Failure<>(failureReason, Message.of(failureReason, messageReplacements));
}
/**
@@ -220,7 +220,7 @@ public sealed interface Result<S extends SuccessReason, F extends FailureReason>
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 @@ public sealed interface Result<S extends SuccessReason, F extends FailureReason>
Failure(F failureReason, MessageReplacement[] replacements) {
this.failureReason = failureReason;
this.message = Message.of(failureReason, "Failed!", replacements);
this.message = Message.of(failureReason, replacements);
}
@Override
@@ -55,6 +55,7 @@ public final class LoadedMultiverseWorld extends MultiverseWorld {
worldConfig.load();
worldConfig.setEnvironment(world.getEnvironment());
worldConfig.setSeed(world.getSeed());
worldConfig.setLegacyWorldName(world.getName());
}
private void setupSpawnLocation(World world) {
@@ -6,6 +6,7 @@ import java.util.Objects;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Difficulty;
@@ -17,6 +18,7 @@ import org.bukkit.configuration.MemoryConfiguration;
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;
@@ -25,14 +27,20 @@ import org.mvplugins.multiverse.core.config.node.ConfigNode;
import org.mvplugins.multiverse.core.config.node.ListConfigNode;
import org.mvplugins.multiverse.core.config.node.NodeGroup;
import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
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;
import org.mvplugins.multiverse.core.world.location.SpawnLocation;
import org.mvplugins.multiverse.core.world.entity.EntitySpawnConfig;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
/**
* Represents nodes in a world configuration.
*/
@@ -43,6 +51,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 +60,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 +105,7 @@ final class WorldConfigNodes {
final ConfigNode<String> 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 +113,10 @@ final class WorldConfigNodes {
);
if (world == null) return;
world.updateColourlessAlias();
if (config.getWarnAliasConflicts()) {
aliasNameConflictChecker.checkDuplicateFor(world)
.sendConflictMessage(commandManager.getCommandIssuer(sender));
}
}));
final ConfigNode<Boolean> allowAdvancementGrant = node(ConfigNode.builder("allow-advancement-grant", Boolean.class)
@@ -159,11 +175,22 @@ final class WorldConfigNodes {
final ConfigNode<Material> entryFeeCurrency = node(ConfigNode.builder("entry-fee.currency", Material.class)
.defaultValue(MVEconomist.VAULT_ECONOMY_MATERIAL)
.name("entryfee-currency")
.stringParser((value, clazz) -> {
if (value != null && value.equalsIgnoreCase(MVEconomist.VAULT_ECONOMY_CODE)) {
return Try.success(MVEconomist.VAULT_ECONOMY_MATERIAL);
}
return Try.of(() -> MaterialConverter.stringToMaterial(value))
.filter(Objects::nonNull)
.orElse(Try.failure(new MultiverseException(Message.of(
MVCorei18n.CONFIG_STRING_PARSER_INVALIDMATERIAL,
replace("{input}").with(value)))));
})
.serializer(new NodeSerializer<>() {
@Override
public Material deserialize(Object object, Class<Material> type) {
return Option.of(object)
.map(String::valueOf)
.filter(s -> !s.isBlank())
.map(materialStr -> {
if (materialStr.equalsIgnoreCase(MVEconomist.VAULT_ECONOMY_CODE)) {
return MVEconomist.VAULT_ECONOMY_MATERIAL;
@@ -296,6 +323,8 @@ final class WorldConfigNodes {
.hidden());
final ConfigNode<String> legacyWorldName = node(ConfigNode.builder("read-only.legacy-world-name", String.class)
.onLoadAndChange((oldValue, newValue) -> worldManager.getWorldStore().changeLegacyWorldName(
oldValue, newValue, keyOrName.usableKey()))
.hidden());
final ConfigNode<Long> seed = node(ConfigNode.builder("read-only.seed", Long.class)
@@ -54,6 +54,7 @@ import org.mvplugins.multiverse.core.utils.compatibility.WorldCreatorCompatibili
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.utils.result.FailureReason;
import org.mvplugins.multiverse.core.utils.FileUtils;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.entity.EntityPurger;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
@@ -154,10 +155,14 @@ public final class WorldManager {
*/
@ApiStatus.Internal
public Try<Void> 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();
});
}
/**
@@ -240,7 +245,10 @@ public final class WorldManager {
.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))
Logging.severe("Failed to autoload world '%s': %s: %s",
world.getName(),
failure,
ChatTextFormatter.removeColor(failure.getFailureMessage().formatted())))
.onSuccess(newMVWorld ->
Logging.fine("Autoloaded world %s", newMVWorld.getName())));
}
@@ -278,15 +286,24 @@ public final class WorldManager {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
CreateWorldOptions options = keyOrNameWithOptions.options();
String generatorString = generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator());
WorldCreator worldCreator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generateStructures(options.generateStructures())
.generatorSettings(options.generatorSettings())
.seed(options.seed())
.type(options.worldType());
WorldCreatorCompatibility.setBonusChest(worldCreator, options.bonusChest());
options.forcedSpawnPosition().peek(position -> WorldCreatorCompatibility.setForcedSpawnPosition(worldCreator, position));
return addBiomeProviderToCreator(worldCreator, keyOrName.usableName(), options.biome())
return Try.of(() -> {
WorldCreator creator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generateStructures(options.generateStructures())
.generatorSettings(options.generatorSettings())
.seed(options.seed())
.type(options.worldType());
WorldCreatorCompatibility.setBonusChest(creator, options.bonusChest());
options.forcedSpawnPosition()
.peek(position -> WorldCreatorCompatibility.setForcedSpawnPosition(creator, position));
return creator;
})
.fold(throwable -> Attempt.<WorldCreator, WorldCreatorFailureReason>failure(WorldCreatorFailureReason.BUKKIT_CREATION_FAILED,
Replace.WORLD.with(keyOrName.usableName()),
Replace.ERROR.with(throwable)),
Attempt::<WorldCreator, WorldCreatorFailureReason>success)
.mapAttempt(creator -> addBiomeProviderToCreator(creator, keyOrName.usableName(), options.biome()))
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(CreateFailureReason.WORLD_CREATOR_FAILED)
@@ -355,11 +372,15 @@ public final class WorldManager {
WorldKeyOrName keyOrName = keyOrNameWithOptions.keyOrName();
ImportWorldOptions options = keyOrNameWithOptions.options();
String generatorString = generatorProvider.parseGeneratorString(keyOrName.usableName(), options.generator());
WorldCreator worldCreator = WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generatorSettings(options.generatorSettings());
return addBiomeProviderToCreator(worldCreator, keyOrName.usableName(), options.biome())
return Try.of(() -> WorldCreatorCompatibility.ofKeyOrName(keyOrName)
.environment(options.environment())
.generatorSettings(options.generatorSettings()))
.fold(throwable -> Attempt.<WorldCreator, WorldCreatorFailureReason>failure(WorldCreatorFailureReason.BUKKIT_CREATION_FAILED,
Replace.WORLD.with(keyOrName.usableName()),
Replace.ERROR.with(throwable)),
Attempt::<WorldCreator, WorldCreatorFailureReason>success)
.mapAttempt(creator -> addBiomeProviderToCreator(creator, keyOrName.usableName(), options.biome()))
.mapAttempt(creator -> addGeneratorToCreator(creator, generatorString))
.mapAttempt(this::createBukkitWorld)
.transform(ImportFailureReason.WORLD_CREATOR_FAILED)
@@ -524,10 +545,14 @@ public final class WorldManager {
}
}
WorldCreator worldCreator = WorldCreatorCompatibility.ofNameAndKey(mvWorld.getKey(), mvWorld.getName())
.environment(mvWorld.getEnvironment())
.seed(mvWorld.getSeed());
return addBiomeProviderToCreator(worldCreator, mvWorld.getName(), mvWorld.getBiome())
return Try.of(() -> WorldCreatorCompatibility.ofNameAndKey(mvWorld.getKey(), mvWorld.getName())
.environment(mvWorld.getEnvironment())
.seed(mvWorld.getSeed()))
.fold(throwable -> Attempt.<WorldCreator, WorldCreatorFailureReason>failure(WorldCreatorFailureReason.BUKKIT_CREATION_FAILED,
Replace.WORLD.with(mvWorld.getName()),
Replace.ERROR.with(throwable)),
Attempt::<WorldCreator, WorldCreatorFailureReason>success)
.mapAttempt(creator -> addBiomeProviderToCreator(creator, mvWorld.getName(), mvWorld.getBiome()))
.mapAttempt(creator -> addGeneratorToCreator(creator, mvWorld.getGenerator()))
.mapAttempt(this::createBukkitWorld)
.transform(LoadFailureReason.WORLD_CREATOR_FAILED)
@@ -120,6 +120,29 @@ final class WorldStore {
unloadedList.add(unloadedMap.get(world.getKey().toString()));
}
void changeLegacyWorldName(@Nullable String oldName, @Nullable String newName, @NotNull NamespacedKey worldKey) {
if (Objects.equals(oldName, newName)) {
return;
}
String key = worldKey.toString();
updateLegacyWorldName(loadedMap, oldName, newName, key);
updateLegacyWorldName(unloadedMap, oldName, newName, key);
}
private <T extends MultiverseWorld> void updateLegacyWorldName(
Map<String, T> map, @Nullable String oldName, @Nullable String newName, String key) {
T world = map.get(key);
if (world == null) {
return;
}
if (oldName != null && !key.equalsIgnoreCase(oldName)) {
map.remove(oldName, world);
}
if (newName != null) {
map.put(newName, world);
}
}
void changeAlias(@Nullable String oldAlias, @Nullable String newAlias, @NotNull NamespacedKey worldKey) {
if (Objects.equals(oldAlias, newAlias)) {
// nothing changed, ignore
@@ -18,7 +18,9 @@ import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.MultiverseCore;
import org.mvplugins.multiverse.core.utils.ServerProperties;
import org.mvplugins.multiverse.core.utils.result.Attempt;
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import org.mvplugins.multiverse.core.world.key.WorldKeyParseFailReason;
@@ -34,16 +36,18 @@ final class WorldsConfigManager {
private final SortedMap<WorldKeyOrName, WorldConfig> worldConfigMap;
private final File worldConfigFile;
private final ServerProperties serverProperties;
private YamlConfiguration worldsConfig;
private final MultiverseCore multiverseCore;
@Inject
WorldsConfigManager(@NotNull MultiverseCore core, @NotNull MultiverseCore multiverseCore) {
WorldsConfigManager(@NotNull MultiverseCore multiverseCore, @NotNull ServerProperties serverProperties) {
worldConfigMap = new TreeMap<>();
worldConfigFile = core.getDataFolder().toPath().resolve(CONFIG_FILENAME).toFile();
worldConfigFile = multiverseCore.getDataFolder().toPath().resolve(CONFIG_FILENAME).toFile();
this.multiverseCore = multiverseCore;
this.serverProperties = serverProperties;
}
/**
@@ -202,9 +206,59 @@ final class WorldsConfigManager {
worldConfigMap.remove(s);
}
migrateLevelNameChange();
return new NewAndRemovedWorlds(newWorldsAdded, worldsRemoved);
}
private void migrateLevelNameChange() {
serverProperties.getLevelName()
.peek(levelName -> {
boolean didMigrate = migrateLevelNameChange(
WorldKeyOrName.parseKey(NamespacedKey.minecraft("overworld")),
levelName
);
didMigrate = migrateLevelNameChange(
WorldKeyOrName.parseKey(NamespacedKey.minecraft("the_nether")),
DimensionFinder.DEFAULT_NETHER_FORMAT.replaceOverworld(levelName)
) || didMigrate;
didMigrate = migrateLevelNameChange(
WorldKeyOrName.parseKey(NamespacedKey.minecraft("the_end")),
DimensionFinder.DEFAULT_END_FORMAT.replaceOverworld(levelName)
) || didMigrate;
if (didMigrate) {
Logging.warning("We have detected and migrated your default world's name due to a level-name change.");
Logging.warning("Please check your worlds.yml file and world list to ensure everything is correct.");
Logging.warning("Additionally, all references to the old world name will no longer work. " +
"Please update all references within your configuration files/plugins.");
}
});
}
private boolean migrateLevelNameChange(WorldKeyOrName keyOrName, String newWorldName) {
return getWorldConfig(keyOrName).map(worldConfig -> {
if (newWorldName.equals(worldConfig.getLegacyWorldName())) {
return false;
}
boolean hasConflictKey = WorldKeyOrName.parseKey(newWorldName).map(this::getWorldConfig)
.fold(failure -> false, Option::isDefined);
if (hasConflictKey) {
Logging.severe("Unable to migrate default world's name caused by level-name change.");
Logging.severe("We have detected a clash between the default name '%s' and an existing non-default world in your worlds.yml file.", newWorldName);
Logging.severe("Please change the level-name in server.properties to something else and restart the server.");
return false;
}
Logging.info("Updating legacy world name for %s from %s to %s due to level-name change.",
keyOrName, worldConfig.getLegacyWorldName(), newWorldName);
worldConfig.setLegacyWorldName(newWorldName);
worldConfig.save();
return true;
}).getOrElse(false);
}
/**
* Whether the worlds.yml file has been loaded.
*
@@ -9,6 +9,7 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.SpawnCategory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.config.handle.MemoryConfigurationHandle;
import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle;
@@ -16,13 +17,19 @@ 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.functions.NodeStringParser;
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
import org.mvplugins.multiverse.core.locale.MVCorei18n;
import org.mvplugins.multiverse.core.locale.message.Message;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import org.mvplugins.multiverse.core.world.MultiverseWorld;
import java.util.ArrayList;
import java.util.List;
import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace;
public final class SpawnCategoryConfig {
private final CoreConfig config;
@@ -73,7 +80,7 @@ public final class SpawnCategoryConfig {
}
private void applyTickPerSpawns(@NotNull World bukkitWorld) {
if (!config.getApplyEntitySpawnLimit()) {
if (!config.getApplyEntitySpawnRate()) {
Logging.finer("World %s %s skipping setTicksPerSpawns due to core config", world.getName(), spawnCategory);
return;
}
@@ -177,6 +184,7 @@ public final class SpawnCategoryConfig {
final ConfigNode<Integer> tickRate = node(ConfigNode.builder("tick-rate", Integer.class)
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.stringParser(SpawnValueStringParser.INSTANCE)
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
@@ -193,6 +201,7 @@ public final class SpawnCategoryConfig {
final ConfigNode<Integer> spawnLimit = node(ConfigNode.builder("spawn-limit", Integer.class)
.defaultValue(-2)
.suggester(input -> List.of("@unset", "@bukkit", "10", "100", "400", "1000"))
.stringParser(SpawnValueStringParser.INSTANCE)
.serializer(SpawnValueSerializer.INSTANCE)
.onLoadAndChange((oldValue, newValue) -> applyConfigToWorld())
.onChange((sender, oldValue, newValue) -> {
@@ -215,6 +224,27 @@ public final class SpawnCategoryConfig {
.build());
}
private static class SpawnValueStringParser implements NodeStringParser<Integer> {
private static final SpawnValueStringParser INSTANCE = new SpawnValueStringParser();
@Override
public @NotNull Try<Integer> parse(@Nullable String input, @NotNull Class<Integer> type) {
String str = String.valueOf(input).trim();
if (str.equalsIgnoreCase("@unset")) {
return Try.success(-2);
}
if (str.equalsIgnoreCase("@bukkit")) {
return Try.success(-1);
}
try {
return Try.success(Integer.parseInt(str));
} catch (NumberFormatException e) {
return Try.failure(new MultiverseException(Message.of(MVCorei18n.CONFIG_STRING_PARSER_INVALIDINTEGER,
replace("{input}").with(input))));
}
}
}
private static class SpawnValueSerializer implements NodeSerializer<Integer> {
private static final SpawnValueSerializer INSTANCE = new SpawnValueSerializer();
@@ -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<MultiverseWorld> duplicateAliases;
private final List<MultiverseWorld> 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<MultiverseWorld> 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<MultiverseWorld> getDuplicateWorldNames() {
return duplicateWorldNames;
}
}
}
@@ -126,6 +126,8 @@ public final class CloneWorldOptions implements KeepWorldSettingsOptions {
* 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")
@@ -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);
}
/**
@@ -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);
}
/**
@@ -7,6 +7,7 @@ mv-core.config.string.parser.invalidinteger=Unable to convert '{input}' to numbe
mv-core.config.string.parser.invaliddouble=Unable to convert '{input}' to number. (double)
mv-core.config.string.parser.invalidfloat=Unable to convert '{input}' to number. (float)
mv-core.config.string.parser.invalidlong=Unable to convert '{input}' to number. (long)
mv-core.config.string.parser.invalidmaterial='{input}' is not a valid material.
# core config
mv-core.core.config.customportalsearchradius.nonnegative=The value must be greater than or equal to 0.
@@ -32,7 +33,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 +54,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 +357,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 +385,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} <environment>&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} <environment>&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!
@@ -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.
@@ -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} <environment>&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} <environment>&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'!
@@ -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}' существует в папках сервера! Вам нужно удалить его перед клонированием.
@@ -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} <ortam>&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} <ortam>&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ı!
mv-core.generic.teleportplayers.failed=Bir veya daha fazla oyuncu dünyadan ışınlanamadı!
@@ -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}' 已经存在于服务器的文件中!在复制前你需要先删除它。
@@ -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
@@ -49,6 +49,38 @@ class WorldManagerTest : TestWithMockBukkit() {
assertNotNull(world2)
}
@Test
fun `Legacy world name changes update loaded and unloaded lookups`() {
val worldKey = world.key.toString()
assertTrue(world.worldConfig.setLegacyWorldName("renamed_world").isSuccess)
assertFalse(worldManager.getWorld("world").isDefined)
assertSame(world, worldManager.getLoadedWorld("RENAMED_WORLD").get())
assertSame(world, worldManager.getLoadedWorld(worldKey).get())
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world)).isSuccess)
assertFalse(worldManager.getWorld("world").isDefined)
assertFalse(worldManager.getLoadedWorld("renamed_world").isDefined)
assertEquals(world.key, worldManager.getUnloadedWorld("RENAMED_WORLD").get().key)
assertEquals(world.key, worldManager.getUnloadedWorld(worldKey).get().key)
}
@Test
fun `Setting up loaded world config refreshes legacy world name lookups`() {
assertTrue(world.worldConfig.setLegacyWorldName("stale_name").isSuccess)
assertTrue(world.worldConfig.save().isSuccess)
world.setWorldConfig(world.worldConfig)
assertEquals("world", world.name)
assertSame(world, worldManager.getLoadedWorld("world").get())
assertFalse(worldManager.getWorld("stale_name").isDefined)
assertTrue(worldManager.unloadWorld(UnloadWorldOptions.world(world)).isSuccess)
assertTrue(worldManager.getUnloadedWorld("world").isDefined)
assertFalse(worldManager.getWorld("stale_name").isDefined)
}
@Test
fun `Create world with custom options`() {
assertTrue(worldManager.createWorld(
@@ -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)
}
}
@@ -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