Compare commits

...
22 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 396ece4d27 Fix various missing parameters and typos in command syntax 2026-08-09 23:32:06 +08:00
29 changed files with 385 additions and 71 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/
@@ -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,
@@ -55,7 +55,7 @@ class CloneCommand extends CoreCommand {
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}")
@@ -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);
@@ -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")
@@ -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,
@@ -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);
});
}
@@ -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");
}
}
@@ -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;
@@ -26,6 +27,9 @@ 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;
@@ -35,6 +39,8 @@ 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.
*/
@@ -169,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;
@@ -306,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;
@@ -244,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())));
}
@@ -282,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)
@@ -359,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)
@@ -528,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;
@@ -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();
@@ -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.
@@ -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(