Skip to content

Commit

Permalink
Add method MVWorldManager#addOrRemoveWorldSafely
Browse files Browse the repository at this point in the history
Addresses Multiverse#2560

Commands that load or unload worlds trigger an IllegalStateException if they are run via a command block while the worlds are being ticked.

Using a BukkitRunnable, the operation can be delayed until the next tick at a time when the worlds are not being ticked.

MVWorldManager#addOrRemoveWorldSafely performs this logic, either running the operation now or delaying it.

8 commands were modified to use MVWorldManager#addOrRemoveWorldSafely, which I think are all the relevant commands.

Unfortunately I haven't found a way to tell when the worlds are being ticked on a Spigot server. There probably won't be any way to do this until [SPIGOT-7089](https://hub.spigotmc.org/jira/browse/SPIGOT-7089) resolves
  • Loading branch information
willkroboth committed Jul 12, 2022
1 parent 20ce469 commit cae29cb
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,13 @@ boolean addWorld(String name, Environment env, String seedString, WorldType type
* @return A collection of world names that are deemed importable.
*/
Collection<String> getPotentialWorlds();

/**
* Performs the given operation that creates and/or unloads a world. If this operation cannot run because the worlds are being ticked, the operation is delayed until the next tick.
*
* @param worldName The world being modified.
* @param operationName The name of the operation being done.
* @param worldModification The operation to perform
*/
void addOrRemoveWorldSafely(String worldName, String operationName, Runnable worldModification);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ public CloneCommand(MultiverseCore plugin) {
@Override
public void runCommand(CommandSender sender, List<String> args) {
String oldName = args.get(0);
if (!this.worldManager.hasUnloadedWorld(oldName, true)) {
String newName = args.get(1);
if (!this.worldManager.hasUnloadedWorld(oldName, true)) {
// If no world was found, we can't clone.
sender.sendMessage("Sorry, Multiverse doesn't know about world " + oldName + ", so we can't clone it!");
sender.sendMessage("Check the " + ChatColor.GREEN + "/mv list" + ChatColor.WHITE + " command to verify it is listed.");
return;
}
if (this.plugin.getMVWorldManager().cloneWorld(oldName, args.get(1))) {
sender.sendMessage(ChatColor.GREEN + "World cloned!");
} else {
sender.sendMessage(ChatColor.RED + "World could NOT be cloned!");
}
this.worldManager.addOrRemoveWorldSafely(oldName, "clone", () -> {
if (this.worldManager.cloneWorld(oldName, newName)) {
sender.sendMessage(ChatColor.GREEN + "World cloned!");
} else {
sender.sendMessage(ChatColor.RED + "World could NOT be cloned!");
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,17 @@ public void runCommand(CommandSender sender, List<String> args) {
return;
}
}
Command.broadcastCommandMessage(sender, "Starting creation of world '" + worldName + "'...");

if (this.worldManager.addWorld(worldName, environment, seed, type, allowStructures, generator, useSpawnAdjust)) {
Command.broadcastCommandMessage(sender, "Complete!");
} else {
Command.broadcastCommandMessage(sender, "FAILED.");
}
boolean finalAllowStructures = allowStructures;
boolean finalUseSpawnAdjust = useSpawnAdjust;
this.worldManager.addOrRemoveWorldSafely(worldName, "create", () -> {
Command.broadcastCommandMessage(sender, "Starting creation of world '" + worldName + "'...");

if (this.worldManager.addWorld(worldName, environment, seed, type, finalAllowStructures, generator, finalUseSpawnAdjust)) {
Command.broadcastCommandMessage(sender, "Complete!");
} else {
Command.broadcastCommandMessage(sender, "FAILED.");
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ public void runCommand(CommandSender sender, List<String> args) {
private Runnable deleteRunnable(@NotNull CommandSender sender,
@NotNull String worldName) {

return () -> {
return () -> this.plugin.getMVWorldManager().addOrRemoveWorldSafely(worldName, "delete", () -> {
sender.sendMessage(String.format("Deleting world '%s'...", worldName));
if (this.plugin.getMVWorldManager().deleteWorld(worldName)) {
sender.sendMessage(String.format("%sWorld %s was deleted!", ChatColor.GREEN, worldName));
return;
}
sender.sendMessage(String.format("%sThere was an issue deleting '%s'! Please check console for errors.",
ChatColor.RED, worldName));
};
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,14 @@ public void runCommand(CommandSender sender, List<String> args) {
sender.sendMessage("That world environment did not exist.");
sender.sendMessage("For a list of available world types, type: " + ChatColor.AQUA + "/mvenv");
} else {
Command.broadcastCommandMessage(sender, String.format("Starting import of world '%s'...", worldName));
if (this.worldManager.addWorld(worldName, environment, null, null, null, generator, useSpawnAdjust))
Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Complete!");
else
Command.broadcastCommandMessage(sender, ChatColor.RED + "Failed!");
boolean finalUseSpawnAdjust = useSpawnAdjust;
this.worldManager.addOrRemoveWorldSafely(worldName, "unload", () -> {
Command.broadcastCommandMessage(sender, String.format("Starting import of world '%s'...", worldName));
if (this.worldManager.addWorld(worldName, environment, null, null, null, generator, finalUseSpawnAdjust))
Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Complete!");
else
Command.broadcastCommandMessage(sender, ChatColor.RED + "Failed!");
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ public LoadCommand(MultiverseCore plugin) {

@Override
public void runCommand(CommandSender sender, List<String> args) {
if (this.plugin.getMVWorldManager().loadWorld(args.get(0))) {
Command.broadcastCommandMessage(sender, "Loaded world '" + args.get(0) + "'!");
} else {
sender.sendMessage("Error trying to load world '" + args.get(0) + "'!");
}
String worldName = args.get(0);
this.plugin.getMVWorldManager().addOrRemoveWorldSafely(worldName, "load", () -> {
if (this.plugin.getMVWorldManager().loadWorld(worldName)) {
Command.broadcastCommandMessage(sender, "Loaded world '" + worldName + "'!");
} else {
sender.sendMessage("Error trying to load world '" + worldName + "'!");
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ private Runnable doWorldRegen(@NotNull CommandSender sender,
@NotNull String seed,
boolean keepGamerules) {

return () -> {
return () -> plugin.getMVWorldManager().addOrRemoveWorldSafely(worldName, "regenerate", () -> {
if (this.plugin.getMVWorldManager().regenWorld(worldName, useSeed, randomSeed, seed, keepGamerules)) {
sender.sendMessage(ChatColor.GREEN + "World Regenerated!");
return;
}
sender.sendMessage(ChatColor.RED + "World could NOT be regenerated!");
};
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ public RemoveCommand(MultiverseCore plugin) {

@Override
public void runCommand(CommandSender sender, List<String> args) {
if (this.plugin.getMVWorldManager().removeWorldFromConfig(args.get(0))) {
sender.sendMessage("World removed from config!");
} else {
sender.sendMessage("Error trying to remove world from config!");
}
String worldName = args.get(0);
this.plugin.getMVWorldManager().addOrRemoveWorldSafely(worldName, "remove", () -> {
if (this.plugin.getMVWorldManager().removeWorldFromConfig(worldName)) {
sender.sendMessage("World removed from config!");
} else {
sender.sendMessage("Error trying to remove world from config!");
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ public UnloadCommand(MultiverseCore plugin) {

@Override
public void runCommand(CommandSender sender, List<String> args) {
if (this.plugin.getMVWorldManager().unloadWorld(args.get(0))) {
Command.broadcastCommandMessage(sender, "Unloaded world '" + args.get(0) + "'!");
} else {
sender.sendMessage("Error trying to unload world '" + args.get(0) + "'!");
}
String worldName = args.get(0);
this.plugin.getMVWorldManager().addOrRemoveWorldSafely(worldName, "unload", () -> {
if (this.plugin.getMVWorldManager().unloadWorld(worldName)) {
Command.broadcastCommandMessage(sender, "Unloaded world '" + worldName + "'!");
} else {
sender.sendMessage("Error trying to unload world '" + worldName + "'!");
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;

import java.io.File;
import java.io.FilenameFilter;
Expand Down Expand Up @@ -1029,4 +1030,24 @@ public Collection<String> getPotentialWorlds() {
.map(File::getName)
.collect(Collectors.toList());
}

/**
* {@inheritDoc}
*/
public void addOrRemoveWorldSafely(String worldName, String operationName, Runnable worldModification) {
// TODO: Find real way to tell if worlds are being ticked
if (!worldName.equals("testWorld")) {
// Operation is fine to do now
worldModification.run();
} else {
// Operation needs to be delayed until worlds are not being ticked

Logging.fine("Worlds were being ticked while attempting to %s %s. Trying again in the next tick", operationName, worldName);
new BukkitRunnable() {
public void run() {
worldModification.run();
}
}.runTask(plugin);
}
}
}

1 comment on commit cae29cb

@willkroboth
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commands that load or unload worlds trigger an IllegalStateException if they are run via a command block while the worlds are being ticked.

This is true on a Paper server, but not a Spigot server. On a Spigot server, running these commands still cause a crash (see Spigot issue for details).

Please sign in to comment.