Skip to content

Commit 3233ae5

Browse files
authored
Merge pull request #3215 from Multiverse/feat/world-name-format
Implement automatic link of respawn world from nether/end to overworld
2 parents d1e1ea5 + 468399a commit 3233ae5

File tree

6 files changed

+379
-40
lines changed

6 files changed

+379
-40
lines changed

src/main/java/org/mvplugins/multiverse/core/config/CoreConfig.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.mvplugins.multiverse.core.config.migration.VersionMigrator;
2828
import org.mvplugins.multiverse.core.config.migration.action.SetMigratorAction;
2929
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
30+
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
3031

3132
@Service
3233
public class CoreConfig {
@@ -185,6 +186,22 @@ public boolean isAutoPurgeEntities() {
185186
return configHandle.get(configNodes.autoPurgeEntities);
186187
}
187188

189+
public Try<Void> setNetherWorldNameFormat(DimensionFormat netherWorldNameFormat) {
190+
return configHandle.set(configNodes.netherWorldNameFormat, netherWorldNameFormat);
191+
}
192+
193+
public DimensionFormat getNetherWorldNameFormat() {
194+
return configHandle.get(configNodes.netherWorldNameFormat);
195+
}
196+
197+
public Try<Void> setEndWorldNameFormat(DimensionFormat endWorldNameFormat) {
198+
return configHandle.set(configNodes.endWorldNameFormat, endWorldNameFormat);
199+
}
200+
201+
public DimensionFormat getEndWorldNameFormat() {
202+
return configHandle.get(configNodes.endWorldNameFormat);
203+
}
204+
188205
/**
189206
* {@inheritDoc}
190207
*/
@@ -311,6 +328,14 @@ public String getJoinDestination() {
311328
return configHandle.get(configNodes.joinDestination);
312329
}
313330

331+
public Try<Void> setDefaultRespawnInOverworld(boolean defaultRespawnInOverworld) {
332+
return configHandle.set(configNodes.defaultRespawnInOverworld, defaultRespawnInOverworld);
333+
}
334+
335+
public boolean getDefaultRespawnInOverworld() {
336+
return configHandle.get(configNodes.defaultRespawnInOverworld);
337+
}
338+
314339
/**
315340
* {@inheritDoc}
316341
*/

src/main/java/org/mvplugins/multiverse/core/config/CoreConfigNodes.java

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77
import org.bukkit.plugin.PluginManager;
88

99
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
1011
import org.mvplugins.multiverse.core.command.MVCommandManager;
1112
import org.mvplugins.multiverse.core.command.queue.ConfirmMode;
1213
import org.mvplugins.multiverse.core.config.node.ConfigHeaderNode;
1314
import org.mvplugins.multiverse.core.config.node.ConfigNode;
1415
import org.mvplugins.multiverse.core.config.node.Node;
1516
import org.mvplugins.multiverse.core.config.node.NodeGroup;
17+
import org.mvplugins.multiverse.core.config.node.functions.NodeStringParser;
18+
import org.mvplugins.multiverse.core.config.node.serializer.NodeSerializer;
1619
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
1720
import org.mvplugins.multiverse.core.destination.core.WorldDestination;
1821
import org.mvplugins.multiverse.core.event.MVDebugModeEvent;
1922
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
2023
import org.mvplugins.multiverse.core.permissions.PermissionUtils;
24+
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder.DimensionFormat;
2125

2226
import java.util.Collection;
2327
import java.util.List;
@@ -113,6 +117,26 @@ private <N extends Node> N node(N node) {
113117
.name("auto-purge-entities")
114118
.build());
115119

120+
private final ConfigHeaderNode worldNameFormat = node(ConfigHeaderNode.builder("world.world-name-format")
121+
.comment("")
122+
.comment("Format for world names for multiverse to automatically detect a world group consist of overworld, nether and end.")
123+
.comment("This is used default-respawn-in-overworld and potentially other features.")
124+
.build());
125+
126+
final ConfigNode<DimensionFormat> netherWorldNameFormat = node(ConfigNode.builder("world.world-name-format.nether", DimensionFormat.class)
127+
.defaultValue(() -> new DimensionFormat("%overworld%_nether"))
128+
.name("nether-world-name-format")
129+
.serializer(DimensionFormatNodeSerializer.INSTANCE)
130+
.stringParser(DimensionFormatNodeStringParser.INSTANCE)
131+
.build());
132+
133+
final ConfigNode<DimensionFormat> endWorldNameFormat = node(ConfigNode.builder("world.world-name-format.end", DimensionFormat.class)
134+
.defaultValue(() -> new DimensionFormat("%overworld%_the_end"))
135+
.name("end-world-name-format")
136+
.serializer(DimensionFormatNodeSerializer.INSTANCE)
137+
.stringParser(DimensionFormatNodeStringParser.INSTANCE)
138+
.build());
139+
116140
private final ConfigHeaderNode teleportHeader = node(ConfigHeaderNode.builder("teleport")
117141
.comment("")
118142
.comment("")
@@ -205,6 +229,20 @@ private <N extends Node> N node(N node) {
205229
.suggester(this::suggestDestinations)
206230
.build());
207231

232+
final ConfigNode<Boolean> defaultRespawnInOverworld = node(ConfigNode.builder("spawn.default-respawn-in-overworld", Boolean.class)
233+
.comment("")
234+
.comment("This only applies if the `respawn-world` property is not set for the world that the player died in,")
235+
.comment("and the player does not have bed or anchor set.")
236+
.comment("----")
237+
.comment("When this option is enabled, players will respawn in the overworld when dying in nether or end, mimicking the vanilla behavior.")
238+
.comment("The automatic selection of overworld is determined by the `world-name-format` config section above.")
239+
.comment("This option takes precedence over the `default-respawn-within-same-world` option.")
240+
.comment("----")
241+
.comment("Set this to false if you want another plugin to handle respawning or do not want this vanilla behavior.")
242+
.defaultValue(true)
243+
.name("default-respawn-in-overworld")
244+
.build());
245+
208246
final ConfigNode<Boolean> defaultRespawnWithinSameWorld = node(ConfigNode.builder("spawn.default-respawn-within-same-world", Boolean.class)
209247
.comment("")
210248
.comment("This only applies if the `respawn-world` property is not set for the world that the player died in,")
@@ -224,13 +262,10 @@ private <N extends Node> N node(N node) {
224262

225263
final ConfigNode<Boolean> enforceRespawnAtWorldSpawn = node(ConfigNode.builder("spawn.enforce-respawn-at-world-spawn", Boolean.class)
226264
.comment("")
227-
.comment("This config will only apply if `respawn-world` is set, or `default-respawn-within-same-world` is enabled.")
228-
.comment("----")
229-
.comment("When this option is enabled, players will always respawn at the world's spawn location of `respawn-world`,")
265+
.comment("When this option is enabled, players will always respawn at the world's spawn location of calculated respawn world,")
230266
.comment("unless bed or anchor is set and `bed-respawn` or `anchor-spawn` is enabled respectively.")
231-
.comment("If respawn-world is set, Multiverse will use that world's spawn location, else it will use the world's spawn where the player died in.")
232267
.comment("----")
233-
.comment("Set this to false if you want to use the /spawnpoint instead of the world's spawn location.")
268+
.comment("Set this to false if you want to use a custom spawn location such as /spawnpoint instead of the world's spawn location.")
234269
.defaultValue(true)
235270
.name("enforce-respawn-at-world-spawn")
236271
.build());
@@ -437,6 +472,35 @@ private Collection<String> suggestDestinations(String input) {
437472
.toList();
438473
}
439474

475+
private static final class DimensionFormatNodeSerializer implements NodeSerializer<DimensionFormat> {
476+
477+
private static final DimensionFormatNodeSerializer INSTANCE = new DimensionFormatNodeSerializer();
478+
479+
private DimensionFormatNodeSerializer() {}
480+
481+
@Override
482+
public DimensionFormat deserialize(Object object, Class<DimensionFormat> type) {
483+
return new DimensionFormat(String.valueOf(object));
484+
}
485+
486+
@Override
487+
public Object serialize(DimensionFormat dimensionFormat, Class<DimensionFormat> type) {
488+
return dimensionFormat.getFormat();
489+
}
490+
}
491+
492+
private static final class DimensionFormatNodeStringParser implements NodeStringParser<DimensionFormat> {
493+
494+
private static final DimensionFormatNodeStringParser INSTANCE = new DimensionFormatNodeStringParser();
495+
496+
private DimensionFormatNodeStringParser() {}
497+
498+
@Override
499+
public @NotNull Try<DimensionFormat> parse(@Nullable String string, @NotNull Class<DimensionFormat> type) {
500+
return Try.of(() -> new DimensionFormat(string));
501+
}
502+
}
503+
440504
// END CHECKSTYLE-SUPPRESSION: Javadoc
441505
// END CHECKSTYLE-SUPPRESSION: MemberName
442506
// END CHECKSTYLE-SUPPRESSION: Abbreviation

src/main/java/org/mvplugins/multiverse/core/listeners/MVPlayerListener.java

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.mvplugins.multiverse.core.world.WorldManager;
4646
import org.mvplugins.multiverse.core.world.entrycheck.EntryFeeResult;
4747
import org.mvplugins.multiverse.core.world.entrycheck.WorldEntryCheckerProvider;
48+
import org.mvplugins.multiverse.core.world.helpers.DimensionFinder;
4849
import org.mvplugins.multiverse.core.world.helpers.EnforcementHandler;
4950
import org.spigotmc.event.player.PlayerSpawnLocationEvent;
5051

@@ -64,6 +65,7 @@ final class MVPlayerListener implements CoreListener {
6465
private final Provider<MVCommandManager> commandManagerProvider;
6566
private final DestinationsProvider destinationsProvider;
6667
private final EnforcementHandler enforcementHandler;
68+
private final DimensionFinder dimensionFinder;
6769

6870
private final Map<String, String> playerWorld = new ConcurrentHashMap<>();
6971

@@ -79,7 +81,8 @@ final class MVPlayerListener implements CoreListener {
7981
WorldEntryCheckerProvider worldEntryCheckerProvider,
8082
Provider<MVCommandManager> commandManagerProvider,
8183
DestinationsProvider destinationsProvider,
82-
EnforcementHandler enforcementHandler) {
84+
EnforcementHandler enforcementHandler,
85+
DimensionFinder dimensionFinder) {
8386
this.plugin = plugin;
8487
this.config = config;
8588
this.worldManagerProvider = worldManagerProvider;
@@ -91,6 +94,7 @@ final class MVPlayerListener implements CoreListener {
9194
this.commandManagerProvider = commandManagerProvider;
9295
this.destinationsProvider = destinationsProvider;
9396
this.enforcementHandler = enforcementHandler;
97+
this.dimensionFinder = dimensionFinder;
9498
}
9599

96100
private WorldManager getWorldManager() {
@@ -122,25 +126,29 @@ public Map<String, String> getPlayerWorld() {
122126
@EventHandler(priority = EventPriority.LOW)
123127
public void playerRespawn(PlayerRespawnEvent event) {
124128
Player player = event.getPlayer();
125-
getWorldManager().getLoadedWorld(player.getWorld())
126-
.onEmpty(() -> Logging.fine("Player '%s' is in a world that is not managed by Multiverse.", player.getName()))
127-
.filter(mvWorld -> {
128-
if (mvWorld.getBedRespawn() && event.isBedSpawn()) {
129-
Logging.fine("Spawning %s at their bed.", player.getName());
130-
return false;
131-
}
132-
if (mvWorld.getAnchorRespawn() && event.isAnchorSpawn()) {
133-
Logging.fine("Spawning %s at their anchor.", player.getName());
134-
return false;
135-
}
136-
if (!config.getDefaultRespawnWithinSameWorld() && mvWorld.getRespawnWorldName().isEmpty()) {
137-
Logging.fine("Not overriding respawn location for player '%s' as " +
138-
"default-respawn-within-same-world is disabled and no respawn-world is set.", player.getName());
139-
return false;
140-
}
141-
return true;
129+
LoadedMultiverseWorld mvWorld = getWorldManager().getLoadedWorld(player.getWorld()).getOrNull();
130+
if (mvWorld == null) {
131+
Logging.finer("Player '%s' died in a world that is not managed by Multiverse.", player.getName());
132+
return;
133+
}
134+
135+
if (mvWorld.getBedRespawn() && event.isBedSpawn()) {
136+
Logging.fine("Spawning %s at their bed.", player.getName());
137+
return;
138+
}
139+
if (mvWorld.getAnchorRespawn() && event.isAnchorSpawn()) {
140+
Logging.fine("Spawning %s at their anchor.", player.getName());
141+
return;
142+
}
143+
144+
getRespawnWorld(mvWorld)
145+
.onEmpty(() -> Logging.fine("No respawn-world determined for world '%s'.", mvWorld.getName()))
146+
.flatMap(respawnWorld -> {
147+
Logging.finer("Using respawn-world '%s' for world '%s'.", respawnWorld.getName(), mvWorld.getName());
148+
return getMostAccurateRespawnLocation(respawnWorld, event.getRespawnLocation())
149+
.onEmpty(() -> Logging.finer("No accurate respawn-location determined for world '%s'.",
150+
mvWorld.getName()));
142151
})
143-
.flatMap(mvWorld -> getMostAccurateRespawnLocation(player, mvWorld, event.getRespawnLocation()))
144152
.peek(newRespawnLocation -> {
145153
MVRespawnEvent respawnEvent = new MVRespawnEvent(newRespawnLocation, event.getPlayer());
146154
this.server.getPluginManager().callEvent(respawnEvent);
@@ -153,22 +161,34 @@ public void playerRespawn(PlayerRespawnEvent event) {
153161
});
154162
}
155163

156-
private Option<Location> getMostAccurateRespawnLocation(Player player, MultiverseWorld mvWorld, Location defaultRespawnLocation) {
157-
return Option.of(mvWorld.getRespawnWorldName().isEmpty()
158-
? player.getWorld()
159-
: server.getWorld(mvWorld.getRespawnWorldName()))
160-
.onEmpty(() -> Logging.warning("World '%s' has respawn-world property of '%s' that does not exist!",
161-
player.getWorld().getName(), mvWorld.getRespawnWorldName()))
162-
.flatMap(newRespawnWorld -> {
163-
if (!config.getEnforceRespawnAtWorldSpawn() && newRespawnWorld.equals(defaultRespawnLocation.getWorld())) {
164-
Logging.fine("Respawn location is within same world as respawn-world, not overriding.");
165-
return Option.none();
166-
}
167-
return getWorldManager()
168-
.getLoadedWorld(newRespawnWorld)
169-
.map(newMVRespawnWorld -> (Location) newMVRespawnWorld.getSpawnLocation())
170-
.orElse(() -> Option.of(newRespawnWorld.getSpawnLocation()));
171-
});
164+
private Option<LoadedMultiverseWorld> getRespawnWorld(LoadedMultiverseWorld mvWorld) {
165+
if (!mvWorld.getRespawnWorldName().isEmpty()) {
166+
Logging.finer("Using configured respawn-world for world '%s'.", mvWorld.getName());
167+
return getWorldManager().getLoadedWorld(mvWorld.getRespawnWorldName())
168+
.onEmpty(() -> {
169+
Logging.warning("World '%s' has respawn-world property of '%s' that does not exist!",
170+
mvWorld.getName(), mvWorld.getRespawnWorldName());
171+
});
172+
} else if (!dimensionFinder.isOverworld(mvWorld) && config.getDefaultRespawnInOverworld()) {
173+
Logging.finer("Defaulting to overworld for world '%s'.", mvWorld.getName());
174+
return dimensionFinder.getOverworldWorld(mvWorld).flatMap(getWorldManager()::getLoadedWorld)
175+
.onEmpty(() -> {
176+
Logging.warning("World '%s' has no overworld to teleport to!",
177+
mvWorld.getName());
178+
});
179+
} else if (config.getDefaultRespawnWithinSameWorld()) {
180+
Logging.finer("Defaulting to same world for world '%s'.", mvWorld.getName());
181+
return Option.of(mvWorld);
182+
}
183+
return Option.none();
184+
}
185+
186+
private Option<Location> getMostAccurateRespawnLocation(LoadedMultiverseWorld mvWorld, Location defaultRespawnLocation) {
187+
if (!config.getEnforceRespawnAtWorldSpawn() && Objects.equals(defaultRespawnLocation.getWorld(), mvWorld.getBukkitWorld().getOrNull())) {
188+
Logging.fine("Respawn location is within same world as respawn-world, not overriding.");
189+
return Option.none();
190+
}
191+
return Option.of(mvWorld.getSpawnLocation());
172192
}
173193

174194
@EventHandler

0 commit comments

Comments
 (0)