diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java index 2d79b1b..627a3bd 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandMigrate.java @@ -1,6 +1,7 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; +import net.knarcraft.bookswithoutborders.config.StaticMessage; import net.knarcraft.bookswithoutborders.config.Translatable; import net.knarcraft.bookswithoutborders.container.MigrationRequest; import net.knarcraft.bookswithoutborders.thread.MigrationQueueThread; @@ -61,7 +62,8 @@ public class CommandMigrate implements TabExecutor { private void findFilesToMigrate(@NotNull File folder, @NotNull Queue queue, @NotNull Player player) { File[] files = folder.listFiles(); if (files == null) { - BooksWithoutBorders.log(Level.WARNING, "Unable to access directory " + folder.getName() + " !"); + BooksWithoutBorders.log(Level.WARNING, StringFormatter.replacePlaceholder( + StaticMessage.EXCEPTION_DIRECTORY_UNAVAILABLE.toString(), "{folder}", folder.getName())); return; } for (File file : files) { diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandReload.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandReload.java index 66ce2f1..74974e0 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandReload.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandReload.java @@ -1,6 +1,8 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; +import net.knarcraft.bookswithoutborders.config.Translatable; +import net.knarcraft.knarlib.formatting.StringFormatter; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; @@ -17,16 +19,16 @@ public class CommandReload implements TabExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] arguments) { - if (BooksWithoutBorders.getConfiguration().loadConfig()) { - BooksWithoutBorders.sendSuccessMessage(sender, "BooksWithoutBorders configuration reloaded!"); - } else { - BooksWithoutBorders.sendErrorMessage(sender, "Reload Failed!"); - BooksWithoutBorders.sendErrorMessage(sender, "See console for details"); - } - + StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter(); // Reload books - BooksWithoutBorders.updateBooks(sender, true); BooksWithoutBorders.clearBookData(); + BooksWithoutBorders.updateBooks(sender, true); + + if (BooksWithoutBorders.getConfiguration().loadConfig()) { + stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_RELOADED); + } else { + stringFormatter.displayErrorMessage(sender, Translatable.ERROR_RELOAD_FAILED); + } return true; } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java index 91893df..2361cef 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java @@ -2,13 +2,15 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; +import net.knarcraft.bookswithoutborders.config.Permission; +import net.knarcraft.bookswithoutborders.config.StaticMessage; +import net.knarcraft.bookswithoutborders.config.Translatable; import net.knarcraft.bookswithoutborders.state.BookDirectory; -import net.knarcraft.bookswithoutborders.state.ItemSlot; import net.knarcraft.bookswithoutborders.utility.BookFileHelper; import net.knarcraft.bookswithoutborders.utility.BookHelper; import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper; -import net.md_5.bungee.api.ChatColor; +import net.knarcraft.knarlib.formatting.StringFormatter; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; @@ -16,6 +18,7 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.BookMeta; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; @@ -30,7 +33,7 @@ public class CommandSave implements TabExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] arguments) { - return saveHeldBook(sender, arguments, false); + return saveHeldBook(sender, arguments, false, label); } /** @@ -39,22 +42,24 @@ public class CommandSave implements TabExecutor { * @param sender

The sender of the command

* @param arguments

The arguments given

* @param savePublic

Whether to save the book in the public directory or the player directory

+ * @param command

The command executed to trigger this method

* @return

True if a book was saved successfully

*/ - protected boolean saveHeldBook(@NotNull CommandSender sender, @NotNull String[] arguments, boolean savePublic) { + protected boolean saveHeldBook(@NotNull CommandSender sender, @NotNull String[] arguments, boolean savePublic, + @NotNull String command) { + StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter(); if (!(sender instanceof Player player)) { - BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!"); + stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY); return false; } - ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, false); - if (holdingSlot != ItemSlot.NONE) { - ItemStack holdingItem = InventoryHelper.getHeldItem(player, holdingSlot == ItemSlot.MAIN_HAND); + ItemStack heldBook = InventoryHelper.getHeldBook(player); + if (heldBook != null) { boolean duplicate = arguments.length == 1 && Boolean.parseBoolean(arguments[0]); - saveBook(player, holdingItem, duplicate, savePublic); + saveBook(player, heldBook, duplicate, savePublic, command); return true; } else { - BooksWithoutBorders.sendErrorMessage(sender, "You must be holding a written book or book and quill to save it!"); + stringFormatter.displayErrorMessage(sender, Translatable.ERROR_NOT_HOLDING_ANY_BOOK); return false; } } @@ -66,11 +71,14 @@ public class CommandSave implements TabExecutor { * @param heldBook

The book held

* @param overwrite

Whether to overwrite any existing books

* @param saveToPublicFolder

Whether to save the book to the public folder instead of the player folder

+ * @param command

The command executed to trigger this method

*/ - public void saveBook(@NotNull Player player, @NotNull ItemStack heldBook, boolean overwrite, boolean saveToPublicFolder) { + public void saveBook(@NotNull Player player, @NotNull ItemStack heldBook, boolean overwrite, + boolean saveToPublicFolder, @NotNull String command) { + StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter(); BookMeta book = (BookMeta) heldBook.getItemMeta(); if (book == null) { - BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata for your held book!"); + stringFormatter.displayErrorMessage(player, Translatable.ERROR_METADATA_MISSING); return; } @@ -78,16 +86,15 @@ public class CommandSave implements TabExecutor { //Only allow saving of own books if enabled if (config.getAuthorOnlySave() && !saveToPublicFolder && - (!player.hasPermission("bookswithoutborders.bypassAuthorOnlySave") && + (!player.hasPermission(Permission.BYPASS_AUTHOR_ONLY_SAVE.toString()) && BookHelper.isNotAuthor(player, book))) { return; } String savePath = BookHelper.getBookDirectoryPathString(saveToPublicFolder ? BookDirectory.PUBLIC : BookDirectory.PLAYER, player); - if (savePath == null) { - BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! Unable to find the save path!"); + stringFormatter.displayErrorMessage(player, Translatable.ERROR_SAVE_INVALID_PATH); return; } @@ -103,55 +110,26 @@ public class CommandSave implements TabExecutor { //Make sure the used folders exist File file = new File(savePath); if (!file.exists() && !file.mkdir()) { - BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! If this continues to happen, consult" + - " a server admin!"); - return; - } - File[] foundFiles = file.listFiles(); - if (foundFiles == null) { - BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! If this continues to happen, consult" + - " a server admin!"); + stringFormatter.displayErrorMessage(player, Translatable.ERROR_SAVE_FILE_SYSTEM_ERROR); return; } - //Find any duplicates of the book - int foundDuplicates = BookFileHelper.findDuplicates(foundFiles, fileName); - - //Deal with duplicates - if (foundDuplicates > 0) { - //TODO: Decide if this makes sense or needs to be changed - //Skip duplicate book - if (!fileName.contains("Untitled" + config.getTitleAuthorSeparator()) && !overwrite) { - BooksWithoutBorders.sendErrorMessage(player, "Book is already saved!"); - BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ? - "/savepublicbook" : "/savebook") + " true " + config.getErrorColor() + "to overwrite!"); - return; - } - - //Skip if duplicate limit is reached - if (foundDuplicates > config.getBookDuplicateLimit()) { - BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName + - " duplicates reached!"); - BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ? - "/savepublicbook" : "/savebook") + " true " + - config.getErrorColor() + "to overwrite!"); - return; - } - - //Alter duplicated filename - if (fileName.contains("Untitled" + config.getTitleAuthorSeparator()) && !overwrite) { - fileName = "(" + foundDuplicates + ")" + fileName; - } + // Deal with possible duplicates of the file + String newName = checkDuplicates(player, file, fileName, command, config.getTitleAuthorSeparator(), + config.getBookDuplicateLimit(), overwrite); + if (newName == null) { + return; } try { - BookToFromTextHelper.bookToYml(savePath, fileName, book); + BookToFromTextHelper.bookToYml(savePath, newName, book); //Update the relevant book list BooksWithoutBorders.updateBooks(player, saveToPublicFolder); - BooksWithoutBorders.sendSuccessMessage(player, "Book Saved as \"" + fileName + ChatColor.RESET + "\""); + stringFormatter.displaySuccessMessage(player, + stringFormatter.replacePlaceholder(Translatable.SUCCESS_SAVED, "{fileName}", newName)); } catch (IOException exception) { - BooksWithoutBorders.log(Level.SEVERE, "Unable to save book"); + BooksWithoutBorders.log(Level.SEVERE, StaticMessage.EXCEPTION_SAVE_BOOK_FAILED.toString()); } } @@ -163,4 +141,65 @@ public class CommandSave implements TabExecutor { return new ArrayList<>(); } + /** + * Checks if the book to save is a duplicate of one or more saved books + * + *

Note that only unnamed books can have duplicates

+ * + * @param player

The player attempting to save the book

+ * @param directory

The directory to search for duplicates

+ * @param fileName

The name the file is to be saved as

+ * @param command

The command executed to trigger this method

+ * @param separator

The title author separator

+ * @param duplicateLimit

The duplicate limit

+ * @param overwrite

Whether the player enabled overwriting

+ * @return

The possible altered filename, or null if saving the book isn't allowed

+ */ + @Nullable + private String checkDuplicates(@NotNull Player player, @NotNull File directory, @NotNull String fileName, + @NotNull String command, @NotNull String separator, int duplicateLimit, + boolean overwrite) { + StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter(); + File[] foundFiles = directory.listFiles(); + if (foundFiles == null) { + stringFormatter.displayErrorMessage(player, Translatable.ERROR_SAVE_FILE_SYSTEM_ERROR); + return null; + } + + // Find any duplicates of the book + int foundDuplicates = BookFileHelper.findDuplicates(foundFiles, fileName); + + // No duplicates to process + if (foundDuplicates <= 0) { + return fileName; + } + + String fullCommand = "/" + command + " true"; + + boolean isUnnamed = fileName.contains("Untitled" + separator); + + // Skip duplicate unnamed book saving + if (!isUnnamed && !overwrite) { + stringFormatter.displayErrorMessage(player, Translatable.ERROR_SAVE_DUPLICATE_NAMED); + stringFormatter.displayErrorMessage(player, stringFormatter.replacePlaceholder( + Translatable.ERROR_SAVE_OVERWRITE_REQUIRED, "{command}", fullCommand)); + return null; + } + + // Skip if duplicate limit is reached + if (foundDuplicates > duplicateLimit) { + stringFormatter.displayErrorMessage(player, stringFormatter.replacePlaceholder( + Translatable.ERROR_SAVE_DUPLICATE_UNNAMED, "{fileName}", fileName)); + stringFormatter.displayErrorMessage(player, stringFormatter.replacePlaceholder( + Translatable.ERROR_SAVE_OVERWRITE_REQUIRED, "{command}", fullCommand)); + return null; + } + + // Alter duplicated filename + if (isUnnamed && !overwrite) { + fileName = "(" + foundDuplicates + ")" + fileName; + } + return fileName; + } + } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSavePublic.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSavePublic.java index 2f18b78..262de2d 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSavePublic.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSavePublic.java @@ -13,7 +13,7 @@ public class CommandSavePublic extends CommandSave implements TabExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] arguments) { - return saveHeldBook(sender, arguments, true); + return saveHeldBook(sender, arguments, true, label); } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java b/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java index d5c664d..9e22961 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java @@ -23,7 +23,6 @@ public class BooksWithoutBordersConfig { private final ChatColor errorColor = ChatColor.RED; private final ChatColor successColor = ChatColor.GREEN; - private final ChatColor commandColor = ChatColor.YELLOW; private final String SLASH = FileSystems.getDefault().getSeparator(); private boolean isInitialized; private final String bookFolder; @@ -99,15 +98,6 @@ public class BooksWithoutBordersConfig { return this.successColor; } - /** - * Gets the color used to color commands - * - * @return

The color used to color commands

- */ - public ChatColor getCommandColor() { - return this.commandColor; - } - /** * Gets the correct slash to use for the used OS * diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java b/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java index f265619..26daacd 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java @@ -32,6 +32,11 @@ public enum Permission { */ FORMAT("format"), + /** + * The permission for bypassing author only saving + */ + BYPASS_AUTHOR_ONLY_SAVE("bypassAuthorOnlySave"), + ; private final @NotNull String node; @@ -45,16 +50,6 @@ public enum Permission { this.node = node; } - /** - * Gets the node of this permission - * - * @return

The permission node

- */ - @NotNull - public String getNode() { - return "bookswithoutborders." + this.node; - } - /** * Return node instead of enum when displayed as a string * @@ -63,7 +58,7 @@ public enum Permission { @Override @NotNull public String toString() { - return getNode(); + return "bookswithoutborders." + this.node; } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/StaticMessage.java b/src/main/java/net/knarcraft/bookswithoutborders/config/StaticMessage.java index 3ac8080..ceb0645 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/StaticMessage.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/StaticMessage.java @@ -13,6 +13,10 @@ public enum StaticMessage { EXCEPTION_VAULT_NOT_AVAILABLE("Vault is unavailable, but book price is set to economy. Unsetting book cost!"), EXCEPTION_VAULT_PRICE_NOT_CHANGED("BooksWithoutBorders failed to hook into Vault! Book price not set!"), EXCEPTION_ENCRYPTED_FILE_DELETE_FAILED("Book encryption data failed to delete upon decryption!\nFile location: {path}"), + EXCEPTION_DIRECTORY_UNAVAILABLE("Unable to access directory {folder} !"), + EXCEPTION_SAVE_BOOK_FAILED("Unable to save book"), + EXCEPTION_BOOKSHELF_SAVING_FAILED("Unable to save bookshelves!"), + NOTICE_NO_BOOKSHELVES("BooksWithoutBorders found no bookshelves to load"), ; private final @NotNull String messageString; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java b/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java index 00979f6..30eacfe 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java @@ -68,6 +68,16 @@ public enum Translatable implements TranslatableMessage { */ SUCCESS_MIGRATION_STARTED, + /** + * The success message displayed when the configuration is successfully reloaded + */ + SUCCESS_RELOADED, + + /** + * The success message displayed when a book is successfully saved + */ + SUCCESS_SAVED, + /** * The error to display when the console attempts to run a player-only command */ @@ -238,6 +248,36 @@ public enum Translatable implements TranslatableMessage { */ ERROR_LOAD_FAILED, + /** + * The error displayed when the configuration cannot be reloaded + */ + ERROR_RELOAD_FAILED, + + /** + * The error displayed when unable to generate a valid save path for a book + */ + ERROR_SAVE_INVALID_PATH, + + /** + * The error displayed when unable to create necessary folders for saving a book + */ + ERROR_SAVE_FILE_SYSTEM_ERROR, + + /** + * The error displayed when enabling overwriting is necessary to save a book + */ + ERROR_SAVE_OVERWRITE_REQUIRED, + + /** + * The error displayed when attempting to save a duplicate unnamed book + */ + ERROR_SAVE_DUPLICATE_NAMED, + + /** + * The error displayed when attempting to save a duplicate named book exceeding the duplicate limit + */ + ERROR_SAVE_DUPLICATE_UNNAMED, + /** * The header displayed before printing all commands */ diff --git a/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java b/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java index a8b8ee2..c1c525a 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java @@ -1,6 +1,7 @@ package net.knarcraft.bookswithoutborders.handler; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; +import net.knarcraft.bookswithoutborders.config.StaticMessage; import net.knarcraft.bookswithoutborders.container.Bookshelf; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -71,8 +72,7 @@ public class BookshelfHandler { YamlConfiguration configuration = YamlConfiguration.loadConfiguration(bookshelfFile); ConfigurationSection bookshelfSection = configuration.getConfigurationSection("bookshelves"); if (bookshelfSection == null) { - BooksWithoutBorders.log(Level.INFO, - "BooksWithoutBorders found no bookshelves to load"); + BooksWithoutBorders.log(Level.INFO, StaticMessage.NOTICE_NO_BOOKSHELVES.toString()); return; } @@ -108,7 +108,7 @@ public class BookshelfHandler { } configuration.save(bookshelfFile); } catch (IOException exception) { - BooksWithoutBorders.log(Level.SEVERE, "Unable to save bookshelves!"); + BooksWithoutBorders.log(Level.SEVERE, StaticMessage.EXCEPTION_BOOKSHELF_SAVING_FAILED.toString()); } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java index 7fdf10c..747005e 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java @@ -372,6 +372,7 @@ public final class EncryptionHelper { * @param bytes

The bytes to convert

* @return

The resulting hexadecimal string

*/ + @NotNull public static String bytesToHex(byte[] bytes) { byte[] hexChars = new byte[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/InventoryHelper.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/InventoryHelper.java index e6d8ea4..461638e 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/InventoryHelper.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/InventoryHelper.java @@ -25,13 +25,9 @@ public final class InventoryHelper { * @return

The held book, or null if not holding one book in the main hand

*/ public static ItemStack getHeldBook(@NotNull Player player) { - @NotNull ItemSlot heldSigned = InventoryHelper.getHeldSlotBook(player, true, true, - true, true); - @NotNull ItemSlot heldUnSigned = InventoryHelper.getHeldSlotBook(player, true, true, - true, false); - if (heldSigned == ItemSlot.MAIN_HAND || heldUnSigned == ItemSlot.MAIN_HAND) { - boolean holdingSigned = heldSigned == ItemSlot.MAIN_HAND; - return InventoryHelper.getHeldBook(player, holdingSigned); + ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, false); + if (holdingSlot != ItemSlot.NONE) { + return InventoryHelper.getHeldItem(player, holdingSlot == ItemSlot.MAIN_HAND); } else { return null; } diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml index 84797f6..6798332 100644 --- a/src/main/resources/strings.yml +++ b/src/main/resources/strings.yml @@ -12,6 +12,8 @@ en: SUCCESS_PAGE_DELETED: "Page deleted!" SUCCESS_BOOK_LOADED: "Book created!" SUCCESS_MIGRATION_STARTED: "Starting book migration..." + SUCCESS_RELOADED: "BooksWithoutBorders configuration reloaded!" + SUCCESS_SAVED: "Book Saved as &e\"{fileName}\"&r" ACTION_COPY: "copy" ACTION_CLEAR: "clear" ACTION_DECRYPT: "decrypt" @@ -54,6 +56,14 @@ en: ERROR_GROUP_ENCRYPT_ARGUMENTS_MISSING: "You must specify a group name and key to encrypt a book!" ERROR_GROUP_ENCRYPTED_ALREADY: "Book is already group encrypted!" ERROR_LOAD_FAILED: "Book failed to load!" + ERROR_RELOAD_FAILED: | + Reload Failed! + See console for details + ERROR_SAVE_INVALID_PATH: "Saving Failed! Unable to find the save path!" + ERROR_SAVE_FILE_SYSTEM_ERROR: "Saving Failed! If this continues to happen, consult a server admin!" + ERROR_SAVE_OVERWRITE_REQUIRED: "Use &e{command}&r to overwrite!" + ERROR_SAVE_DUPLICATE_NAMED: "Book is already saved!" + ERROR_SAVE_DUPLICATE_UNNAMED: "Maximum amount of {fileName} duplicates reached!" NEUTRAL_COMMANDS_HEADER: | &e[] denote optional parameters <> denote required parameters