Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
.addVersionMigrator(VersionMigrator.builder(5.2)
.addAction(MoveMigratorAction.of("spawn.default-respawn-to-world-spawn", "world.enforce-respawn-at-world-spawn"))
.build())
.addVersionMigrator(VersionMigrator.builder(5.3)

Check warning on line 86 in src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 '5.3' is a magic number. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java:86:69: warning: '5.3' is a magic number. (com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck)
.addAction(SetMigratorAction.of("spawn.apply-respawn-radius", false))
.build())
.build())
.build();
this.stringPropertyHandle = new StringPropertyHandle(configHandle);
Expand Down Expand Up @@ -443,6 +446,16 @@
return configHandle.get(configNodes.enforceRespawnAtWorldSpawn);
}

@ApiStatus.AvailableSince("5.9")

Check warning on line 449 in src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Missing a Javadoc comment. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java:449:5: warning: Missing a Javadoc comment. (com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck)
public Try<Void> setApplyRespawnRadius(boolean applyRespawnRadius) {
return configHandle.set(configNodes.applyRespawnRadius, applyRespawnRadius);
}

@ApiStatus.AvailableSince("5.9")
public boolean getApplyRespawnRadius() {
return configHandle.get(configNodes.applyRespawnRadius);
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,19 @@
.name("enforce-respawn-at-world-spawn")
.build());

final ConfigNode<Boolean> applyRespawnRadius = node(ConfigNode.builder("spawn.apply-respawn-radius", Boolean.class)
.comment("")
.comment("When this option is enabled and players respawn at the world's spawn location,")
.comment("they will be spread to a random safe location around it based on the `respawn_radius` game rule")
.comment("(`spawnRadius` on older versions), mimicking the vanilla behavior.")
.comment("Same as vanilla, this does not apply if the respawn radius is 0, the world's game mode is adventure")

Check warning on line 371 in src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Line is longer than 120 characters (found 123). Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java:371:0: warning: Line is longer than 120 characters (found 123). (com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck)
.comment("or the world has no sky light (e.g. the nether and the end).")
.comment("----")
.comment("Set this to false if you want players to always respawn exactly at the world's spawn location.")
.defaultValue(true)
.name("apply-respawn-radius")
.build());

private final ConfigHeaderNode portalHeader = node(ConfigHeaderNode.builder("portal")
.comment("")
.comment("")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker;
import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter;
import org.mvplugins.multiverse.core.teleportation.BlockSafety;
import org.mvplugins.multiverse.core.teleportation.SpawnRadiusLocator;
import org.mvplugins.multiverse.core.teleportation.TeleportQueue;
import org.mvplugins.multiverse.core.utils.result.ResultChain;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
Expand Down Expand Up @@ -72,6 +73,7 @@ final class MVPlayerListener implements CoreListener {
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
private final SpawnRadiusLocator spawnRadiusLocator;

@Inject
MVPlayerListener(
Expand All @@ -88,7 +90,8 @@ final class MVPlayerListener implements CoreListener {
EnforcementHandler enforcementHandler,
DimensionFinder dimensionFinder,
CorePermissionsChecker corePermissionsChecker,
AsyncSafetyTeleporter asyncSafetyTeleporter) {
AsyncSafetyTeleporter asyncSafetyTeleporter,
SpawnRadiusLocator spawnRadiusLocator) {
this.plugin = plugin;
this.config = config;
this.worldManagerProvider = worldManagerProvider;
Expand All @@ -103,6 +106,7 @@ final class MVPlayerListener implements CoreListener {
this.dimensionFinder = dimensionFinder;
this.corePermissionsChecker = corePermissionsChecker;
this.asyncSafetyTeleporter = asyncSafetyTeleporter;
this.spawnRadiusLocator = spawnRadiusLocator;
}

private WorldManager getWorldManager() {
Expand Down Expand Up @@ -189,6 +193,9 @@ private Option<Location> getMostAccurateRespawnLocation(LoadedMultiverseWorld mv
Logging.fine("Respawn location is within same world as respawn-world, not overriding.");
return Option.none();
}
if (config.getApplyRespawnRadius()) {
return Option.of(spawnRadiusLocator.getLocationWithinSpawnRadius(mvWorld));
}
return Option.of(mvWorld.getSpawnLocation());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package org.mvplugins.multiverse.core.teleportation;

import java.util.concurrent.ThreadLocalRandom;

import com.dumptruckman.minecraft.util.Logging;
import jakarta.inject.Inject;
import org.bukkit.GameMode;
import org.bukkit.GameRule;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldBorder;
import org.bukkit.block.Block;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;

Check warning on line 18 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 'org.mvplugins.multiverse.core.world.LoadedMultiverseWorld' should be separated from previous imports. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:18:1: warning: 'org.mvplugins.multiverse.core.world.LoadedMultiverseWorld' should be separated from previous imports. (com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck)

/**
* Finds a location around a world's spawn based on the respawn radius game rule, similar to how vanilla spreads
* players around the world spawn when they respawn.
*/
@ApiStatus.AvailableSince("5.9")
@Service
public final class SpawnRadiusLocator {
/**
* How many locations to try when spreading around the spawn, same as vanilla.
*/
private static final int MAX_CANDIDATES = 1024;

/**
* How many chunks around the spawn chunk may be loaded while looking for a location. Candidates are checked
* synchronously, so candidates in other chunks are only used if those chunks are already loaded.
*/
private static final int LOADABLE_CHUNK_RANGE = 1;

private final BlockSafety blockSafety;
private final LocationManipulation locationManipulation;

@Inject
SpawnRadiusLocator(
@NotNull BlockSafety blockSafety,
@NotNull LocationManipulation locationManipulation) {
this.blockSafety = blockSafety;
this.locationManipulation = locationManipulation;
}

/**
* Gets a random safe location within the respawn radius game rule around the given world's spawn location. Same
* as vanilla, no spreading is done if the radius is 0, the world's game mode is adventure or the world has no sky
* light (e.g. the nether and the end).
*
* @param world The world to get the location around its spawn for.
* @return A safe location within the respawn radius of the spawn, or the spawn location itself if none is found.
*/
@ApiStatus.AvailableSince("5.9")

Check warning on line 57 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Return count is 3 (max allowed for non-void methods/lambdas is 2). Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:57:5: info: Return count is 3 (max allowed for non-void methods/lambdas is 2). (com.puppycrawl.tools.checkstyle.checks.coding.ReturnCountCheck)
public @NotNull Location getLocationWithinSpawnRadius(@NotNull LoadedMultiverseWorld world) {
Location spawnLocation = world.getSpawnLocation();
World bukkitWorld = spawnLocation.getWorld();
if (bukkitWorld == null) {
return spawnLocation;
}
if (world.getGameMode() == GameMode.ADVENTURE || !bukkitWorld.hasSkyLight()) {
Logging.finer("Not spreading spawn location for world '%s' with game mode %s and sky light %s.",
world.getName(), world.getGameMode(), bukkitWorld.hasSkyLight());
return spawnLocation;
}
return getLocationWithinSpawnRadius(spawnLocation);
}

/**
* Gets a random safe location within the respawn radius game rule of the given spawn location. Unlike
* {@link #getLocationWithinSpawnRadius(LoadedMultiverseWorld)}, the game mode and sky light of the world are not
* checked, so the location is spread whenever the radius is greater than 0.
*
* @param spawnLocation The spawn location to spread around.
* @return A safe location within the respawn radius of the spawn, or the spawn location itself if none is found.
*/
@ApiStatus.AvailableSince("5.9")

Check warning on line 80 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Return count is 4 (max allowed for non-void methods/lambdas is 2). Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:80:5: info: Return count is 4 (max allowed for non-void methods/lambdas is 2). (com.puppycrawl.tools.checkstyle.checks.coding.ReturnCountCheck)
public @NotNull Location getLocationWithinSpawnRadius(@NotNull Location spawnLocation) {
World world = spawnLocation.getWorld();
if (world == null) {
return spawnLocation;
}
int respawnRadius = getRespawnRadius(world);
if (respawnRadius <= 0) {
Logging.finer("Not spreading spawn location for world '%s' with respawn radius %d.",
world.getName(), respawnRadius);
return spawnLocation;
}
int spawnX = spawnLocation.getBlockX();
int spawnZ = spawnLocation.getBlockZ();
int radius = Math.max(1, Math.min(respawnRadius, getDistanceToBorder(world.getWorldBorder(), spawnX, spawnZ)));
Logging.finer("Spreading spawn location for world '%s' with radius %d.", world.getName(), radius);

// Go through the candidates in a random order without repeats. A random step spreads the attempts over the
// whole area, so they are not limited to a few rows when the radius is large.
int sideLength = radius * 2 + 1;
long candidateCount = (long) sideLength * sideLength;
long step = getRandomCoprime(candidateCount);
long offset = ThreadLocalRandom.current().nextLong(candidateCount);
long attempts = Math.min(candidateCount, MAX_CANDIDATES);
for (long i = 0; i < attempts; i++) {
long index = (offset + i * step) % candidateCount;
int x = spawnX + (int) (index % sideLength) - radius;
int z = spawnZ + (int) (index / sideLength) - radius;
if (!canCheckCandidate(world, spawnX, spawnZ, x, z)) {
continue;
}
Location candidate = getCandidate(world, spawnLocation, x, z);
if (candidate != null) {
Logging.fine("Using location '%s' within radius %d of spawn for world '%s'.",
locationManipulation.locationToString(candidate), radius, world.getName());
return candidate;
}
}
Logging.finer("No safe location found within radius %d of spawn for world '%s'.", radius, world.getName());
return spawnLocation;
}

private boolean canCheckCandidate(World world, int spawnX, int spawnZ, int x, int z) {

Check warning on line 122 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 More than 4 parameters (found 5). Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:122:21: info: More than 4 parameters (found 5). (ConsiderLessMethodParameters)
int chunkX = x >> 4;

Check warning on line 123 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 '4' is a magic number. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:123:27: warning: '4' is a magic number. (com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck)
int chunkZ = z >> 4;

Check warning on line 124 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 '4' is a magic number. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:124:27: warning: '4' is a magic number. (com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck)
return (Math.abs(chunkX - (spawnX >> 4)) <= LOADABLE_CHUNK_RANGE

Check warning on line 125 in src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java

View workflow job for this annotation

GitHub Actions / checkstyle / checkstyle

[checkstyle] reported by reviewdog 🐶 Unnecessary parentheses around expression. Raw Output: /github/workspace/src/main/java/org/mvplugins/multiverse/core/teleportation/SpawnRadiusLocator.java:125:16: warning: Unnecessary parentheses around expression. (com.puppycrawl.tools.checkstyle.checks.coding.UnnecessaryParenthesesCheck)
&& Math.abs(chunkZ - (spawnZ >> 4)) <= LOADABLE_CHUNK_RANGE)
|| world.isChunkLoaded(chunkX, chunkZ);
}

private @Nullable Location getCandidate(World world, Location spawnLocation, int x, int z) {
int y = world.getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1;
if (y + 1 >= world.getMaxHeight()) {
return null;
}
Location location = new Location(world, x + 0.5, y, z + 0.5,
spawnLocation.getYaw(), spawnLocation.getPitch());
return blockSafety.canSpawnAtLocationSafely(location) && !isInLiquid(location) ? location : null;
}

private boolean isInLiquid(Location location) {
Block block = location.getBlock();
return block.isLiquid() || block.getRelative(0, 1, 0).isLiquid();
}

private long getRandomCoprime(long number) {
if (number <= 2) {
return 1;
}
long coprime = ThreadLocalRandom.current().nextLong(1, number);
while (gcd(coprime, number) != 1) {
coprime = coprime % (number - 1) + 1;
}
return coprime;
}

private long gcd(long a, long b) {
return b == 0 ? a : gcd(b, a % b);
}

private int getRespawnRadius(World world) {
// Older servers only have the spawnRadius game rule, while newer ones renamed it to respawn_radius.
GameRule<?> respawnRadiusRule = GameRule.getByName("respawn_radius");
if (respawnRadiusRule == null) {
respawnRadiusRule = GameRule.getByName("spawnRadius");
}
if (respawnRadiusRule == null) {
return 0;
}
Object value = world.getGameRuleValue(respawnRadiusRule);
return value instanceof Integer ? Math.max(0, (Integer) value) : 0;
}

private int getDistanceToBorder(WorldBorder worldBorder, int x, int z) {
double halfSize = worldBorder.getSize() / 2.0;
double minX = worldBorder.getCenter().getX() - halfSize;
double maxX = worldBorder.getCenter().getX() + halfSize;
double minZ = worldBorder.getCenter().getZ() - halfSize;
double maxZ = worldBorder.getCenter().getZ() + halfSize;
double distance = Math.min(
Math.min(x - minX, maxX - x),
Math.min(z - minZ, maxZ - z));
return (int) Math.floor(distance);
}
}
19 changes: 19 additions & 0 deletions src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ class ConfigTest : TestWithMockBukkit() {
assertEquals(128, config.customPortalSearchRadius)
assertEquals(true, config.isAutoPurgeEntities)
assertEquals(false, config.isShowingDonateMessage)
assertEquals(false, config.applyRespawnRadius)
}

@Test
fun `Respawn radius is disabled when migrating an existing config`() {
val existingConfig = getResourceAsText("/configs/fresh_config.yml")
?.replace(" apply-respawn-radius: true\n", "")
?.replace("version: 5.3", "version: 5.2")
assertNotNull(existingConfig)
configFile.writeText(existingConfig)
assertTrue(config.load().isSuccess)
assertTrue(config.save().isSuccess)

assertEquals(false, config.applyRespawnRadius)
}

@Test
fun `Respawn radius is enabled for a fresh config`() {
assertEquals(true, config.applyRespawnRadius)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.mvplugins.multiverse.core.listeners

import org.mvplugins.multiverse.core.TestWithMockBukkit
import org.mvplugins.multiverse.core.config.CoreConfig
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld
import org.mvplugins.multiverse.core.world.WorldManager
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 RespawnRadiusTest : TestWithMockBukkit() {

private lateinit var config: CoreConfig
private lateinit var mvWorld: LoadedMultiverseWorld

@BeforeTest
fun setUpRespawnRadiusTest() {
config = requireNotNull(serviceLocator.getActiveService(CoreConfig::class.java))
val worldManager = requireNotNull(serviceLocator.getActiveService(WorldManager::class.java))
assertTrue(worldManager.createWorld(CreateWorldOptions.worldName("respawn_world")).isSuccess)
mvWorld = requireNotNull(worldManager.getLoadedWorld("respawn_world").getOrNull())
@Suppress("DEPRECATION")
assertTrue(mvWorld.bukkitWorld.get().setGameRuleValue("respawn_radius", "5"))
}

@Test
fun `Player respawns exactly at world spawn when respawn radius is disabled`() {
assertTrue(config.setApplyRespawnRadius(false).isSuccess)

repeat(10) {
assertEquals(mvWorld.spawnLocation, respawnPlayer().respawnLocation)
}
}

@Test
fun `Player respawns around world spawn when respawn radius is enabled`() {
assertTrue(config.setApplyRespawnRadius(true).isSuccess)
val spawnLocation = mvWorld.spawnLocation

val results = (1..20).map { respawnPlayer().respawnLocation }

results.forEach { result ->
assertTrue(result.blockX - spawnLocation.blockX in -5..5)
assertTrue(result.blockZ - spawnLocation.blockZ in -5..5)
}
assertTrue(results.any { it.blockX != spawnLocation.blockX || it.blockZ != spawnLocation.blockZ })
}

private fun respawnPlayer() = server.addPlayer().run {
teleport(mvWorld.spawnLocation)
respawn()
}
}
Loading
Loading