From f9908db88f61d9d8131a82f29dbda5f56b1bec7b Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Fri, 23 Jun 2023 17:57:54 +0200 Subject: [PATCH] 1.20 update Adds an optional feature which displays the contents of a bookshelf when left-clicked. Sneaking must be used to destroy the bookshelf when enabled. Updates depreciated sign code. Bumps the API version to 1.20 Builds against 1.20 --- README.md | 9 +- pom.xml | 4 +- .../BooksWithoutBorders.java | 2 + .../config/BooksWithoutBordersConfig.java | 40 ++--- .../config/ConfigOption.java | 8 +- .../encryption/SubstitutionCipher.java | 2 + .../listener/BookshelfListener.java | 143 ++++++++++++++++++ .../listener/SignEventListener.java | 44 +++--- .../state/BookHoldingState.java | 8 +- .../bookswithoutborders/state/ItemSlot.java | 2 +- .../utility/IntegerToRomanConverter.java | 90 +++++++++++ src/main/resources/config.yml | 4 +- src/main/resources/plugin.yml | 7 +- .../util/IntegerToRomanConverterTest.java | 40 +++++ 14 files changed, 346 insertions(+), 57 deletions(-) create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/listener/BookshelfListener.java create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/utility/IntegerToRomanConverter.java create mode 100644 src/test/java/net/knarcraft/bookswithoutborders/util/IntegerToRomanConverterTest.java diff --git a/README.md b/README.md index e0b323c..387c8cb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Books without Borders has got your back! value - Change generation of books. Create tattered books for your RPG server! - Optionally, make it impossible to duplicate the original version of a book +- Optionally, hit a bookshelf to display the contained books. Sneak to destroy bookshelves with this enabled #### Group encryption @@ -98,6 +99,7 @@ An in-game description of available commands is available through the /bwb comma | bookswithoutborders.setbookprice | Allows player to set the cost of creating a book | | bookswithoutborders.setgeneration | Allows player to change the generation of a book (Original, Copy, Copy of Copy) | | bookswithoutborders.clear | Allows player to clear the contents of the held writable book | +| bookswithoutborders.peekbookshelf | Allows player to left-click a bookshelf to see the contents of the shelf | ### Signs @@ -106,13 +108,13 @@ line. #### Give sign -The **_give_** sign must have **\[Give]** on its second line. The third and fourth line contains the book to be loaded. +The **_give_**-sign must have **\[Give]** on its second line. The third and fourth line contains the book to be loaded. This can either be a numerical id pointing to a publicly saved book, or the full text identifier of the book (book name, author). #### Encrypt sign -The **_encrypt_** sign must have **\[Encrypt]** on its second line. The third line must contain the encryption key The +The **_encrypt_**-sign must have **\[Encrypt]** on its second line. The third line must contain the encryption key The fourth line can be empty or contain "dna" for dna-based encryption. #### Decrypt sign @@ -136,4 +138,5 @@ The **_decrypt_** sign must have **\[Decrypt]** on its second line. The third li | Author_Only_Unsign | Whether to only allow the author of a book to unsign it | | Author_Only_Save | Whether to only allow saving a player's own books with /savebook | | Format_Book_After_Signing | Whether to automatically format every book when it's signed | -| Change_Generation_On_Copy | 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 or tattered book cannot be copied further. | \ No newline at end of file +| Change_Generation_On_Copy | 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 or tattered book cannot be copied further. | +| Enable_Book_Peeking | Whether to enable hitting a chiseled bookshelf to see the shelf's contents. Sneaking is required to destroy a bookshelf if this is enabled. | \ No newline at end of file diff --git a/pom.xml b/pom.xml index 45dd668..b49da6e 100644 --- a/pom.xml +++ b/pom.xml @@ -103,7 +103,7 @@ org.spigotmc spigot-api - 1.19.2-R0.1-SNAPSHOT + 1.20.1-R0.1-SNAPSHOT provided @@ -121,7 +121,7 @@ org.jetbrains annotations - 23.0.0 + 24.0.1 provided diff --git a/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java b/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java index 0e37173..dc6140b 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java @@ -24,6 +24,7 @@ import net.knarcraft.bookswithoutborders.command.CommandSetTitle; import net.knarcraft.bookswithoutborders.command.CommandUnSign; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.listener.BookEventListener; +import net.knarcraft.bookswithoutborders.listener.BookshelfListener; import net.knarcraft.bookswithoutborders.listener.PlayerEventListener; import net.knarcraft.bookswithoutborders.listener.SignEventListener; import net.knarcraft.bookswithoutborders.utility.BookFileHelper; @@ -130,6 +131,7 @@ public class BooksWithoutBorders extends JavaPlugin { pluginManager.registerEvents(new PlayerEventListener(), this); pluginManager.registerEvents(new SignEventListener(), this); pluginManager.registerEvents(new BookEventListener(), this); + pluginManager.registerEvents(new BookshelfListener(), this); } else { this.getPluginLoader().disablePlugin(this); } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java b/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java index 24ad2d1..06ba658 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java @@ -40,6 +40,7 @@ public class BooksWithoutBordersConfig { private static boolean adminDecrypt; private static boolean formatBooks; private static boolean changeGenerationOnCopy; + private static boolean enableBookshelfPeeking; /** * Initializes the books without borders settings class @@ -127,6 +128,15 @@ public class BooksWithoutBordersConfig { return authorOnlySave; } + /** + * Gets whether players can left-click a bookshelf to peek at the contained books + * + * @return

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 config

The configuration to read from

- * @param configOption

The configuration option to get the value for

- * @return

The 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 config

The configuration to read from

- * @param configOption

The configuration option to get the value for

- * @return

The 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 bookshelfInventory

The inventory of the bookshelf to describe

+ * @return

A 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 bookMeta

The metadata for the book to describe

+ * @return

The 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 enchantmentStorageMeta

The metadata for the enchanted book to describe

+ * @return

The enchanted book's description

+ */ + private String getEnchantedBookDescription(EnchantmentStorageMeta enchantmentStorageMeta) { + StringBuilder builder = new StringBuilder(); + builder.append("Enchanted book ("); + Map enchantmentMap = enchantmentStorageMeta.getStoredEnchants(); + List enchantments = new ArrayList<>(enchantmentMap.size()); + for (Map.Entry enchantmentEntry : enchantmentMap.entrySet()) { + enchantments.add(getEnchantmentName(enchantmentEntry.getKey()) + " " + + IntegerToRomanConverter.getRomanNumber(enchantmentEntry.getValue())); + } + builder.append(String.join(", ", enchantments)); + builder.append(")"); + return builder.toString(); + } + + /** + * Gets a prettified name of an enchantment + * + * @param enchantment

The enchantment to get the name of

+ * @return

The 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 input

The input to uppercase

+ * @return

The 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 { * @return

The 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 { * @return

True 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 player

The 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 List romanValues = new ArrayList<>(); + private final static List romanCharacters = new ArrayList<>(); + + static { + // Initialize the roman numbers + romanValues.add(1000); + romanValues.add(500); + romanValues.add(100); + romanValues.add(50); + romanValues.add(10); + romanValues.add(5); + romanValues.add(1); + + romanCharacters.add('M'); + romanCharacters.add('D'); + romanCharacters.add('C'); + romanCharacters.add('L'); + romanCharacters.add('X'); + romanCharacters.add('V'); + romanCharacters.add('I'); + } + + private IntegerToRomanConverter() { + + } + + /** + * Gets the given number as a roman number string + * + * @param number

The number to convert

+ * @return

The 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 character

The character to repeat

+ * @param times

The number of times to repeat the character

+ * @return

The 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)); + } + +}