True if players can peek at the contained books
+ */ + public static boolean getEnableBookshelfPeeking() { + return enableBookshelfPeeking; + } + /** * Gets whether to use YML, not TXT, for saving books * @@ -272,6 +282,7 @@ public class BooksWithoutBordersConfig { 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); + config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), enableBookshelfPeeking); String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode(); if (bookPriceType != null) { @@ -319,7 +330,8 @@ public class BooksWithoutBordersConfig { Configuration config = BooksWithoutBorders.getInstance().getConfig(); try { useYml = getBoolean(config, ConfigOption.USE_YAML); - bookDuplicateLimit = getInt(config, ConfigOption.MAX_DUPLICATES); + bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(), + (Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue()); titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR); loreSeparator = getString(config, ConfigOption.LORE_LINE_SEPARATOR); adminDecrypt = getBoolean(config, ConfigOption.ADMIN_AUTO_DECRYPT); @@ -330,6 +342,7 @@ public class BooksWithoutBordersConfig { welcomeMessage = getString(config, ConfigOption.MESSAGE_FOR_NEW_PLAYERS); formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING); changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY); + enableBookshelfPeeking = getBoolean(config, ConfigOption.ENABLE_BOOKSHELF_PEEKING); //Convert string into material String paymentMaterial = getString(config, ConfigOption.PRICE_ITEM_TYPE); @@ -347,7 +360,8 @@ public class BooksWithoutBordersConfig { bookPriceType = material; } } - bookPriceQuantity = getDouble(config, ConfigOption.PRICE_QUANTITY); + bookPriceQuantity = config.getDouble(ConfigOption.PRICE_QUANTITY.getConfigNode(), + (Double) ConfigOption.PRICE_QUANTITY.getDefaultValue()); //Make sure titleAuthorSeparator is a valid value titleAuthorSeparator = cleanString(titleAuthorSeparator); @@ -366,28 +380,6 @@ public class BooksWithoutBordersConfig { return true; } - /** - * Gets the double value of the given config option - * - * @param configThe configuration to read from
- * @param configOptionThe configuration option to get the value for
- * @returnThe value of the option
- */ - private static double getDouble(Configuration config, ConfigOption configOption) { - return config.getDouble(configOption.getConfigNode(), (Double) configOption.getDefaultValue()); - } - - /** - * Gets the integer value of the given config option - * - * @param configThe configuration to read from
- * @param configOptionThe configuration option to get the value for
- * @returnThe value of the option
- */ - private static int getInt(Configuration config, ConfigOption configOption) { - return config.getInt(configOption.getConfigNode(), (Integer) configOption.getDefaultValue()); - } - /** * Gets the string value of the given config option * diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java b/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java index cc3d1b0..554990f 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java @@ -73,7 +73,13 @@ public enum ConfigOption { /** * Whether to automatically format every signed book */ - FORMAT_AFTER_SIGNING("Options.Format_Book_After_Signing", false); + FORMAT_AFTER_SIGNING("Options.Format_Book_After_Signing", false), + + /** + * Whether hitting a bookshelf should display information about the contained books + */ + ENABLE_BOOKSHELF_PEEKING("Options.Enable_Book_Peeking", false), + ; private final String configNode; private final Object defaultValue; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java index 503a3b0..61567b4 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java @@ -52,6 +52,7 @@ public class SubstitutionCipher { // but in reverse. Could probably be combined into one // method with a flag for encryption / decryption, but // I'm lazy. + @SuppressWarnings("unused") public String decrypt(String in, String key) { StringBuilder output = new StringBuilder(); if (key != null && key.length() > 0) { @@ -79,6 +80,7 @@ public class SubstitutionCipher { // the one time pad encryption is very secure, and // encryption works just like decryption, but is // vulnerable if the same key is used more than once. + @SuppressWarnings("unused") public String oneTimePad(String in, String key) { StringBuilder output = new StringBuilder(); for (int i = 0; i < in.length(); i++) { diff --git a/src/main/java/net/knarcraft/bookswithoutborders/listener/BookshelfListener.java b/src/main/java/net/knarcraft/bookswithoutborders/listener/BookshelfListener.java new file mode 100644 index 0000000..da082a5 --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/listener/BookshelfListener.java @@ -0,0 +1,143 @@ +package net.knarcraft.bookswithoutborders.listener; + +import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; +import net.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.block.ChiseledBookshelf; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ChiseledBookshelfInventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BookMeta; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSuccessColor; + +/** + * A listener for bookshelf clicking + */ +public class BookshelfListener implements Listener { + + @EventHandler + public void onBookshelfClick(PlayerInteractEvent event) { + Player player = event.getPlayer(); + + // If left-clicking a chiseled bookshelf and not sneaking, display contents + if (!event.hasBlock() || !(event.getClickedBlock().getState() instanceof ChiseledBookshelf chiseledBookshelf) || + event.getAction() != Action.LEFT_CLICK_BLOCK || player.isSneaking()) { + return; + } + + // Check if bookshelf peeking is enabled, and the player can peek + if (!BooksWithoutBordersConfig.getEnableBookshelfPeeking() || + !event.getPlayer().hasPermission("bookswithoutborders.peekbookshelf")) { + return; + } + + event.setUseInteractedBlock(Event.Result.DENY); + event.setUseItemInHand(Event.Result.DENY); + + ChiseledBookshelfInventory bookshelfInventory = chiseledBookshelf.getInventory(); + player.sendMessage(getBookshelfDescription(bookshelfInventory)); + } + + /** + * Gets the description for a bookshelf's contents + * + * @param bookshelfInventoryThe inventory of the bookshelf to describe
+ * @returnA textual description of the bookshelf's contents
+ */ + private String getBookshelfDescription(ChiseledBookshelfInventory bookshelfInventory) { + StringBuilder builder = new StringBuilder(getSuccessColor() + "Books in shelf:"); + for (ItemStack itemStack : bookshelfInventory.getStorageContents()) { + if (itemStack == null) { + continue; + } + ItemMeta meta = itemStack.getItemMeta(); + builder.append("\n ").append(ChatColor.GRAY).append(" - "); + if (meta instanceof BookMeta bookMeta) { + builder.append(getBookDescription(bookMeta)); + } else if (meta instanceof EnchantmentStorageMeta enchantmentStorageMeta) { + builder.append(getEnchantedBookDescription(enchantmentStorageMeta)); + } + } + return builder.toString(); + } + + /** + * Gets the description of a book + * + * @param bookMetaThe metadata for the book to describe
+ * @returnThe book's description
+ */ + private String getBookDescription(BookMeta bookMeta) { + String title; + String author; + if (!bookMeta.hasTitle()) { + title = "Untitled"; + } else { + title = bookMeta.getTitle(); + } + if (!bookMeta.hasAuthor()) { + author = "Unknown"; + } else { + author = bookMeta.getAuthor(); + } + return title + " by " + author; + } + + /** + * Gets the description of an enchanted book + * + * @param enchantmentStorageMetaThe metadata for the enchanted book to describe
+ * @returnThe enchanted book's description
+ */ + private String getEnchantedBookDescription(EnchantmentStorageMeta enchantmentStorageMeta) { + StringBuilder builder = new StringBuilder(); + builder.append("Enchanted book ("); + MapThe enchantment to get the name of
+ * @returnThe prettified enchantment name
+ */ + private String getEnchantmentName(Enchantment enchantment) { + return uppercaseFirst(enchantment.getKey().getKey().replace("_", " ")); + } + + /** + * Turns the first character of each of a string's words into uppercase + * + * @param inputThe input to uppercase
+ * @returnThe input string with more uppercase
+ */ + private String uppercaseFirst(String input) { + String[] parts = input.split(" "); + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1); + } + return String.join(" ", parts); + } + +} diff --git a/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java b/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java index c2385f6..d46225c 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java @@ -11,6 +11,8 @@ import net.md_5.bungee.api.ChatColor; import org.bukkit.Material; import org.bukkit.Tag; import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.block.sign.SignSide; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; @@ -100,18 +102,22 @@ public class SignEventListener implements Listener { event.setUseItemInHand(Event.Result.DENY); //The player right-clicked a sign Sign sign = (Sign) event.getClickedBlock().getState(); - if (signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) { - if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) { - encryptHeldBookUsingSign(sign, heldItemType, player, hand); - } else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) { - decryptHeldBookUsingSign(sign, heldItemType, player, hand); - } else if (signLineEquals(sign, 1, "[Give]", ChatColor.DARK_BLUE) && - getSignLine2Color(sign) == ChatColor.DARK_GREEN) { - giveBook(sign, player); - } else { - player.sendMessage("Sign command " + sign.getLine(1) + " " + sign.getLine(2) + " invalid"); - player.sendMessage(String.valueOf(getSignLine2Color(sign))); - } + if (!signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) { + return; + } + + if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) { + encryptHeldBookUsingSign(sign, heldItemType, player, hand); + } else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) { + decryptHeldBookUsingSign(sign, heldItemType, player, hand); + } else if (signLineEquals(sign, 1, "[Give]", ChatColor.DARK_BLUE) && + getSignLine2Color(sign) == ChatColor.DARK_GREEN) { + giveBook(sign, player); + } else { + SignSide front = sign.getSide(Side.FRONT); + player.sendMessage(String.format("Sign command %s %s is invalid", front.getLine(1), + front.getLine(2))); + player.sendMessage(String.valueOf(getSignLine2Color(sign))); } } else if (heldItemType == Material.WRITTEN_BOOK && (event.getAction() == Action.LEFT_CLICK_AIR || event.getAction() == Action.LEFT_CLICK_BLOCK)) { @@ -138,7 +144,7 @@ public class SignEventListener implements Listener { player.closeInventory(); //Converts user supplied key into integer form - String lineText = ChatColor.stripColor(sign.getLine(2)); + String lineText = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2)); String key = EncryptionHelper.getNumberKeyFromStringKey(lineText); ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false); @@ -156,9 +162,9 @@ public class SignEventListener implements Listener { * @returnThe color of the sign
*/ private ChatColor getSignLine2Color(Sign sign) { - String line = sign.getLine(2); + String line = sign.getSide(Side.FRONT).getLine(2); if (!ChatColor.stripColor(line).equals(line)) { - return ChatColor.getByChar(sign.getLine(2).substring(1, 2).charAt(0)); + return ChatColor.getByChar(sign.getSide(Side.FRONT).getLine(2).substring(1, 2).charAt(0)); } else { return null; } @@ -174,7 +180,7 @@ public class SignEventListener implements Listener { * @returnTrue if the given string is what's on the sign
*/ private boolean signLineEquals(Sign sign, int lineNumber, String compareTo, ChatColor color) { - String line = sign.getLine(lineNumber); + String line = sign.getSide(Side.FRONT).getLine(lineNumber); return line.equalsIgnoreCase(color + compareTo); } @@ -281,7 +287,7 @@ public class SignEventListener implements Listener { */ private void encryptHeldBookUsingSign(Sign sign, Material heldItemType, Player player, EquipmentSlot hand) { ItemStack eBook; - String[] lines = sign.getLines(); + String[] lines = sign.getSide(Side.FRONT).getLines(); boolean mainHand = hand == EquipmentSlot.HAND; if (heldItemType == Material.WRITTEN_BOOK) { player.closeInventory(); @@ -300,7 +306,7 @@ public class SignEventListener implements Listener { * @param playerThe player which clicked the sign
*/ private void giveBook(Sign sign, Player player) { - String fileName = ChatColor.stripColor(sign.getLine(2)); + String fileName = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2)); boolean isLoadListNumber = false; try { @@ -310,7 +316,7 @@ public class SignEventListener implements Listener { } //Add the third line to the second line for the full filename - String thirdLine = sign.getLine(3); + String thirdLine = sign.getSide(Side.FRONT).getLine(3); if (!isLoadListNumber && thirdLine.length() >= 2) { fileName += ChatColor.stripColor(thirdLine); } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/state/BookHoldingState.java b/src/main/java/net/knarcraft/bookswithoutborders/state/BookHoldingState.java index a30454b..ad4e6c5 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/state/BookHoldingState.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/state/BookHoldingState.java @@ -26,22 +26,22 @@ public enum BookHoldingState { UNSIGNED_MAIN_HAND, /** - * The player is holding one signed book in their off hand + * The player is holding one signed book in their off-hand */ SIGNED_OFF_HAND, /** - * The player is holding one unsigned book in their off hand + * The player is holding one unsigned book in their off-hand */ UNSIGNED_OFF_HAND, /** - * The player is holding one signed book in their main hand and one unsigned book in their off hand + * The player is holding one signed book in their main hand and one unsigned book in their off-hand */ SIGNED_MAIN_HAND_UNSIGNED_OFF_HAND, /** - * The player is holding one unsigned book in their main hand and one signed book in their off hand + * The player is holding one unsigned book in their main hand and one signed book in their off-hand */ UNSIGNED_MAIN_HAND_SIGNED_OFF_HAND, diff --git a/src/main/java/net/knarcraft/bookswithoutborders/state/ItemSlot.java b/src/main/java/net/knarcraft/bookswithoutborders/state/ItemSlot.java index 5b8204a..040603e 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/state/ItemSlot.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/state/ItemSlot.java @@ -11,7 +11,7 @@ public enum ItemSlot { MAIN_HAND, /** - * The item is in the player's off hand + * The item is in the player's off-hand */ OFF_HAND, diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/IntegerToRomanConverter.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/IntegerToRomanConverter.java new file mode 100644 index 0000000..4d7450b --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/IntegerToRomanConverter.java @@ -0,0 +1,90 @@ +package net.knarcraft.bookswithoutborders.utility; + +import java.util.ArrayList; +import java.util.List; + +/** + * A converter from an integer to a roman numeral + */ +public final class IntegerToRomanConverter { + + private final static ListThe number to convert
+ * @returnThe roman representation of the number
+ */ + public static String getRomanNumber(int number) { + StringBuilder output = new StringBuilder(); + int remainder = number; + for (int i = 0; i < romanCharacters.size(); i++) { + int romanValue = romanValues.get(i); + char romanCharacter = romanCharacters.get(i); + + // Repeat the roman character, and calculate the new remainder + if (remainder >= romanValue) { + output.append(repeat(romanCharacter, remainder / romanValue)); + remainder = remainder % romanValue; + } + + // Exit early to prevent unexpected trailing characters + if (remainder == 0) { + return output.toString(); + } + + // Generate the special case IV and similar + for (int j = i; j < romanCharacters.size(); j++) { + int value = romanValues.get(j); + int difference = Math.max(romanValue - value, 0); + + /* If the remainder is "one" less than the current roman value, we hit the IV/IX/XL case. + Note that 5 triggers the special case when 10 is tested, as 5 = 10 - 5, which requires a test, so it + can be filtered out. */ + if (remainder == difference && value != romanValue / 2) { + output.append(romanCharacters.get(j)).append(romanCharacter); + remainder = 0; + } + } + } + + return output.toString(); + } + + /** + * Repeats the given character + * + * @param characterThe character to repeat
+ * @param timesThe number of times to repeat the character
+ * @returnThe repeated string
+ */ + private static String repeat(char character, int times) { + return String.valueOf(character).repeat(Math.max(0, times)); + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 064a432..f8c8f4c 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -30,4 +30,6 @@ Options: # Whether to display "COPY" or "COPY_OF_COPY" instead of "ORIGINAL" when a book is copied. This also uses the # vanilla behavior where a copy of a copy cannot be copied further. Change_Generation_On_Copy: false - \ No newline at end of file + # Whether to enable hitting a chiseled bookshelf to see the shelf's contents. Sneaking is required to destroy a + # bookshelf if this is enabled. + Enable_Book_Peeking: false \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7927997..97fca58 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: BooksWithoutBorders version: '${project.version}' main: net.knarcraft.bookswithoutborders.BooksWithoutBorders -api-version: 1.18 +api-version: 1.20 prefix: Books Without Borders authors: [ EpicKnarvik97, AkiraAkiba ] description: A continuation of the original Books Without Borders @@ -131,6 +131,7 @@ permissions: bookswithoutborders.save: true bookswithoutborders.load: true bookswithoutborders.delete: true + bookswithoutborders.peekbookshelf: true bookswithoutborders.alterbooks: description: Allows player to change books' data such as lore/title/author/generation/formatting and unsigning books children: @@ -190,4 +191,6 @@ permissions: bookswithoutborders.reload: description: Allows player to reload this plugin bookswithoutborders.setgeneration: - description: Allows player to change the generation of a book (Original, Copy, Copy of Copy) \ No newline at end of file + description: Allows player to change the generation of a book (Original, Copy, Copy of Copy) + bookswithoutborders.peekbookshelf: + description: Allows player to left-click a bookshelf to see the contents of the shelf \ No newline at end of file diff --git a/src/test/java/net/knarcraft/bookswithoutborders/util/IntegerToRomanConverterTest.java b/src/test/java/net/knarcraft/bookswithoutborders/util/IntegerToRomanConverterTest.java new file mode 100644 index 0000000..3d25aec --- /dev/null +++ b/src/test/java/net/knarcraft/bookswithoutborders/util/IntegerToRomanConverterTest.java @@ -0,0 +1,40 @@ +package net.knarcraft.bookswithoutborders.util; + +import org.junit.Test; + +import static net.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter.getRomanNumber; +import static org.junit.Assert.assertEquals; + +/** + * A test class for IntegerToRomanConverter + */ +public class IntegerToRomanConverterTest { + + @Test + public void basicNumbersTest() { + assertEquals("I", getRomanNumber(1)); + assertEquals("II", getRomanNumber(2)); + assertEquals("III", getRomanNumber(3)); + assertEquals("IV", getRomanNumber(4)); + assertEquals("V", getRomanNumber(5)); + assertEquals("X", getRomanNumber(10)); + assertEquals("XV", getRomanNumber(15)); + assertEquals("XX", getRomanNumber(20)); + assertEquals("L", getRomanNumber(50)); + assertEquals("C", getRomanNumber(100)); + assertEquals("D", getRomanNumber(500)); + assertEquals("M", getRomanNumber(1000)); + } + + @Test + public void nineFourTest() { + assertEquals("IV", getRomanNumber(4)); + assertEquals("IX", getRomanNumber(9)); + assertEquals("XIV", getRomanNumber(14)); + assertEquals("XIX", getRomanNumber(19)); + assertEquals("XXIV", getRomanNumber(24)); + assertEquals("XL", getRomanNumber(40)); + assertEquals("IL", getRomanNumber(49)); + } + +}