Add debug command to save the world and flush scheduled changed chunks

Also change the status command messages a little
This commit is contained in:
Blue (Lukas Rieger) 2020-09-25 18:24:07 +02:00
parent f6008ac4a3
commit e62c5d5be3
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
13 changed files with 350 additions and 11 deletions

View File

@ -372,6 +372,15 @@ public MapUpdateHandler getUpdateHandler() {
return updateHandler;
}
public boolean flushWorldUpdates(UUID worldUUID) throws IOException {
if (serverInterface.persistWorldChanges(worldUUID)) {
updateHandler.onWorldSaveToDisk(worldUUID);
return true;
}
return false;
}
public WebServer getWebServer() {
return webServer;
}
@ -380,4 +389,8 @@ public boolean isLoaded() {
return loaded;
}
public String getImplementationType() {
return implementationType;
}
}

View File

@ -42,6 +42,7 @@
import de.bluecolored.bluemap.common.plugin.serverinterface.CommandSource;
import de.bluecolored.bluemap.common.plugin.text.Text;
import de.bluecolored.bluemap.common.plugin.text.TextColor;
import de.bluecolored.bluemap.common.plugin.text.TextFormat;
import de.bluecolored.bluemap.core.render.hires.HiresModelManager;
import de.bluecolored.bluemap.core.world.World;
@ -62,12 +63,29 @@ public List<Text> createStatusMessage(){
lines.add(Text.of(TextColor.BLUE, "Tile-Updates:"));
if (renderer.isRunning()) {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ", Text.of(TextColor.GREEN, "running").setHoverText(Text.of("click to pause rendering")).setClickCommand("/bluemap pause"), TextColor.GRAY, "!"));
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ",
Text.of(TextColor.GREEN, "running")
.setHoverText(Text.of("click to pause rendering"))
.setClickCommand("/bluemap pause"),
TextColor.GRAY, "!"));
} else {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ", Text.of(TextColor.RED, "paused").setHoverText(Text.of("click to resume rendering")).setClickCommand("/bluemap resume"), TextColor.GRAY, "!"));
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ",
Text.of(TextColor.RED, "paused")
.setHoverText(Text.of("click to resume rendering"))
.setClickCommand("/bluemap resume"),
TextColor.GRAY, "!"));
}
lines.add(Text.of(TextColor.WHITE, " Scheduled tile-updates: ", Text.of(TextColor.GOLD, renderer.getQueueSize()).setHoverText(Text.of("tiles waiting for a free render-thread")), TextColor.GRAY, " + " , Text.of(TextColor.GRAY, plugin.getUpdateHandler().getUpdateBufferCount()).setHoverText(Text.of("tiles waiting for world-save"))));
lines.add(Text.of(
TextColor.WHITE, " Scheduled tile-updates: ",
TextColor.GOLD, renderer.getQueueSize()).setHoverText(
Text.of(
TextColor.WHITE, "Tiles waiting for a free render-thread: ", TextColor.GOLD, renderer.getQueueSize(),
TextColor.WHITE, "\n\nChunks marked as changed: ", TextColor.GOLD, plugin.getUpdateHandler().getUpdateBufferCount(),
TextColor.GRAY, TextFormat.ITALIC, "\n(Changed chunks will be rendered as soon as they are saved back to the world-files)"
)
)
);
RenderTask[] tasks = renderer.getRenderTasks();
if (tasks.length > 0) {

View File

@ -119,6 +119,12 @@ public void init() {
.then(argument("y", DoubleArgumentType.doubleArg())
.then(argument("z", DoubleArgumentType.doubleArg())
.executes(this::debugBlockCommand))))))
.then(literal("flush")
.executes(this::debugFlushCommand)
.then(argument("world", StringArgumentType.string()).suggests(new WorldSuggestionProvider<>(plugin))
.executes(this::debugFlushCommand)))
.then(literal("cache")
.executes(this::debugClearCacheCommand))
@ -377,6 +383,47 @@ public int debugClearCacheCommand(CommandContext<S> context) throws CommandSynta
return 1;
}
public int debugFlushCommand(CommandContext<S> context) throws CommandSyntaxException {
CommandSource source = commandSourceInterface.apply(context.getSource());
// parse arguments
Optional<String> worldName = getOptionalArgument(context, "world", String.class);
final World world;
if (worldName.isPresent()) {
world = parseWorld(worldName.get()).orElse(null);
if (world == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this name: ", TextColor.WHITE, worldName.get()));
return 0;
}
} else {
world = source.getWorld().orElse(null);
if (world == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a location from this command-source, you'll have to define a world!"));
return 0;
}
}
new Thread(() -> {
source.sendMessage(Text.of(TextColor.GOLD, "Saving world and flushing changes..."));
try {
if (plugin.flushWorldUpdates(world.getUUID())) {
source.sendMessage(Text.of(TextColor.GREEN, "Successfully saved and flushed all changes."));
} else {
source.sendMessage(Text.of(TextColor.RED, "This operation is not supported by this implementation (" + plugin.getImplementationType() + ")"));
}
} catch (IOException ex) {
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to save the world. Please check the console for more details..."));
Logger.global.logError("Unexpected exception trying to save the world!", ex);
}
}).start();
return 1;
}
public int debugBlockCommand(CommandContext<S> context) throws CommandSyntaxException {
final CommandSource source = commandSourceInterface.apply(context.getSource());
@ -515,10 +562,17 @@ public int renderCommand(CommandContext<S> context) {
// execute render
new Thread(() -> {
if (world != null) {
helper.createWorldRenderTask(source, world, center, radius);
} else {
helper.createMapRenderTask(source, map, center, radius);
try {
if (world != null) {
plugin.getServerInterface().persistWorldChanges(world.getUUID());
helper.createWorldRenderTask(source, world, center, radius);
} else {
plugin.getServerInterface().persistWorldChanges(map.getWorld().getUUID());
helper.createMapRenderTask(source, map, center, radius);
}
} catch (IOException ex) {
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to save the world. Please check the console for more details..."));
Logger.global.logError("Unexpected exception trying to save the world!", ex);
}
}).start();

View File

@ -63,6 +63,19 @@ default String getWorldName(UUID worldUUID) {
return null;
}
/**
* Attempts to persist all changes that have been made in a world to disk.
*
* @param worldUUID The {@link UUID} of the world to be persisted.
* @return <code>true</code> if the changes have been successfully persisted, <code>false</code> if this operation is not supported by the implementation
*
* @throws IOException if something went wrong trying to persist the changes
* @throws IllegalArgumentException if there is no world with this UUID
*/
default boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
return false;
}
/**
* Returns the Folder containing the configurations for the plugin
*/

View File

@ -33,7 +33,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.LogManager;
@ -164,6 +166,37 @@ private UUID loadUUIDForWorld(ServerWorld world) throws IOException {
File dimensionDir = world.getDimension().getType().getSaveDirectory(world.getSaveHandler().getWorldDir());
return getUUIDForWorld(dimensionDir.getCanonicalFile());
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
final CompletableFuture<Boolean> taskResult = new CompletableFuture<>();
serverInstance.execute(() -> {
try {
for (ServerWorld world : serverInstance.getWorlds()) {
if (getUUIDForWorld(world).equals(worldUUID)) {
world.save(null, true, false);
}
}
taskResult.complete(true);
} catch (Exception e) {
taskResult.completeExceptionally(e);
}
});
try {
return taskResult.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
@Override
public File getConfigFolder() {

View File

@ -33,7 +33,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.LogManager;
@ -169,6 +171,37 @@ private UUID loadUUIDForWorld(ServerWorld world) throws IOException {
File dimensionDir = dimensionFolder.getCanonicalFile();
return getUUIDForWorld(dimensionDir);
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
final CompletableFuture<Boolean> taskResult = new CompletableFuture<>();
serverInstance.execute(() -> {
try {
for (ServerWorld world : serverInstance.getWorlds()) {
if (getUUIDForWorld(world).equals(worldUUID)) {
world.save(null, true, false);
}
}
taskResult.complete(true);
} catch (Exception e) {
taskResult.completeExceptionally(e);
}
});
try {
return taskResult.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
@Override
public File getConfigFolder() {

View File

@ -33,7 +33,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.LogManager;
@ -169,6 +171,37 @@ private UUID loadUUIDForWorld(ServerWorld world) throws IOException {
File dimensionDir = dimensionFolder.getCanonicalFile();
return getUUIDForWorld(dimensionDir);
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
final CompletableFuture<Boolean> taskResult = new CompletableFuture<>();
serverInstance.execute(() -> {
try {
for (ServerWorld world : serverInstance.getWorlds()) {
if (getUUIDForWorld(world).equals(worldUUID)) {
world.save(null, true, false);
}
}
taskResult.complete(true);
} catch (Exception e) {
taskResult.completeExceptionally(e);
}
});
try {
return taskResult.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
@Override
public File getConfigFolder() {

View File

@ -33,7 +33,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.LogManager;
@ -185,6 +187,37 @@ private File getFolderForWorld(ServerWorld world) throws IOException {
return worldFolder.getCanonicalFile();
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
final CompletableFuture<Boolean> taskResult = new CompletableFuture<>();
serverInstance.execute(() -> {
try {
for (ServerWorld world : serverInstance.getWorlds()) {
if (getUUIDForWorld(world).equals(worldUUID)) {
world.save(null, true, false);
}
}
taskResult.complete(true);
} catch (Exception e) {
taskResult.completeExceptionally(e);
}
});
try {
return taskResult.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
@Override
public File getConfigFolder() {

View File

@ -33,7 +33,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.LogManager;
@ -185,6 +187,37 @@ private File getFolderForWorld(ServerWorld world) throws IOException {
return worldFolder.getCanonicalFile();
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
final CompletableFuture<Boolean> taskResult = new CompletableFuture<>();
serverInstance.execute(() -> {
try {
for (ServerWorld world : serverInstance.getWorlds()) {
if (getUUIDForWorld(world).equals(worldUUID)) {
world.save(null, true, false);
}
}
taskResult.complete(true);
} catch (Exception e) {
taskResult.completeExceptionally(e);
}
});
try {
return taskResult.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
@Override
public File getConfigFolder() {

View File

@ -33,7 +33,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import org.apache.logging.log4j.LogManager;
@ -183,6 +185,37 @@ private File getFolderForWorld(ServerWorld world) throws IOException {
File dimensionFolder = DimensionType.func_236031_a_(world.func_234923_W_(), worldFolder);
return dimensionFolder.getCanonicalFile();
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
final CompletableFuture<Boolean> taskResult = new CompletableFuture<>();
serverInstance.execute(() -> {
try {
for (ServerWorld world : serverInstance.getWorlds()) {
if (getUUIDForWorld(world).equals(worldUUID)) {
world.save(null, true, false);
}
}
taskResult.complete(true);
} catch (Exception e) {
taskResult.completeExceptionally(e);
}
});
try {
return taskResult.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
@Override
public File getConfigFolder() {

View File

@ -259,6 +259,27 @@ public Optional<Player> getPlayer(UUID uuid) {
return Optional.ofNullable(onlinePlayerMap.get(uuid));
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
try {
return Bukkit.getScheduler().callSyncMethod(this, () -> {
World world = Bukkit.getWorld(worldUUID);
if (world == null) throw new IllegalArgumentException("There is no world with this uuid: " + worldUUID);
world.save();
return true;
}).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
/**
* Only update some of the online players each tick to minimize performance impact on the server-thread.
* Only call this method on the server-thread.

View File

@ -72,10 +72,8 @@ public synchronized void removeAllListeners() {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public synchronized void onChunkSaveToDisk(ChunkUnloadEvent evt) {
if (evt.isSaveChunk()) {
Vector2i chunkPos = new Vector2i(evt.getChunk().getX(), evt.getChunk().getZ());
for (ServerEventListener listener : listeners) listener.onChunkSaveToDisk(evt.getWorld().getUID(), chunkPos);
}
Vector2i chunkPos = new Vector2i(evt.getChunk().getX(), evt.getChunk().getZ());
for (ServerEventListener listener : listeners) listener.onChunkSaveToDisk(evt.getWorld().getUID(), chunkPos);
}
/* Use ChunkSaveToDisk as it is the preferred event to use and more reliable on the chunk actually saved to disk

View File

@ -35,6 +35,7 @@
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
@ -84,6 +85,7 @@ public class SpongePlugin implements ServerInterface {
private SpongeCommands commands;
private SpongeExecutorService asyncExecutor;
private SpongeExecutorService syncExecutor;
private int playerUpdateIndex = 0;
private Map<UUID, Player> onlinePlayerMap;
@ -110,6 +112,7 @@ public SpongePlugin(org.slf4j.Logger logger) {
@Listener
public void onServerStart(GameStartingServerEvent evt) {
asyncExecutor = Sponge.getScheduler().createAsyncExecutor(this);
syncExecutor = Sponge.getScheduler().createSyncExecutor(this);
//save all world properties to generate level_sponge.dat files
for (WorldProperties properties : Sponge.getServer().getAllWorldProperties()) {
@ -237,6 +240,27 @@ public boolean isMetricsEnabled(boolean configValue) {
return Sponge.getMetricsConfigManager().getGlobalCollectionState().asBoolean();
}
@Override
public boolean persistWorldChanges(UUID worldUUID) throws IOException, IllegalArgumentException {
try {
return syncExecutor.submit(() -> {
World world = Sponge.getServer().getWorld(worldUUID).orElse(null);
if (world == null) throw new IllegalArgumentException("There is no world with this uuid: " + worldUUID);
world.save();
return true;
}).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) throw (IOException) t;
if (t instanceof IllegalArgumentException) throw (IllegalArgumentException) t;
throw new IOException(t);
}
}
/**
* Only update some of the online players each tick to minimize performance impact on the server-thread.
* Only call this method on the server-thread.