Adds a new option to format books when signed

Also cleans up config stuff a bit and moves config-related tasks to its own class
Moves book loading code to its own class
Adds a default config file to make the config file have comments
Adds missing command info about the formatBook command
Adds information about required permissions to the command info
This commit is contained in:
Kristian Knarvik 2022-01-17 11:38:43 +01:00
parent 7acaa9fc81
commit d423b1e109
26 changed files with 729 additions and 488 deletions

View File

@ -20,22 +20,18 @@ import net.knarcraft.bookswithoutborders.command.CommandSetBookPrice;
import net.knarcraft.bookswithoutborders.command.CommandSetLore;
import net.knarcraft.bookswithoutborders.command.CommandSetTitle;
import net.knarcraft.bookswithoutborders.command.CommandUnSign;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings;
import net.knarcraft.bookswithoutborders.listener.BookEventListener;
import net.knarcraft.bookswithoutborders.listener.PlayerEventListener;
import net.knarcraft.bookswithoutborders.listener.SignEventListener;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import org.bukkit.Material;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.Configuration;
import org.bukkit.entity.Player;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.inventory.ItemFactory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -45,25 +41,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getErrorColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSuccessColor;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getErrorColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSuccessColor;
public class BooksWithoutBorders extends JavaPlugin {
private static int bookDuplicateLimit;
private static String titleAuthorSeparator;
private static String loreSeparator;
private static List<String> firstBooks;
private static String welcomeMessage;
private static Material bookPriceType = null;
private static double bookPriceQuantity;
private static boolean authorOnlyCopy;
private static boolean useYml;
private static boolean adminDecrypt;
private static ItemFactory itemFactory;
private static Map<String, List<String>> playerBooksList;
private static List<String> publicBooksList;
@ -116,11 +100,15 @@ public class BooksWithoutBorders extends JavaPlugin {
@Override
public void onEnable() {
FileConfiguration config = this.getConfig();
config.options().copyDefaults(true);
this.saveDefaultConfig();
booksWithoutBorders = this;
consoleSender = this.getServer().getConsoleSender();
playerBooksList = new HashMap<>();
firstBooks = new ArrayList<>();
BooksWithoutBordersSettings.initialize(this);
BooksWithoutBordersConfig.loadConfig();
publicBooksList = FileHelper.listFiles(consoleSender, true);
PluginManager pluginManager = this.getServer().getPluginManager();
@ -128,6 +116,7 @@ public class BooksWithoutBorders extends JavaPlugin {
if (getSlash() != null && initialize()) {
pluginManager.registerEvents(new PlayerEventListener(), this);
pluginManager.registerEvents(new SignEventListener(), this);
pluginManager.registerEvents(new BookEventListener(), this);
} else {
this.getPluginLoader().disablePlugin(this);
}
@ -135,120 +124,6 @@ public class BooksWithoutBorders extends JavaPlugin {
registerCommands();
}
/**
* Gets whether only the author of a book should be able to copy it
*
* @return <p>Whether only the book author can copy it</p>
*/
public static boolean getAuthorOnlyCopy() {
return authorOnlyCopy;
}
/**
* Gets whether to use YML, not TXT, for saving books
*
* @return <p>Whether to use YML for saving books</p>
*/
public static boolean getUseYml() {
return useYml;
}
/**
* Gets whether admins should be able to decrypt books without a password, and decrypt all group encrypted books
*
* @return <p>Whether admins can bypass the encryption password</p>
*/
public static boolean getAdminDecrypt() {
return adminDecrypt;
}
/**
* Sets the quantity of items/currency necessary for copying books
*
* @param newQuantity <p>The new quantity necessary for payment</p>
*/
public static void setBookPriceQuantity(double newQuantity) {
bookPriceQuantity = newQuantity;
}
/**
* Gets the quantity of items/currency necessary for copying books
*
* @return <p>The quantity necessary for payment</p>
*/
public static double getBookPriceQuantity() {
return bookPriceQuantity;
}
/**
* Sets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @param newType <p>The new item type to use for book pricing</p>
*/
public static void setBookPriceType(Material newType) {
bookPriceType = newType;
}
/**
* Gets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @return <p>The item type used for book pricing</p>
*/
public static Material getBookPriceType() {
return bookPriceType;
}
/**
* Gets the welcome message to show to new players
*
* @return <p>The welcome message to show new players</p>
*/
public static String getWelcomeMessage() {
return welcomeMessage;
}
/**
* Gets the limit of duplicates for each book
*
* @return <p>The book duplicate limit</p>
*/
public static int getBookDuplicateLimit() {
return bookDuplicateLimit;
}
/**
* Gets the separator used to split book title from book author
*
* @return <p>The separator between title and author</p>
*/
public static String getTitleAuthorSeparator() {
return titleAuthorSeparator;
}
/**
* Gets the separator used to denote a newline in a lore string
*
* @return <p>The separator used to denote lore newline</p>
*/
public static String getLoreSeparator() {
return loreSeparator;
}
/**
* Gets a copy of the list of books to give new players
*
* @return <p>The books to give new players</p>
*/
public static List<String> getFirstBooks() {
return new ArrayList<>(firstBooks);
}
/**
* Gets an instance of this plugin
*
@ -316,16 +191,25 @@ public class BooksWithoutBorders extends JavaPlugin {
}
//Load config
if (!loadConfig()) {
if (!BooksWithoutBordersConfig.loadConfig()) {
return false;
}
//Save config with loaded values to fix invalid config values
saveConfigValues();
BooksWithoutBordersConfig.saveConfigValues();
return testFileSaving();
}
/**
* Gets the server's item factory
*
* @return <p>The server's item factory</p>
*/
public static ItemFactory getItemFactory() {
return itemFactory;
}
/**
* Makes sure necessary folders exist
*
@ -359,213 +243,6 @@ public class BooksWithoutBorders extends JavaPlugin {
return true;
}
/**
* Saves the config
*/
public void saveConfigValues() {
Configuration config = this.getConfig();
config.set("Options.Save_Books_in_Yaml_Format", useYml);
config.set("Options.Max_Number_of_Duplicates", bookDuplicateLimit);
config.set("Options.Title-Author_Separator", titleAuthorSeparator);
config.set("Options.Lore_line_separator", loreSeparator);
config.set("Options.Books_for_new_players", firstBooks);
config.set("Options.Message_for_new_players", welcomeMessage);
if (bookPriceType != null) {
if (bookPriceType != Material.AIR) {
config.set("Options.Price_to_create_book.Item_type", bookPriceType.toString());
} else {
config.set("Options.Price_to_create_book.Item_type", "Economy");
}
} else {
config.set("Options.Price_to_create_book.Item_type", "Item type name");
}
config.set("Options.Price_to_create_book.Required_quantity", bookPriceQuantity);
config.set("Options.Admin_Auto_Decrypt", adminDecrypt);
config.set("Options.Author_Only_Copy", authorOnlyCopy);
//Handles old book and quill settings
if (config.contains("Options.Require_book_and_quill_to_create_book")) {
sendSuccessMessage(consoleSender, "[BooksWithoutBorders] Found old config setting \"Require_book_and_quill_to_create_book\"");
sendSuccessMessage(consoleSender, "Updating to \"Price_to_create_book\" settings");
if (config.getBoolean("Options.Require_book_and_quill_to_create_book")) {
bookPriceType = Material.WRITABLE_BOOK;
bookPriceQuantity = 1;
config.set("Options.Price_to_create_book.Item_type", bookPriceType.toString());
config.set("Options.Price_to_create_book.Required_quantity", bookPriceQuantity);
}
config.set("Options.Require_book_and_quill_to_create_book", null);
}
this.saveConfig();
}
/**
* Loads the config
*
* @return <p>True if the config was loaded successfully</p>
*/
public boolean loadConfig() {
this.reloadConfig();
Configuration config = this.getConfig();
try {
useYml = config.getBoolean("Options.Save_Books_in_Yaml_Format", true);
bookDuplicateLimit = config.getInt("Options.Max_Number_of_Duplicates", 5);
titleAuthorSeparator = config.getString("Options.Title-Author_Separator", ",");
loreSeparator = config.getString("Options.Lore_line_separator", "~");
adminDecrypt = config.getBoolean("Options.Admin_Auto_Decrypt", false);
authorOnlyCopy = config.getBoolean("Options.Author_Only_Copy", false);
//Set books to give new players
firstBooks = config.getStringList("Options.Books_for_new_players");
if (config.contains("Options.Book_for_new_players")) {
firstBooks.add(config.getString("Options.Book_for_new_players"));
}
welcomeMessage = config.getString("Options.Message_for_new_players", " ");
//Convert string into material
String paymentMaterial = config.getString("Options.Price_to_create_book.Item_type", " ");
if (paymentMaterial.equalsIgnoreCase("Economy")) {
if (EconomyHelper.setupEconomy()) {
bookPriceType = Material.AIR;
} else {
sendErrorMessage(consoleSender, "BooksWithoutBorders failed to hook into Vault! Book price not set!");
bookPriceType = null;
}
} else if (!paymentMaterial.equalsIgnoreCase(" ")) {
Material material = Material.matchMaterial(paymentMaterial);
if (material != null) {
bookPriceType = material;
}
}
bookPriceQuantity = config.getDouble("Options.Price_to_create_book.Required_quantity", 0);
//Make sure titleAuthorSeparator is a valid value
titleAuthorSeparator = cleanString(titleAuthorSeparator);
if (titleAuthorSeparator.length() != 1) {
sendErrorMessage(consoleSender, "Title-Author_Separator is set to an invalid value!");
sendErrorMessage(consoleSender, "Reverting to default value of \",\"");
titleAuthorSeparator = ",";
config.set("Options.Title-Author_Separator", titleAuthorSeparator);
}
} catch (Exception e) {
sendErrorMessage(consoleSender, "Warning! Config.yml failed to load!");
sendErrorMessage(consoleSender, "Try Looking for settings that are missing values!");
return false;
}
return true;
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @return <p>The loaded book</p>
*/
public ItemStack loadBook(CommandSender sender, String fileName, String isSigned, String directory) {
return loadBook(sender, fileName, isSigned, directory, 1);
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</p>
*/
public ItemStack loadBook(CommandSender sender, String fileName, String isSigned, String directory, int numCopies) {
BookDirectory bookDirectory = BookDirectory.getFromString(directory);
//Find the filename if a book index is given
try {
int bookIndex = Integer.parseInt(fileName);
List<String> availableFiles = getAvailableBooks(sender, bookDirectory == BookDirectory.PUBLIC);
if (bookIndex <= availableFiles.size()) {
fileName = availableFiles.get(Integer.parseInt(fileName) - 1);
}
} catch (NumberFormatException ignored) {
}
//Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) {
return null;
}
//Make sure the player can pay for the book
if (booksHavePrice() && !sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
EconomyHelper.cannotPayForBookPrinting((Player) sender, numCopies)) {
return null;
}
//Generate a new empty book
ItemStack book;
BookMeta bookMetadata = (BookMeta) itemFactory.getItemMeta(Material.WRITTEN_BOOK);
if (isSigned.equalsIgnoreCase("true")) {
book = new ItemStack(Material.WRITTEN_BOOK);
} else {
book = new ItemStack(Material.WRITABLE_BOOK);
}
//Load the book from the given file
BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
sendErrorMessage(sender, "File was blank!!");
return null;
}
//Remove "encrypted" from the book lore
if (bookDirectory == BookDirectory.ENCRYPTED && bookMetadata.hasLore()) {
List<String> oldLore = bookMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
bookMetadata.setLore(newLore);
}
}
//Set the metadata and amount to the new book
book.setItemMeta(bookMetadata);
book.setAmount(numCopies);
return book;
}
/**
* Gets a File pointing to the wanted book
*
* @param sender <p>The sender to send errors to</p>
* @param fileName <p>The name of the book file</p>
* @param bookDirectory <p>The book directory the file resides in</p>
* @param directory <p>The relative directory given</p>
* @return <p>A file or null if it does not exist</p>
*/
private File getFullPath(CommandSender sender, String fileName, BookDirectory bookDirectory, String directory) {
File file = null;
if (bookDirectory == BookDirectory.PUBLIC) {
file = FileHelper.getBookFile(getBookFolder() + fileName);
} else if (bookDirectory == BookDirectory.PLAYER) {
file = FileHelper.getBookFile(getBookFolder() + cleanString(sender.getName()) + getSlash() + fileName);
} else if (bookDirectory == BookDirectory.ENCRYPTED) {
file = FileHelper.getBookFile(getBookFolder() + "Encrypted" + getSlash() + directory + getSlash() + fileName);
}
if (file == null || !file.isFile()) {
sendErrorMessage(sender, "Incorrect file name!");
return null;
} else {
return file;
}
}
/**
* Sends a success message to a command sender (player or a console)
*
@ -586,13 +263,4 @@ public class BooksWithoutBorders extends JavaPlugin {
sender.sendMessage(getErrorColor() + message);
}
/**
* Checks whether books have a price for printing them
*
* @return <p>True if players need to pay for printing books</p>
*/
public boolean booksHavePrice() {
return (bookPriceType != null && bookPriceQuantity > 0);
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import org.bukkit.Material;
import org.bukkit.command.Command;
@ -14,20 +15,19 @@ import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendErrorMessage;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getCommandColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSuccessColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getCommandColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSuccessColor;
/**
* Command executor for the books without borders (bwb) command
*/
public class CommandBooksWithoutBorders implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override
public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(getCommandColor() + "[] denote optional parameters");
sender.sendMessage(getCommandColor() + "<> denote required parameters");
sender.sendMessage(getCommandColor() + "{} denote required permission");
sender.sendMessage(getCommandColor() + "In some cases, commands with required parameters can be called with no parameters");
if (sender instanceof Player) {
showPlayerCommands(sender);
@ -44,9 +44,9 @@ public class CommandBooksWithoutBorders implements TabExecutor {
*/
private void showConsoleCommands(CommandSender sender) {
sender.sendMessage(getCommandColor() + "Commands:");
showCommandInfo("reload", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("deletePublicBook", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("reload", sender);
}
/**
@ -56,9 +56,9 @@ public class CommandBooksWithoutBorders implements TabExecutor {
*/
private void showPlayerCommands(CommandSender sender) {
//Lists all commands
Material bookPriceType = BooksWithoutBorders.getBookPriceType();
double bookPriceQuantity = BooksWithoutBorders.getBookPriceQuantity();
if (booksWithoutBorders.booksHavePrice()) {
Material bookPriceType = BooksWithoutBordersConfig.getBookPriceType();
double bookPriceQuantity = BooksWithoutBordersConfig.getBookPriceQuantity();
if (BooksWithoutBordersConfig.booksHavePrice()) {
if (bookPriceType != Material.AIR) {
sendErrorMessage(sender, "[" + (int) bookPriceQuantity + " " + bookPriceType.toString() +
"(s) are required to create a book]");
@ -69,24 +69,25 @@ public class CommandBooksWithoutBorders implements TabExecutor {
}
sender.sendMessage(getCommandColor() + "Commands:");
showCommandInfo("loadBook", sender);
showCommandInfo("loadPublicBook", sender);
showCommandInfo("saveBook", sender);
showCommandInfo("savePublicBook", sender);
showCommandInfo("giveBook", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("copyBook", sender);
showCommandInfo("decryptBook", sender);
showCommandInfo("deleteBook", sender);
showCommandInfo("deletePublicBook", sender);
showCommandInfo("unsignBook", sender);
showCommandInfo("copyBook", sender);
showCommandInfo("encryptBook", sender);
showCommandInfo("formatBook", sender);
showCommandInfo("giveBook", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("groupEncryptBook", sender);
showCommandInfo("decryptBook", sender);
showCommandInfo("setTitle", sender);
showCommandInfo("setAuthor", sender);
showCommandInfo("setLore", sender);
showCommandInfo("setBookPrice", sender);
showCommandInfo("loadBook", sender);
showCommandInfo("loadPublicBook", sender);
showCommandInfo("reload", sender);
showCommandInfo("saveBook", sender);
showCommandInfo("savePublicBook", sender);
showCommandInfo("setAuthor", sender);
showCommandInfo("setBookPrice", sender);
showCommandInfo("setLore", sender);
showCommandInfo("setTitle", sender);
showCommandInfo("unsignBook", sender);
}
/**
@ -100,9 +101,12 @@ public class CommandBooksWithoutBorders implements TabExecutor {
if (pluginCommand != null) {
String permission = pluginCommand.getPermission();
if (permission == null || sender.hasPermission(permission)) {
sender.sendMessage("\n" + getCommandColor() +
pluginCommand.getUsage().replace("<command>", pluginCommand.getName()) + ": " +
getSuccessColor() + pluginCommand.getDescription());
String commandInfo = "\n" + getCommandColor() + pluginCommand.getUsage().replace("<command>",
pluginCommand.getName()) + ": " + getSuccessColor() + pluginCommand.getDescription();
if (sender.hasPermission("bookswithoutborders.admin")) {
commandInfo += getCommandColor() + " {" + permission + "}";
}
sender.sendMessage(commandInfo);
}
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
@ -22,8 +23,6 @@ import java.util.Objects;
*/
public class CommandCopy implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
@ -46,13 +45,13 @@ public class CommandCopy implements TabExecutor {
try {
int copies = Integer.parseInt(args[0]);
if (copies > 0) {
if (BooksWithoutBorders.getAuthorOnlyCopy() && !player.hasPermission("bookswithoutborders.bypassAuthorOnlyCopy")) {
if (BooksWithoutBordersConfig.getAuthorOnlyCopy() && !player.hasPermission("bookswithoutborders.bypassAuthorOnlyCopy")) {
if (!isAuthor(player, (BookMeta) Objects.requireNonNull(heldBook.getItemMeta()))) {
return false;
}
}
if (booksWithoutBorders.booksHavePrice() &&
if (BooksWithoutBordersConfig.booksHavePrice() &&
!player.hasPermission("bookswithoutborders.bypassBookPrice") &&
EconomyHelper.cannotPayForBookPrinting(player, copies)) {
return false;

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.command.Command;
@ -15,8 +16,8 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
/**
* Command executor for the decrypt command
@ -44,11 +45,11 @@ public class CommandDecrypt implements TabExecutor {
}
//Warning: admin decrypt only allows decrypting files created by the same player. Not sure if intended
if (args.length == 0 && BooksWithoutBorders.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin")) {
if (args.length == 0 && BooksWithoutBordersConfig.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin")) {
String path = getBookFolder() + "Encrypted" + getSlash();
String fileName;
if (bookMetadata.hasTitle()) {
fileName = bookMetadata.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = bookMetadata.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetadata.getAuthor();
} else {
fileName = "Untitled," + player.getName();
}

View File

@ -13,8 +13,8 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
/**
* Command executor for the delete command

View File

@ -1,8 +1,8 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -13,9 +13,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A command for converting color codes to color formatting
@ -35,36 +32,13 @@ public class CommandFormat implements TabExecutor {
}
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
List<String> formattedPages = new ArrayList<>(Objects.requireNonNull(bookMeta).getPageCount());
for (String page : bookMeta.getPages()) {
formattedPages.add(translateAllColorCodes(page));
}
bookMeta.setPages(formattedPages);
heldBook.setItemMeta(bookMeta);
heldBook.setItemMeta(BookFormatter.formatPages((BookMeta) heldBook.getItemMeta()));
BooksWithoutBorders.sendSuccessMessage(sender, "Book formatted!");
return true;
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
public static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
return new ArrayList<>();

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper;
@ -88,7 +89,7 @@ public class CommandGive implements TabExecutor {
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
try {
ItemStack newBook = booksWithoutBorders.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(copies));
ItemStack newBook = BookLoader.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(copies));
if (newBook != null) {
receivingPlayer.getInventory().addItem(newBook);
BooksWithoutBorders.sendSuccessMessage(sender, "Book sent!");

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper;
@ -19,8 +20,6 @@ import java.util.List;
*/
public class CommandLoad implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return loadBook(sender, args, "player", false);
@ -79,7 +78,7 @@ public class CommandLoad implements TabExecutor {
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
try {
//Give the new book if it can be loaded
ItemStack newBook = booksWithoutBorders.loadBook(player, bookToLoad, isSigned, directory, Integer.parseInt(copies));
ItemStack newBook = BookLoader.loadBook(player, bookToLoad, isSigned, directory, Integer.parseInt(copies));
if (newBook != null) {
player.getInventory().addItem(newBook);
BooksWithoutBorders.sendSuccessMessage(player, "Book created!");

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -14,11 +15,9 @@ import java.util.List;
*/
public class CommandReload implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (booksWithoutBorders.loadConfig()) {
if (BooksWithoutBordersConfig.loadConfig()) {
BooksWithoutBorders.sendSuccessMessage(sender, "BooksWithoutBorders configuration reloaded!");
} else {
BooksWithoutBorders.sendErrorMessage(sender, "Reload Failed!");

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
@ -18,11 +19,10 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.getTitleAuthorSeparator;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getCommandColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getErrorColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getCommandColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getErrorColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
@ -89,7 +89,7 @@ public class CommandSave implements TabExecutor {
if (!book.hasTitle()) {
fileName = "Untitled," + player.getName();
} else {
fileName = book.getTitle() + getTitleAuthorSeparator() + book.getAuthor();
fileName = book.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + book.getAuthor();
}
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
@ -120,7 +120,7 @@ public class CommandSave implements TabExecutor {
}
//Skip if duplicate limit is reached
if (foundDuplicates > BooksWithoutBorders.getBookDuplicateLimit()) {
if (foundDuplicates > BooksWithoutBordersConfig.getBookDuplicateLimit()) {
BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName + " duplicates reached!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " + getErrorColor() + "to overwrite!");
return;
@ -133,7 +133,7 @@ public class CommandSave implements TabExecutor {
}
try {
if (BooksWithoutBorders.getUseYml()) {
if (BooksWithoutBordersConfig.getUseYml()) {
BookToFromTextHelper.bookToYml(savePath, fileName, book);
} else {
BookToFromTextHelper.bookToTXT(savePath, fileName, book);

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper;
@ -66,10 +67,10 @@ public class CommandSetBookPrice implements TabExecutor {
* @param sender <p>The sender of the command</p>
*/
private void clearItemPrice(CommandSender sender) {
BooksWithoutBorders.setBookPriceType(null);
BooksWithoutBorders.setBookPriceQuantity(0);
BooksWithoutBordersConfig.setBookPriceType(null);
BooksWithoutBordersConfig.setBookPriceQuantity(0);
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Item_type", "Item type name");
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Required_quantity", BooksWithoutBorders.getBookPriceQuantity());
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Required_quantity", BooksWithoutBordersConfig.getBookPriceQuantity());
booksWithoutBorders.saveConfig();
BooksWithoutBorders.sendSuccessMessage(sender, "Price to create books removed!");
@ -94,10 +95,10 @@ public class CommandSetBookPrice implements TabExecutor {
return false;
}
BooksWithoutBorders.setBookPriceType(heldItem.getType());
BooksWithoutBorders.setBookPriceQuantity(price);
String newPriceType = BooksWithoutBorders.getBookPriceType().toString();
double newPriceQuantity = BooksWithoutBorders.getBookPriceQuantity();
BooksWithoutBordersConfig.setBookPriceType(heldItem.getType());
BooksWithoutBordersConfig.setBookPriceQuantity(price);
String newPriceType = BooksWithoutBordersConfig.getBookPriceType().toString();
double newPriceQuantity = BooksWithoutBordersConfig.getBookPriceQuantity();
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Item_type", newPriceType);
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Required_quantity", newPriceQuantity);
booksWithoutBorders.saveConfig();
@ -116,9 +117,9 @@ public class CommandSetBookPrice implements TabExecutor {
*/
private boolean setEconomyPrice(CommandSender sender, double price) {
if (EconomyHelper.setupEconomy()) {
BooksWithoutBorders.setBookPriceQuantity(price);
BooksWithoutBorders.setBookPriceType(Material.AIR);
double newPriceQuantity = BooksWithoutBorders.getBookPriceQuantity();
BooksWithoutBordersConfig.setBookPriceQuantity(price);
BooksWithoutBordersConfig.setBookPriceType(Material.AIR);
double newPriceQuantity = BooksWithoutBordersConfig.getBookPriceQuantity();
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Item_type", "Economy");
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Required_quantity", newPriceQuantity);
booksWithoutBorders.saveConfig();

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.ChatColor;
import org.bukkit.Material;
@ -44,7 +45,7 @@ public class CommandSetLore implements TabExecutor {
//Format lore
rawLore = ChatColor.translateAlternateColorCodes('&', rawLore);
String[] loreParts = rawLore.split(BooksWithoutBorders.getLoreSeparator());
String[] loreParts = rawLore.split(BooksWithoutBordersConfig.getLoreSeparator());
List<String> newLore = new ArrayList<>(Arrays.asList(loreParts));
//Update lore

View File

@ -0,0 +1,271 @@
package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.Configuration;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendErrorMessage;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendSuccessMessage;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
public class BooksWithoutBordersConfig {
private static int bookDuplicateLimit;
private static String titleAuthorSeparator;
private static String loreSeparator;
private static List<String> firstBooks = new ArrayList<>();
private static String welcomeMessage;
private static Material bookPriceType = null;
private static double bookPriceQuantity;
private static boolean authorOnlyCopy;
private static boolean useYml;
private static boolean adminDecrypt;
private static boolean formatBooks;
/**
* Gets whether only the author of a book should be able to copy it
*
* @return <p>Whether only the book author can copy it</p>
*/
public static boolean getAuthorOnlyCopy() {
return authorOnlyCopy;
}
/**
* Gets whether to use YML, not TXT, for saving books
*
* @return <p>Whether to use YML for saving books</p>
*/
public static boolean getUseYml() {
return useYml;
}
/**
* Gets whether admins should be able to decrypt books without a password, and decrypt all group encrypted books
*
* @return <p>Whether admins can bypass the encryption password</p>
*/
public static boolean getAdminDecrypt() {
return adminDecrypt;
}
/**
* Sets the quantity of items/currency necessary for copying books
*
* @param newQuantity <p>The new quantity necessary for payment</p>
*/
public static void setBookPriceQuantity(double newQuantity) {
bookPriceQuantity = newQuantity;
}
/**
* Gets the quantity of items/currency necessary for copying books
*
* @return <p>The quantity necessary for payment</p>
*/
public static double getBookPriceQuantity() {
return bookPriceQuantity;
}
/**
* Sets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @param newType <p>The new item type to use for book pricing</p>
*/
public static void setBookPriceType(Material newType) {
bookPriceType = newType;
}
/**
* Gets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @return <p>The item type used for book pricing</p>
*/
public static Material getBookPriceType() {
return bookPriceType;
}
/**
* Gets the welcome message to show to new players
*
* @return <p>The welcome message to show new players</p>
*/
public static String getWelcomeMessage() {
return welcomeMessage;
}
/**
* Gets the limit of duplicates for each book
*
* @return <p>The book duplicate limit</p>
*/
public static int getBookDuplicateLimit() {
return bookDuplicateLimit;
}
/**
* Gets the separator used to split book title from book author
*
* @return <p>The separator between title and author</p>
*/
public static String getTitleAuthorSeparator() {
return titleAuthorSeparator;
}
/**
* Gets the separator used to denote a newline in a lore string
*
* @return <p>The separator used to denote lore newline</p>
*/
public static String getLoreSeparator() {
return loreSeparator;
}
/**
* Gets whether all books should be formatted when they are signed
*
* @return <p>Whether all books should be formatted</p>
*/
public static boolean formatBooks() {
return formatBooks;
}
/**
* Gets a copy of the list of books to give new players
*
* @return <p>The books to give new players</p>
*/
public static List<String> getFirstBooks() {
return new ArrayList<>(firstBooks);
}
/**
* Checks whether books have a price for printing them
*
* @return <p>True if players need to pay for printing books</p>
*/
public static boolean booksHavePrice() {
return (bookPriceType != null && bookPriceQuantity > 0);
}
/**
* Saves the config
*/
public static void saveConfigValues() {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getInstance().getServer().getConsoleSender();
Configuration config = BooksWithoutBorders.getInstance().getConfig();
config.set(ConfigOption.USE_YAML.getConfigNode(), useYml);
config.set(ConfigOption.MAX_DUPLICATES.getConfigNode(), bookDuplicateLimit);
config.set(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(), titleAuthorSeparator);
config.set(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(), loreSeparator);
config.set(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode(), firstBooks);
config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), welcomeMessage);
config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), formatBooks);
String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode();
if (bookPriceType != null) {
if (bookPriceType != Material.AIR) {
config.set(itemTypeNode, bookPriceType.toString());
} else {
config.set(itemTypeNode, "Economy");
}
} else {
config.set(itemTypeNode, "Item type name");
}
config.set(ConfigOption.PRICE_QUANTITY.getConfigNode(), bookPriceQuantity);
config.set(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(), adminDecrypt);
config.set(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(), authorOnlyCopy);
//Handles old book and quill settings
if (config.contains("Options.Require_book_and_quill_to_create_book")) {
sendSuccessMessage(consoleSender, "[BooksWithoutBorders] Found old config setting \"Require_book_and_quill_to_create_book\"");
sendSuccessMessage(consoleSender, "Updating to \"Price_to_create_book\" settings");
if (config.getBoolean("Options.Require_book_and_quill_to_create_book")) {
bookPriceType = Material.WRITABLE_BOOK;
bookPriceQuantity = 1;
config.set("Options.Price_to_create_book.Item_type", bookPriceType.toString());
config.set("Options.Price_to_create_book.Required_quantity", bookPriceQuantity);
}
config.set("Options.Require_book_and_quill_to_create_book", null);
}
BooksWithoutBorders.getInstance().saveConfig();
}
/**
* Loads the config
*
* @return <p>True if the config was loaded successfully</p>
*/
public static boolean loadConfig() {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getInstance().getServer().getConsoleSender();
BooksWithoutBorders.getInstance().reloadConfig();
Configuration config = BooksWithoutBorders.getInstance().getConfig();
try {
useYml = config.getBoolean(ConfigOption.USE_YAML.getConfigNode(),
(Boolean) ConfigOption.USE_YAML.getDefaultValue());
bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(),
(Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue());
titleAuthorSeparator = config.getString(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(),
(String) ConfigOption.TITLE_AUTHOR_SEPARATOR.getDefaultValue());
loreSeparator = config.getString(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(),
(String) ConfigOption.LORE_LINE_SEPARATOR.getDefaultValue());
adminDecrypt = config.getBoolean(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(),
(Boolean) ConfigOption.ADMIN_AUTO_DECRYPT.getDefaultValue());
authorOnlyCopy = config.getBoolean(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(),
(Boolean) ConfigOption.AUTHOR_ONLY_COPY.getDefaultValue());
firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode());
welcomeMessage = config.getString(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(),
(String) ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getDefaultValue());
formatBooks = config.getBoolean(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(),
(Boolean) ConfigOption.FORMAT_AFTER_SIGNING.getDefaultValue());
//Convert string into material
String paymentMaterial = config.getString(ConfigOption.PRICE_ITEM_TYPE.getConfigNode(),
(String) ConfigOption.PRICE_ITEM_TYPE.getDefaultValue());
if (paymentMaterial.equalsIgnoreCase("Economy")) {
if (EconomyHelper.setupEconomy()) {
bookPriceType = Material.AIR;
} else {
sendErrorMessage(consoleSender, "BooksWithoutBorders failed to hook into Vault! Book price not set!");
bookPriceType = null;
}
} else if (!paymentMaterial.trim().isEmpty()) {
Material material = Material.matchMaterial(paymentMaterial);
if (material != null) {
bookPriceType = material;
}
}
bookPriceQuantity = config.getDouble(ConfigOption.PRICE_QUANTITY.getConfigNode(),
(Double) ConfigOption.PRICE_QUANTITY.getDefaultValue());
//Make sure titleAuthorSeparator is a valid value
titleAuthorSeparator = cleanString(titleAuthorSeparator);
if (titleAuthorSeparator.length() != 1) {
sendErrorMessage(consoleSender, "Title-Author_Separator is set to an invalid value!");
sendErrorMessage(consoleSender, "Reverting to default value of \",\"");
titleAuthorSeparator = ",";
config.set("Options.Title-Author_Separator", titleAuthorSeparator);
}
} catch (Exception e) {
sendErrorMessage(consoleSender, "Warning! Config.yml failed to load!");
sendErrorMessage(consoleSender, "Try Looking for settings that are missing values!");
return false;
}
return true;
}
}

View File

@ -1,5 +1,6 @@
package net.knarcraft.bookswithoutborders;
package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import org.bukkit.ChatColor;
/**

View File

@ -0,0 +1,95 @@
package net.knarcraft.bookswithoutborders.config;
/**
* A representation of the different available config options
*/
public enum ConfigOption {
/**
* Whether YAML should be used to store books instead of simple text files
*/
USE_YAML("Options.Save_Books_in_Yaml_Format", true),
/**
* The max duplicates of a book that can be saved
*/
MAX_DUPLICATES("Options.Max_Number_of_Duplicates", 5),
/**
* The separator used to separate book title and book author
*/
TITLE_AUTHOR_SEPARATOR("Options.Title-Author_Separator", ","),
/**
* The separator used to specify a new line in an item's lore
*/
LORE_LINE_SEPARATOR("Options.Lore_line_separator", "~"),
/**
* The books given to new players when they first join
*/
BOOKS_FOR_NEW_PLAYERS("Options.Books_for_new_players", "[]"),
/**
* The message to display to new players when they first join
*/
MESSAGE_FOR_NEW_PLAYERS("Options.Message_for_new_players", ""),
/**
* The item type used to pay for book copying
*/
PRICE_ITEM_TYPE("Options.Price_to_create_book.Item_type", ""),
/**
* The amount of items used to pay for book copying
*/
PRICE_QUANTITY("Options.Price_to_create_book.Required_quantity", 0.0),
/**
* Whether admins should be able to decrypt books for all groups
*/
ADMIN_AUTO_DECRYPT("Options.Admin_Auto_Decrypt", false),
/**
* Whether only the book author should be able to copy a book
*/
AUTHOR_ONLY_COPY("Options.Author_Only_Copy", false),
/**
* Whether to automatically format every signed book
*/
FORMAT_AFTER_SIGNING("Options.Format_Book_After_Signing", false);
private final String configNode;
private final Object defaultValue;
/**
* Instantiates a new config option
*
* @param configNode <p>The config node in the config file this option represents</p>
* @param defaultValue <p>The default value for this config option</p>
*/
ConfigOption(String configNode, Object defaultValue) {
this.configNode = configNode;
this.defaultValue = defaultValue;
}
/**
* Gets the config node used for loading/saving this config value
*
* @return <p>The config node</p>
*/
public String getConfigNode() {
return this.configNode;
}
/**
* Gets the default value of this config option
*
* @return <p>The default value of this config option</p>
*/
public Object getDefaultValue() {
return this.defaultValue;
}
}

View File

@ -0,0 +1,24 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerEditBookEvent;
/**
* A listener for listening to book events
*
* <p>Mainly used for auto-formatting signed books if enabled</p>
*/
public class BookEventListener implements Listener {
@EventHandler
public void onBookSign(PlayerEditBookEvent event) {
if (event.isCancelled() || !event.isSigning() || !BooksWithoutBordersConfig.formatBooks()) {
return;
}
event.setNewBookMeta(BookFormatter.formatPages(event.getNewBookMeta()));
}
}

View File

@ -1,6 +1,8 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.Material;
@ -16,8 +18,8 @@ import org.bukkit.inventory.meta.ItemMeta;
import java.io.File;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
public class PlayerEventListener implements Listener {
@ -58,7 +60,7 @@ public class PlayerEventListener implements Listener {
boolean sendMessage = true;
//Gives new players necessary books
for (String bookName : BooksWithoutBorders.getFirstBooks()) {
for (String bookName : BooksWithoutBordersConfig.getFirstBooks()) {
sendMessage = giveBookToNewPlayer(bookName, player, sendMessage);
}
}
@ -88,13 +90,13 @@ public class PlayerEventListener implements Listener {
if (!bookName.trim().isEmpty()) {
//Give the book to the player if it exists
ItemStack newBook = booksWithoutBorders.loadBook(player, bookName, "true", "public");
ItemStack newBook = BookLoader.loadBook(player, bookName, "true", "public");
if (newBook != null) {
player.getInventory().addItem(newBook);
}
//Send the player a welcome message if it exists
String welcomeMessage = BooksWithoutBorders.getWelcomeMessage();
String welcomeMessage = BooksWithoutBordersConfig.getWelcomeMessage();
if (!welcomeMessage.trim().isEmpty() && newBook != null && sendMessage) {
sendMessage = false;
booksWithoutBorders.getServer().getScheduler().scheduleSyncDelayedTask(booksWithoutBorders,
@ -143,7 +145,7 @@ public class PlayerEventListener implements Listener {
//Unknown author is ignored
fileName = oldBook.getTitle();
} else {
fileName = oldBook.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + oldBook.getAuthor();
fileName = oldBook.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + oldBook.getAuthor();
}
String cleanPlayerName = InputCleaningHelper.cleanString(player.getName());
@ -158,7 +160,7 @@ public class PlayerEventListener implements Listener {
for (String path : possiblePaths) {
File file = new File(path);
if (file.isFile()) {
return booksWithoutBorders.loadBook(player, fileName, "true", "player");
return BookLoader.loadBook(player, fileName, "true", "player");
}
}
return null;

View File

@ -1,7 +1,9 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
@ -22,8 +24,8 @@ import org.bukkit.inventory.meta.BookMeta;
import java.io.File;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.FileHelper.isBookListIndex;
public class SignEventListener implements Listener {
@ -234,11 +236,11 @@ public class SignEventListener implements Listener {
//Permission check
if (!player.hasPermission("bookswithoutborders.decrypt." + groupName) &&
!(BooksWithoutBorders.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin"))) {
!(BooksWithoutBordersConfig.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin"))) {
return;
}
String fileName = oldBook.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + oldBook.getAuthor();
String fileName = oldBook.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + oldBook.getAuthor();
String encryptionFile = InputCleaningHelper.cleanString(groupName) + slash + fileName + ".yml";
@ -249,7 +251,7 @@ public class SignEventListener implements Listener {
return;
}
}
newBook = BooksWithoutBorders.getInstance().loadBook(player, fileName, "true", groupName, heldItem.getAmount());
newBook = BookLoader.loadBook(player, fileName, "true", groupName, heldItem.getAmount());
if (newBook == null) {
return;
@ -304,7 +306,7 @@ public class SignEventListener implements Listener {
fileName += ChatColor.stripColor(thirdLine);
}
ItemStack newBook = BooksWithoutBorders.getInstance().loadBook(player, fileName, "true", "public");
ItemStack newBook = BookLoader.loadBook(player, fileName, "true", "public");
if (newBook != null) {
player.getInventory().addItem(newBook);

View File

@ -1,6 +1,13 @@
package net.knarcraft.bookswithoutborders.utility;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.inventory.meta.BookMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A class for formatting text to fit books
@ -94,4 +101,35 @@ public final class BookFormatter {
}
}
/**
* Formats every page in the given book meta by converting color and formatting codes
*
* @param bookMeta <p>The book meta to change</p>
* @return <p>The changed book meta</p>
*/
public static BookMeta formatPages(BookMeta bookMeta) {
List<String> formattedPages = new ArrayList<>(Objects.requireNonNull(bookMeta).getPageCount());
for (String page : bookMeta.getPages()) {
formattedPages.add(BookFormatter.translateAllColorCodes(page));
}
bookMeta.setPages(formattedPages);
return bookMeta;
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
private static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
}

View File

@ -0,0 +1,129 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public final class BookLoader {
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @return <p>The loaded book</p>
*/
public static ItemStack loadBook(CommandSender sender, String fileName, String isSigned, String directory) {
return loadBook(sender, fileName, isSigned, directory, 1);
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</p>
*/
public static ItemStack loadBook(CommandSender sender, String fileName, String isSigned, String directory, int numCopies) {
BookDirectory bookDirectory = BookDirectory.getFromString(directory);
//Find the filename if a book index is given
try {
int bookIndex = Integer.parseInt(fileName);
List<String> availableFiles = BooksWithoutBorders.getAvailableBooks(sender, bookDirectory == BookDirectory.PUBLIC);
if (bookIndex <= availableFiles.size()) {
fileName = availableFiles.get(Integer.parseInt(fileName) - 1);
}
} catch (NumberFormatException ignored) {
}
//Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) {
return null;
}
//Make sure the player can pay for the book
if (BooksWithoutBordersConfig.booksHavePrice() && !sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
EconomyHelper.cannotPayForBookPrinting((Player) sender, numCopies)) {
return null;
}
//Generate a new empty book
ItemStack book;
BookMeta bookMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK);
if (isSigned.equalsIgnoreCase("true")) {
book = new ItemStack(Material.WRITTEN_BOOK);
} else {
book = new ItemStack(Material.WRITABLE_BOOK);
}
//Load the book from the given file
BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(sender, "File was blank!!");
return null;
}
//Remove "encrypted" from the book lore
if (bookDirectory == BookDirectory.ENCRYPTED && bookMetadata.hasLore()) {
List<String> oldLore = bookMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
bookMetadata.setLore(newLore);
}
}
//Set the metadata and amount to the new book
book.setItemMeta(bookMetadata);
book.setAmount(numCopies);
return book;
}
/**
* Gets a File pointing to the wanted book
*
* @param sender <p>The sender to send errors to</p>
* @param fileName <p>The name of the book file</p>
* @param bookDirectory <p>The book directory the file resides in</p>
* @param directory <p>The relative directory given</p>
* @return <p>A file or null if it does not exist</p>
*/
private static File getFullPath(CommandSender sender, String fileName, BookDirectory bookDirectory, String directory) {
File file = null;
String slash = BooksWithoutBordersSettings.getSlash();
String bookFolder = BooksWithoutBordersSettings.getBookFolder();
if (bookDirectory == BookDirectory.PUBLIC) {
file = FileHelper.getBookFile(bookFolder + fileName);
} else if (bookDirectory == BookDirectory.PLAYER) {
file = FileHelper.getBookFile(bookFolder + InputCleaningHelper.cleanString(sender.getName()) + slash + fileName);
} else if (bookDirectory == BookDirectory.ENCRYPTED) {
file = FileHelper.getBookFile(bookFolder + "Encrypted" + slash + directory + slash + fileName);
}
if (file == null || !file.isFile()) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
return null;
} else {
return file;
}
}
}

View File

@ -1,6 +1,6 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.meta.BookMeta;
@ -121,7 +121,7 @@ public final class BookToFromTextHelper {
private static BookMeta bookFromTXT(String fileName, File file, BookMeta bookMetadata) {
String author;
String title;
String titleAuthorSeparator = BooksWithoutBorders.getTitleAuthorSeparator();
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
//Get title and author from the file name
if (fileName.contains(titleAuthorSeparator)) {

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material;
import org.bukkit.Server;
@ -67,8 +68,8 @@ public final class EconomyHelper {
public static boolean cannotPayForBookPrinting(Player player, int numCopies) {
//BookPriceQuantity: How many items are required to pay for each book
//BookPriceType: Which item is used to pay for the books. AIR = use economy
Material bookCurrency = BooksWithoutBorders.getBookPriceType();
double cost = BooksWithoutBorders.getBookPriceQuantity() * numCopies;
Material bookCurrency = BooksWithoutBordersConfig.getBookPriceType();
double cost = BooksWithoutBordersConfig.getBookPriceQuantity() * numCopies;
int itemCost = (int) cost;
if (bookCurrency == Material.AIR) {
@ -116,7 +117,7 @@ public final class EconomyHelper {
int clearedAmount = 0;
while (clearedAmount < itemCost) {
int firstItemIndex = playerInventory.first(BooksWithoutBorders.getBookPriceType());
int firstItemIndex = playerInventory.first(BooksWithoutBordersConfig.getBookPriceType());
ItemStack firstItem = playerInventory.getItem(firstItemIndex);
if (Objects.requireNonNull(firstItem).getAmount() <= itemCost - clearedAmount) {

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.encryption.GenenCrypt;
import net.knarcraft.bookswithoutborders.encryption.SubstitutionCipher;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
@ -16,8 +17,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
@ -199,7 +200,7 @@ public final class EncryptionHelper {
}
String fileName = (!bookMetadata.hasTitle()) ? "Untitled," + player.getName() : bookMetadata.getTitle() +
BooksWithoutBorders.getTitleAuthorSeparator() + bookMetadata.getAuthor();
BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = "[" + key + "]" + fileName;
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
@ -265,7 +266,7 @@ public final class EncryptionHelper {
}
//Creates file
String fileName = (!bookMetadata.hasTitle()) ? "Untitled," + player.getName() :
bookMetadata.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + bookMetadata.getAuthor();
bookMetadata.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
@ -281,7 +282,7 @@ public final class EncryptionHelper {
bookMetadata.setLore(newLore);
//Save file
File file = (BooksWithoutBorders.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt");
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt");
if (!file.isFile()) {
try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetadata);
@ -306,14 +307,14 @@ public final class EncryptionHelper {
private static Boolean saveEncryptedBook(Player player, BookMeta bookMetaData, String key) {
String path = getBookFolder() + "Encrypted" + getSlash();
String fileName = (!bookMetaData.hasTitle()) ? "Untitled," + player.getName() :
bookMetaData.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + bookMetaData.getAuthor();
bookMetaData.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetaData.getAuthor();
fileName = "[" + key + "]" + fileName;
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
//cancels saving if file is already encrypted
File file = (BooksWithoutBorders.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt");
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt");
if (file.isFile()) {
return true;
}

View File

@ -10,8 +10,8 @@ import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
/**

View File

@ -0,0 +1,26 @@
Options:
# Whether to use YAML for saved books instead of just storing them as text
Save_Books_in_Yaml_Format: true
# The maximum number of duplicates of a saved book allowed
Max_Number_of_Duplicates: 5
# The separator used to separate the book title and the book author
Title-Author_Separator: ","
# The separator used to denote a new line in the book lore
Lore_line_separator: "~"
# A list of books given to new players the first time they join the server
Books_for_new_players: [ ]
# An optional message displayed to new players the first time they join the server
Message_for_new_players: ""
# Price settings for book copying
Price_to_create_book:
# The item type used as currency for copying books. Use "Economy" to use money instead of items
Item_type: ""
# The quantity of currency required to pay for each book produced
Required_quantity: 0
# Whether any admin can decrypt any book regardless of the group it was encrypted for
Admin_Auto_Decrypt: false
# Whether to only allow the author of a book to create copies
Author_Only_Copy: false
# Whether to automatically format every book when it's signed
Format_Book_After_Signing: false

View File

@ -13,6 +13,14 @@ commands:
description: Lists Books Without Borders's commands and uses.
aliases: [ bwb ]
usage: /<command>
decryptbook:
description: Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt held book
usage: /<command> <key>
permission: bookswithoutborders.decrypt
formatbook:
description: Replaces color/formatting codes in a written book with formatted text
usage: /<command>
permission: bookswithoutborders.format
givebook:
description: Gives the selected player a book from your personal directory
usage: /<command> <file name or number> <playername> [# of copies (num)] [signed (true/false)]
@ -21,10 +29,6 @@ commands:
description: Same as givebook, but uses books from the public directory
usage: /<command> <file name or number> <playername> [# of copies (num)] [signed (true/false)]
permission: bookswithoutborders.givepublic
decryptbook:
description: Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt held book
usage: /<command> <key>
permission: bookswithoutborders.decrypt
groupencryptbook:
description: Encrypts book so that only players with the bookswithoutborders.decrypt.<group name> permission may decrypt the book by holding and left clicking the book
usage: /<command> <group name> <key> [encryption style]
@ -85,10 +89,6 @@ commands:
description: Reloads BwB's configuration file
usage: /<command>
permission: bookswithoutborders.admin
formatbook:
description: Replaces color/formatting codes in a book with formatted text
usage: /<command>
permission: bookswithoutborders.format
permissions:
bookswithoutborders.*:
description: Grants all permissions
@ -100,29 +100,33 @@ permissions:
default: op
children:
bookswithoutborders.use: true
bookswithoutborders.unsign: true
bookswithoutborders.alterbooks: true
bookswithoutborders.copy: true
bookswithoutborders.loadpublic: true
bookswithoutborders.savepublic: true
bookswithoutborders.encrypt: true
bookswithoutborders.decrypt: true
bookswithoutborders.groupencrypt: true
bookswithoutborders.signs: true
bookswithoutborders.give: true
bookswithoutborders.givepublic: true
bookswithoutborders.settitle: true
bookswithoutborders.setauthor: true
bookswithoutborders.setlore: true
bookswithoutborders.bypassauthoronlycopy: true
bookswithoutborders.bypassbookprice: true
bookswithoutborders.groupencrypt: true
bookswithoutborders.setbookprice: true
bookswithoutborders.format: true
bookswithoutborders.use:
description: Allows player to use commands and to save/load/delete in their personal directory
children:
bookswithoutborders.save: true
bookswithoutborders.load: true
bookswithoutborders.delete: true
bookswithoutborders.alterbooks:
description: Allows player to change books' data such as lore/title/author/formatting and unsigning books
children:
bookswithoutborders.unsign: true
bookswithoutborders.settitle: true
bookswithoutborders.setauthor: true
bookswithoutborders.setlore: true
bookswithoutborders.format: true
bookswithoutborders.format:
description: Allows a player to format a book
bookswithoutborders.save: