diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SaveCommand.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SaveCommand.java new file mode 100644 index 0000000000..a258ce0e8d --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SaveCommand.java @@ -0,0 +1,30 @@ +package io.github.thebusybiscuit.slimefun4.core.commands.subcommands; + +import io.github.thebusybiscuit.slimefun4.core.commands.SlimefunCommand; +import io.github.thebusybiscuit.slimefun4.core.commands.SubCommand; +import io.github.thebusybiscuit.slimefun4.core.services.SavingService; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import org.bukkit.command.CommandSender; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +public class SaveCommand extends SubCommand { + @ParametersAreNonnullByDefault + SaveCommand(Slimefun plugin, SlimefunCommand cmd) { + super(plugin, cmd, "save", false); + } + + @Override + public void onExecute(@Nonnull CommandSender sender, @Nonnull String[] args) { + if (!sender.hasPermission("slimefun.command.save")) { + Slimefun.getLocalization().sendMessage(sender, "messages.no-permission", true); + return; + } + + boolean savedPlayers = Slimefun.getSavingService().saveAllPlayers(false); + Slimefun.getLocalization().sendMessage(sender, "commands.save.players." + savedPlayers, true); + boolean savedBlocks = Slimefun.getSavingService().saveAllBlocks(false); + Slimefun.getLocalization().sendMessage(sender, "commands.save.blocks." + savedBlocks, true); + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java index 17d70bce3e..8983a7ebad 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java @@ -42,6 +42,7 @@ public static Collection getAllCommands(@Nonnull SlimefunCommand cmd commands.add(new BackpackCommand(plugin, cmd)); commands.add(new ChargeCommand(plugin, cmd)); commands.add(new DebugCommand(plugin, cmd)); + commands.add(new SaveCommand(plugin, cmd)); return commands; } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java deleted file mode 100644 index 060ce0d772..0000000000 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/AutoSavingService.java +++ /dev/null @@ -1,117 +0,0 @@ -package io.github.thebusybiscuit.slimefun4.core.services; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.logging.Level; - -import javax.annotation.Nonnull; - -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.entity.Player; - -import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; -import io.github.thebusybiscuit.slimefun4.core.debug.Debug; -import io.github.thebusybiscuit.slimefun4.core.debug.TestCase; -import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; - -import me.mrCookieSlime.Slimefun.api.BlockStorage; - -/** - * This Service is responsible for automatically saving {@link Player} and {@link Block} - * data. - * - * @author TheBusyBiscuit - * - */ -public class AutoSavingService { - - private int interval; - - /** - * This method starts the {@link AutoSavingService} with the given interval. - * - * @param plugin - * The current instance of Slimefun - * @param interval - * The interval in which to run this task - */ - public void start(@Nonnull Slimefun plugin, int interval) { - this.interval = interval; - - plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, this::saveAllPlayers, 2000L, interval * 60L * 20L); - plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, this::saveAllBlocks, 2000L, interval * 60L * 20L); - } - - /** - * This method saves every {@link PlayerProfile} in memory and removes profiles - * that were marked for deletion. - */ - private void saveAllPlayers() { - Iterator iterator = PlayerProfile.iterator(); - int players = 0; - - Debug.log(TestCase.PLAYER_PROFILE_DATA, "Saving all players data"); - - while (iterator.hasNext()) { - PlayerProfile profile = iterator.next(); - - if (profile.isDirty()) { - players++; - profile.save(); - - Debug.log(TestCase.PLAYER_PROFILE_DATA, "Saved data for {} ({})", - profile.getPlayer() != null ? profile.getPlayer().getName() : "Unknown", profile.getUUID() - ); - } - - // Remove the PlayerProfile from memory if the player has left the server (marked from removal) - // and they're still not on the server - // At this point, we've already saved their profile so we can safely remove it - // without worry for having a data sync issue (e.g. data is changed but then we try to re-load older data) - if (profile.isMarkedForDeletion() && profile.getPlayer() == null) { - iterator.remove(); - - Debug.log(TestCase.PLAYER_PROFILE_DATA, "Removed data from memory for {}", - profile.getUUID() - ); - } - } - - if (players > 0) { - Slimefun.logger().log(Level.INFO, "Auto-saved all player data for {0} player(s)!", players); - } - } - - /** - * This method saves the data of every {@link Block} marked dirty by {@link BlockStorage}. - */ - private void saveAllBlocks() { - Set worlds = new HashSet<>(); - - for (World world : Bukkit.getWorlds()) { - BlockStorage storage = BlockStorage.getStorage(world); - - if (storage != null) { - storage.computeChanges(); - - if (storage.getChanges() > 0) { - worlds.add(storage); - } - } - } - - if (!worlds.isEmpty()) { - Slimefun.logger().log(Level.INFO, "Auto-saving block data... (Next auto-save: {0}m)", interval); - - for (BlockStorage storage : worlds) { - storage.save(); - } - } - - BlockStorage.saveChunks(); - } - -} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/SavingService.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/SavingService.java new file mode 100644 index 0000000000..aa1ccb7a89 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/services/SavingService.java @@ -0,0 +1,150 @@ +package io.github.thebusybiscuit.slimefun4.core.services; + +import java.util.Iterator; +import java.util.logging.Level; + +import javax.annotation.Nonnull; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import io.github.thebusybiscuit.slimefun4.api.player.PlayerProfile; +import io.github.thebusybiscuit.slimefun4.core.debug.Debug; +import io.github.thebusybiscuit.slimefun4.core.debug.TestCase; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; + +import me.mrCookieSlime.Slimefun.api.BlockStorage; + +/** + * This Service is responsible for saving {@link Player} and {@link Block} + * data. + * + * @author TheBusyBiscuit + * @author JustAHuman + */ +public class SavingService { + + private int interval; + private long lastPlayerSave; + private long lastBlockSave; + private boolean startedAutoSave; + private boolean savingPlayers; + private boolean savingBlocks; + + /** + * This method starts a {@link SavingService} task with the given interval. + * + * @param plugin + * The current instance of Slimefun + * @param interval + * The interval in which to run this task + */ + public void startAutoSave(@Nonnull Slimefun plugin, int interval) { + if (this.startedAutoSave) { + // TODO: handle this + return; + } + + this.interval = interval; + this.startedAutoSave = true; + + plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, () -> saveAllPlayers(true), 2000L, interval * 60L * 20L); + plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, () -> saveAllBlocks(true), 2000L, interval * 60L * 20L); + } + + /** + * This method saves every {@link PlayerProfile} in memory and removes profiles + * that were marked for deletion. + */ + public boolean saveAllPlayers(boolean auto) { + if (this.savingPlayers) { + return false; + } + + this.savingPlayers = true; + long startTime = System.currentTimeMillis(); + Iterator iterator = PlayerProfile.iterator(); + int players = 0; + + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Saving all player data"); + + while (iterator.hasNext()) { + PlayerProfile profile = iterator.next(); + + if (profile.isDirty()) { + players++; + profile.save(); + + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Saved data for {} ({})", + profile.getPlayer() != null ? profile.getPlayer().getName() : "Unknown", profile.getUUID() + ); + } + + // Remove the PlayerProfile from memory if the player has left the server (marked from removal) + // and they're still not on the server + // At this point, we've already saved their profile so we can safely remove it + // without worry for having a data sync issue (e.g. data is changed but then we try to re-load older data) + if (profile.isMarkedForDeletion() && profile.getPlayer() == null) { + iterator.remove(); + + Debug.log(TestCase.PLAYER_PROFILE_DATA, "Removed data from memory for {}", + profile.getUUID() + ); + } + } + + if (players > 0) { + long endTime = System.currentTimeMillis(); + if (auto) { + this.lastPlayerSave = endTime; + Slimefun.logger().log(Level.INFO, "Auto-saved all player data for {0} player(s)! (Next auto-save: {1}m)", new Object[] { players, this.interval }); + } else { + long nextAutoSave = (this.interval * 60L) - ((endTime - this.lastPlayerSave) / 1000L); + Slimefun.logger().log(Level.INFO, "Saved all player data for {0} player(s)! (Next auto-save: {1}m {2}s)", new Object[] { players, nextAutoSave / 60, nextAutoSave % 60 }); + } + Slimefun.logger().log(Level.INFO, "Took {0}ms!", endTime - startTime); + } + + this.savingPlayers = false; + return true; + } + + /** + * This method saves the data of every {@link Block} marked dirty by {@link BlockStorage}. + */ + public boolean saveAllBlocks(boolean auto) { + if (this.savingBlocks) { + return false; + } + + this.savingBlocks = true; + long startTime = System.currentTimeMillis(); + int savedChanges = 0; + + for (World world : Bukkit.getWorlds()) { + BlockStorage storage = BlockStorage.getStorage(world); + if (storage == null) { + continue; + } + + savedChanges += storage.saveChanges(); + } + BlockStorage.saveChunks(); + + long endTime = System.currentTimeMillis(); + if (auto) { + Slimefun.logger().log(Level.INFO, "Auto-saved all block data from {0} changes! (Next auto-save: {1}m)", new Object[] { savedChanges, this.interval }); + } else { + long nextAutoSave = (this.interval * 60L) - ((endTime - this.lastBlockSave) / 1000L); + Slimefun.logger().log(Level.INFO, "Saved all block data from {0} changes! (Next auto-save: {1}m {2}s)", new Object[] { savedChanges, nextAutoSave / 60, nextAutoSave % 60 }); + } + Slimefun.logger().log(Level.INFO, "Took {0}ms!", new Object[] { endTime - startTime }); + + this.lastBlockSave = endTime; + this.savingBlocks = false; + return true; + } + +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java index ae065bc062..d46b40b68f 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/Slimefun.java @@ -43,7 +43,7 @@ import io.github.thebusybiscuit.slimefun4.core.commands.SlimefunCommand; import io.github.thebusybiscuit.slimefun4.core.networks.NetworkManager; import io.github.thebusybiscuit.slimefun4.core.services.AnalyticsService; -import io.github.thebusybiscuit.slimefun4.core.services.AutoSavingService; +import io.github.thebusybiscuit.slimefun4.core.services.SavingService; import io.github.thebusybiscuit.slimefun4.core.services.BackupService; import io.github.thebusybiscuit.slimefun4.core.services.BlockDataService; import io.github.thebusybiscuit.slimefun4.core.services.CustomItemDataService; @@ -177,7 +177,7 @@ public class Slimefun extends JavaPlugin implements SlimefunAddon { private final GitHubService gitHubService = new GitHubService("Slimefun/Slimefun4"); private final UpdaterService updaterService = new UpdaterService(this, getDescription().getVersion(), getFile()); private final MetricsService metricsService = new MetricsService(this); - private final AutoSavingService autoSavingService = new AutoSavingService(); + private final SavingService savingService = new SavingService(); private final BackupService backupService = new BackupService(); private final PermissionsService permissionsService = new PermissionsService(this); private final PerWorldSettingsService worldSettingsService = new PerWorldSettingsService(this); @@ -379,7 +379,7 @@ private void onPluginStart() { } // Starting our tasks - autoSavingService.start(this, config.getInt("options.auto-save-delay-in-minutes")); + savingService.startAutoSave(this, config.getInt("options.auto-save-delay-in-minutes")); hologramsService.start(); ticker.start(this); @@ -828,6 +828,11 @@ private static void validateInstance() { return instance.blockDataService; } + public static @Nonnull SavingService getSavingService() { + validateInstance(); + return instance.savingService; + } + /** * This method returns out world settings service. * That service is responsible for managing item settings per diff --git a/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java b/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java index c2df4fd2c9..deb93d1f8d 100644 --- a/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java +++ b/src/main/java/me/mrCookieSlime/Slimefun/api/BlockStorage.java @@ -304,15 +304,16 @@ public int getChanges() { return changes; } - public void save() { + public int saveChanges() { computeChanges(); - + if (changes == 0) { - return; + return 0; } Slimefun.logger().log(Level.INFO, "Saving block data for world \"{0}\" ({1} change(s) queued)", new Object[] { world.getName(), changes }); Map cache = new HashMap<>(blocksCache); + int savedChanges = 0; for (Map.Entry entry : cache.entrySet()) { blocksCache.remove(entry.getKey()); @@ -324,6 +325,7 @@ public void save() { if (file.exists()) { try { Files.delete(file.toPath()); + savedChanges++; } catch (IOException e) { Slimefun.logger().log(Level.WARNING, e, () -> "Could not delete file \"" + file.getName() + '"'); } @@ -334,6 +336,7 @@ public void save() { try { Files.move(tmpFile.toPath(), cfg.getFile().toPath(), StandardCopyOption.ATOMIC_MOVE); + savedChanges++; } catch (IOException x) { Slimefun.logger().log(Level.SEVERE, x, () -> "An Error occurred while copying a temporary File for Slimefun " + Slimefun.getVersion()); } @@ -342,15 +345,24 @@ public void save() { Map unsavedInventories = new HashMap<>(inventories); for (Map.Entry entry : unsavedInventories.entrySet()) { + savedChanges += entry.getValue().getUnsavedChanges(); entry.getValue().save(entry.getKey()); } Map unsavedUniversalInventories = new HashMap<>(Slimefun.getRegistry().getUniversalInventories()); for (Map.Entry entry : unsavedUniversalInventories.entrySet()) { + savedChanges += entry.getValue().getUnsavedChanges(); entry.getValue().save(); } - changes = 0; + Slimefun.logger().log(Level.INFO, "Saved block data for world \"{0}\" ({1} change(s) saved)", new Object[] { world.getName(), savedChanges }); + + this.changes = 0; + return savedChanges; + } + + public void save() { + saveChanges(); } public void saveAndRemove() { diff --git a/src/main/resources/languages/en/messages.yml b/src/main/resources/languages/en/messages.yml index 81d5115c30..879046f989 100644 --- a/src/main/resources/languages/en/messages.yml +++ b/src/main/resources/languages/en/messages.yml @@ -39,6 +39,14 @@ commands: running: '&7Running debug mode with test: &6%test%' disabled: '&7Disabled debug mode.' + save: + players: + true: 'Successfully saved player-data!' + false: 'Player-data is already saving!' + blocks: + true: 'Successfully saved block-data!' + false: 'Block-data is already saving!' + placeholderapi: profile-loading: 'Loading...'