Compare commits

..
14 Commits
Author SHA1 Message Date
Ben Woo 892ce24845 Merge pull request #3507 from Multiverse/chore/glare-down
Switch to use jitpack for config dep as glare is down
2026-07-23 21:41:50 +08:00
Ben Woo 091eec96b9 Switch to use jitpack for config dep as glare is down 2026-07-23 21:37:28 +08:00
Ben Woo 37ecceb179 Merge pull request #3506 from Multiverse/fix/load-callback
Improve error handling for when loaded API callback
2026-07-21 13:49:15 +08:00
Ben Woo 7697065519 Improve error handling for when loaded API callback 2026-07-21 12:41:26 +08:00
Ben Woo ad7b9638f1 Merge pull request #3505 from Multiverse/feat/nested-potential-worlds
Add support for searching potential world in nested sub-folders
2026-07-20 20:02:49 +08:00
Ben Woo 37ba5ecc04 Add support for searching potential world in nested sub-folders 2026-07-20 19:59:07 +08:00
Ben Woo 2445464ab8 Merge pull request #3504 from Multiverse/fix/exact-dest-alias
Fix exact destination not working with alias names
2026-07-20 14:37:11 +08:00
Ben Woo 7fad9aa2e4 Fix exact destination not working with alias names 2026-07-20 14:19:20 +08:00
Ben Woo a8c0b221cd Merge pull request #3503 from Multiverse/fix/alias-mapping
Map alias even when world isn't loaded and case insensitive lookup
2026-07-20 13:18:39 +08:00
Ben Woo 826fe4c6d0 Map alias even when world isn't loaded and case insensitive lookup 2026-07-20 13:15:00 +08:00
Ben Woo 97cbb4576d Merge pull request #3499 from Multiverse/refactor/entity-purge
Clean up entity purger and ensure player is not removed
2026-07-16 12:48:58 +08:00
Ben Woo d50cd6cce5 Merge pull request #3498 from Multiverse/fix/purge-entities-chunk
Remove entities on chunk load based on spawn config
2026-07-16 12:44:30 +08:00
Ben Woo b500956555 Clean up entity purger and ensure player is not removed 2026-07-16 12:43:22 +08:00
Ben Woo de80667bb4 Remove entities on chunk load based on spawn config 2026-07-16 12:22:37 +08:00
9 changed files with 111 additions and 71 deletions

No files matched your search

+5 -5
View File
@@ -9,13 +9,13 @@ description = 'Multiverse-Core'
repositories {
maven {
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
name = 'jitpack repo'
url = uri('https://jitpack.io')
}
maven {
name = 'glaremasters repo'
url = 'https://repo.glaremasters.me/repository/towny/'
name = 'aikar repo'
url = uri('https://repo.aikar.co/content/groups/aikar/')
}
maven {
@@ -43,7 +43,7 @@ dependencies {
shadowed 'co.aikar:acf-paper:0.5.1-SNAPSHOT'
// Config
shadowed('io.github.townyadvanced.commentedconfiguration:CommentedConfiguration:1.0.1') {
shadowed('com.github.TownyAdvanced:CommentedConfiguration:1.0.3') {
exclude group: 'org.spigotmc', module: 'spigot-api'
}
@@ -1,9 +1,17 @@
package org.mvplugins.multiverse.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Try;
import org.bukkit.Bukkit;
import org.bukkit.plugin.ServicePriority;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.anchor.AnchorManager;
import org.mvplugins.multiverse.core.config.CoreConfig;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
@@ -16,18 +24,13 @@ import org.mvplugins.multiverse.core.world.WorldManager;
import org.mvplugins.multiverse.core.world.biomeprovider.BiomeProviderFactory;
import org.mvplugins.multiverse.core.world.generators.GeneratorProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Provides access to the MultiverseCore API.
*/
public final class MultiverseCoreApi {
private static MultiverseCoreApi instance;
private static final List<Consumer<MultiverseCoreApi>> whenLoadedCallbacks = new ArrayList<>();
private static final List<Consumer<MultiverseCoreApi>> WHEN_LOADED_CALLBACKS = new ArrayList<>();
static void init(@NotNull MultiverseCore multiverseCore) {
if (instance != null) {
@@ -36,11 +39,24 @@ public final class MultiverseCoreApi {
instance = new MultiverseCoreApi(multiverseCore.getServiceLocator());
Bukkit.getServicesManager().register(MultiverseCoreApi.class, instance, multiverseCore, ServicePriority.Normal);
whenLoadedCallbacks.forEach(c -> c.accept(instance));
whenLoadedCallbacks.clear();
List<Consumer<MultiverseCoreApi>> callbacks = List.copyOf(WHEN_LOADED_CALLBACKS);
WHEN_LOADED_CALLBACKS.clear();
MultiverseCoreApi loadedApi = instance;
callbacks.forEach(callback -> runLoadCallback(callback, loadedApi));
}
private static void runLoadCallback(
@NotNull Consumer<MultiverseCoreApi> callback,
@NotNull MultiverseCoreApi loadedApi) {
Try.run(() -> callback.accept(loadedApi))
.onFailure(exception -> Logging.warning(
"A Multiverse-Core API load callback failed: %s", exception.getMessage()));
}
static void shutdown() {
if (instance == null) {
return;
}
Bukkit.getServicesManager().unregister(instance);
instance = null;
}
@@ -62,7 +78,7 @@ public final class MultiverseCoreApi {
if (instance != null) {
consumer.accept(instance);
} else {
whenLoadedCallbacks.add(consumer);
WHEN_LOADED_CALLBACKS.add(consumer);
}
}
@@ -96,12 +96,14 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_FORMAT);
}
String worldName = items[0];
String worldNameOrAlias = items[0];
String positionStr = items[1];
World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull();
World world = getLoadedMultiverseWorld(worldNameOrAlias)
.flatMap(LoadedMultiverseWorld::getBukkitWorld)
.getOrNull();
if (world == null) {
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName));
return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldNameOrAlias));
}
EntityPosition position;
@@ -111,7 +113,7 @@ public final class ExactDestination implements Destination<ExactDestination, Exa
return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e));
}
return Attempt.success(new ExactDestinationInstance(this, worldManager, worldName, position));
return Attempt.success(new ExactDestinationInstance(this, worldManager, world.getName(), position));
}
//TODO: Extract to a world finder class
@@ -10,6 +10,7 @@ package org.mvplugins.multiverse.core.listeners;
import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent;
import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
@@ -19,6 +20,7 @@ import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason;
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.jvnet.hk2.annotations.Service;
@@ -27,6 +29,8 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass;
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod;
import org.mvplugins.multiverse.core.world.WorldManager;
import java.util.Arrays;
/**
* Multiverse's Entity {@link Listener}.
*/
@@ -132,7 +136,7 @@ final class MVEntityListener implements CoreListener {
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntity())) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) {
Logging.finest("Cancelling Creature Spawn Event for: " + event.getEntity());
event.setCancelled(true);
}
@@ -151,10 +155,26 @@ final class MVEntityListener implements CoreListener {
worldManager.getLoadedWorld(event.getEntity().getWorld())
.peek(world -> {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntity())) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) {
Logging.finest("Cancelling Entity Spawn Event for: " + event.getEntity());
event.setCancelled(true);
}
});
}
@EventMethod
void chunkLoad(ChunkLoadEvent event) {
worldManager.getLoadedWorld(event.getWorld())
.peek(world -> {
long count = Arrays.stream(event.getChunk().getEntities())
.filter(entity -> !(entity instanceof Player))
.filter(entity -> !world.getEntitySpawnConfig().shouldAllowSpawn(entity))
.peek(Entity::remove)
.count();
if (count > 0) {
Logging.finest("Removed %d entities from chunk %d, %d in world %s due to spawn category settings.",
count, event.getChunk().getX(), event.getChunk().getZ(), event.getWorld().getName());
}
});
}
}
@@ -49,7 +49,7 @@ final class WorldConfig {
@NotNull ConfigurationSection configSection,
@NotNull MultiverseCore multiverseCore) {
this.keyOrName = keyOrName;
this.configNodes = new WorldConfigNodes(multiverseCore);
this.configNodes = new WorldConfigNodes(multiverseCore, keyOrName);
this.configHandle = MemoryConfigurationHandle.builder(configSection, configNodes.getNodes())
.logger(Logging.getLogger())
.migrator(migrator())
@@ -28,6 +28,7 @@ import org.mvplugins.multiverse.core.economy.MVEconomist;
import org.mvplugins.multiverse.core.utils.MaterialConverter;
import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter;
import org.mvplugins.multiverse.core.world.helpers.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;
@@ -42,12 +43,14 @@ final class WorldConfigNodes {
private WorldManager worldManager;
private EnforcementHandler enforcementHandler;
private CoreConfig config;
private WorldKeyOrName keyOrName;
private MultiverseWorld world = null;
WorldConfigNodes(@NotNull MultiverseCore multiverseCore) {
WorldConfigNodes(@NotNull MultiverseCore multiverseCore, @NotNull WorldKeyOrName keyOrName) {
this.worldManager = multiverseCore.getServiceLocator().getService(WorldManager.class);
this.enforcementHandler = multiverseCore.getServiceLocator().getService(EnforcementHandler.class);
this.config = multiverseCore.getServiceLocator().getService(CoreConfig.class);
this.keyOrName = keyOrName;
}
MultiverseWorld getWorld() {
@@ -91,10 +94,13 @@ final class WorldConfigNodes {
final ConfigNode<String> alias = node(ConfigNode.builder("alias", String.class)
.defaultValue("")
.onLoadAndChange((oldValue, newValue) -> {
worldManager.getWorldStore().changeAlias(
ChatTextFormatter.removeColor(oldValue),
ChatTextFormatter.removeColor(newValue),
keyOrName.usableKey()
);
if (world == null) return;
world.updateColourlessAlias();
worldManager.getWorldStore().changeAlias(
ChatTextFormatter.removeColor(oldValue), ChatTextFormatter.removeColor(newValue), world);
}));
final ConfigNode<Boolean> allowAdvancementGrant = node(ConfigNode.builder("allow-advancement-grant", Boolean.class)
@@ -15,6 +15,7 @@ import org.mvplugins.multiverse.core.world.key.WorldKeyOrName;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -119,16 +120,16 @@ final class WorldStore {
unloadedList.add(unloadedMap.get(world.getKey().toString()));
}
void changeAlias(@Nullable String oldAlias, @Nullable String newAlias, @NotNull MultiverseWorld world) {
void changeAlias(@Nullable String oldAlias, @Nullable String newAlias, @NotNull NamespacedKey worldKey) {
if (Objects.equals(oldAlias, newAlias)) {
// nothing changed, ignore
return;
}
if (!Strings.isNullOrEmpty(oldAlias)) {
aliasMap.remove(oldAlias, world.getKey().toString());
aliasMap.remove(oldAlias.toLowerCase(Locale.ROOT), worldKey.toString());
}
if (!Strings.isNullOrEmpty(newAlias)) {
aliasMap.put(newAlias, world.getKey().toString());
aliasMap.put(newAlias.toLowerCase(Locale.ROOT), worldKey.toString());
}
}
@@ -263,7 +264,7 @@ final class WorldStore {
return null;
}
//TODO: Not sure if we should fail if there is multiple alias of the same name, but for now just return the first one
return aliasMap.get(worldNameOrAlias).stream()
return aliasMap.get(worldNameOrAlias.toLowerCase(Locale.ROOT)).stream()
.findFirst()
.orElse(worldNameOrAlias);
}
@@ -7,58 +7,35 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
@Service
public final class EntityPurger {
public int purgeEntities(LoadedMultiverseWorld world) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
if (!world.getEntitySpawnConfig().shouldAllowSpawn(entity)) {
entity.remove();
purgeCount.incrementAndGet();
}
}
});
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> !world.getEntitySpawnConfig().shouldAllowSpawn(entity));
}
public int purgeEntities(LoadedMultiverseWorld world, SpawnCategory spawnCategory) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> {
for (Entity entity : bukkitWorld.getEntities()) {
if (entity.getSpawnCategory() == spawnCategory) {
entity.remove();
purgeCount.incrementAndGet();
}
}
});
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> entity.getSpawnCategory().equals(spawnCategory));
}
public int purgeEntities(LoadedMultiverseWorld world, SpawnCategory... spawnCategories) {
Set<SpawnCategory> spawnCategoriesSet = Set.of(spawnCategories);
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.getEntities().stream()
.filter(entity -> !(entity instanceof Player))
.filter(entity -> spawnCategoriesSet.contains(entity.getSpawnCategory()))
.forEach(entity -> {
entity.remove();
purgeCount.incrementAndGet();
}));
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> spawnCategoriesSet.contains(entity.getSpawnCategory()));
}
public int purgeAllEntities(LoadedMultiverseWorld world) {
AtomicInteger purgeCount = new AtomicInteger(0);
world.getBukkitWorld().peek(bukkitWorld -> bukkitWorld.getEntities().stream()
.filter(entity -> !(entity instanceof Player))
.forEach(entity -> {
entity.remove();
purgeCount.incrementAndGet();
}));
return purgeCount.get();
return purgeEntitiesWithCondition(world, entity -> true);
}
private int purgeEntitiesWithCondition(LoadedMultiverseWorld world, Predicate<Entity> condition) {
return Math.toIntExact(world.getBukkitWorld()
.map(bukkitWorld -> bukkitWorld.getEntities().stream()
.filter(entity -> !(entity instanceof Player))
.filter(condition)
.peek(Entity::remove)
.count())
.getOrElse(0L));
}
}
@@ -27,6 +27,9 @@ import java.util.stream.Stream;
@Service
public final class PotentialWorldFinder {
private static final int MAX_RECURSION_DEPTH = 3;
private static final int MAX_FOLDER_TRAVERSAL_LIMIT = 100;
private final WorldManager worldManager;
private final WorldNameChecker worldNameChecker;
@@ -56,6 +59,7 @@ public final class PotentialWorldFinder {
private Stream<String> findFromRootFolder() {
Path worldContainer = Bukkit.getWorldContainer().toPath();
return Arrays.stream(listFolders(worldContainer))
.limit(MAX_FOLDER_TRAVERSAL_LIMIT)
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.filter(worldNameChecker::isValidWorldName)
@@ -65,15 +69,29 @@ public final class PotentialWorldFinder {
private Stream<String> findFromDimensionsFolder() {
Path worldContainer = BukkitCompatibility.getWorldFoldersDirectory();
return Arrays.stream(listFolders(worldContainer))
.map(File::getName)
.flatMap(namespace -> Arrays.stream(listFolders(worldContainer.resolve(namespace)))
.filter(worldNameChecker::isValidWorldFolder)
.map(File::getName)
.map(key -> namespace + ":" + key))
.limit(MAX_FOLDER_TRAVERSAL_LIMIT)
.flatMap(namespaceFile -> recursiveFindFromNamespaceFolder("", namespaceFile.toPath(), 0)
.map(worldKey -> namespaceFile.getName() + ":" + worldKey))
.filter(namespacedKey -> NamespacedKey.fromString(namespacedKey) != null)
.filter(namespacedKey -> !worldManager.isWorld(namespacedKey));
}
private Stream<String> recursiveFindFromNamespaceFolder(String prefix, Path subWorldContainer, int depth) {
if (depth >= MAX_RECURSION_DEPTH) {
return Stream.empty();
}
return Arrays.stream(listFolders(subWorldContainer))
.limit(MAX_FOLDER_TRAVERSAL_LIMIT)
.flatMap(folder -> worldNameChecker.isValidWorldFolder(folder)
? Stream.of(concatPath(prefix, folder.getName()))
: recursiveFindFromNamespaceFolder(
concatPath(prefix, folder.getName()), folder.toPath(), depth + 1));
}
private String concatPath(String prefix, String folderName) {
return prefix.isEmpty() ? folderName : prefix + "/" + folderName;
}
private @NotNull File[] listFolders(Path folder) {
return Option.of(folder.toFile().listFiles(File::isDirectory))
.getOrElse(() -> new File[0]);