60 Commits

Author SHA1 Message Date
63ca95d247 Makes formatting on book signing require the format permission
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-14 20:01:34 +02:00
f05a15586a Makes some changes to formatting and un-signing
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
Makes the unsign command convert formatting codes to human editable ones
Makes the format command work on unsigned books
2025-08-14 19:08:21 +02:00
888287b447 Updates the README with new commands and options
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-14 02:20:31 +02:00
5d5ed725d9 Fixes an exception when trying to load an empty book list 2025-08-14 02:19:57 +02:00
bde43e78ca Prevents book migration from locking up the server thread
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-14 01:09:58 +02:00
790e3d1531 Adds a migrate command for fixing book names and text -> yml 2025-08-14 00:23:22 +02:00
4243c484c4 Merge branch 'refs/heads/dev' into string-formatting
# Conflicts:
#	src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java
2025-08-13 21:56:47 +02:00
baa0200f20 Fixes a missing space in a command prompt
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-13 21:41:41 +02:00
61957d0e1e Makes a string translatable
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-13 21:39:33 +02:00
f08d378295 Merge fixes
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-10 19:21:14 +02:00
e43649fef4 Merge branch 'refs/heads/dev' into string-formatting
# Conflicts:
#	src/main/java/net/knarcraft/bookswithoutborders/BooksWithoutBorders.java
2025-08-10 19:13:14 +02:00
af094f9931 Adds commands for adding book title pages and for deleting book pages
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-10 19:10:08 +02:00
324658070a Replaces a static string
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-10 16:55:31 +02:00
21b5b9647d Replaces static strings for some commands 2025-08-10 15:50:19 +02:00
32f0f9f7a1 Rewrites encryption
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
Adds an optional real encryption mode, which encrypts pages using AES, without saving plaintext.
Re-implements the old magic encryption in non-real encryption mode.
Fixes incorrect key generation for use in the substitution cipher and the gene cipher.
Removes the option for saving books as txt.
Adds tests for all encryption methods.
Saves all necessary decryption data when storing encrypted books.
Removes the old book updating code.
2025-08-10 14:23:18 +02:00
0ac051e24e Improves tab-completions for books with spaces
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-09 13:56:50 +02:00
fef38b894f Makes deletebook support filenames with spaces
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-08 15:23:07 +02:00
b5bff2400b Makes givebook support filenames with spaces 2025-08-08 15:05:54 +02:00
b963f83dee Supports spaces in author filtering
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-08 12:07:18 +02:00
a986411990 Improves tab-completion when loading books with spaces 2025-08-08 11:33:46 +02:00
6cd8895cce Adds full support for spaces in book names, and fixes unsigned book loading 2025-08-08 02:06:08 +02:00
f6d5108c7b Fixes an exception encountered when importing a text file as a book
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-08 00:49:54 +02:00
fb225a2351 Cleans up the decrypt command somewhat 2025-08-08 00:48:04 +02:00
e23e861e32 Fixes some problems related to the changed economy class 2025-08-07 20:33:54 +02:00
aa02f5ca2b Changes some static classes into instantiated classes 2025-08-07 20:10:30 +02:00
150dff7a03 Removes static strings from the clear command 2025-08-07 17:11:59 +02:00
ad08d65c80 Merge branch 'refs/heads/dev' into string-formatting 2025-08-07 16:42:34 +02:00
8affc42eaa Fixes a hard-coded , separator and improves book list formatting
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-07 16:35:43 +02:00
b1da544109 Starts adding string translations and cleanup 2025-08-07 16:00:37 +02:00
2146f00014 Fixes an error not differentiating between the two command variations
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-06 00:00:50 +02:00
14dd99ac85 Adds an error when /settitle is given a book name > 32 characters
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-05 16:27:33 +02:00
a3e3326786 Removes unnecessary duplicate file name cleaning 2025-08-05 04:49:19 +02:00
73157330d8 Enables bookshelf peeking by default 2025-08-05 04:48:31 +02:00
7f92588e32 Updates README
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-04 02:50:55 +02:00
bed0f4f518 Bumps version for development
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-04 02:42:59 +02:00
ebfca05859 Release
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-04 02:26:18 +02:00
3e3b8e7ad2 Fixes some capitalization
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 17:27:30 +02:00
b2ce31234d Adds ability to set name and lore shown when chiseled bookshelves are peeked
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 17:23:19 +02:00
e5aaa29c66 Fixes problems in sorting and generating the character indexes
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 04:45:21 +02:00
f9674568ba Improves the books in shelf description
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 04:21:05 +02:00
2203037b00 Adds a warning when trying to save books containing the title author separator in title or author
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 02:40:13 +02:00
6b44ada84a Improves some hover text
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 02:11:50 +02:00
9f979dd56e Makes the book list's colors more consistent
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 02:07:38 +02:00
10ffd17c04 Fixes a lot of nullability issues
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 01:28:20 +02:00
0aff3fad02 Adds book reloading, and fixes a RegEx expression
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-03 00:47:36 +02:00
32d31fced6 Fixes som nullability, and alters special character removal somewhat
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 21:35:26 +02:00
2627407e6b Adds ability to filter books by author
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 20:00:48 +02:00
175b66465a Moves book index generation to its own class
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 19:01:15 +02:00
36b57b9191 Removes some debug output, and fixes some formatting
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 18:07:52 +02:00
0d5ad490ff Fixes sorting and character indexes for filenames with color codes
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 18:02:33 +02:00
9e300afbef Adds missing spacing when replacing underscores in book names
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 08:02:09 +02:00
a84a56391a Prevents the § from being used in filenames
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 07:52:05 +02:00
b15ad18ae3 Makes it easier to manually go to any book page
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 07:09:07 +02:00
67ccdf3b1d Removes spacing for letter search
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 06:58:36 +02:00
b9bd686ae9 Removes the unnecessary display of manual command input in the book menu
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 06:52:23 +02:00
a5be6bb72c Adds ability to easier find books by first letter
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 06:47:46 +02:00
ed0a750eb4 Improves the book list somewhat
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-02 03:33:44 +02:00
6aa422d461 Fixes wrong color on the inactive next button
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-01 21:05:03 +02:00
4be023bd63 Adds a ChatComponent-enhanced book list
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-01 20:59:16 +02:00
35e98e0f18 Bumps version for development
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-28 04:24:40 +02:00
76 changed files with 4449 additions and 1348 deletions

117
README.md
View File

@@ -12,7 +12,7 @@ Books without Borders has got your back!
### Features
- Export written books and book and quills to .txt or .yml files
- Export written books and book and quills to .yml files
- Import books from files as written books or unsigned books
- Text files can be any length, and the import process fits the content to the correct page length
- Books can be saved privately, or to a directory visible server wide
@@ -24,46 +24,70 @@ Books without Borders has got your back!
- Configurable option to require certain items or pay via Vault compatible economy to create books via command
- Add lore to any item with a simple command
- Supports adding and saving color to title, lore, and book contents
- Color and formatting codes can be manually turned into formatting using /formatbook
- Clear a book, and start anew with /clearbook
- Color and formatting codes can be manually turned into formatting using `/formatbook`
- Clear a book, and start anew with `/clearbook`
- Formatting and color codes can be turned into formatting once any book is signed. This is enabled through a config
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 while sneaking to display the contained books
- Optionally, hit a bookshelf while sneaking to display the contained books. The bookshelf can be given a title and lore
with `/setBookshelfData`.
- Easily add a title page or chapter page (for an unsigned book, you can add a blank page as well)
with `/addBookTitlePage`.
- Remove extra blank pages or unneeded chapter pages with `/deleteBookPage`
#### Group encryption
- Group encryption allows every player with the bookswithoutborders.decrypt.\<group> permission to decrypt the encrypted
book without using a password.
### Migration from previous versions
- The `/migrateBooks` command allows for easy fixing of old book naming, changing the title author separator (the
default changed from `,` to `¤`, as a comma is a natural character to use in a title), or updating books saved as txt
to yml.
- Note that if real encryption is enabled, migrating the books will store them unencrypted (like they used to before
real encryption was implemented) afterward.
### Book formatting
- Formatting codes are automatically turned back into `&` codes after un-signing a book.
- `/formatbook` can be used on an unsigned book to preview formatting, but note that RGB colors will show up as
incorrect colors. That's just how that works. You must sign the book to see the real result.
- `/formatbook` can be used on a signed book (if `Format_Book_After_Signing` is disabled) in order to make any color or
formatting codes in the book display as intended.
### Commands:
An in-game description of available commands is available through the /bwb command.
| Command | Arguments | Description |
|----------------------|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| /bookswithoutborders | None | Displays information about commands (and permissions if the user has bookswithoutborders.admin) |
| /copybook | \<# of copies> | Copies the book the player is holding |
| /decryptbook | \<key> | Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt the held book |
| /deletebook | \<file name or number> | Deletes the specified file in the player's directory |
| /deletepublicbook | \<file name or number> | Same as deletebook, but deletes files in the public directory |
| /encryptbook | \<key> \[encryption style] | Encrypts the book the player is holding. "key" is required and can be any phrase or number excluding spaces. "style" is not required. Possible values are "DNA" or "" |
| /formatbook | None | Formats the held written book (converts color and formatting codes to the corresponding formatted text) |
| /givebook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | Gives the selected player a book from your personal directory |
| /givepublicbook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | Same as givebook, but uses books from the public directory |
| /loadbook | \<file name or number> \[# of copies] \[signed (true/false)] | Creates a book from the specified file and gives it to the player. If no file is specified, a list of available files is returned. If true is specified, the book will be signed, if false it will be unsigned |
| /loadpublicbook | \<file name or number> \[# of copies] \[signed (true/false)] | Same as loadbook, but views files in the public directory |
| /reload | None | Reloads BwB's configuration file |
| /savebook | \[overwrite (true/false)] | Saves the book the player is holding to a text file in a private directory. If true is specified, a book of the same name by the same author will be overwritten by the new book |
| /savepublicbook | \[overwrite (true/false)] | Same as savebook, but saves files in the public directory |
| /setbookauthor | \<author> | Sets the author of the book the player is holding |
| /setbookgeneration | \<generation> | Sets the generation of the held book (ORIGINAL, COPY_OF_ORIGINAL, COPY_OF_COPY, TATTERED) |
| /setbookprice | \<item/eco> \<quantity> | Sets the per-book price to create a book via commands. If "Item", the item in the player's hand in the amount of \<quantity> will be the price. If "Eco", a Vault based economy will be used for price. If neither \<Item/Eco> nor \<quantity> are specified, the current price to create books will be removed. |
| /setlore | \<new lore> | Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default) |
| /settitle | \<title> | Sets the title of the book/item the player is holding |
| /unsignbook | None | Un-signs the book the player is holding |
| /clearbook | None | Removes all text from the held un-signed book |
| Command | Arguments | Permission | Description |
|----------------------|----------------------------------------------------------------------------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| /bookswithoutborders | None | | Displays information about commands (and permissions if the user has bookswithoutborders.admin) |
| /copybook | \<# of copies> | bookswithoutborders.copy | Copies the book the player is holding |
| /decryptbook | \<key> | bookswithoutborders.decrypt | Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt the held book |
| /deletebook | \<file name or number> | bookswithoutborders.delete | Deletes the specified file in the player's directory |
| /deletepublicbook | \<file name or number> | bookswithoutborders.admin | Same as deletebook, but deletes files in the public directory |
| /encryptbook | \<key> \[encryption style] | bookswithoutborders.encrypt | Encrypts the book the player is holding. "key" is required and can be any phrase or number excluding spaces. "style" is not required. Possible values are "DNA" or "" |
| /formatbook | None | bookswithoutborders.format | Formats the held written book (converts color and formatting codes to the corresponding formatted text) |
| /givebook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | bookswithoutborders.give | Gives the selected player a book from your personal directory |
| /givepublicbook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | bookswithoutborders.givepublic | Same as givebook, but uses books from the public directory |
| /loadbook | \<file name or number> \[# of copies] \[signed (true/false)] | bookswithoutborders.load | Creates a book from the specified file and gives it to the player. If no file is specified, a list of available files is returned. If true is specified, the book will be signed, if false it will be unsigned |
| /loadpublicbook | \<file name or number> \[# of copies] \[signed (true/false)] | bookswithoutborders.loadpublic | Same as loadbook, but views files in the public directory |
| /reload | None | bookswithoutborders.reload | Reloads BwB's configuration file |
| /savebook | \[overwrite (true/false)] | bookswithoutborders.save | Saves the book the player is holding to a text file in a private directory. If true is specified, a book of the same name by the same author will be overwritten by the new book |
| /savepublicbook | \[overwrite (true/false)] | bookswithoutborders.savepublic | Same as savebook, but saves files in the public directory |
| /setbookauthor | \<author> | bookswithoutborders.setauthor | Sets the author of the book the player is holding |
| /setbookgeneration | \<generation> | bookswithoutborders.setgeneration | Sets the generation of the held book (ORIGINAL, COPY_OF_ORIGINAL, COPY_OF_COPY, TATTERED) |
| /setbookprice | \<item/eco> \<quantity> | bookswithoutborders.setbookprice | Sets the per-book price to create a book via commands. If "Item", the item in the player's hand in the amount of \<quantity> will be the price. If "Eco", a Vault based economy will be used for price. If neither \<Item/Eco> nor \<quantity> are specified, the current price to create books will be removed. |
| /setlore | \<new lore> | bookswithoutborders.setlore | Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default) |
| /settitle | \<title> | bookswithoutborders.settitle | Sets the title of the book/item the player is holding |
| /unsignbook | None | bookswithoutborders.unsign | Un-signs the book the player is holding |
| /clearbook | None | bookswithoutborders.clear | Removes all text from the held un-signed book |
| /setBookshelfData | \<delete/name/lore> \text> \[more text] | bookswithoutborders.editbookshelf | Sets the name/lore for a bookshelf which is shown when peeking at its contents. |
| /addBookTitlePage | \[page index] \[title~description] | bookswithoutborders.addtitlepage | Adds a blank page, title page or chapter page depending on input and whether the book is signed |
| /deleteBookPage | \<page> | bookswithoutborders.deletepage | Deletes one page from a book |
| /migrateBooks | None | bookswithoutborders.admin | Migrates all txt books to yml, and fixes any incorrect filenames. |
### Permissions:
@@ -72,7 +96,7 @@ An in-game description of available commands is available through the /bwb comma
| bookswithoutborders.* | Grants all permissions |
| bookswithoutborders.admin | Grants all permissions |
| bookswithoutborders.use | bookswithoutborders.use - Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled |
| bookswithoutborders.alterbooks | Allows player to change books' data such as lore/title/author/generation/formatting and un-signing books |
| bookswithoutborders.alterbooks | Allows player to change books' data such as lore/title/author/generation/formatting and un-signing books, and setting bookshelf data |
| bookswithoutborders.reload | Allows player to reload this plugin |
| bookswithoutborders.format | Allows a player to format a book |
| bookswithoutborders.save | Allows a player to save books to their personal directory |
@@ -100,6 +124,9 @@ An in-game description of available commands is available through the /bwb comma
| 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 |
| bookswithoutborders.editbookshelf | Allows player to set name/lore for bookshelves, used for peeking |
| bookswithoutborders.addtitlepage | Allows player to add a blank title page to a book |
| bookswithoutborders.deletepage | Allows player to delete a page from a book |
### Signs
@@ -123,20 +150,20 @@ The **_decrypt_** sign must have **\[Decrypt]** on its second line. The third li
### Configuration options:
| Option | Description |
|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Save_Books_in_Yaml_Format | Whether to use YAML for saved books instead of just storing them as text |
| Max_Number_of_Duplicates | The maximum number of duplicates of a saved book allowed |
| Author_Separator | The separator used to separate the book title and the book author |
| Lore_line_separator | The separator used to denote a new line in the book/item lore |
| Books_for_new_players | A list of books given to new players the first time they join the server |
| Message_for_new_players | An optional message displayed to new players the first time they join the server |
| Price_to_create_book.Item_type | The item type used as currency for copying books. Use "Economy" to use money instead of items |
| Price_to_create_book.Required_quantity | The quantity of currency required to pay for each book produced |
| Admin_Auto_Decrypt | Whether any admin can decrypt any book regardless of the group it was encrypted for |
| Author_Only_Copy | Whether to only allow the author of a book to create copies |
| 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. |
| Enable_Book_Peeking | Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents. |
| Option | Description |
|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Max_Number_of_Duplicates | The maximum number of duplicates of a saved book allowed |
| Author_Separator | The separator used to separate the book title and the book author |
| Lore_line_separator | The separator used to denote a new line in the book/item lore |
| Books_for_new_players | A list of books given to new players the first time they join the server |
| Message_for_new_players | An optional message displayed to new players the first time they join the server |
| Price_to_create_book.Item_type | The item type used as currency for copying books. Use "Economy" to use money instead of items |
| Price_to_create_book.Required_quantity | The quantity of currency required to pay for each book produced |
| Admin_Auto_Decrypt | Whether any admin can decrypt any book regardless of the group it was encrypted for |
| Author_Only_Copy | Whether to only allow the author of a book to create copies |
| 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. |
| Enable_Book_Peeking | Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents. |
| Use_Real_Encryption | Enables true AES encryption instead of the very fake legacy encryption. The encryption key is stored in the book file to allow admin decryption, but looking at the encrypted book in the file system, only reveals the encrypted pages. |

View File

@@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId>
<artifactId>BooksWithoutBorders</artifactId>
<version>1.3.8</version>
<version>1.3.10-SNAPSHOT</version>
<packaging>jar</packaging>
<licenses>
@@ -20,7 +20,7 @@
<description>A continuation of the original Books Without Borders</description>
<properties>
<java.version>16</java.version>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

View File

@@ -1,10 +1,12 @@
package net.knarcraft.bookswithoutborders;
import net.knarcraft.bookswithoutborders.command.CommandAddTitlePage;
import net.knarcraft.bookswithoutborders.command.CommandBooksWithoutBorders;
import net.knarcraft.bookswithoutborders.command.CommandClear;
import net.knarcraft.bookswithoutborders.command.CommandCopy;
import net.knarcraft.bookswithoutborders.command.CommandDecrypt;
import net.knarcraft.bookswithoutborders.command.CommandDelete;
import net.knarcraft.bookswithoutborders.command.CommandDeletePage;
import net.knarcraft.bookswithoutborders.command.CommandDeletePublic;
import net.knarcraft.bookswithoutborders.command.CommandEncrypt;
import net.knarcraft.bookswithoutborders.command.CommandFormat;
@@ -13,25 +15,35 @@ import net.knarcraft.bookswithoutborders.command.CommandGivePublic;
import net.knarcraft.bookswithoutborders.command.CommandGroupEncrypt;
import net.knarcraft.bookswithoutborders.command.CommandLoad;
import net.knarcraft.bookswithoutborders.command.CommandLoadPublic;
import net.knarcraft.bookswithoutborders.command.CommandMigrate;
import net.knarcraft.bookswithoutborders.command.CommandReload;
import net.knarcraft.bookswithoutborders.command.CommandSave;
import net.knarcraft.bookswithoutborders.command.CommandSavePublic;
import net.knarcraft.bookswithoutborders.command.CommandSetAuthor;
import net.knarcraft.bookswithoutborders.command.CommandSetBookPrice;
import net.knarcraft.bookswithoutborders.command.CommandSetBookshelfData;
import net.knarcraft.bookswithoutborders.command.CommandSetGeneration;
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.BwBCommand;
import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.handler.BookshelfHandler;
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;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.formatting.Translator;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.UpdateChecker;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
@@ -39,38 +51,72 @@ import org.bukkit.inventory.ItemFactory;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getErrorColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSuccessColor;
/**
* The main Books Without Borders class
*/
public class BooksWithoutBorders extends JavaPlugin {
private static ItemFactory itemFactory;
private static Map<UUID, List<String>> playerBooksList;
private static List<String> publicBooksList;
private static BooksWithoutBorders booksWithoutBorders;
private static ConsoleCommandSender consoleSender;
private ItemFactory itemFactory;
private @NotNull Map<UUID, List<String>> playerBooksList = new HashMap<>();
private @NotNull List<String> publicBooksList = new ArrayList<>();
private Map<Character, Integer> publicLetterIndex;
private Map<UUID, Map<Character, Integer>> playerLetterIndex;
private BookshelfHandler bookshelfHandler;
private StringFormatter stringFormatter;
private BooksWithoutBordersConfig booksWithoutBordersConfig;
private final Queue<MigrationRequest> migrationQueue = new LinkedList<>();
/**
* Gets the console sender for printing to the console
* Logs a message to the console
*
* @return <p>The console's console sender</p>
* @param level <p>The log level to use</p>
* @param message <p>The message to log</p>
*/
public static ConsoleCommandSender getConsoleSender() {
return consoleSender;
public static void log(@NotNull Level level, @NotNull String message) {
getInstance().getLogger().log(level, message);
}
/**
* Gets the configuration for BwB
*
* @return <p>The BwB configuration</p>
*/
public static BooksWithoutBordersConfig getConfiguration() {
return getInstance().booksWithoutBordersConfig;
}
/**
* Gets the string formatter
*
* @return <p>The string formatter</p>
*/
public static StringFormatter getStringFormatter() {
return getInstance().stringFormatter;
}
/**
* Gets the migration queue
*
* @return <p>The migration queue</p>
*/
public static Queue<MigrationRequest> getMigrationQueue() {
return getInstance().migrationQueue;
}
/**
@@ -80,36 +126,75 @@ public class BooksWithoutBorders extends JavaPlugin {
* @param getPublic <p>Whether to get available public books</p>
* @return <p>A list of available books</p>
*/
public static List<String> getAvailableBooks(CommandSender sender, boolean getPublic) {
@NotNull
public static List<String> getAvailableBooks(@NotNull CommandSender sender, boolean getPublic) {
if (getPublic) {
return new ArrayList<>(publicBooksList);
return new ArrayList<>(getInstance().publicBooksList);
} else if (sender instanceof Player player) {
UUID playerUUID = player.getUniqueId();
if (!playerBooksList.containsKey(playerUUID)) {
if (!getInstance().playerBooksList.containsKey(playerUUID)) {
List<String> newFiles = BookFileHelper.listFiles(sender, false);
playerBooksList.put(playerUUID, newFiles);
if (newFiles != null) {
getInstance().playerBooksList.put(playerUUID, newFiles);
getInstance().playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles));
}
}
List<String> books = getInstance().playerBooksList.get(playerUUID);
if (books != null) {
return new ArrayList<>(books);
} else {
return new ArrayList<>();
}
return playerBooksList.get(playerUUID);
} else {
return new ArrayList<>();
}
}
/**
* Gets the letter index map for public books, or a specific player's books
*
* @param playerIndex <p>The player to get the index for, or null for the public index</p>
* @return <p>An index mapping between a character and the first index containing that character</p>
*/
@NotNull
public static Map<Character, Integer> getLetterIndex(@Nullable UUID playerIndex) {
if (playerIndex == null) {
return getInstance().publicLetterIndex;
} else {
Map<Character, Integer> letterIndex = getInstance().playerLetterIndex.get(playerIndex);
return Objects.requireNonNullElseGet(letterIndex, HashMap::new);
}
}
/**
* Updates available books
*
* @param sender <p>The sender to update books for</p>
* @param updatePublic <p>Whether to update public books</p>
*/
public static void updateBooks(CommandSender sender, boolean updatePublic) {
public static void updateBooks(@NotNull CommandSender sender, boolean updatePublic) {
List<String> newFiles = BookFileHelper.listFiles(sender, updatePublic);
if (newFiles == null) {
return;
}
if (updatePublic) {
publicBooksList = newFiles;
getInstance().publicBooksList = newFiles;
getInstance().publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles);
} else if (sender instanceof Player player) {
playerBooksList.put(player.getUniqueId(), newFiles);
getInstance().playerBooksList.put(player.getUniqueId(), newFiles);
getInstance().playerLetterIndex.put(player.getUniqueId(), BookFileHelper.populateLetterIndices(newFiles));
}
}
/**
* Clears book data such as per-player lists and per-player character indexes
*/
public static void clearBookData() {
getInstance().playerBooksList = new HashMap<>();
getInstance().playerLetterIndex = new HashMap<>();
}
@Override
public void onEnable() {
FileConfiguration config = this.getConfig();
@@ -120,15 +205,28 @@ public class BooksWithoutBorders extends JavaPlugin {
PluginDescriptionFile pluginDescriptionFile = this.getDescription();
String pluginVersion = pluginDescriptionFile.getVersion();
Translator translator = new Translator();
translator.registerMessageCategory(Translatable.SUCCESS_COPY);
stringFormatter = new StringFormatter(this.getDescription().getName(), translator);
stringFormatter.setColorConversion(ColorConversion.RGB);
stringFormatter.setSuccessColor(ChatColor.of("#A9FF84"));
stringFormatter.setErrorColor(ChatColor.of("#FF84A9"));
booksWithoutBorders = this;
consoleSender = this.getServer().getConsoleSender();
playerBooksList = new HashMap<>();
BooksWithoutBordersConfig.initialize(this);
publicBooksList = BookFileHelper.listFiles(consoleSender, true);
playerLetterIndex = new HashMap<>();
booksWithoutBordersConfig = new BooksWithoutBordersConfig(this, translator);
@Nullable List<String> files = BookFileHelper.listFiles(this.getServer().getConsoleSender(), true);
if (files != null) {
publicBooksList = files;
publicLetterIndex = BookFileHelper.populateLetterIndices(files);
}
bookshelfHandler = new BookshelfHandler();
bookshelfHandler.load();
PluginManager pluginManager = this.getServer().getPluginManager();
if (getSlash() != null && initialize()) {
if (getConfiguration().getSlash() != null && initialize()) {
pluginManager.registerEvents(new PlayerEventListener(), this);
pluginManager.registerEvents(new SignEventListener(), this);
pluginManager.registerEvents(new BookEventListener(), this);
@@ -148,6 +246,7 @@ public class BooksWithoutBorders extends JavaPlugin {
*
* @return <p>An instance of this plugin</p>
*/
@NotNull
public static BooksWithoutBorders getInstance() {
return booksWithoutBorders;
}
@@ -156,28 +255,32 @@ public class BooksWithoutBorders extends JavaPlugin {
* Registers all commands used by this plugin
*/
private void registerCommands() {
registerCommand("giveBook", new CommandGive());
registerCommand("givePublicBook", new CommandGivePublic());
registerCommand("decryptBook", new CommandDecrypt());
registerCommand("groupEncryptBook", new CommandGroupEncrypt());
registerCommand("deleteBook", new CommandDelete());
registerCommand("deletePublicBook", new CommandDeletePublic());
registerCommand("copyBook", new CommandCopy());
registerCommand("unSignBook", new CommandUnSign());
registerCommand("encryptBook", new CommandEncrypt());
registerCommand("setBookPrice", new CommandSetBookPrice());
registerCommand("setLore", new CommandSetLore());
registerCommand("savePublicBook", new CommandSavePublic());
registerCommand("saveBook", new CommandSave());
registerCommand("setBookAuthor", new CommandSetAuthor());
registerCommand("setTitle", new CommandSetTitle());
registerCommand("loadBook", new CommandLoad());
registerCommand("loadPublicBook", new CommandLoadPublic());
registerCommand("booksWithoutBorders", new CommandBooksWithoutBorders());
registerCommand("reload", new CommandReload());
registerCommand("formatBook", new CommandFormat());
registerCommand("setBookGeneration", new CommandSetGeneration());
registerCommand("clearBook", new CommandClear());
registerCommand(BwBCommand.GIVE_BOOK.toString(), new CommandGive());
registerCommand(BwBCommand.GIVE_PUBLIC_BOOK.toString(), new CommandGivePublic());
registerCommand(BwBCommand.DECRYPT_BOOK.toString(), new CommandDecrypt());
registerCommand(BwBCommand.GROUP_ENCRYPT_BOOK.toString(), new CommandGroupEncrypt());
registerCommand(BwBCommand.DELETE_BOOK.toString(), new CommandDelete());
registerCommand(BwBCommand.DELETE_PUBLIC_BOOK.toString(), new CommandDeletePublic());
registerCommand(BwBCommand.COPY_BOOK.toString(), new CommandCopy());
registerCommand(BwBCommand.UNSIGN_BOOK.toString(), new CommandUnSign());
registerCommand(BwBCommand.ENCRYPT_BOOK.toString(), new CommandEncrypt());
registerCommand(BwBCommand.SET_BOOK_PRICE.toString(), new CommandSetBookPrice());
registerCommand(BwBCommand.SET_LORE.toString(), new CommandSetLore());
registerCommand(BwBCommand.SAVE_PUBLIC_BOOK.toString(), new CommandSavePublic());
registerCommand(BwBCommand.SAVE_BOOK.toString(), new CommandSave());
registerCommand(BwBCommand.SET_BOOK_AUTHOR.toString(), new CommandSetAuthor());
registerCommand(BwBCommand.SET_TITLE.toString(), new CommandSetTitle());
registerCommand(BwBCommand.LOAD_BOOK.toString(), new CommandLoad());
registerCommand(BwBCommand.LOAD_PUBLIC_BOOK.toString(), new CommandLoadPublic());
registerCommand(BwBCommand.BOOKS_WITHOUT_BORDERS.toString(), new CommandBooksWithoutBorders());
registerCommand(BwBCommand.RELOAD.toString(), new CommandReload());
registerCommand(BwBCommand.FORMAT_BOOK.toString(), new CommandFormat());
registerCommand(BwBCommand.SET_BOOK_GENERATION.toString(), new CommandSetGeneration());
registerCommand(BwBCommand.CLEAR_BOOK.toString(), new CommandClear());
registerCommand(BwBCommand.SET_BOOKSHELF_DATA.toString(), new CommandSetBookshelfData());
registerCommand(BwBCommand.ADD_TITLE_PAGE.toString(), new CommandAddTitlePage());
registerCommand(BwBCommand.DELETE_PAGE.toString(), new CommandDeletePage());
registerCommand(BwBCommand.MIGRATE.toString(), new CommandMigrate());
}
/**
@@ -186,12 +289,12 @@ public class BooksWithoutBorders extends JavaPlugin {
* @param commandName <p>The name of the command to register</p>
* @param executor <p>The executor to register for the command</p>
*/
private void registerCommand(String commandName, CommandExecutor executor) {
private void registerCommand(@NotNull String commandName, @NotNull CommandExecutor executor) {
PluginCommand pluginCommand = this.getCommand(commandName);
if (pluginCommand != null) {
pluginCommand.setExecutor(executor);
} else {
sendErrorMessage(consoleSender, "Failed to register command " + commandName);
getLogger().log(Level.SEVERE, "Failed to register command " + commandName);
}
}
@@ -204,31 +307,37 @@ public class BooksWithoutBorders extends JavaPlugin {
//Initialize Item Factory
try {
itemFactory = this.getServer().getItemFactory();
} catch (java.lang.NoSuchMethodError nsmE) {
sendErrorMessage(consoleSender, "Warning! [BooksWithoutBorders] failed to initialize!");
sendErrorMessage(consoleSender, "Please confirm the correct version of [BooksWithoutBorders] is");
sendErrorMessage(consoleSender, "being run for this version of bukkit!");
return false;
}
//Load config
if (!BooksWithoutBordersConfig.loadConfig()) {
} catch (java.lang.NoSuchMethodError noSuchMethodError) {
getLogger().log(Level.SEVERE, """
Warning! [BooksWithoutBorders] failed to initialize!
Please confirm the correct version of [BooksWithoutBorders] is
being run for this version of spigot!""");
return false;
}
//Save config with loaded values to fix invalid config values
BooksWithoutBordersConfig.saveConfigValues();
getConfiguration().saveConfigValues();
return testFileSaving();
}
/**
* Gets the bookshelf handler
*
* @return <p>The bookshelf handler</p>
*/
public static BookshelfHandler getBookshelfHandler() {
return getInstance().bookshelfHandler;
}
/**
* Gets the server's item factory
*
* @return <p>The server's item factory</p>
*/
@NotNull
public static ItemFactory getItemFactory() {
return itemFactory;
return getInstance().itemFactory;
}
/**
@@ -237,27 +346,27 @@ public class BooksWithoutBorders extends JavaPlugin {
* @return <p>True if necessary folders exist</p>
*/
private boolean testFileSaving() {
File fileTest = new File(getBookFolder());
File encryptedFileTest = new File(getBookFolder() + "Encrypted" + getSlash());
File fileTest = new File(getConfiguration().getBookFolder());
File encryptedFileTest = new File(getConfiguration().getEncryptedBookPath());
if (!fileTest.exists()) {
try {
if (!fileTest.mkdir()) {
sendErrorMessage(consoleSender, "Saving failed! Aborting...");
getLogger().log(Level.SEVERE, StaticMessage.BOOK_SAVING_FAILED.toString());
return false;
}
} catch (Exception exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create necessary folders");
getLogger().log(Level.SEVERE, StaticMessage.BOOK_FOLDER_CREATE_FAILED.toString());
return false;
}
}
if (!encryptedFileTest.exists()) {
try {
if (!encryptedFileTest.mkdir()) {
sendErrorMessage(consoleSender, "Saving failed! Aborting...");
getLogger().log(Level.SEVERE, StaticMessage.BOOK_SAVING_FAILED.toString());
return false;
}
} catch (Exception exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create necessary folders");
getLogger().log(Level.SEVERE, StaticMessage.BOOK_FOLDER_CREATE_FAILED.toString());
return false;
}
}
@@ -270,8 +379,8 @@ public class BooksWithoutBorders extends JavaPlugin {
* @param sender <p>The sender to send the message to</p>
* @param message <p>The message to send</p>
*/
public static void sendSuccessMessage(CommandSender sender, String message) {
sender.sendMessage(getSuccessColor() + message);
public static void sendSuccessMessage(@NotNull CommandSender sender, @NotNull String message) {
sender.sendMessage(getConfiguration().getSuccessColor() + message);
}
/**
@@ -280,8 +389,8 @@ public class BooksWithoutBorders extends JavaPlugin {
* @param sender <p>The sender to send the message to</p>
* @param message <p>The message to send</p>
*/
public static void sendErrorMessage(CommandSender sender, String message) {
sender.sendMessage(getErrorColor() + message);
public static void sendErrorMessage(@NotNull CommandSender sender, @NotNull String message) {
sender.sendMessage(getConfiguration().getErrorColor() + message);
}
}

View File

@@ -0,0 +1,157 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A command for adding a title page to a book
*/
public class CommandAddTitlePage implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player);
if (heldBook == null) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_NOT_HOLDING_ANY_BOOK);
return false;
}
int index;
if (arguments.length < 1) {
if (InventoryHelper.notHoldingOneWrittenBookCheck(player, "You must be holding a written book to " +
"add an author title page!", "You cannot add an author title page to two books at once!")) {
return false;
}
index = 0;
} else {
try {
index = Integer.parseInt(arguments[0]) - 1;
} catch (NumberFormatException exception) {
BooksWithoutBorders.sendErrorMessage(sender, "Invalid page index given!");
return false;
}
}
String title = null;
if (arguments.length > 1) {
// Get all arguments as a space-separated string
StringBuilder builder = new StringBuilder(arguments[1]);
for (int i = 2; i < arguments.length; i++) {
builder.append(" ").append(arguments[i]);
}
title = builder.toString();
}
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unable to get metadata for the held book!");
return false;
}
List<String> pages = new ArrayList<>(bookMeta.getPages());
if (index < 0) {
BooksWithoutBorders.sendErrorMessage(sender, "The given page index is out of bounds!");
return false;
}
if (title == null && heldBook.getType() == Material.WRITTEN_BOOK) {
String loreSeparator = BooksWithoutBorders.getConfiguration().getLoreSeparator();
if (index > pages.size()) {
pages.add(formatTitle(bookMeta.getTitle() + loreSeparator + "By: " + bookMeta.getAuthor()));
} else {
pages.add(index, formatTitle(bookMeta.getTitle() + loreSeparator + "By: " + bookMeta.getAuthor()));
}
} else if (title == null) {
if (index > pages.size()) {
pages.add("");
} else {
pages.add(index, "");
}
} else {
if (index > pages.size()) {
pages.add(formatTitle(title));
} else {
pages.add(index, formatTitle(title));
}
}
bookMeta.setPages(pages);
heldBook.setItemMeta(bookMeta);
BooksWithoutBorders.sendSuccessMessage(sender, "Title page added!");
return true;
}
/**
* Formats a book title
*
* @param input <p>The input to format</p>
* @return <p>The formatted input</p>
*/
private String formatTitle(@NotNull String input) {
String loreSeparator = BooksWithoutBorders.getConfiguration().getLoreSeparator();
if (input.contains(loreSeparator)) {
String[] parts = input.split(loreSeparator);
StringBuilder output = new StringBuilder("\n");
output.append(ChatColor.UNDERLINE).append(ChatColor.BOLD).append(BookFormatter.stripColor(parts[0])).append(ChatColor.RESET);
for (int i = 1; i < parts.length; i++) {
output.append("\n").append("\n").append(ChatColor.ITALIC).append(BookFormatter.stripColor(parts[i])).append(ChatColor.RESET);
}
return output.toString();
} else {
return ChatColor.UNDERLINE + ChatColor.BOLD.toString() + input;
}
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
if (!(commandSender instanceof Player player)) {
return List.of("1", "2", "3", "4");
}
ItemStack heldBook = InventoryHelper.getHeldBook(player);
if (heldBook != null) {
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta != null) {
List<String> pages = new ArrayList<>();
pages.add("1");
for (int i = 1; i <= bookMeta.getPages().size(); i++) {
pages.add(String.valueOf(i + 1));
}
return pages;
}
}
} else if (arguments.length == 2) {
return List.of("Title", "Chapter~Description");
}
return List.of();
}
}

View File

@@ -2,7 +2,12 @@ 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.config.BwBCommand;
import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.manager.EconomyManager;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -13,10 +18,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendErrorMessage;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getCommandColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSuccessColor;
import java.util.logging.Level;
/**
* Command executor for the books without borders (bwb) command
@@ -24,29 +26,75 @@ import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig
public class CommandBooksWithoutBorders implements TabExecutor {
@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);
} else {
showConsoleCommands(sender);
}
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
String header = stringFormatter.replacePlaceholders(Translatable.NEUTRAL_COMMANDS_HEADER,
List.of("{bookPrice}", "{commands}"), List.of(getBookPrice(), getCommands(sender)));
sender.sendMessage(header);
return true;
}
/**
* Gets the list of commands
*
* @param sender <p>The command sender trying to see available commands</p>
* @return <p>The string representation of all commands</p>
*/
@NotNull
private String getCommands(@NotNull CommandSender sender) {
if (sender instanceof Player) {
return showPlayerCommands(sender);
} else {
return showConsoleCommands(sender);
}
}
/**
* Gets the price of duplicating a book
*
* @return <p>The book price</p>
*/
@NotNull
private String getBookPrice() {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
if (!config.booksHavePrice()) {
return "";
}
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
Material bookPriceType = config.getBookPriceType();
double bookPriceQuantity = config.getBookPriceQuantity();
if (bookPriceType != Material.AIR) {
return stringFormatter.replacePlaceholders(Translatable.NEUTRAL_COMMANDS_BOOK_PRICE_ITEM,
List.of("{quantity}", "{type}"),
List.of(String.valueOf((int) bookPriceQuantity), bookPriceType.toString()));
} else {
EconomyManager economyManager = BooksWithoutBorders.getConfiguration().getEconomyManager();
if (economyManager.getEconomy() == null) {
return BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.ERROR_VAULT_COST_BUT_UNAVAILABLE) + "\n";
} else {
return stringFormatter.replacePlaceholder(Translatable.NEUTRAL_COMMANDS_BOOK_PRICE_ECO,
"{price}", economyManager.getEconomy().format(bookPriceQuantity));
}
}
}
/**
* Shows all commands available to the console
*
* @param sender <p>The console which sent the command</p>
*/
private void showConsoleCommands(CommandSender sender) {
sender.sendMessage(getCommandColor() + "Commands:");
showCommandInfo("deletePublicBook", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("reload", sender);
@NotNull
private String showConsoleCommands(@NotNull CommandSender sender) {
StringBuilder builder = new StringBuilder();
for (BwBCommand command : BwBCommand.values()) {
if (!command.requiresPlayer()) {
builder.append(showCommandInfo(command.toString(), sender));
}
}
return builder.toString();
}
/**
@@ -54,41 +102,13 @@ public class CommandBooksWithoutBorders implements TabExecutor {
*
* @param sender <p>The player which sent the command</p>
*/
private void showPlayerCommands(CommandSender sender) {
//Lists all commands
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]");
} else {
sendErrorMessage(sender, "[" + EconomyHelper.getEconomy().format(bookPriceQuantity) +
" is required to create a book]");
}
@NotNull
private String showPlayerCommands(@NotNull CommandSender sender) {
StringBuilder builder = new StringBuilder();
for (BwBCommand command : BwBCommand.values()) {
builder.append(showCommandInfo(command.toString(), sender));
}
sender.sendMessage(getCommandColor() + "Commands:");
showCommandInfo("copyBook", sender);
showCommandInfo("decryptBook", sender);
showCommandInfo("deleteBook", sender);
showCommandInfo("deletePublicBook", sender);
showCommandInfo("encryptBook", sender);
showCommandInfo("formatBook", sender);
showCommandInfo("giveBook", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("groupEncryptBook", sender);
showCommandInfo("loadBook", sender);
showCommandInfo("loadPublicBook", sender);
showCommandInfo("reload", sender);
showCommandInfo("saveBook", sender);
showCommandInfo("savePublicBook", sender);
showCommandInfo("setAuthor", sender);
showCommandInfo("setBookGeneration", sender);
showCommandInfo("setBookPrice", sender);
showCommandInfo("setLore", sender);
showCommandInfo("setTitle", sender);
showCommandInfo("unsignBook", sender);
return builder.toString();
}
/**
@@ -97,26 +117,37 @@ public class CommandBooksWithoutBorders implements TabExecutor {
* @param command <p>The command to get information about</p>
* @param sender <p>The sender asking to see command info</p>
*/
private void showCommandInfo(String command, CommandSender sender) {
@NotNull
private String showCommandInfo(@NotNull String command, @NotNull CommandSender sender) {
PluginCommand pluginCommand = BooksWithoutBorders.getInstance().getCommand(command);
if (pluginCommand != null) {
String permission = pluginCommand.getPermission();
if (permission == null || sender.hasPermission(permission)) {
String commandInfo = "\n" + getCommandColor() + pluginCommand.getUsage().replace("<command>",
pluginCommand.getName()) + ": " + getSuccessColor() + pluginCommand.getDescription();
if (sender.hasPermission("bookswithoutborders.admin")) {
if (permission == null) {
permission = "None";
}
commandInfo += getCommandColor() + " {" + permission + "}";
}
sender.sendMessage(commandInfo);
}
if (pluginCommand == null) {
BooksWithoutBorders.log(Level.SEVERE, StringFormatter.replacePlaceholder(
StaticMessage.COMMAND_NOT_REGISTERED.toString(), "{command}", command));
return "";
}
String permission = pluginCommand.getPermission();
if (permission != null && !sender.hasPermission(permission)) {
return "";
}
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
String commandDescription = stringFormatter.replacePlaceholders(Translatable.NEUTRAL_COMMANDS_COMMAND,
List.of("{usage}", "{description}"), List.of(pluginCommand.getUsage().replace("<command>",
pluginCommand.getName()), pluginCommand.getDescription()));
if (sender.hasPermission(Permission.ADMIN.toString())) {
if (permission == null) {
permission = stringFormatter.getUnFormattedColoredMessage(Translatable.NEUTRAL_COMMANDS_COMMAND_NO_PERMISSION_REQUIRED);
}
commandDescription += stringFormatter.replacePlaceholder(Translatable.NEUTRAL_COMMANDS_COMMAND_PERMISSION, "{permission}", permission);
}
return commandDescription;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return new ArrayList<>();
}

View File

@@ -1,7 +1,9 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@@ -22,13 +24,18 @@ public class CommandClear implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
if (InventoryHelper.notHoldingOneWritableBookCheck(player, "You must be holding a writable book to " +
"clear it!", "You cannot clear two books at once!")) {
if (InventoryHelper.notHoldingOneWritableBookCheck(player,
stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITABLE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_CLEAR)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_CLEAR)))) {
return false;
}
@@ -36,7 +43,7 @@ public class CommandClear implements TabExecutor {
ItemStack heldBook = InventoryHelper.getHeldBook(player, false);
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unable to get metadata for the held book!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_METADATA_MISSING);
return false;
}
bookMeta.setPages("");
@@ -44,14 +51,14 @@ public class CommandClear implements TabExecutor {
bookMeta.setGeneration(null);
bookMeta.setTitle(null);
heldBook.setItemMeta(bookMeta);
BooksWithoutBorders.sendSuccessMessage(sender, "Book cleared!");
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_CLEARED);
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
@NotNull String[] arguments) {
return new ArrayList<>();
}

View File

@@ -2,10 +2,12 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.Material;
import org.bukkit.command.Command;
@@ -26,32 +28,38 @@ import java.util.Objects;
public class CommandCopy implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
if (InventoryHelper.notHoldingOneWrittenBookCheck(player, "You must be holding a written book to copy it!",
"You cannot copy two books at once!")) {
if (InventoryHelper.notHoldingOneWrittenBookCheck(player,
stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITTEN_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_COPY)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_COPY)))) {
return false;
}
if (args.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "You must specify the number of copies to be made!");
if (arguments.length < 1) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_COPY_COUNT_NOT_SPECIFIED);
return false;
}
try {
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
int copies = Integer.parseInt(args[0]);
int copies = Integer.parseInt(arguments[0]);
if (copies <= 0) {
throw new NumberFormatException("Number of copies must be larger than 0");
stringFormatter.displayErrorMessage(player, Translatable.ERROR_COPY_NEGATIVE_AMOUNT);
return false;
}
return performCopy(copies, player, heldBook);
} catch (NumberFormatException ignored) {
BooksWithoutBorders.sendErrorMessage(player, "Book not copied!");
BooksWithoutBorders.sendErrorMessage(player, "Number specified was invalid!");
stringFormatter.displayErrorMessage(player, Translatable.ERROR_COPY_INVALID_AMOUNT);
return false;
}
}
@@ -64,17 +72,19 @@ public class CommandCopy implements TabExecutor {
* @param heldBook <p>The book to be copied</p>
* @return <p>True if the copying was successful</p>
*/
private boolean performCopy(int copies, Player player, ItemStack heldBook) {
private boolean performCopy(int copies, @NotNull Player player, @NotNull ItemStack heldBook) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//Make sure the player owns the book if authorOnlyCopy is enabled
if (BooksWithoutBordersConfig.getAuthorOnlyCopy() &&
!player.hasPermission("bookswithoutborders.bypassAuthorOnlyCopy")) {
if (config.getAuthorOnlyCopy() &&
!player.hasPermission(Permission.BYPASS_AUTHOR_ONLY_COPY.toString())) {
if (BookHelper.isNotAuthor(player, (BookMeta) Objects.requireNonNull(heldBook.getItemMeta()))) {
return false;
}
}
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (BooksWithoutBordersConfig.changeGenerationOnCopy() && bookMeta != null) {
if (config.changeGenerationOnCopy() && bookMeta != null) {
return copyNextGenerationBook(bookMeta, player, copies);
} else {
//Make sure the player can pay for the copying
@@ -83,7 +93,7 @@ public class CommandCopy implements TabExecutor {
}
heldBook.setAmount(heldBook.getAmount() + copies);
BooksWithoutBorders.sendSuccessMessage(player, "Book copied!");
BooksWithoutBorders.getStringFormatter().displaySuccessMessage(player, Translatable.SUCCESS_COPY);
return true;
}
}
@@ -95,10 +105,11 @@ public class CommandCopy implements TabExecutor {
* @param copies <p>The number of copies to create for the player</p>
* @return <p>True if the payment failed</p>
*/
private boolean paymentUnSuccessful(Player player, int copies) {
return BooksWithoutBordersConfig.booksHavePrice() &&
!player.hasPermission("bookswithoutborders.bypassBookPrice") &&
EconomyHelper.cannotPayForBookPrinting(player, copies);
private boolean paymentUnSuccessful(@NotNull Player player, int copies) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
return (config.booksHavePrice() &&
!player.hasPermission(Permission.BYPASS_BOOK_PRICE.toString()) &&
config.getEconomyManager().cannotPayForBookPrinting(player, copies));
}
/**
@@ -109,18 +120,17 @@ public class CommandCopy implements TabExecutor {
* @param copies <p>The number of copies requested</p>
* @return <p>True if the book was successfully copied</p>
*/
private boolean copyNextGenerationBook(BookMeta bookMeta, Player player, int copies) {
private boolean copyNextGenerationBook(@NotNull BookMeta bookMeta, @NotNull Player player, int copies) {
//Copy the vanilla behavior of refusing copying any further
if (bookMeta.getGeneration() == BookMeta.Generation.COPY_OF_COPY ||
bookMeta.getGeneration() == BookMeta.Generation.TATTERED) {
BooksWithoutBorders.sendErrorMessage(player, "You cannot copy this book any further. " +
"You must have the original or a direct copy.");
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_BOOK_COPIED_TOO_FAR);
return false;
}
//Make sure the player can fit the book in their inventory
int nextAvailableSlot = player.getInventory().firstEmpty();
if (nextAvailableSlot == -1) {
BooksWithoutBorders.sendErrorMessage(player, "You need an available slot in your inventory.");
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_INVENTORY_FULL);
return false;
}
//Make sure the player can pay for the copying
@@ -140,10 +150,10 @@ public class CommandCopy implements TabExecutor {
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
int argumentCount = args.length;
@NotNull String[] arguments) {
int argumentCount = arguments.length;
if (argumentCount == 1) {
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 20), args[0]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 20), arguments[0]);
}
return new ArrayList<>();
}

View File

@@ -1,10 +1,12 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@@ -17,94 +19,111 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
/**
* Command executor for the decrypt command
*/
public class CommandDecrypt implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
if (InventoryHelper.notHoldingOneWrittenBookCheck(player,
"You must be holding a written book to decrypt it!",
"You cannot decrypt two books at once!")) {
stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITTEN_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_DECRYPT)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_DECRYPT)))) {
return false;
}
ItemStack heldItem = InventoryHelper.getHeldBook(player, true);
BookMeta bookMetadata = (BookMeta) heldItem.getItemMeta();
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(player, "Your book seems to be corrupt!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_METADATA_MISSING);
return false;
}
//Warning: admin decrypt only allows decrypting files created by the same player. Not sure if intended
if (args.length == 0 && BooksWithoutBordersConfig.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin")) {
String path = getBookFolder() + "Encrypted" + getSlash();
File encryptedDirectory = new File(path);
String[] encryptedFiles = encryptedDirectory.list();
if (encryptedFiles == null) {
BooksWithoutBorders.sendErrorMessage(player, "Could not find any encrypted files!");
return false;
}
//Get the "encryption key" from the filename
String key = "";
for (String encryptedFile : encryptedFiles) {
if (encryptedFile.contains(BookHelper.getBookFile(bookMetadata, player, true))) {
key = encryptedFile.substring(encryptedFile.indexOf("[") + 1, encryptedFile.indexOf("]"));
break;
}
}
if (!key.equalsIgnoreCase("")) {
//Decrypt the book
ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false);
if (book != null) {
InventoryHelper.setHeldWrittenBook(player, book);
BooksWithoutBorders.sendSuccessMessage(player, "Book auto-decrypted!");
return true;
} else {
return false;
}
} else {
BooksWithoutBorders.sendErrorMessage(player, "No matching encrypted book found!");
return false;
}
} else if (args.length == 0) {
BooksWithoutBorders.sendErrorMessage(player, "No decryption password given!");
if (arguments.length == 0 && BooksWithoutBorders.getConfiguration().getAdminDecrypt() &&
player.hasPermission(Permission.ADMIN.toString())) {
return adminDecrypt(player, bookMetadata);
} else if (arguments.length == 0) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_DECRYPT_MISSING_KEY);
return false;
}
String key = EncryptionHelper.getNumberKeyFromStringKey(args[0]);
//Decrypt the book normally
ItemStack book = EncryptionHelper.loadEncryptedBook(player, arguments[0], true, false);
if (book == null) {
book = EncryptionHelper.loadEncryptedBookLegacy(player, arguments[0], true);
}
//Decrypt the book
ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, true);
if (book != null) {
InventoryHelper.setHeldWrittenBook(player, book);
BooksWithoutBorders.sendSuccessMessage(player, "Book decrypted!");
stringFormatter.displaySuccessMessage(player, Translatable.SUCCESS_DECRYPTED);
return true;
} else {
BooksWithoutBorders.sendErrorMessage(player, "Failed to decrypt book!");
stringFormatter.displayErrorMessage(player, Translatable.ERROR_DECRYPT_FAILED);
return false;
}
}
/**
* Uses the admin decrypt to decrypt a book without providing the password
*
* @param player <p>The admin decrypting the book</p>
* @param bookMetadata <p>The metadata of the book to decrypt</p>
* @return <p>True if successful</p>
*/
private boolean adminDecrypt(@NotNull Player player, @NotNull BookMeta bookMetadata) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
File encryptedDirectory = new File(BooksWithoutBorders.getConfiguration().getEncryptedBookPath());
String[] encryptedFiles = encryptedDirectory.list();
if (encryptedFiles == null) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_ENCRYPTED_DIRECTORY_EMPTY_OR_MISSING);
return false;
}
//Get the "encryption key" from the filename
String key = "";
for (String encryptedFile : encryptedFiles) {
if (encryptedFile.contains(BookHelper.getBookFile(bookMetadata, player, true))) {
key = encryptedFile.substring(encryptedFile.indexOf("[") + 1, encryptedFile.indexOf("]"));
break;
}
}
if (!key.equalsIgnoreCase("")) {
//Decrypt the book
ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false, true);
if (book == null) {
book = EncryptionHelper.loadEncryptedBookLegacy(player, key, false);
}
if (book != null) {
InventoryHelper.setHeldWrittenBook(player, book);
stringFormatter.displaySuccessMessage(player, Translatable.SUCCESS_AUTO_DECRYPTED);
return true;
} else {
return false;
}
} else {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_ENCRYPTED_BOOK_UNKNOWN);
return false;
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
int argumentCount = args.length;
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
int argumentCount = arguments.length;
if (argumentCount == 1) {
List<String> info = new ArrayList<>();
info.add("<password>");
return info;
return List.of("<password>");
}
return new ArrayList<>();
}

View File

@@ -1,9 +1,15 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BwBCommand;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.gui.PagedBookIndex;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -12,7 +18,6 @@ import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
@@ -21,44 +26,42 @@ import java.util.List;
public class CommandDelete implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (!(sender instanceof Player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
return deleteBook(sender, args, false);
return deleteBook(sender, arguments, false);
}
/**
* Deletes a book
*
* @param sender <p>The sender trying to delete the book</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param deletePublic <p>Whether to delete a public book</p>
* @return <p>True if the book was deleted successfully</p>
*/
boolean deleteBook(CommandSender sender, String[] args, boolean deletePublic) {
//List deletable files
if (args.length == 0) {
BookFileHelper.printBooks(sender, deletePublic);
protected boolean deleteBook(@NotNull CommandSender sender, @NotNull String[] arguments, boolean deletePublic) {
String command = deletePublic ? BwBCommand.DELETE_PUBLIC_BOOK.toString().toLowerCase() :
BwBCommand.DELETE_BOOK.toString().toLowerCase();
if (PagedBookIndex.displayPage(arguments, sender, deletePublic, command)) {
return true;
}
//Delete the file
if (args.length == 1) {
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, deletePublic);
if (!availableBooks.isEmpty()) {
performBookDeletion(sender, args[0], deletePublic);
//Update the book list
BooksWithoutBorders.updateBooks(sender, deletePublic);
return true;
} else {
BooksWithoutBorders.sendErrorMessage(sender, "No files available to delete!");
return false;
}
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, deletePublic);
if (availableBooks.isEmpty()) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_DELETE_EMPTY);
return false;
}
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect number of arguments for this command!");
return false;
performBookDeletion(sender, InputCleaningHelper.mergeArguments(arguments, 0), deletePublic);
//Update the book list
BooksWithoutBorders.updateBooks(sender, deletePublic);
return true;
}
/**
@@ -68,7 +71,8 @@ public class CommandDelete implements TabExecutor {
* @param fileName <p>The file name of the book</p>
* @param isPublic <p>Whether the book to delete is public or not</p>
*/
public void performBookDeletion(CommandSender sender, String fileName, Boolean isPublic) {
public void performBookDeletion(@NotNull CommandSender sender, @NotNull String fileName, @NotNull Boolean isPublic) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
//If the file name is an index of the load list, load the book
try {
int loadListIndex = Integer.parseInt(fileName);
@@ -86,42 +90,48 @@ public class CommandDelete implements TabExecutor {
//Send message if no such file could be found
if (file == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_INCORRECT_FILE_NAME);
return;
}
//Try to delete the file
try {
if (file.delete()) {
BooksWithoutBorders.sendSuccessMessage(sender, "\"" + fileName + "\" deleted successfully");
stringFormatter.displaySuccessMessage(sender,
stringFormatter.replacePlaceholder(Translatable.SUCCESS_DELETED, "{file}", fileName));
} else {
BooksWithoutBorders.sendErrorMessage(sender, "Deletion failed without an exception!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_DELETE_FAILED_SILENT);
}
} catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(sender, "Deletion failed!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_DELETE_FAILED_EXCEPTION);
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
return doTabCompletion(sender, args, false);
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, false);
}
/**
* Performs tab completion
*
* @param sender <p>The sender of the command</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param deletePublic <p>Whether to delete a public book</p>
* @return <p>A list of available arguments</p>
*/
protected List<String> doTabCompletion(CommandSender sender, String[] args, boolean deletePublic) {
int argumentCount = args.length;
if (argumentCount == 1) {
return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, deletePublic),
args[0]);
@NotNull
protected List<String> doTabCompletion(@NotNull CommandSender sender, @NotNull String[] arguments, boolean deletePublic) {
List<String> filtered = TabCompletionHelper.filterMatchingContains(
BooksWithoutBorders.getAvailableBooks(sender, deletePublic),
InputCleaningHelper.mergeArguments(arguments, 0));
if (arguments.length > 1) {
return TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
} else {
return filtered;
}
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,91 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A command for deleting a single page from a book
*/
public class CommandDeletePage implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s, @NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
if (arguments.length == 0) {
BooksWithoutBorders.sendErrorMessage(sender, "You must supply a page index");
return false;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player);
if (heldBook == null) {
BooksWithoutBorders.sendErrorMessage(sender, "You must be holding a book to perform this command");
return false;
}
int index;
try {
index = Integer.parseInt(arguments[0]) - 1;
} catch (NumberFormatException exception) {
BooksWithoutBorders.sendErrorMessage(sender, "Invalid page index given!");
return false;
}
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unable to get metadata for the held book!");
return false;
}
List<String> pages = new ArrayList<>(bookMeta.getPages());
if (index < 0 || index >= pages.size()) {
BooksWithoutBorders.sendErrorMessage(sender, "The given page index is out of bounds!");
return false;
}
pages.remove(index);
bookMeta.setPages(pages);
heldBook.setItemMeta(bookMeta);
BooksWithoutBorders.sendSuccessMessage(sender, "Page deleted!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
if (!(commandSender instanceof Player player)) {
return List.of("1", "2", "3", "4");
}
ItemStack heldBook = InventoryHelper.getHeldBook(player);
if (heldBook != null) {
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta != null) {
List<String> pages = new ArrayList<>();
for (int i = 0; i < bookMeta.getPages().size(); i++) {
pages.add(String.valueOf(i + 1));
}
return pages;
}
}
}
return List.of();
}
}

View File

@@ -13,13 +13,15 @@ import java.util.List;
public class CommandDeletePublic extends CommandDelete implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
return deleteBook(sender, args, true);
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
return deleteBook(sender, arguments, true);
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return doTabCompletion(sender, args, true);
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, true);
}
}

View File

@@ -1,10 +1,12 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -13,6 +15,7 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -23,54 +26,68 @@ import java.util.List;
public class CommandEncrypt implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (performPreChecks(sender, args, 1, "You must specify a key to encrypt a book!") == null) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (performPreChecks(sender, arguments, 1,
stringFormatter.getUnFormattedColoredMessage(Translatable.ERROR_ENCRYPT_NO_KEY)) == null) {
return false;
}
EncryptionStyle encryptionStyle = args.length == 2 ? EncryptionStyle.getFromString(args[1]) : EncryptionStyle.SUBSTITUTION;
return encryptBook(encryptionStyle, (Player) sender, args[0], "");
EncryptionStyle encryptionStyle = arguments.length == 2 ? EncryptionStyle.getFromString(arguments[1]) : EncryptionStyle.SUBSTITUTION;
// AES is the only reliable method for retaining the plaintext
if (BooksWithoutBorders.getConfiguration().useRealEncryption()) {
encryptionStyle = EncryptionStyle.AES;
}
return encryptBook(encryptionStyle, (Player) sender, arguments[0], "");
}
/**
* Performs necessary pre-checks before going through with the encryption
*
* @param sender <p>The sender trying to encrypt a book</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param necessaryArguments <p>How many arguments is the minimum requirement</p>
* @param missingArgumentsError <p>The error to show if a required argument is missing</p>
* @return <p>The metadata of the book to encrypt, or null if any checks fail</p>
*/
BookMeta performPreChecks(CommandSender sender, String[] args, int necessaryArguments, String missingArgumentsError) {
@Nullable
protected BookMeta performPreChecks(@NotNull CommandSender sender, @NotNull String[] arguments,
int necessaryArguments, @NotNull String missingArgumentsError) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return null;
}
if (InventoryHelper.notHoldingOneWrittenBookCheck(player,
"You must be holding a written book to encrypt it!",
"You cannot encrypt two books at once!")) {
stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITTEN_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_ENCRYPT)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_ENCRYPT)))) {
return null;
}
int argumentCount = args.length;
int argumentCount = arguments.length;
if (argumentCount < necessaryArguments) {
BooksWithoutBorders.sendErrorMessage(player, missingArgumentsError);
stringFormatter.displayErrorMessage(player, missingArgumentsError);
return null;
}
if (argumentCount > necessaryArguments + 1) {
BooksWithoutBorders.sendErrorMessage(player, "Too many command options specified!");
stringFormatter.displayErrorMessage(player, Translatable.ERROR_TOO_MANY_ARGUMENTS_COMMAND);
return null;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta();
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(player, "Your book seems to be corrupt!");
stringFormatter.displayErrorMessage(player, Translatable.ERROR_METADATA_MISSING);
return null;
}
if (!bookMetadata.hasPages()) {
BooksWithoutBorders.sendErrorMessage(player, "Book must have contents to encrypt!");
stringFormatter.displayErrorMessage(player, Translatable.ERROR_ENCRYPT_EMPTY);
return null;
}
return bookMetadata;
@@ -85,7 +102,8 @@ public class CommandEncrypt implements TabExecutor {
* @param group <p>The group to encrypt for</p>
* @return <p>True if the book was encrypted successfully</p>
*/
boolean encryptBook(EncryptionStyle encryptionStyle, Player player, String key, String group) {
protected boolean encryptBook(@NotNull EncryptionStyle encryptionStyle, @NotNull Player player, @NotNull String key,
@NotNull String group) {
ItemSlot heldSlot = InventoryHelper.getHeldSlotBook(player, false, false, true, true);
ItemStack encryptedBook = EncryptionHelper.encryptBook(player, heldSlot == ItemSlot.MAIN_HAND, key, encryptionStyle, group);
@@ -98,8 +116,10 @@ public class CommandEncrypt implements TabExecutor {
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return doTabCompletion(args, false);
@NotNull
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(arguments, false);
}
/**
@@ -109,29 +129,34 @@ public class CommandEncrypt implements TabExecutor {
* @param groupEncrypt <p>Whether to auto-complete for group encryption</p>
* @return <p>The strings to auto-complete</p>
*/
protected List<String> doTabCompletion(String[] args, boolean groupEncrypt) {
@NotNull
protected List<String> doTabCompletion(@NotNull String[] args, boolean groupEncrypt) {
int argumentsCount = args.length;
List<String> encryptionStyles = new ArrayList<>();
encryptionStyles.add("dna");
encryptionStyles.add("substitution");
for (EncryptionStyle encryptionStyle : EncryptionStyle.values()) {
encryptionStyles.add(encryptionStyle.toString());
}
if (argumentsCount == 1) {
List<String> info = new ArrayList<>();
info.add("<password>");
return info;
} else if (argumentsCount == 2) {
if (groupEncrypt) {
List<String> info = new ArrayList<>();
info.add("<group>");
return info;
} else {
if (groupEncrypt) {
if (argumentsCount == 1) {
return List.of("<group>");
} else if (argumentsCount == 2) {
return List.of("<password>");
} else if (argumentsCount == 3) {
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[2]);
}
} else {
if (argumentsCount == 1) {
return List.of("<password>");
} else if (argumentsCount == 2) {
if (BooksWithoutBorders.getConfiguration().useRealEncryption()) {
return List.of();
}
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[1]);
}
} else if (argumentsCount == 3 && groupEncrypt) {
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[2]);
}
return new ArrayList<>();
return List.of();
}

View File

@@ -1,8 +1,10 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@@ -20,27 +22,37 @@ import java.util.List;
public class CommandFormat implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
if (InventoryHelper.notHoldingOneWrittenBookCheck(player, "You must be holding a written book to format it!",
"You cannot format two books at once!")) {
ItemStack heldBook = InventoryHelper.getHeldBook(player);
if (heldBook == null) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_NOT_HOLDING_ANY_BOOK);
return false;
}
BookMeta meta = (BookMeta) heldBook.getItemMeta();
if (meta == null) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_METADATA_MISSING);
return false;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
heldBook.setItemMeta(BookFormatter.formatPages((BookMeta) heldBook.getItemMeta()));
heldBook.setItemMeta(BookFormatter.formatPages(meta));
BooksWithoutBorders.sendSuccessMessage(sender, "Book formatted!");
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_FORMATTED);
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return new ArrayList<>();
}

View File

@@ -1,11 +1,14 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.gui.PagedBookIndex;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -13,6 +16,7 @@ import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -25,62 +29,75 @@ public class CommandGive implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (!(sender instanceof Player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
return giveBook(sender, args, false, "player");
return giveBook(sender, arguments, false, "player");
}
/**
* Gives a book to another player
*
* @param sender <p>The sender trying to give a book</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param givePublic <p>Whether to give a public book</p>
* @param folder <p>The folder containing the book to load</p>
* @return <p>True if the book was given successfully</p>
*/
boolean giveBook(CommandSender sender, String[] args, boolean givePublic, String folder) {
if (args.length == 1 || args.length > 4) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect number of arguments for this command!");
return false;
}
if (args.length == 0) {
BookFileHelper.printBooks(sender, givePublic);
boolean giveBook(@NotNull CommandSender sender, @NotNull String[] arguments, boolean givePublic,
@NotNull String folder) {
String command = givePublic ? "givepublicbook" : "givebook";
if (PagedBookIndex.displayPage(arguments, sender, givePublic, command)) {
return true;
}
int argumentCount = arguments.length;
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (arguments.length == 1) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_NO_RECIPIENT);
return false;
}
//Organize and parse input
String bookIdentifier = args[0];
String receivingPlayerName = args[1];
String bookIdentifier;
String receivingPlayerName;
String copies = "1";
String isSigned = "true";
if (args.length == 4) {
copies = args[2];
isSigned = args[3];
} else if (args.length == 3) {
if (args[2].equalsIgnoreCase("true") || args[2].equalsIgnoreCase("false")) {
isSigned = args[2];
} else {
copies = args[2];
}
if (argumentCount > 3 && InputCleaningHelper.isInt(arguments[argumentCount - 2]) &&
InputCleaningHelper.isBoolean(arguments[argumentCount - 1])) {
receivingPlayerName = arguments[argumentCount - 3];
isSigned = arguments[argumentCount - 1];
copies = arguments[argumentCount - 2];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 3);
} else if (argumentCount > 2 && InputCleaningHelper.isBoolean(arguments[argumentCount - 1])) {
isSigned = arguments[argumentCount - 1];
receivingPlayerName = arguments[argumentCount - 2];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 2);
} else if (argumentCount > 2 && InputCleaningHelper.isInt(arguments[argumentCount - 1])) {
copies = arguments[argumentCount - 1];
receivingPlayerName = arguments[argumentCount - 2];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 2);
} else {
receivingPlayerName = arguments[argumentCount - 1];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 1);
}
//Try and find the target player
Player receivingPlayer = booksWithoutBorders.getServer().getPlayer(receivingPlayerName);
Player receivingPlayer = booksWithoutBorders.getServer().getPlayerExact(receivingPlayerName);
if (receivingPlayer == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Player not found!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_RECIPIENT_UNKNOWN);
return false;
}
//Make sure the receiver is able to fit the book
if (receivingPlayer.getInventory().firstEmpty() == -1) {
BooksWithoutBorders.sendErrorMessage(sender, "Receiving player must have space in their inventory" +
" to receive books!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_RECIPIENT_FULL);
return false;
}
@@ -94,56 +111,64 @@ public class CommandGive implements TabExecutor {
try {
return loadAndGiveBook(bookIdentifier, sender, receivingPlayer, isSigned, folder, copies);
} catch (NumberFormatException e) {
BooksWithoutBorders.sendErrorMessage(sender, "Invalid number of book copies specified!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_INVALID_COPIES_AMOUNT);
return false;
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
return doTabCompletion(sender, args, false);
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, false);
}
/**
* Performs the actual tab completion
*
* @param sender <p>The sender of the command</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param listPublic <p>Whether to list public files or player files</p>
* @return <p>A list of available choices</p>
*/
protected List<String> doTabCompletion(CommandSender sender, String[] args, boolean listPublic) {
@Nullable
protected List<String> doTabCompletion(@NotNull CommandSender sender, @NotNull String[] arguments, boolean listPublic) {
Server server = booksWithoutBorders.getServer();
int argumentCount = args.length;
if (argumentCount > 2) {
//Don't continue with autocompletion if the recipient is invalid
if (server.getPlayer(args[1]) == null) {
return new ArrayList<>();
}
}
int argumentCount = arguments.length;
if (argumentCount == 1) {
//Return list of books
return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, listPublic),
args[0]);
} else if (argumentCount == 2) {
//Return online players
return null;
} else if (argumentCount == 3) {
//Number of copies
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), args[2]);
} else if (argumentCount == 4) {
//Signed
try {
Integer.parseInt(args[2]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), args[3]);
} catch (NumberFormatException e) {
return new ArrayList<>();
return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, listPublic), arguments[0]);
}
List<String> output = new ArrayList<>();
List<String> books = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
List<String> filtered = TabCompletionHelper.filterMatchingContains(books,
InputCleaningHelper.mergeArguments(arguments, 0));
if (!filtered.isEmpty()) {
List<String> cleaned = TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
if (!books.contains(InputCleaningHelper.mergeArguments(arguments, 1))) {
return cleaned;
} else {
output.addAll(cleaned);
}
}
return new ArrayList<>();
if (argumentCount > 2 && InputCleaningHelper.isBoolean(arguments[argumentCount - 2])) {
return output;
} else if (argumentCount > 2 && server.getPlayerExact(arguments[argumentCount - 3]) != null &&
InputCleaningHelper.isInt(arguments[argumentCount - 2])) {
output.addAll(TabCompletionHelper.filterMatchingStartsWith(
TabCompletionTypeHelper.getBooleans(), arguments[argumentCount - 1]));
} else if (argumentCount > 2 && server.getPlayerExact(arguments[argumentCount - 2]) != null) {
output.addAll(TabCompletionHelper.filterMatchingStartsWith(
TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), arguments[argumentCount - 1]));
} else {
List<String> players = new ArrayList<>();
for (Player player : Bukkit.getOnlinePlayers()) {
players.add(player.getName());
}
output.addAll(TabCompletionHelper.filterMatchingStartsWith(players, arguments[argumentCount - 1]));
}
return output;
}
/**
@@ -157,18 +182,20 @@ public class CommandGive implements TabExecutor {
* @param copies <p>The number of copies the player wants to give</p>
* @return <p>True if the book was successfully given</p>
*/
private boolean loadAndGiveBook(String bookIdentifier, CommandSender sender, Player receivingPlayer,
String isSigned, String folder, String copies) throws NumberFormatException {
private boolean loadAndGiveBook(@NotNull String bookIdentifier, @NotNull CommandSender sender,
@NotNull Player receivingPlayer, @NotNull String isSigned, @NotNull String folder,
@NotNull String copies) throws NumberFormatException {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
ItemStack newBook = BookLoader.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(copies));
if (newBook != null) {
//NOTE: As this method bypasses cost, it should also bypass the generation change
receivingPlayer.getInventory().addItem(newBook);
BooksWithoutBorders.sendSuccessMessage(sender, "Book sent!");
BooksWithoutBorders.sendSuccessMessage(receivingPlayer, "Book received!");
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_GIVE_SENT);
stringFormatter.displaySuccessMessage(receivingPlayer, Translatable.SUCCESS_GIVE_RECEIVED);
return true;
} else {
BooksWithoutBorders.sendErrorMessage(sender, "Book failed to load!");
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_LOAD_FAILED);
return false;
}
}

View File

@@ -4,6 +4,7 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -13,13 +14,16 @@ import java.util.List;
public class CommandGivePublic extends CommandGive implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
return giveBook(sender, args, true, "public");
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
return giveBook(sender, arguments, true, "public");
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return doTabCompletion(sender, args, true);
@Nullable
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, true);
}
}

View File

@@ -1,7 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@@ -17,8 +17,9 @@ import java.util.List;
public class CommandGroupEncrypt extends CommandEncrypt implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
BookMeta bookMetadata = performPreChecks(sender, args, 2,
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
BookMeta bookMetadata = performPreChecks(sender, arguments, 2,
"You must specify a group name and key to encrypt a book!");
if (bookMetadata == null) {
@@ -32,13 +33,14 @@ public class CommandGroupEncrypt extends CommandEncrypt implements TabExecutor {
return false;
}
EncryptionStyle encryptionStyle = args.length == 3 ? EncryptionStyle.getFromString(args[2]) : EncryptionStyle.SUBSTITUTION;
return encryptBook(encryptionStyle, (Player) sender, args[1], args[0]);
EncryptionStyle encryptionStyle = arguments.length == 3 ? EncryptionStyle.getFromString(arguments[2]) : EncryptionStyle.SUBSTITUTION;
return encryptBook(encryptionStyle, (Player) sender, arguments[1], arguments[0]);
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return doTabCompletion(args, true);
public @NotNull List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] arguments) {
return doTabCompletion(arguments, true);
}
}

View File

@@ -1,7 +1,7 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.gui.PagedBookIndex;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
@@ -22,20 +22,22 @@ import java.util.List;
public class CommandLoad implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return loadBook(sender, args, "player", false);
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
return loadBook(sender, arguments, "player", false);
}
/**
* Loads a stored book
*
* @param sender <p>The sender asking to load the book</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param directory <p>The directory to load from (public/player)</p>
* @param loadPublic <p>Whether to list public files as loadable</p>
* @return <p>True if the book was loaded successfully</p>
*/
public boolean loadBook(CommandSender sender, String[] args, String directory, boolean loadPublic) {
public boolean loadBook(@NotNull CommandSender sender, @NotNull String[] arguments, @NotNull String directory,
boolean loadPublic) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
@@ -46,26 +48,32 @@ public class CommandLoad implements TabExecutor {
return false;
}
int argumentCount = args.length;
int argumentCount = arguments.length;
//Show books available to the player
if (argumentCount == 0) {
BookFileHelper.printBooks(sender, loadPublic);
String command = loadPublic ? "loadpublicbook" : "loadbook";
if (PagedBookIndex.displayPage(arguments, sender, loadPublic, command)) {
return true;
}
//Organize and parse input
String bookIdentifier = args[0];
String bookIdentifier = arguments[0];
String copies = "1";
String isSigned = "true";
if (args.length == 3) {
copies = args[1];
isSigned = args[2];
} else if (args.length == 2) {
if (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("false")) {
isSigned = args[1];
if (argumentCount > 1) {
if (argumentCount > 2 && InputCleaningHelper.isInt(arguments[argumentCount - 2]) &&
InputCleaningHelper.isBoolean(arguments[argumentCount - 1])) {
isSigned = arguments[argumentCount - 1];
copies = arguments[argumentCount - 2];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 2);
} else if (InputCleaningHelper.isBoolean(arguments[argumentCount - 1])) {
isSigned = arguments[argumentCount - 1];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 1);
} else if (InputCleaningHelper.isInt(arguments[argumentCount - 1])) {
copies = arguments[argumentCount - 1];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 1);
} else {
copies = args[1];
bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 0);
}
}
@@ -95,37 +103,48 @@ public class CommandLoad implements TabExecutor {
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
return doTabCompletion(sender, args, false);
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, false);
}
/**
* Performs the actual tab completion
*
* @param sender <p>The sender of the command</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param loadPublic <p>Whether to list public files or player files</p>
* @return <p>A list of available choices</p>
*/
protected List<String> doTabCompletion(CommandSender sender, String[] args, boolean loadPublic) {
int argumentCount = args.length;
@NotNull
protected List<String> doTabCompletion(@NotNull CommandSender sender, @NotNull String[] arguments, boolean loadPublic) {
int argumentCount = arguments.length;
if (argumentCount == 1) {
//Return list of books
return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, loadPublic),
args[0]);
} else if (argumentCount == 2) {
//Number of copies
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), args[1]);
} else if (argumentCount == 3) {
//Signed
try {
Integer.parseInt(args[1]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), args[2]);
} catch (NumberFormatException e) {
return new ArrayList<>();
return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, loadPublic), arguments[0]);
}
List<String> output = new ArrayList<>();
List<String> books = BooksWithoutBorders.getAvailableBooks(sender, loadPublic);
List<String> filtered = TabCompletionHelper.filterMatchingContains(books,
InputCleaningHelper.mergeArguments(arguments, 0));
if (!filtered.isEmpty()) {
List<String> cleaned = TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
if (!books.contains(InputCleaningHelper.mergeArguments(arguments, 1))) {
return cleaned;
} else {
output.addAll(cleaned);
}
}
return new ArrayList<>();
if (InputCleaningHelper.isBoolean(arguments[argumentCount - 2])) {
return output;
} else if (InputCleaningHelper.isInt(arguments[argumentCount - 2])) {
output.addAll(TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), arguments[argumentCount - 1]));
} else {
output.addAll(TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), arguments[argumentCount - 1]));
}
return output;
}
}

View File

@@ -13,13 +13,15 @@ import java.util.List;
public class CommandLoadPublic extends CommandLoad implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
return loadBook(sender, args, "public", true);
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
return loadBook(sender, arguments, "public", true);
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return doTabCompletion(sender, args, true);
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, true);
}
}

View File

@@ -0,0 +1,74 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.thread.MigrationQueueThread;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
/**
* Command executor for the migrate command
*/
public class CommandMigrate implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder());
BooksWithoutBorders.sendSuccessMessage(player, "Starting book migration...");
Queue<MigrationRequest> filesToMigrate = new LinkedList<>();
findFilesToMigrate(bookDirectory, filesToMigrate, player);
BooksWithoutBorders.getMigrationQueue().addAll(filesToMigrate);
BooksWithoutBorders instance = BooksWithoutBorders.getInstance();
MigrationQueueThread queueThread = new MigrationQueueThread();
int taskId = instance.getServer().getScheduler().runTaskTimer(instance, queueThread, 1L, 1L).getTaskId();
queueThread.setTaskId(taskId);
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return List.of();
}
/**
* Finds all books that should be migrated
*
* @param folder <p>The folder to search for books</p>
* @param queue <p>The list to append found files to</p>
* @param player <p>The player that initiated the migration</p>
*/
private void findFilesToMigrate(@NotNull File folder, @NotNull Queue<MigrationRequest> queue, @NotNull Player player) {
File[] files = folder.listFiles();
if (files == null) {
BooksWithoutBorders.log(Level.WARNING, "Unable to access directory " + folder.getName() + " !");
return;
}
for (File file : files) {
if (file.isDirectory()) {
findFilesToMigrate(file, queue, player);
} else if (file.isFile()) {
queue.add(new MigrationRequest(file, player));
}
}
}
}

View File

@@ -1,7 +1,6 @@
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;
@@ -16,18 +15,24 @@ import java.util.List;
public class CommandReload implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (BooksWithoutBordersConfig.loadConfig()) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (BooksWithoutBorders.getConfiguration().loadConfig()) {
BooksWithoutBorders.sendSuccessMessage(sender, "BooksWithoutBorders configuration reloaded!");
} else {
BooksWithoutBorders.sendErrorMessage(sender, "Reload Failed!");
BooksWithoutBorders.sendErrorMessage(sender, "See console for details");
}
// Reload books
BooksWithoutBorders.updateBooks(sender, true);
BooksWithoutBorders.clearBookData();
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return new ArrayList<>();
}

View File

@@ -8,6 +8,7 @@ import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@@ -22,38 +23,34 @@ import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getCommandColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getErrorColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getTitleAuthorSeparator;
/**
* Command executor for the save command
*/
public class CommandSave implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
return saveHeldBook(sender, args, false);
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] arguments) {
return saveHeldBook(sender, arguments, false);
}
/**
* Saves the player's held book if it exists
*
* @param sender <p>The sender of the command</p>
* @param args <p>The arguments given</p>
* @param arguments <p>The arguments given</p>
* @param savePublic <p>Whether to save the book in the public directory or the player directory</p>
* @return <p>True if a book was saved successfully</p>
*/
boolean saveHeldBook(CommandSender sender, String[] args, boolean savePublic) {
protected boolean saveHeldBook(@NotNull CommandSender sender, @NotNull String[] arguments, boolean savePublic) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, false);
if (holdingSlot != null && holdingSlot != ItemSlot.NONE) {
if (holdingSlot != ItemSlot.NONE) {
ItemStack holdingItem = InventoryHelper.getHeldItem(player, holdingSlot == ItemSlot.MAIN_HAND);
boolean duplicate = args.length == 1 && Boolean.parseBoolean(args[0]);
boolean duplicate = arguments.length == 1 && Boolean.parseBoolean(arguments[0]);
saveBook(player, holdingItem, duplicate, savePublic);
return true;
} else {
@@ -70,25 +67,38 @@ public class CommandSave implements TabExecutor {
* @param overwrite <p>Whether to overwrite any existing books</p>
* @param saveToPublicFolder <p>Whether to save the book to the public folder instead of the player folder</p>
*/
public void saveBook(Player player, ItemStack heldBook, boolean overwrite, boolean saveToPublicFolder) {
public void saveBook(@NotNull Player player, @NotNull ItemStack heldBook, boolean overwrite, boolean saveToPublicFolder) {
BookMeta book = (BookMeta) heldBook.getItemMeta();
if (book == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata for your held book!");
return;
}
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//Only allow saving of own books if enabled
if (BooksWithoutBordersConfig.getAuthorOnlySave() && !saveToPublicFolder &&
if (config.getAuthorOnlySave() && !saveToPublicFolder &&
(!player.hasPermission("bookswithoutborders.bypassAuthorOnlySave") &&
BookHelper.isNotAuthor(player, book))) {
return;
}
String savePath = BookHelper.getBookDirectoryPathString(
saveToPublicFolder ? BookDirectory.PUBLIC : BookDirectory.PLAYER, player);
String savePath = BookHelper.getBookDirectoryPathString(saveToPublicFolder ?
BookDirectory.PUBLIC : BookDirectory.PLAYER, player);
if (savePath == null) {
BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! Unable to find the save path!");
return;
}
//Generate book filename
String fileName = BookHelper.getBookFile(book, player, saveToPublicFolder);
String fileName;
try {
fileName = BookHelper.getBookFile(book, player, saveToPublicFolder);
} catch (IllegalArgumentException exception) {
BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return;
}
//Make sure the used folders exist
File file = new File(savePath);
@@ -111,47 +121,45 @@ public class CommandSave implements TabExecutor {
if (foundDuplicates > 0) {
//TODO: Decide if this makes sense or needs to be changed
//Skip duplicate book
if (!fileName.contains("Untitled" + getTitleAuthorSeparator()) && !overwrite) {
if (!fileName.contains("Untitled" + config.getTitleAuthorSeparator()) && !overwrite) {
BooksWithoutBorders.sendErrorMessage(player, "Book is already saved!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " +
getErrorColor() + "to overwrite!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ?
"/savepublicbook" : "/savebook") + " true " + config.getErrorColor() + "to overwrite!");
return;
}
//Skip if duplicate limit is reached
if (foundDuplicates > BooksWithoutBordersConfig.getBookDuplicateLimit()) {
if (foundDuplicates > config.getBookDuplicateLimit()) {
BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName +
" duplicates reached!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " +
getErrorColor() + "to overwrite!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ?
"/savepublicbook" : "/savebook") + " true " +
config.getErrorColor() + "to overwrite!");
return;
}
//Alter duplicated filename
if (fileName.contains("Untitled" + getTitleAuthorSeparator()) && !overwrite) {
if (fileName.contains("Untitled" + config.getTitleAuthorSeparator()) && !overwrite) {
fileName = "(" + foundDuplicates + ")" + fileName;
}
}
try {
if (BooksWithoutBordersConfig.getUseYml()) {
BookToFromTextHelper.bookToYml(savePath, fileName, book);
} else {
BookToFromTextHelper.bookToTXT(savePath, fileName, book);
}
BookToFromTextHelper.bookToYml(savePath, fileName, book);
//Update the relevant book list
BooksWithoutBorders.updateBooks(player, saveToPublicFolder);
BooksWithoutBorders.sendSuccessMessage(player, "Book Saved as \"" + fileName + "\"");
BooksWithoutBorders.sendSuccessMessage(player, "Book Saved as \"" + fileName + ChatColor.RESET + "\"");
} catch (IOException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to save book");
BooksWithoutBorders.log(Level.SEVERE, "Unable to save book");
}
}
@Override
@NotNull
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
@NotNull String[] arguments) {
return new ArrayList<>();
}

View File

@@ -11,8 +11,9 @@ import org.jetbrains.annotations.NotNull;
public class CommandSavePublic extends CommandSave implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return saveHeldBook(sender, args, true);
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
return saveHeldBook(sender, arguments, true);
}
}

View File

@@ -12,6 +12,7 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -22,13 +23,14 @@ import java.util.List;
public class CommandSetAuthor implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
if (args.length < 1) {
if (arguments.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "Too few command arguments!");
return false;
}
@@ -43,8 +45,12 @@ public class CommandSetAuthor implements TabExecutor {
boolean mainHand = heldBookSlot == ItemSlot.MAIN_HAND;
ItemStack heldBook = InventoryHelper.getHeldItem(player, mainHand);
BookMeta bookMetaData = InventoryHelper.getHeldBookMetadata(player, mainHand);
if (bookMetaData == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata for the held book!");
return false;
}
String author = ColorHelper.translateColorCodes(String.join(" ", args), ColorConversion.RGB);
String author = ColorHelper.translateColorCodes(String.join(" ", arguments), ColorConversion.RGB);
bookMetaData.setAuthor(author);
heldBook.setItemMeta(bookMetaData);
BooksWithoutBorders.sendSuccessMessage(player, "Book author set to " + author + "!");
@@ -52,8 +58,10 @@ public class CommandSetAuthor implements TabExecutor {
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
if (args.length == 1) {
@Nullable
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return null;
} else {
return new ArrayList<>();

View File

@@ -2,7 +2,8 @@ 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.config.StaticMessage;
import net.knarcraft.bookswithoutborders.manager.EconomyManager;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
@@ -26,35 +27,36 @@ public class CommandSetBookPrice implements TabExecutor {
private List<String> paymentTypes;
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
//Clear the current price
if (args.length == 0) {
if (arguments.length == 0) {
clearItemPrice(sender);
return true;
}
//Warn about missing arguments
if (args.length < 2) {
if (arguments.length < 2) {
BooksWithoutBorders.sendErrorMessage(sender, "[Item/Eco] and [quantity] must be specified!");
return false;
}
//Warn about invalid argument
if (!args[0].equalsIgnoreCase("Item") && !args[0].equalsIgnoreCase("Eco")) {
if (!arguments[0].equalsIgnoreCase("Item") && !arguments[0].equalsIgnoreCase("Eco")) {
BooksWithoutBorders.sendErrorMessage(sender, "Price type must be \"Item\" or \"Eco\"!");
return false;
}
try {
double price = Double.parseDouble(args[1]);
double price = Double.parseDouble(arguments[1]);
if (price <= 0) {
BooksWithoutBorders.sendErrorMessage(sender, "[quantity] must be greater than 0!");
return false;
}
if (args[0].equalsIgnoreCase("Item")) {
if (arguments[0].equalsIgnoreCase("Item")) {
return setItemPrice(sender, price);
} else if (args[0].equalsIgnoreCase("Eco")) {
} else if (arguments[0].equalsIgnoreCase("Eco")) {
return setEconomyPrice(sender, price);
}
} catch (NumberFormatException e) {
@@ -68,11 +70,12 @@ public class CommandSetBookPrice implements TabExecutor {
*
* @param sender <p>The sender of the command</p>
*/
private void clearItemPrice(CommandSender sender) {
BooksWithoutBordersConfig.setBookPriceType(null);
BooksWithoutBordersConfig.setBookPriceQuantity(0);
private void clearItemPrice(@NotNull CommandSender sender) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
config.setBookPriceType(null);
config.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", BooksWithoutBordersConfig.getBookPriceQuantity());
booksWithoutBorders.getConfig().set("Options.Price_to_create_book.Required_quantity", config.getBookPriceQuantity());
booksWithoutBorders.saveConfig();
BooksWithoutBorders.sendSuccessMessage(sender, "Price to create books removed!");
@@ -85,7 +88,7 @@ public class CommandSetBookPrice implements TabExecutor {
* @param price <p>The new price</p>
* @return <p>True if the price was changed successfully</p>
*/
private boolean setItemPrice(CommandSender sender, double price) {
private boolean setItemPrice(@NotNull CommandSender sender, double price) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "[Item] price can only be used by a player!");
return false;
@@ -97,10 +100,11 @@ public class CommandSetBookPrice implements TabExecutor {
return false;
}
BooksWithoutBordersConfig.setBookPriceType(heldItem.getType());
BooksWithoutBordersConfig.setBookPriceQuantity(price);
String newPriceType = BooksWithoutBordersConfig.getBookPriceType().toString();
double newPriceQuantity = BooksWithoutBordersConfig.getBookPriceQuantity();
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
config.setBookPriceType(heldItem.getType());
config.setBookPriceQuantity(price);
String newPriceType = config.getBookPriceType().toString();
double newPriceQuantity = config.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();
@@ -117,35 +121,38 @@ public class CommandSetBookPrice implements TabExecutor {
* @param price <p>The new price</p>
* @return <p>True if the price was changed successfully</p>
*/
private boolean setEconomyPrice(CommandSender sender, double price) {
if (EconomyHelper.setupEconomy()) {
BooksWithoutBordersConfig.setBookPriceQuantity(price);
BooksWithoutBordersConfig.setBookPriceType(Material.AIR);
double newPriceQuantity = BooksWithoutBordersConfig.getBookPriceQuantity();
private boolean setEconomyPrice(@NotNull CommandSender sender, double price) {
EconomyManager economyManager = BooksWithoutBorders.getConfiguration().getEconomyManager();
if (economyManager.getEconomy() != null) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
config.setBookPriceQuantity(price);
config.setBookPriceType(Material.AIR);
double newPriceQuantity = config.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();
BooksWithoutBorders.sendSuccessMessage(sender, "Book creation price set to " +
EconomyHelper.getEconomy().format(newPriceQuantity) + "!");
economyManager.getEconomy().format(newPriceQuantity) + "!");
return true;
} else {
BooksWithoutBorders.sendErrorMessage(sender, "BooksWithoutBorders failed to hook into Vault! Book price not set!");
BooksWithoutBorders.sendErrorMessage(sender, StaticMessage.EXCEPTION_VAULT_PRICE_NOT_CHANGED.toString());
return false;
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
if (paymentTypes == null) {
initializeTabCompleteLists();
}
int argumentCount = args.length;
int argumentCount = arguments.length;
if (argumentCount == 1) {
return TabCompletionHelper.filterMatchingStartsWith(paymentTypes, args[0]);
return TabCompletionHelper.filterMatchingStartsWith(paymentTypes, arguments[0]);
} else if (argumentCount == 2) {
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 3), args[1]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 3), arguments[1]);
}
return new ArrayList<>();
}

View File

@@ -0,0 +1,110 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.Bookshelf;
import net.knarcraft.bookswithoutborders.handler.BookshelfHandler;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* The command for setting information for a chiseled bookshelf
*/
public class CommandSetBookshelfData implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(commandSender, "This command must be used by a player!");
return false;
}
Block targetBlock = player.getTargetBlockExact(7);
if (targetBlock == null || targetBlock.getType() != Material.CHISELED_BOOKSHELF) {
BooksWithoutBorders.sendErrorMessage(commandSender, "You are not looking at a bookshelf!");
return false;
}
BookshelfHandler shelfHandler = BooksWithoutBorders.getBookshelfHandler();
Bookshelf bookshelf = shelfHandler.getFromLocation(targetBlock.getLocation());
if (arguments.length < 2) {
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("delete")) {
if (bookshelf != null) {
shelfHandler.unregisterBookshelf(bookshelf);
shelfHandler.save();
BooksWithoutBorders.sendSuccessMessage(commandSender, "Bookshelf successfully deleted");
} else {
BooksWithoutBorders.sendErrorMessage(commandSender, "The block you are looking at is not a registered bookshelf");
}
return true;
} else {
return false;
}
}
// Get all arguments as a space-separated string
StringBuilder builder = new StringBuilder(arguments[1]);
for (int i = 2; i < arguments.length; i++) {
builder.append(" ").append(arguments[i]);
}
switch (arguments[0].toLowerCase()) {
case "name":
if (bookshelf == null) {
Bookshelf newShelf = new Bookshelf(targetBlock.getLocation(), arguments[1], new ArrayList<>());
shelfHandler.registerBookshelf(newShelf);
} else {
bookshelf.setTitle(builder.toString());
}
shelfHandler.save();
BooksWithoutBorders.sendSuccessMessage(commandSender, "Title successfully saved");
return true;
case "lore":
if (bookshelf == null) {
BooksWithoutBorders.sendErrorMessage(commandSender, "You must name the bookshelf before " +
"assigning lore!");
} else {
List<String> loreParts = Arrays.asList(builder.toString().split(BooksWithoutBorders.getConfiguration().getLoreSeparator()));
bookshelf.setLore(loreParts);
shelfHandler.save();
BooksWithoutBorders.sendSuccessMessage(commandSender, "Lore successfully saved");
}
return true;
}
return false;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompletionHelper.filterMatchingStartsWith(List.of("delete", "name", "lore"), arguments[0]);
} else if (arguments.length == 2) {
return switch (arguments[0].toLowerCase()) {
case "delete" -> new ArrayList<>();
case "name" ->
TabCompletionHelper.filterMatchingStartsWith(List.of("Epic Title", "Lame Title"), arguments[1]);
case "lore" ->
TabCompletionHelper.filterMatchingStartsWith(List.of("Interesting lore", "Line1~Line2~Line3"), arguments[1]);
default -> null;
};
} else {
return List.of();
}
}
}

View File

@@ -21,7 +21,7 @@ public class CommandSetGeneration implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
@@ -32,14 +32,14 @@ public class CommandSetGeneration implements TabExecutor {
return false;
}
if (args.length < 1) {
if (arguments.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "You must specify the new generation for your book!");
return false;
}
BookMeta.Generation generation;
try {
generation = BookMeta.Generation.valueOf(args[0]);
generation = BookMeta.Generation.valueOf(arguments[0]);
} catch (IllegalArgumentException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Invalid book generation specified!");
return false;
@@ -60,8 +60,8 @@ public class CommandSetGeneration implements TabExecutor {
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length == 1) {
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> generations = new ArrayList<>();
for (BookMeta.Generation generation : BookMeta.Generation.values()) {
generations.add(generation.name());

View File

@@ -1,7 +1,6 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
@@ -24,13 +23,14 @@ import java.util.List;
public class CommandSetLore implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
if (args.length < 1) {
if (arguments.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "Missing a command argument!");
return false;
}
@@ -42,12 +42,11 @@ public class CommandSetLore implements TabExecutor {
}
//Treat all arguments as lore input
String rawLore = String.join(" ", args);
String rawLore = String.join(" ", arguments);
//Format lore
rawLore = ColorHelper.translateColorCodes(rawLore, ColorConversion.RGB);
String[] loreParts = rawLore.split(BooksWithoutBordersConfig.getLoreSeparator());
List<String> newLore = new ArrayList<>(Arrays.asList(loreParts));
List<String> newLore = Arrays.asList(rawLore.split(BooksWithoutBorders.getConfiguration().getLoreSeparator()));
//Update lore
ItemMeta meta = heldItem.getItemMeta();
@@ -62,7 +61,8 @@ public class CommandSetLore implements TabExecutor {
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
//TODO: Figure out if there is a better way to display that an argument is required
List<String> options = new ArrayList<>();
options.add("<new lore>");

View File

@@ -24,13 +24,13 @@ public class CommandSetTitle implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
if (args.length < 1) {
if (arguments.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "Too few command arguments!");
return false;
}
@@ -41,7 +41,7 @@ public class CommandSetTitle implements TabExecutor {
return false;
}
String title = String.join(" ", args);
String title = String.join(" ", arguments).strip();
title = ColorHelper.translateColorCodes(title, ColorConversion.RGB);
ItemMeta itemMetadata = heldItem.getItemMeta();
@@ -53,6 +53,10 @@ public class CommandSetTitle implements TabExecutor {
//Get and change metadata
ItemMeta newMetaData;
if (heldItem.getType() == Material.WRITTEN_BOOK) {
if (title.length() > 32) {
BooksWithoutBorders.sendErrorMessage(sender, "Book titles are capped at 32 characters!");
return false;
}
BookMeta bookMetadata = (BookMeta) itemMetadata;
bookMetadata.setTitle(title);
newMetaData = bookMetadata;
@@ -69,7 +73,7 @@ public class CommandSetTitle implements TabExecutor {
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
@NotNull String[] arguments) {
List<String> options = new ArrayList<>();
options.add("<new title>");
return options;

View File

@@ -1,24 +1,20 @@
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.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.WritableBookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
/**
* Command executor for the unsign command
@@ -26,7 +22,8 @@ import java.util.logging.Level;
public class CommandUnSign implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
@@ -49,44 +46,59 @@ public class CommandUnSign implements TabExecutor {
* @param player <p>The player holding the book</p>
* @param mainHand <p>Whether the player is holding the book in its main hand or its off hand</p>
*/
public void unSignHeldBook(Player player, boolean mainHand) {
protected void unSignHeldBook(@NotNull Player player, boolean mainHand) {
//Get the old book
BookMeta oldMetadata = InventoryHelper.getHeldBookMetadata(player, mainHand);
if (oldMetadata == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata from the held book!");
return;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player, mainHand);
//Only allow the owner to un-sign the book
if (BooksWithoutBordersConfig.getAuthorOnlyUnsign() && !player.hasPermission("bookswithoutborders.bypassAuthorOnlyUnsign")) {
if (BooksWithoutBorders.getConfiguration().getAuthorOnlyUnsign() &&
!player.hasPermission("bookswithoutborders.bypassAuthorOnlyUnsign")) {
if (BookHelper.isNotAuthor(player, Objects.requireNonNull(oldMetadata))) {
return;
}
}
WritableBookMeta newMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITABLE_BOOK);
if (newMetadata == null) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create writable book metadata");
// Give the player the new book
ItemStack book = BookHelper.unsignBook(oldMetadata, heldBook.getAmount());
if (book == null) {
return;
}
//Create a new unsigned book with the same data
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK, heldBook.getAmount());
if (oldMetadata.hasLore()) {
List<String> oldLore = oldMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
newMetadata.setLore(newLore);
}
}
newMetadata.setPages(oldMetadata.getPages());
newBook.setItemMeta(newMetadata);
// Give the player the new book
InventoryHelper.replaceHeldItem(player, newBook, mainHand);
reverseColorCodes(book);
InventoryHelper.replaceHeldItem(player, book, mainHand);
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
@NotNull
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return new ArrayList<>();
}
/**
* Reverses colors of a previously formatted book
*
* @param book <p>The book to reverse colors of</p>
*/
private void reverseColorCodes(@NotNull ItemStack book) {
try {
BookMeta meta = (BookMeta) book.getItemMeta();
if (meta != null) {
List<String> newPages = new ArrayList<>(meta.getPages().size());
for (String page : meta.getPages()) {
newPages.add(page.replaceAll("§", "&"));
}
meta.setPages(newPages);
book.setItemMeta(meta);
}
} catch (NullPointerException ignored) {
}
}
}

View File

@@ -1,18 +1,19 @@
package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import net.knarcraft.bookswithoutborders.manager.EconomyManager;
import net.knarcraft.knarlib.formatting.Translator;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.Configuration;
import org.jetbrains.annotations.NotNull;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendErrorMessage;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendSuccessMessage;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
/**
@@ -20,50 +21,64 @@ import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.clea
*/
public class BooksWithoutBordersConfig {
private static final ChatColor errorColor = ChatColor.RED;
private static final ChatColor successColor = ChatColor.GREEN;
private static final ChatColor commandColor = ChatColor.YELLOW;
private static final String SLASH = FileSystems.getDefault().getSeparator();
private static boolean isInitialized;
public static String bookFolder;
private final ChatColor errorColor = ChatColor.RED;
private final ChatColor successColor = ChatColor.GREEN;
private final ChatColor commandColor = ChatColor.YELLOW;
private final String SLASH = FileSystems.getDefault().getSeparator();
private boolean isInitialized;
private final String bookFolder;
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 authorOnlyUnsign;
private static boolean authorOnlySave;
private static boolean useYml;
private static boolean adminDecrypt;
private static boolean formatBooks;
private static boolean changeGenerationOnCopy;
private static boolean enableBookshelfPeeking;
private int bookDuplicateLimit;
private String titleAuthorSeparator;
private String loreSeparator;
private List<String> firstBooks = new ArrayList<>();
private String welcomeMessage;
private Material bookPriceType = null;
private double bookPriceQuantity;
private boolean authorOnlyCopy;
private boolean authorOnlyUnsign;
private boolean authorOnlySave;
private boolean adminDecrypt;
private boolean formatBooks;
private boolean changeGenerationOnCopy;
private boolean enableBookshelfPeeking;
private boolean useRealEncryption;
private final Translator translator;
private EconomyManager economyManager;
/**
* Initializes the books without borders settings class
*
* @param booksWithoutBorders <p>The books without borders object used for getting required data</p>
*/
public static void initialize(BooksWithoutBorders booksWithoutBorders) {
public BooksWithoutBordersConfig(@NotNull BooksWithoutBorders booksWithoutBorders, @NotNull Translator translator) {
if (isInitialized) {
throw new IllegalArgumentException("Settings class initialized twice. This should not happen!");
}
this.translator = translator;
isInitialized = true;
bookFolder = booksWithoutBorders.getDataFolder().getAbsolutePath() + getSlash() + "Books" + getSlash();
loadConfig();
}
/**
* Gets the economy manager
*
* @return <p>The economy manager</p>
*/
@NotNull
public EconomyManager getEconomyManager() {
return this.economyManager;
}
/**
* Gets the folder used for storing books
*
* @return <p>The folder used for storing books</p>
*/
public static String getBookFolder() {
return bookFolder;
public String getBookFolder() {
return this.bookFolder;
}
/**
@@ -71,8 +86,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The color to use for error messages</p>
*/
public static ChatColor getErrorColor() {
return errorColor;
public ChatColor getErrorColor() {
return this.errorColor;
}
/**
@@ -80,8 +95,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The color to use for success messages</p>
*/
public static ChatColor getSuccessColor() {
return successColor;
public ChatColor getSuccessColor() {
return this.successColor;
}
/**
@@ -89,8 +104,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The color used to color commands</p>
*/
public static ChatColor getCommandColor() {
return commandColor;
public ChatColor getCommandColor() {
return this.commandColor;
}
/**
@@ -98,8 +113,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The slash to use for file separators</p>
*/
public static String getSlash() {
return SLASH;
public String getSlash() {
return this.SLASH;
}
/**
@@ -107,8 +122,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>Whether only the book author can copy it</p>
*/
public static boolean getAuthorOnlyCopy() {
return authorOnlyCopy;
public boolean getAuthorOnlyCopy() {
return this.authorOnlyCopy;
}
/**
@@ -116,8 +131,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>Whether only the book author can unsign it</p>
*/
public static boolean getAuthorOnlyUnsign() {
return authorOnlyUnsign;
public boolean getAuthorOnlyUnsign() {
return this.authorOnlyUnsign;
}
/**
@@ -125,8 +140,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>Whether a player can only save their own books</p>
*/
public static boolean getAuthorOnlySave() {
return authorOnlySave;
public boolean getAuthorOnlySave() {
return this.authorOnlySave;
}
/**
@@ -134,17 +149,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>True if players can peek at the contained books</p>
*/
public static boolean getEnableBookshelfPeeking() {
return enableBookshelfPeeking;
}
/**
* 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;
public boolean getEnableBookshelfPeeking() {
return this.enableBookshelfPeeking;
}
/**
@@ -152,8 +158,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>Whether admins can bypass the encryption password</p>
*/
public static boolean getAdminDecrypt() {
return adminDecrypt;
public boolean getAdminDecrypt() {
return this.adminDecrypt;
}
/**
@@ -161,8 +167,8 @@ public class BooksWithoutBordersConfig {
*
* @param newQuantity <p>The new quantity necessary for payment</p>
*/
public static void setBookPriceQuantity(double newQuantity) {
bookPriceQuantity = newQuantity;
public void setBookPriceQuantity(double newQuantity) {
this.bookPriceQuantity = newQuantity;
}
/**
@@ -170,8 +176,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The quantity necessary for payment</p>
*/
public static double getBookPriceQuantity() {
return bookPriceQuantity;
public double getBookPriceQuantity() {
return this.bookPriceQuantity;
}
/**
@@ -182,8 +188,8 @@ public class BooksWithoutBordersConfig {
*
* @param newType <p>The new item type to use for book pricing</p>
*/
public static void setBookPriceType(Material newType) {
bookPriceType = newType;
public void setBookPriceType(Material newType) {
this.bookPriceType = newType;
}
/**
@@ -194,8 +200,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The item type used for book pricing</p>
*/
public static Material getBookPriceType() {
return bookPriceType;
public Material getBookPriceType() {
return this.bookPriceType;
}
/**
@@ -203,8 +209,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The welcome message to show new players</p>
*/
public static String getWelcomeMessage() {
return welcomeMessage;
public String getWelcomeMessage() {
return this.welcomeMessage;
}
/**
@@ -212,8 +218,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The book duplicate limit</p>
*/
public static int getBookDuplicateLimit() {
return bookDuplicateLimit;
public int getBookDuplicateLimit() {
return this.bookDuplicateLimit;
}
/**
@@ -221,8 +227,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>True if books should change their generation</p>
*/
public static boolean changeGenerationOnCopy() {
return changeGenerationOnCopy;
public boolean changeGenerationOnCopy() {
return this.changeGenerationOnCopy;
}
/**
@@ -230,8 +236,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The separator between title and author</p>
*/
public static String getTitleAuthorSeparator() {
return titleAuthorSeparator;
public String getTitleAuthorSeparator() {
return this.titleAuthorSeparator;
}
/**
@@ -239,8 +245,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The separator used to denote lore newline</p>
*/
public static String getLoreSeparator() {
return loreSeparator;
public String getLoreSeparator() {
return this.loreSeparator;
}
/**
@@ -248,8 +254,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>Whether all books should be formatted</p>
*/
public static boolean formatBooks() {
return formatBooks;
public boolean formatBooks() {
return this.formatBooks;
}
/**
@@ -257,8 +263,8 @@ public class BooksWithoutBordersConfig {
*
* @return <p>The books to give new players</p>
*/
public static List<String> getFirstBooks() {
return new ArrayList<>(firstBooks);
public List<String> getFirstBooks() {
return new ArrayList<>(this.firstBooks);
}
/**
@@ -266,29 +272,48 @@ public class BooksWithoutBordersConfig {
*
* @return <p>True if players need to pay for printing books</p>
*/
public static boolean booksHavePrice() {
return (bookPriceType != null && bookPriceQuantity > 0);
public boolean booksHavePrice() {
return (this.bookPriceType != null && this.bookPriceQuantity > 0);
}
/**
* Checks whether to use real encryption for encrypted books
*
* @return <p>True if real encryption should be used</p>
*/
public boolean useRealEncryption() {
return this.useRealEncryption;
}
/**
* Gets the path used to store encrypted books
*
* @return <p>The encrypted book path</p>
*/
@NotNull
public String getEncryptedBookPath() {
return getBookFolder() + "Encrypted" + getSlash();
}
/**
* Saves the config
*/
public static void saveConfigValues() {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getInstance().getServer().getConsoleSender();
public void saveConfigValues() {
Logger logger = BooksWithoutBorders.getInstance().getLogger();
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);
config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), enableBookshelfPeeking);
config.set(ConfigOption.MAX_DUPLICATES.getConfigNode(), this.bookDuplicateLimit);
config.set(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(), this.titleAuthorSeparator);
config.set(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(), this.loreSeparator);
config.set(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode(), this.firstBooks);
config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), this.welcomeMessage);
config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), this.formatBooks);
config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), this.enableBookshelfPeeking);
config.set(ConfigOption.USE_REAL_ENCRYPTION.getConfigNode(), this.useRealEncryption);
String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode();
if (bookPriceType != null) {
if (bookPriceType != Material.AIR) {
config.set(itemTypeNode, bookPriceType.toString());
if (this.bookPriceType != null) {
if (this.bookPriceType != Material.AIR) {
config.set(itemTypeNode, this.bookPriceType.toString());
} else {
config.set(itemTypeNode, "Economy");
}
@@ -296,24 +321,23 @@ public class BooksWithoutBordersConfig {
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);
config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), authorOnlyUnsign);
config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), authorOnlySave);
config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), changeGenerationOnCopy);
config.set(ConfigOption.PRICE_QUANTITY.getConfigNode(), this.bookPriceQuantity);
config.set(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(), this.adminDecrypt);
config.set(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(), this.authorOnlyCopy);
config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), this.authorOnlyUnsign);
config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), this.authorOnlySave);
config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), this.changeGenerationOnCopy);
//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");
logger.log(Level.INFO, "[BooksWithoutBorders] Found old config setting " +
"\"Require_book_and_quill_to_create_book\"\nUpdating 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);
this.bookPriceType = Material.WRITABLE_BOOK;
this.bookPriceQuantity = 1;
config.set("Options.Price_to_create_book.Item_type", this.bookPriceType.toString());
config.set("Options.Price_to_create_book.Required_quantity", this.bookPriceQuantity);
}
config.set("Options.Require_book_and_quill_to_create_book", null);
}
@@ -325,56 +349,59 @@ public class BooksWithoutBordersConfig {
*
* @return <p>True if the config was loaded successfully</p>
*/
public static boolean loadConfig() {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getInstance().getServer().getConsoleSender();
public boolean loadConfig() {
Logger logger = BooksWithoutBorders.getInstance().getLogger();
BooksWithoutBorders.getInstance().reloadConfig();
Configuration config = BooksWithoutBorders.getInstance().getConfig();
try {
useYml = getBoolean(config, ConfigOption.USE_YAML);
bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(),
this.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);
authorOnlyCopy = getBoolean(config, ConfigOption.AUTHOR_ONLY_COPY);
authorOnlyUnsign = getBoolean(config, ConfigOption.AUTHOR_ONLY_UNSIGN);
authorOnlySave = getBoolean(config, ConfigOption.AUTHOR_ONLY_SAVE);
firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode());
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);
this.titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR);
this.loreSeparator = getString(config, ConfigOption.LORE_LINE_SEPARATOR);
this.adminDecrypt = getBoolean(config, ConfigOption.ADMIN_AUTO_DECRYPT);
this.authorOnlyCopy = getBoolean(config, ConfigOption.AUTHOR_ONLY_COPY);
this.authorOnlyUnsign = getBoolean(config, ConfigOption.AUTHOR_ONLY_UNSIGN);
this.authorOnlySave = getBoolean(config, ConfigOption.AUTHOR_ONLY_SAVE);
this.firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode());
this.welcomeMessage = getString(config, ConfigOption.MESSAGE_FOR_NEW_PLAYERS);
this.formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING);
this.changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY);
this.enableBookshelfPeeking = getBoolean(config, ConfigOption.ENABLE_BOOKSHELF_PEEKING);
this.useRealEncryption = getBoolean(config, ConfigOption.USE_REAL_ENCRYPTION);
String language = config.getString("language", "en");
this.translator.loadLanguages(BooksWithoutBorders.getInstance().getDataFolder(), "en", language);
//Convert string into material
this.economyManager = new EconomyManager();
String paymentMaterial = getString(config, ConfigOption.PRICE_ITEM_TYPE);
if (paymentMaterial.equalsIgnoreCase("Economy")) {
if (EconomyHelper.setupEconomy()) {
bookPriceType = Material.AIR;
if (this.economyManager.getEconomy() == null) {
logger.log(Level.SEVERE, StaticMessage.EXCEPTION_VAULT_NOT_AVAILABLE.toString());
this.bookPriceType = null;
} else {
sendErrorMessage(consoleSender,
"BooksWithoutBorders failed to hook into Vault! Book price not set!");
bookPriceType = null;
this.bookPriceType = Material.AIR;
}
} else if (!paymentMaterial.trim().isEmpty()) {
Material material = Material.matchMaterial(paymentMaterial);
if (material != null) {
bookPriceType = material;
this.bookPriceType = material;
}
}
bookPriceQuantity = config.getDouble(ConfigOption.PRICE_QUANTITY.getConfigNode(),
this.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);
this.titleAuthorSeparator = cleanString(this.titleAuthorSeparator);
if (this.titleAuthorSeparator.length() != 1) {
logger.log(Level.SEVERE, "Title-Author_Separator is set to an invalid value!\n" +
"Reverting to default value of \",\"");
this.titleAuthorSeparator = ",";
config.set("Options.Title-Author_Separator", this.titleAuthorSeparator);
}
} catch (Exception e) {
sendErrorMessage(consoleSender, "Warning! Config.yml failed to load!");
sendErrorMessage(consoleSender, "Try Looking for settings that are missing values!");
} catch (Exception exception) {
logger.log(Level.SEVERE, exception.getMessage());
logger.log(Level.SEVERE, "Warning! Config.yml failed to load!\n" +
"Try Looking for settings that are missing values!");
return false;
}
@@ -388,7 +415,7 @@ public class BooksWithoutBordersConfig {
* @param configOption <p>The configuration option to get the value for</p>
* @return <p>The value of the option</p>
*/
private static String getString(Configuration config, ConfigOption configOption) {
private static String getString(@NotNull Configuration config, @NotNull ConfigOption configOption) {
return config.getString(configOption.getConfigNode(), (String) configOption.getDefaultValue());
}
@@ -399,7 +426,7 @@ public class BooksWithoutBordersConfig {
* @param configOption <p>The configuration option to get the value for</p>
* @return <p>The value of the option</p>
*/
private static boolean getBoolean(Configuration config, ConfigOption configOption) {
private static boolean getBoolean(@NotNull Configuration config, @NotNull ConfigOption configOption) {
return config.getBoolean(configOption.getConfigNode(), (Boolean) configOption.getDefaultValue());
}

View File

@@ -0,0 +1,182 @@
package net.knarcraft.bookswithoutborders.config;
import org.jetbrains.annotations.NotNull;
/**
* A representation of a BwB command
*/
public enum BwBCommand {
/**
* The help command
*/
BOOKS_WITHOUT_BORDERS("booksWithoutBorders", false),
/**
* Clears the contents of a book
*/
CLEAR_BOOK("clearBook", true),
/**
* Copies the held book
*/
COPY_BOOK("copyBook", true),
/**
* Decrypts the held encrypted book
*/
DECRYPT_BOOK("decryptBook", true),
/**
* Deletes a book from a player's private collection
*/
DELETE_BOOK("deleteBook", true),
/**
* Deletes a book from the public collection
*/
DELETE_PUBLIC_BOOK("deletePublicBook", false),
/**
* Encrypts the held book
*/
ENCRYPT_BOOK("encryptBook", true),
/**
* Executes formatting codes in the held book
*/
FORMAT_BOOK("formatBook", true),
/**
* Gives a book from a player's private collection to another player
*/
GIVE_BOOK("giveBook", true),
/**
* Gives a book from the public collection to a player
*/
GIVE_PUBLIC_BOOK("givePublicBook", false),
/**
* Encrypts a book for specific group
*/
GROUP_ENCRYPT_BOOK("groupEncryptBook", true),
/**
* Loads a book from a player's private collection
*/
LOAD_BOOK("loadBook", true),
/**
* Loads a book from the public collection
*/
LOAD_PUBLIC_BOOK("loadPublicBook", true),
/**
* Reloads the plugin's configuration and the book lists
*/
RELOAD("reload", false),
/**
* Saves a book to a player's private collection
*/
SAVE_BOOK("saveBook", true),
/**
* Saves a book to the public collection
*/
SAVE_PUBLIC_BOOK("savePublicBook", true),
/**
* Sets the author of the held book
*/
SET_BOOK_AUTHOR("setBookAuthor", true),
/**
* Sets the generation of the held book
*/
SET_BOOK_GENERATION("setBookGeneration", true),
/**
* Sets the price of copying, loading and giving books
*/
SET_BOOK_PRICE("setBookPrice", false),
/**
* Sets the name/lore for the chiseled bookshelf in front of the player, displayed when peeking the bookshelf
*/
SET_BOOKSHELF_DATA("setBookshelfData", true),
/**
* Sets the lore of the held book/item
*/
SET_LORE("setLore", true),
/**
* Sets the book title of the held signed book, or the display name of the held item
*/
SET_TITLE("setTitle", true),
/**
* Un-signs the held signed book
*/
UNSIGN_BOOK("unsignBook", true),
/**
* Adds a title page, blank page or chapter page to the held book
*
* <p>If no input is given, and a signed book is provided, a title page will be added to the beginning of the book.
* If an index is given, and a signed book is provided, a title page will be added to the specified index.
* If no input is given, and an unsigned book is provided, an error will be displayed.
* If an index is given, but no text, and a signed book is provided, a blank page is added to the specified index.
* If both an index and text is given, and a signed or unsigned book is provided, a custom chapter page depending on
* the text will be added to the specified index.</p>
*/
ADD_TITLE_PAGE("addBookTitlePage", true),
/**
* Deletes a page from the held book
*/
DELETE_PAGE("deleteBookPage", true),
/**
* Migrates all books, fixing any problems with their names, and converts txt books to yml
*/
MIGRATE("migrateBooks", true),
;
private final @NotNull String commandName;
private final boolean requiresPlayer;
/**
* Instantiates a new command
*
* @param commandName <p>The name of the command</p>
* @param requiresPlayer <p>Whether the command requires to be run by a player</p>
*/
BwBCommand(@NotNull String commandName, boolean requiresPlayer) {
this.commandName = commandName;
this.requiresPlayer = requiresPlayer;
}
/**
* Gets whether this command can only be used by a player
*
* @return <p>True if this command must be run by a player</p>
*/
public boolean requiresPlayer() {
return this.requiresPlayer;
}
/**
* Return name instead of enum when displayed as a string
*
* @return <p>The command name</p>
*/
@Override
@NotNull
public String toString() {
return this.commandName;
}
}

View File

@@ -1,15 +1,12 @@
package net.knarcraft.bookswithoutborders.config;
import org.jetbrains.annotations.NotNull;
/**
* 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
*/
@@ -78,7 +75,12 @@ public enum ConfigOption {
/**
* Whether hitting a bookshelf should display information about the contained books
*/
ENABLE_BOOKSHELF_PEEKING("Options.Enable_Book_Peeking", false),
ENABLE_BOOKSHELF_PEEKING("Options.Enable_Book_Peeking", true),
/**
* Whether to use real AES encryption instead of storing garbled book text, while the full plaintext is stored in a file
*/
USE_REAL_ENCRYPTION("Options.Use_Real_Encryption", false),
;
private final String configNode;
@@ -90,7 +92,7 @@ public enum ConfigOption {
* @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) {
ConfigOption(@NotNull String configNode, @NotNull Object defaultValue) {
this.configNode = configNode;
this.defaultValue = defaultValue;
}

View File

@@ -0,0 +1,69 @@
package net.knarcraft.bookswithoutborders.config;
import org.jetbrains.annotations.NotNull;
/**
* A representation of a BwB permission
*/
public enum Permission {
/**
* The permission for bypassing paying the set book printing price
*/
BYPASS_BOOK_PRICE("bypassBookPrice"),
/**
* The admin permission, giving most/all other permissions
*/
ADMIN("admin"),
/**
* The permission for bypassing the setting making sure a book can only be copied by its author
*/
BYPASS_AUTHOR_ONLY_COPY("bypassAuthorOnlyCopy"),
/**
* Does nothing by itself, but its child nodes allow decrypting group encrypted books for the specified group
*/
DECRYPT("decrypt"),
/**
* The permission for formatting a book's text either through the `/formatBook` command or automatically
*/
FORMAT("format"),
;
private final @NotNull String node;
/**
* Instantiates a new permission
*
* @param node <p>The permission's permission node</p>
*/
Permission(@NotNull String node) {
this.node = node;
}
/**
* Gets the node of this permission
*
* @return <p>The permission node</p>
*/
@NotNull
public String getNode() {
return "bookswithoutborders." + this.node;
}
/**
* Return node instead of enum when displayed as a string
*
* @return <p>The permission node string</p>
*/
@Override
@NotNull
public String toString() {
return getNode();
}
}

View File

@@ -0,0 +1,33 @@
package net.knarcraft.bookswithoutborders.config;
import org.jetbrains.annotations.NotNull;
/**
* Messages shown in the console that cannot be altered by users
*/
public enum StaticMessage {
BOOK_SAVING_FAILED("Saving failed! Aborting..."),
BOOK_FOLDER_CREATE_FAILED("Unable to create necessary folders"),
COMMAND_NOT_REGISTERED("Command {command} has not been registered!"),
EXCEPTION_VAULT_NOT_AVAILABLE("Vault is unavailable, but book price is set to economy. Unsetting book cost!"),
EXCEPTION_VAULT_PRICE_NOT_CHANGED("BooksWithoutBorders failed to hook into Vault! Book price not set!");
private final @NotNull String messageString;
/**
* Instantiates a new static message
*
* @param messageString <p>The message string</p>
*/
StaticMessage(@NotNull String messageString) {
this.messageString = messageString;
}
@Override
@NotNull
public String toString() {
return this.messageString;
}
}

View File

@@ -0,0 +1,257 @@
package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.knarlib.formatting.TranslatableMessage;
import org.jetbrains.annotations.NotNull;
/**
* An enum representing all translatable messages
*/
public enum Translatable implements TranslatableMessage {
/**
* The success message displayed when the copy command succeeds
*/
SUCCESS_COPY,
/**
* The success message displayed when the clear command succeeds
*/
SUCCESS_CLEARED,
/**
* The success message displayed when the decrypt command succeeds
*/
SUCCESS_DECRYPTED,
/**
* The success message displayed when a book is successfully decrypted without providing the key
*/
SUCCESS_AUTO_DECRYPTED,
/**
* The success message displayed when a book is successfully deleted
*/
SUCCESS_DELETED,
/**
* The success message displayed when a book is successfully formatted
*/
SUCCESS_FORMATTED,
/**
* The success message displayed when a book is successfully sent
*/
SUCCESS_GIVE_SENT,
/**
* The success message displayed when a book is successfully received
*/
SUCCESS_GIVE_RECEIVED,
/**
* The error to display when the console attempts to run a player-only command
*/
ERROR_PLAYER_ONLY,
/**
* The error displayed when running a relevant command without holding a written book
*/
ERROR_NOT_HOLDING_WRITTEN_BOOK,
/**
* The error displayed when running a relevant command without holding a writable book
*/
ERROR_NOT_HOLDING_WRITABLE_BOOK,
/**
* The error displayed when running a relevant command while holding one book in each hand
*/
ERROR_ONLY_ONE_BOOK,
/**
* The error displayed when running the copy command without specifying the amount of copies
*/
ERROR_COPY_COUNT_NOT_SPECIFIED,
/**
* The error displayed when supplying the copy command with a negative number
*/
ERROR_COPY_NEGATIVE_AMOUNT,
/**
* The error displayed when supplying the copy command with a non-number
*/
ERROR_COPY_INVALID_AMOUNT,
/**
* The error displayed when trying to copy a tattered book or a copy of a copy
*/
ERROR_BOOK_COPIED_TOO_FAR,
/**
* The error displayed when unable to get book metadata from the held book
*/
ERROR_METADATA_MISSING,
/**
* The error displayed when trying to receive a book with a full inventory
*/
ERROR_INVENTORY_FULL,
/**
* The error displayed when trying to do a Vault transaction while Vault is unavailable
*/
ERROR_VAULT_COST_BUT_UNAVAILABLE,
/**
* The error displayed when trying to decrypt a book without displaying the key
*/
ERROR_DECRYPT_MISSING_KEY,
/**
* The error displayed when failing to decrypt a book for any reason
*/
ERROR_DECRYPT_FAILED,
/**
* The error displayed when listing all encrypted files returns null
*/
ERROR_ENCRYPTED_DIRECTORY_EMPTY_OR_MISSING,
/**
* The error displayed when failing to match an encrypted book during admin decrypt
*/
ERROR_ENCRYPTED_BOOK_UNKNOWN,
/**
* The error displayed when trying to delete a book while the book folder is empty or missing
*/
ERROR_DELETE_EMPTY,
/**
* The error displayed when given the name of a book that does not exist
*/
ERROR_INCORRECT_FILE_NAME,
/**
* The error displayed when a file failed to be deleted, without throwing an exception
*/
ERROR_DELETE_FAILED_SILENT,
/**
* The error displayed when a file failed to be deleted, after throwing an exception
*/
ERROR_DELETE_FAILED_EXCEPTION,
/**
* The error displayed when trying to encrypt a book without supplying a key
*/
ERROR_ENCRYPT_NO_KEY,
/**
* The error displayed when exceeding the number of allowed arguments for a command
*/
ERROR_TOO_MANY_ARGUMENTS_COMMAND,
/**
* The error displayed when trying to encrypt an empty book
*/
ERROR_ENCRYPT_EMPTY,
/**
* The error displayed after using the give command without specifying a recipient
*/
ERROR_GIVE_NO_RECIPIENT,
/**
* The error displayed when using the give command with an invalid recipient
*/
ERROR_GIVE_RECIPIENT_UNKNOWN,
/**
* The error displayed when using the give command with a recipient with a full inventory
*/
ERROR_GIVE_RECIPIENT_FULL,
/**
* The error displayed when using the give command with an invalid number of copies (unlikely to ever happen)
*/
ERROR_GIVE_INVALID_COPIES_AMOUNT,
/**
* The error displayed when a book is not properly loaded when using the give command
*/
ERROR_GIVE_LOAD_FAILED,
/**
* The error displayed when running a command without holding any book in the main hand
*/
ERROR_NOT_HOLDING_ANY_BOOK,
/**
* The header displayed before printing all commands
*/
NEUTRAL_COMMANDS_HEADER,
/**
* The format used when displaying a book's economy price
*/
NEUTRAL_COMMANDS_BOOK_PRICE_ECO,
/**
* The format used when displaying a book's item price
*/
NEUTRAL_COMMANDS_BOOK_PRICE_ITEM,
/**
* The format used when displaying one command entry
*/
NEUTRAL_COMMANDS_COMMAND,
/**
* The string used when displaying that a command requires no permission
*/
NEUTRAL_COMMANDS_COMMAND_NO_PERMISSION_REQUIRED,
/**
* The format used when displaying a command's required permission
*/
NEUTRAL_COMMANDS_COMMAND_PERMISSION,
/**
* The translation of unknown author for a book
*/
NEUTRAL_UNKNOWN_AUTHOR,
/**
* The translation of the copy action
*/
ACTION_COPY,
/**
* The translation of the clear action
*/
ACTION_CLEAR,
/**
* The translation of the decrypt action
*/
ACTION_DECRYPT,
/**
* The translation of the encrypt action
*/
ACTION_ENCRYPT,
/**
* The translation of the format action
*/
ACTION_FORMAT,
;
@Override
public @NotNull TranslatableMessage[] getAllMessages() {
return Translatable.values();
}
}

View File

@@ -0,0 +1,81 @@
package net.knarcraft.bookswithoutborders.container;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A representation of a bookshelf with extra data used when displaying its contents
*/
public class Bookshelf {
private final @NotNull Location location;
private @NotNull String title;
private @NotNull List<String> lore;
/**
* Instantiates a new bookshelf
*
* @param location <p>The location of the bookshelf</p>
* @param title <p>The title of the bookshelf</p>
* @param lore <p>The lore of the bookshelf</p>
*/
public Bookshelf(@NotNull Location location, @NotNull String title, @NotNull List<String> lore) {
this.location = location;
this.title = title;
this.lore = lore;
}
/**
* Gets the location of this bookshelf
*
* @return <p>The location of this bookshelf</p>
*/
@NotNull
public Location getLocation() {
return this.location;
}
/**
* Gets the title of this bookshelf
*
* @return <p>The title of this bookshelf</p>
*/
@NotNull
public String getTitle() {
return this.title;
}
/**
* Gets the lore of this bookshelf
*
* @return <p>The lore of this bookshelf</p>
*/
@NotNull
public List<String> getLore() {
return this.lore;
}
/**
* Sets the title of this bookshelf
*
* @param title <p>The new title</p>
*/
public void setTitle(@NotNull String title) {
if (title.isBlank()) {
throw new IllegalArgumentException("Bookshelves cannot have empty titles!");
}
this.title = title;
}
/**
* Sets the lore of this bookshelf
*
* @param lore <p>The new lore</p>
*/
public void setLore(@NotNull List<String> lore) {
this.lore = lore;
}
}

View File

@@ -0,0 +1,15 @@
package net.knarcraft.bookswithoutborders.container;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.io.File;
/**
* A request for migrating a book
*
* @param file <p>The file to migrate</p>
* @param player <p>The player that initiated the migration</p>
*/
public record MigrationRequest(@NotNull File file, @NotNull Player player) {
}

View File

@@ -1,6 +1,8 @@
package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -27,32 +29,58 @@ import java.util.logging.Level;
*
* @author EpicKnarvik97
*/
public class AES {
public class AES implements Encryptor {
//TODO: Generate salt for each installation, and figure out what to to with the iv parameter
private final IvParameterSpec ivParameterSpec;
private final @NotNull IvParameterSpec ivParameterSpec;
private final byte[] passwordSalt;
private final @NotNull String password;
/**
* Instantiates a new AES encryptor
*
* @param initializationVector <p>The initialization vector to use for CBC</p>
* @param passwordSalt <p>The password salt to use</p>
* @param aesConfiguration <p>The AES configuration to use</p>
*/
public AES(byte[] initializationVector, byte[] passwordSalt) {
this.ivParameterSpec = new IvParameterSpec(initializationVector);
this.passwordSalt = passwordSalt;
public AES(@NotNull AESConfiguration aesConfiguration) {
this.ivParameterSpec = new IvParameterSpec(aesConfiguration.iv());
this.passwordSalt = aesConfiguration.salt();
this.password = aesConfiguration.key();
}
@Override
@Nullable
public String encryptText(@NotNull String input) {
return encryptDecryptText(input, true);
}
@Override
@Nullable
public String decryptText(@NotNull String input) {
return encryptDecryptText(input, false);
}
/**
* Generates a 16-byte initialization vector
*
* @return <p>An initialization vector</p>
*/
public static byte[] generateIV() {
byte[] initializationVector = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(initializationVector);
return initializationVector;
}
/**
* Encrypts or decrypts the given text
*
* @param input <p>The input to encrypt or decrypt</p>
* @param password <p>The password to use for key generation</p>
* @param encrypt <p>Whether to encrypt or decrypt the input</p>
* <p>Note: The same IV and salt must be used during instantiation in order to decrypt an encrypted message.</p>
*
* @param input <p>The input to encrypt or decrypt</p>
* @param encrypt <p>Whether to encrypt or decrypt the input</p>
* @return <p>The encrypted/decrypted input, or null if anything went wrong</p>
*/
public String encryptDecryptText(String input, String password, boolean encrypt) {
@Nullable
private String encryptDecryptText(@NotNull String input, boolean encrypt) {
//Make a key from the password
SecretKeySpec secretKeySpec = getKeyFromPassword(password);
//Get cipher instance
@@ -70,31 +98,22 @@ public class AES {
try {
aes.init(mode, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES input given!");
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES input given!");
return null;
}
//Perform encryption/decryption and output result
try {
byte[] output = aes.doFinal(getInputBytes(input, encrypt));
return createResult(output, encrypt);
} catch (IllegalBlockSizeException | BadPaddingException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES block size or padding");
} catch (IllegalBlockSizeException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES block size during finalization");
return null;
} catch (BadPaddingException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES padding during finalization");
return null;
}
}
/**
* Generates a 16-byte initialization vector
*
* @return <p>An initialization vector</p>
*/
public static byte[] generateIV() {
byte[] initializationVector = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(initializationVector);
return initializationVector;
}
/**
* Transforms the input string into bytes
*
@@ -102,7 +121,7 @@ public class AES {
* @param encryption <p>Whether the input should be encrypted or decrypted</p>
* @return <p>The input in byte format</p>
*/
private byte[] getInputBytes(String input, boolean encryption) {
private byte[] getInputBytes(@NotNull String input, boolean encryption) {
if (encryption) {
return input.getBytes();
} else {
@@ -117,6 +136,7 @@ public class AES {
* @param encryption <p>Whether the output came from encryption or decryption</p>
* @return <p>The output as a string</p>
*/
@NotNull
private String createResult(byte[] output, boolean encryption) {
if (encryption) {
return Base64.getEncoder().encodeToString(output);
@@ -130,12 +150,16 @@ public class AES {
*
* @return <p>An AES cipher instance, or null if something went wrong</p>
*/
@Nullable
private Cipher getAESCipher() {
Cipher aes;
try {
aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES algorithm or padding");
} catch (NoSuchAlgorithmException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES algorithm during Cipher generation");
return null;
} catch (NoSuchPaddingException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES padding during Cipher generation");
return null;
}
return aes;
@@ -147,20 +171,21 @@ public class AES {
* @param password <p>A user supplied password</p>
* @return <p>A secret key spec or null if something went wrong</p>
*/
private SecretKeySpec getKeyFromPassword(String password) {
@Nullable
private SecretKeySpec getKeyFromPassword(@NotNull String password) {
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), this.passwordSalt, 1000, 128);
SecretKeyFactory keyFactory;
try {
keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
} catch (NoSuchAlgorithmException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES algorithm");
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES algorithm");
return null;
}
SecretKey tmp;
try {
tmp = keyFactory.generateSecret(spec);
} catch (InvalidKeySpecException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES key specification");
BooksWithoutBorders.log(Level.SEVERE, "Invalid AES key specification");
return null;
}
return new SecretKeySpec(tmp.getEncoded(), "AES");

View File

@@ -0,0 +1,24 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
/**
* A configuration for AES encryption
*
* @param iv <p>The initialization vector</p>
* @param salt <p>The encryption salt</p>
* @param key <p>The encryption key</p>
*/
public record AESConfiguration(byte @NotNull [] iv, byte @NotNull [] salt, @NotNull String key) {
/**
* Generates a new AES configuration with randomized IV and salt
*
* @param key <p>The encryption key to use</p>
* @return <p>The new AES configuration</p>
*/
public static AESConfiguration getNewConfiguration(@NotNull String key) {
return new AESConfiguration(AES.generateIV(), AES.generateIV(), key);
}
}

View File

@@ -0,0 +1,69 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
/**
* This enum represents the different available encryption styles
*/
public enum EncryptionStyle {
/**
* Possibly lossy encryption using DNA codons
*/
DNA("dna"),
/**
* A simple cipher using the key to substitute one character for another
*/
SUBSTITUTION("substitution"),
/**
* A military-grade encryption cypher
*/
AES("aes"),
/**
* An unbreakable encryption method assuming the key is completely random and never used more than once, ever
*/
ONE_TIME_PAD("onetimepad"),
/**
* Just a way of using magic text to make text illegible
*/
MAGIC("magic"),
;
private final String name;
/**
* Instantiates a new encryption style
*
* @param name <p>The human-readable encryption style name</p>
*/
EncryptionStyle(@NotNull String name) {
this.name = name;
}
/**
* Gets an encryption style given its name
*
* @param name <p>The name of the encryption style</p>
* @return <p>An encryption style or null if no match is found</p>
*/
@NotNull
public static EncryptionStyle getFromString(@NotNull String name) {
for (EncryptionStyle style : EncryptionStyle.values()) {
if (style.name.equalsIgnoreCase(name)) {
return style;
}
}
return SUBSTITUTION;
}
@Override
@NotNull
public String toString() {
return this.name;
}
}

View File

@@ -0,0 +1,29 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* An interface describing a style of encryption
*/
public interface Encryptor {
/**
* Encrypts the given plaintext
*
* @param input <p>The input to encrypt</p>
* @return <p>The resulting cypher text, or null if unsuccessful</p>
*/
@Nullable
String encryptText(@NotNull String input);
/**
* Decrypts the given cypher text
*
* @param input <p>The cypher text to decrypt</p>
* @return <p>The resulting plaintext, or null if unsuccessful</p>
*/
@Nullable
String decryptText(@NotNull String input);
}

View File

@@ -1,5 +1,8 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
@@ -10,11 +13,11 @@ import java.util.Random;
* <p>Not sure where this was gotten from, but it does exist at
* <a href="https://crypto.stackexchange.com/questions/11614/how-do-i-test-my-encryption-absolute-amateur">Stack Exchange</a>.</p>
*/
public class GenenCrypt {
public class GenenCrypt implements Encryptor {
private final Random ranGen;
private final String[] bases;
private final String[] charList;
private final String[] availableCharacters;
private final HashMap<String, String[]> codonTable;
private final HashMap<String, String> decryptTable;
private final String key;
@@ -24,7 +27,7 @@ public class GenenCrypt {
*
* @param key <p>The key used to generate the codon table</p>
*/
public GenenCrypt(String key) {
public GenenCrypt(@NotNull String key) {
// define the initial, unshuffled codon list of 4 base codons
ArrayList<String> originalCodonList = new ArrayList<>();
@@ -60,32 +63,40 @@ public class GenenCrypt {
// 10 digits
// space, newline, and tab
// the symbols . , ? " ! @ # $ % ^ & * ( ) - + = / _ \ : ; < >
charList = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
availableCharacters = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
"R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", " ",
"\t", "\n", ".", ",", "?", "\"", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "+", "=", "/",
"_", "\\", ":", ";", "<", ">", "|"};
// define the codon table to encode text
codonTable = new HashMap<>();
for (int i = 0; i < charList.length; i++) {
for (int i = 0; i < availableCharacters.length; i++) {
String[] tempArray = new String[]{shuffledCodonList.get(4 * i), shuffledCodonList.get(4 * i + 1),
shuffledCodonList.get(4 * i + 2), shuffledCodonList.get(4 * i + 3)};
//System.out.println(i);
codonTable.put(charList[i], tempArray);
codonTable.put(availableCharacters[i], tempArray);
}
// define the decryption table
decryptTable = new HashMap<>();
for (int i = 0; i < codonTable.size(); i++) {
String s = charList[i];
String s = availableCharacters[i];
String[] sa = codonTable.get(s);
decryptTable.put(sa[0], s);
decryptTable.put(sa[1], s);
decryptTable.put(sa[2], s);
decryptTable.put(sa[3], s);
}
}
@Override
public @Nullable String encryptText(@NotNull String input) {
return encrypt(input);
}
@Override
public @Nullable String decryptText(@NotNull String input) {
return decrypt(input);
}
/**
@@ -94,7 +105,7 @@ public class GenenCrypt {
public void printCodonTable() {
// print the codon table
for (int i = 0; i < codonTable.size(); i++) {
String s = charList[i];
String s = availableCharacters[i];
String[] sa = codonTable.get(s);
switch (s) {
case "\t" ->
@@ -114,7 +125,8 @@ public class GenenCrypt {
* @param input <p>The input to encrypt</p>
* @return <p>The encrypted input</p>
*/
public String encrypt(String input) {
@NotNull
private String encrypt(@NotNull String input) {
StringBuilder output = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
// insert junk bases
@@ -147,7 +159,8 @@ public class GenenCrypt {
* @param input <p>The input to decrypt</p>
* @return <p>The decrypted input</p>
*/
public String decrypt(String input) {
@NotNull
private String decrypt(@NotNull String input) {
StringBuilder output = new StringBuilder();
int keyCount = 0;
int junk = key.charAt(0);

View File

@@ -0,0 +1,22 @@
package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* So-called "Magic" encryption which simply makes the contents unreadable
*/
public class Magic implements Encryptor {
@Override
public @Nullable String encryptText(@NotNull String input) {
return "§k" + BookFormatter.stripColor(input.replace("§", ""));
}
@Override
public @Nullable String decryptText(@NotNull String input) {
return null;
}
}

View File

@@ -0,0 +1,64 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* A one-time pad implementation
*/
public class OneTimePad implements Encryptor {
private final @NotNull String key;
/**
* Instantiates a new one-time pad
*
* @param key <p>The key to use</p>
*/
public OneTimePad(@NotNull String key) {
this.key = key;
}
@Override
public @Nullable String encryptText(@NotNull String input) {
return oneTimePad(input);
}
@Override
public @Nullable String decryptText(@NotNull String input) {
return oneTimePad(input);
}
/**
* Encrypts/decrypts the input using a one-time pad
*
* <p>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.</p>
*
* @param input <p>The input to encrypt/decrypt</p>
* @return <p>The encrypted/decrypted output</p>
*/
@NotNull
public String oneTimePad(@NotNull String input) {
String longKey;
try {
final MessageDigest digest = MessageDigest.getInstance("SHA3-256");
final byte[] hashBytes = digest.digest(key.getBytes(StandardCharsets.UTF_8));
longKey = Base64.getEncoder().encodeToString(hashBytes);
} catch (NoSuchAlgorithmException exception) {
longKey = key;
}
StringBuilder output = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
output.append((char) (input.charAt(i) ^ longKey.charAt(i % longKey.length())));
}
return output.toString();
}
}

View File

@@ -1,92 +1,97 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigInteger;
import java.util.StringTokenizer;
/**
* A simple substitution cipher
*/
public class SubstitutionCipher {
public class SubstitutionCipher implements Encryptor {
public SubstitutionCipher() {
private final @NotNull String key;
/**
* Instantiates a new substitution cipher
*
* @param key <p>The key to use</p>
*/
public SubstitutionCipher(@NotNull String key) {
this.key = key;
}
// encrypts a string using a substitution cipher.
// the substitution is made harder to crack by
// using a string for the key, it is converted
// a series of offsets that each character in the
// original message is offset by
public String encrypt(String in, String key) {
StringBuilder output = new StringBuilder();
if (key != null && !key.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key
// converts each number in the key to an integer and adds to an array
int[] offsetArray = new int[tokenizer.countTokens()];
for (int i = 0; i < offsetArray.length; i++) {
String nt = tokenizer.nextToken();
@Override
public @Nullable String encryptText(@NotNull String input) {
return encryptDecrypt(input, true);
}
try {
offsetArray[i] = Integer.parseInt(nt);
} catch (NumberFormatException e) {
BigInteger big = new BigInteger(nt);
offsetArray[i] = Math.abs(big.intValue());
}
@Override
public @Nullable String decryptText(@NotNull String input) {
return encryptDecrypt(input, false);
}
/**
* Encrypts or decrypts a string using a substitution cipher
*
* <p>The substitution is made harder to crack by using a string for the key, it is converted a series of offsets
* that each character in the original message is offset by.</p>
*
* @param input <p>The input to encrypt/decrypt</p>
* @param encrypt <p>Whether to encrypt or decrypt the input</p>
* @return <p>The encryption output</p>
*/
@NotNull
private String encryptDecrypt(@NotNull String input, boolean encrypt) {
StringBuilder output = new StringBuilder();
if (this.key.isBlank()) {
return output.toString();
}
// converts each number in the key to an integer and adds to an array
int[] offsetArray = getOffsetArray(this.key);
int offsetPosition = 0;
for (int i = 0; i < input.length(); i++) {
// encrypts the letter and adds to the output string
if (encrypt) {
output.append((char) (input.charAt(i) + offsetArray[offsetPosition]));
} else {
output.append((char) (input.charAt(i) - offsetArray[offsetPosition]));
}
int offsetPosition = 0;
for (int i = 0; i < in.length(); i++) {
output.append((char) (in.charAt(i) + offsetArray[offsetPosition])); //encrypts the letter and adds to the output string
// uses the next offset in the key, goes back to first offset if at end of list
if (offsetPosition < offsetArray.length - 1) {
offsetPosition++;
} else {
offsetPosition = 0;
}
// uses the next offset in the key, goes back to first offset if at end of list
if (offsetPosition < offsetArray.length - 1) {
offsetPosition++;
} else {
offsetPosition = 0;
}
}
return output.toString();
}
// decrypts a string using the same substitution method,
// 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.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key
// converts each number in the key to an integer and adds to an array
int[] offsetArray = new int[tokenizer.countTokens()];
for (int i = 0; i < offsetArray.length; i++) {
offsetArray[i] = Integer.parseInt(tokenizer.nextToken());
}
int offsetPosition = 0;
for (int i = 0; i < in.length(); i++) {
output.append((char) (in.charAt(i) - offsetArray[offsetPosition])); //encrypts the letter and adds to the output string
// uses the next offset in the key, goes back to first offset if at end of list
if (offsetPosition < offsetArray.length - 1) {
offsetPosition++;
} else {
offsetPosition = 0;
}
/**
* Tokenizes a key and generates an offset array for substitution
*
* @param key <p>The key to make an offset array for</p>
* @return <p>The offset array</p>
*/
private int[] getOffsetArray(@NotNull String key) {
StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key
// converts each number in the key to an integer and adds to an array
int[] offsetArray = new int[tokenizer.countTokens()];
for (int i = 0; i < offsetArray.length; i++) {
String nextToken = tokenizer.nextToken();
try {
offsetArray[i] = Integer.parseInt(nextToken);
} catch (NumberFormatException e) {
BigInteger big = new BigInteger(nextToken);
offsetArray[i] = Math.abs(big.intValue());
}
}
return output.toString();
}
// 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++) {
output.append((char) (in.charAt(i) ^ key.charAt(i % key.length())));
}
return output.toString();
return offsetArray;
}
}

View File

@@ -0,0 +1,95 @@
package net.knarcraft.bookswithoutborders.gui;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AuthorBookIndex extends BookIndex {
private final static int booksPerPage = 10;
/**
* Prints the available books
*
* @param sender <p>The sender to display the books to</p>
* @param listPublic <p>Whether to display public books</p>
* @param command <p>The base command causing this to be called</p>
* @param page <p>The page of the book list to display</p>
*/
public static void printBooks(@NotNull CommandSender sender, boolean listPublic, @NotNull String command, int page,
@NotNull String authorName) {
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
availableBooks.removeIf((bookPath) ->
!BookFormatter.stripColor(BookFileHelper.getBookAuthorFromPath(bookPath)).equalsIgnoreCase(authorName));
int totalPages = (int) Math.ceil((double) availableBooks.size() / booksPerPage);
if (page > totalPages) {
sender.sendMessage(ChatColor.GRAY + "No such page");
} else {
showAuthorBooks(sender, command, page, totalPages, availableBooks, authorName);
}
}
/**
* Shows a menu listing available books from an author
*
* @param sender <p>The sender wanting to see the book menu</p>
* @param command <p>The main command used to trigger display of the book menu</p>
* @param page <p>The currently selected page</p>
* @param totalPages <p>The total amount of pages</p>
* @param availableBooks <p>All books available to the sender</p>
* @param authorName <p>The name of the author currently shown</p>
*/
private static void showAuthorBooks(@NotNull CommandSender sender, @NotNull String command, int page,
int totalPages, @NotNull List<String> availableBooks, @NotNull String authorName) {
ComponentBuilder componentBuilder = new ComponentBuilder();
String navigationCommand = command + " author" + authorName;
componentBuilder.append("--- ");
if (command.toLowerCase().contains("public")) {
componentBuilder.append("Publicly saved books by: ").color(ChatColor.GREEN).append(authorName).color(ChatColor.AQUA);
} else {
componentBuilder.append("Your saved books by: ").color(ChatColor.GREEN).append(authorName).color(ChatColor.AQUA);
}
componentBuilder.append(" ---", ComponentBuilder.FormatRetention.NONE).append("\n");
displayBookList(componentBuilder, command, page, availableBooks);
displayPreviousButton(componentBuilder, navigationCommand, page);
componentBuilder.append(" | ", ComponentBuilder.FormatRetention.NONE);
displayTotalPages(componentBuilder, navigationCommand, page, totalPages);
componentBuilder.append(" | ", ComponentBuilder.FormatRetention.NONE);
displayNextButton(componentBuilder, navigationCommand, page, totalPages);
sender.spigot().sendMessage(componentBuilder.create());
}
/**
* Displays the list of books on the current page
*
* @param componentBuilder <p>The component builder to append to</p>
* @param command <p>The command used for switching pages</p>
* @param page <p>The current page</p>
* @param availableBooks <p>All available books</p>
*/
protected static void displayBookList(@NotNull ComponentBuilder componentBuilder, @NotNull String command, int page,
@NotNull List<String> availableBooks) {
int startIndex = (page - 1) * booksPerPage;
for (int bookIndex = startIndex; bookIndex < Math.min(startIndex + booksPerPage, availableBooks.size()); bookIndex++) {
componentBuilder.append(getNiceName(availableBooks.get(bookIndex))).color(ChatColor.WHITE).event(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command + " " +
availableBooks.get(bookIndex))).event(
new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Select book by path")));
componentBuilder.append("\n");
}
}
}

View File

@@ -0,0 +1,170 @@
package net.knarcraft.bookswithoutborders.gui;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
public abstract class BookIndex {
protected final static int booksPerPage = 10;
protected final static @NotNull ChatColor interactColor = ChatColor.of("#FFD700");
protected final static @NotNull ChatColor inactiveColor = ChatColor.of("#999999");
/**
* Displays the correct GUI, if specified in the given arguments
*
* @param arguments <p>The arguments given by a command sender</p>
* @param sender <p>The sender executing the command</p>
* @param selectPublic <p>Whether to display public books, or only those available to the command sender</p>
* @param command <p>The command used for changing pages and making the final selection</p>
* @return <p>True if the GUI was displayed</p>
*/
public static boolean displayPage(@NotNull String[] arguments, @NotNull CommandSender sender, boolean selectPublic,
@NotNull String command) {
if (arguments.length == 0) {
PagedBookIndex.printBooks(sender, selectPublic, command, 1);
return true;
} else if (arguments.length == 1) {
int page = InputCleaningHelper.parsePageNumber(arguments[0]);
if (page > 0) {
PagedBookIndex.printBooks(sender, selectPublic, command, page);
return true;
}
} else if (arguments.length == 2) {
String author = InputCleaningHelper.parseAuthorSpecifier(arguments[0]);
if (author != null) {
int page = InputCleaningHelper.parsePageNumber(arguments[1]);
if (page > 0) {
AuthorBookIndex.printBooks(sender, selectPublic, command, page, author);
}
return true;
}
}
// Parse book author from input
for (int authorIndex = 0; authorIndex < arguments.length; authorIndex++) {
String author = InputCleaningHelper.parseAuthorSpecifier(arguments[authorIndex]);
if (author == null) {
continue;
}
for (int pageIndex = authorIndex + 1; pageIndex < arguments.length; pageIndex++) {
int pageNumber = InputCleaningHelper.parsePageNumber(arguments[pageIndex]);
if (pageNumber <= 0) {
continue;
}
StringBuilder builder = new StringBuilder(author);
for (int authorPartCounter = authorIndex + 1; authorPartCounter < pageIndex; authorPartCounter++) {
builder.append(" ").append(arguments[authorPartCounter]);
}
AuthorBookIndex.printBooks(sender, selectPublic, command, pageNumber, builder.toString());
return true;
}
}
return false;
}
/**
* Displays the current page and total amount of pages
*
* @param componentBuilder <p>The component builder to append to</p>
* @param command <p>The command used for switching pages</p>
* @param page <p>The current page</p>
* @param totalPages <p>The total amount of pages</p>
*/
protected static void displayTotalPages(@NotNull ComponentBuilder componentBuilder, @NotNull String command, int page, int totalPages) {
componentBuilder.append("Page " + page + " of " + totalPages,
ComponentBuilder.FormatRetention.NONE).color(interactColor).event(new HoverEvent(
HoverEvent.Action.SHOW_TEXT, new Text("/" + command + " page" + page))).event(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command + " page" + page));
}
/**
* Displays the alphabet-based page index
*
* @param componentBuilder <p>The component builder to append to</p>
* @param command <p>The command used for switching pages</p>
* @param firstInstances <p>The map of where the first index of a letter is found</p>
*/
protected static void displayAlphabetIndex(@NotNull ComponentBuilder componentBuilder,
@NotNull String command, @NotNull Map<Character, Integer> firstInstances) {
componentBuilder.append("[index] <", ComponentBuilder.FormatRetention.NONE).color(inactiveColor);
for (int characterIndex = 0; characterIndex <= 25; characterIndex++) {
char character = (char) ('a' + characterIndex);
if (firstInstances.containsKey(character)) {
int pageIndex = (firstInstances.get(character) / booksPerPage) + 1;
componentBuilder.append(character + "").color(interactColor).event(
new ClickEvent(ClickEvent.Action.RUN_COMMAND,
"/" + command + " page" + pageIndex)).event(new HoverEvent(
HoverEvent.Action.SHOW_TEXT, new Text("Books starting with " + character)));
} else {
componentBuilder.append(character + "", ComponentBuilder.FormatRetention.NONE).color(inactiveColor);
}
}
componentBuilder.append(">", ComponentBuilder.FormatRetention.NONE).color(inactiveColor);
}
/**
* Displays the previous page button
*
* @param componentBuilder <p>The component builder to append to</p>
* @param command <p>The command used for switching pages</p>
* @param page <p>The current page</p>
*/
protected static void displayPreviousButton(@NotNull ComponentBuilder componentBuilder,
@NotNull String command, int page) {
if (page > 1) {
String fullCommand = "/" + command + " page" + (page - 1);
HoverEvent prevPagePreview = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("To page " + (page - 1)));
ClickEvent prevPageClick = new ClickEvent(ClickEvent.Action.RUN_COMMAND, fullCommand);
componentBuilder.append("[<] Previous", ComponentBuilder.FormatRetention.NONE).color(interactColor)
.event(prevPagePreview).event(prevPageClick);
} else {
componentBuilder.append("[<] Previous", ComponentBuilder.FormatRetention.NONE).color(inactiveColor);
}
}
/**
* Displays the next page button
*
* @param componentBuilder <p>The component builder to append to</p>
* @param command <p>The command used for switching pages</p>
* @param page <p>The current page</p>
* @param totalPages <p>The total amount of pages</p>
*/
protected static void displayNextButton(@NotNull ComponentBuilder componentBuilder,
@NotNull String command, int page, int totalPages) {
if (page < totalPages) {
String fullCommand = "/" + command + " page" + (page + 1);
HoverEvent nextPagePreview = new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("To page " + (page + 1)));
ClickEvent nextPageClick = new ClickEvent(ClickEvent.Action.RUN_COMMAND, fullCommand);
componentBuilder.append("Next [>]", ComponentBuilder.FormatRetention.NONE).color(interactColor)
.event(nextPagePreview).event(nextPageClick);
} else {
componentBuilder.append("Next [>]", ComponentBuilder.FormatRetention.NONE).color(inactiveColor);
}
}
/**
* Gets a nice name from a book's path
*
* @param bookPath <p>The path of a book</p>
* @return <p>The prettified book name</p>
*/
@NotNull
protected static String getNiceName(@NotNull String bookPath) {
String title = BookFileHelper.getBookTitleFromPath(bookPath);
String author = BookFileHelper.getBookAuthorFromPath(bookPath);
return ChatColor.translateAlternateColorCodes('&',
title + ChatColor.RESET + " by " + author + ChatColor.RESET);
}
}

View File

@@ -0,0 +1,119 @@
package net.knarcraft.bookswithoutborders.gui;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A class for displaying a paged index of all available books
*/
public class PagedBookIndex extends BookIndex {
/**
* Prints the available books
*
* @param sender <p>The sender to display the books to</p>
* @param listPublic <p>Whether to display public books</p>
* @param command <p>The base command causing this to be called</p>
* @param page <p>The page of the book list to display</p>
*/
public static void printBooks(@NotNull CommandSender sender, boolean listPublic, @NotNull String command, int page) {
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
Map<Character, Integer> firstInstances;
if (listPublic) {
firstInstances = BooksWithoutBorders.getLetterIndex(null);
} else if (sender instanceof Player player) {
firstInstances = BooksWithoutBorders.getLetterIndex(player.getUniqueId());
} else {
firstInstances = new HashMap<>();
}
int totalPages = (int) Math.ceil((double) availableBooks.size() / booksPerPage);
if (page > totalPages) {
sender.sendMessage(ChatColor.GRAY + "No such page");
} else {
showBookMenu(sender, command, page, totalPages, availableBooks, firstInstances);
}
}
/**
* Shows a menu listing available books
*
* @param sender <p>The sender wanting to see the book menu</p>
* @param command <p>The main command used to trigger display of the book menu</p>
* @param page <p>The currently selected page</p>
* @param totalPages <p>The total amount of pages</p>
* @param availableBooks <p>All books available to the sender</p>
* @param firstInstances <p>The map between a character, and the index of the first instance of that character in the book list</p>
*/
private static void showBookMenu(@NotNull CommandSender sender, @NotNull String command, int page,
int totalPages, @NotNull List<String> availableBooks,
@NotNull Map<Character, Integer> firstInstances) {
ComponentBuilder componentBuilder = new ComponentBuilder();
componentBuilder.append("--- ");
if (command.toLowerCase().contains("public")) {
componentBuilder.append("Publicly saved books").color(ChatColor.GREEN);
} else {
componentBuilder.append("Your saved books").color(ChatColor.GREEN);
}
componentBuilder.append(" ---", ComponentBuilder.FormatRetention.NONE).append("\n");
displayBookList(componentBuilder, command, page, availableBooks);
displayPreviousButton(componentBuilder, command, page);
componentBuilder.append(" | ", ComponentBuilder.FormatRetention.NONE);
displayTotalPages(componentBuilder, command, page, totalPages);
componentBuilder.append(" | ", ComponentBuilder.FormatRetention.NONE);
displayNextButton(componentBuilder, command, page, totalPages);
componentBuilder.append("\n");
displayAlphabetIndex(componentBuilder, command, firstInstances);
sender.spigot().sendMessage(componentBuilder.create());
}
/**
* Displays the list of books on the current page
*
* @param componentBuilder <p>The component builder to append to</p>
* @param command <p>The command used for switching pages</p>
* @param page <p>The current page</p>
* @param availableBooks <p>All available books</p>
*/
protected static void displayBookList(@NotNull ComponentBuilder componentBuilder, @NotNull String command, int page,
@NotNull List<String> availableBooks) {
int startIndex = (page - 1) * booksPerPage;
for (int bookIndex = startIndex; bookIndex < Math.min(startIndex + booksPerPage, availableBooks.size()); bookIndex++) {
componentBuilder.append("[" + (bookIndex + 1) + "]").color(interactColor).event(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command + " " +
(bookIndex + 1))).event(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new Text("Select book by index")));
componentBuilder.append(" ", ComponentBuilder.FormatRetention.NONE);
String[] parts = getNiceName(availableBooks.get(bookIndex)).split(" by ");
componentBuilder.append(parts[0]).color(ChatColor.WHITE).event(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command + " " +
availableBooks.get(bookIndex))).event(
new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Select book by path")));
componentBuilder.append(" by ", ComponentBuilder.FormatRetention.NONE).color(ChatColor.WHITE);
componentBuilder.append(parts[1]).color(ChatColor.WHITE).event(
new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/" + command + " author" +
BookFormatter.stripColor(parts[1]) + " page1")).event(
new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Books by " +
BookFormatter.stripColor(parts[1]))));
componentBuilder.append("\n");
}
}
}

View File

@@ -0,0 +1,135 @@
package net.knarcraft.bookswithoutborders.handler;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.Bookshelf;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* A handler keeping track of all bookshelves
*/
public class BookshelfHandler {
private static final File bookshelfFile = new File(BooksWithoutBorders.getInstance().getDataFolder(),
"bookshelves.yml");
private Set<Bookshelf> bookshelves;
private Map<Location, Bookshelf> locationLookup;
/**
* Gets a bookshelf from the given location
*
* @param location <p>The location of the bookshelf</p>
* @return <p>The bookshelf at the location, or null if no such bookshelf exists</p>
*/
@Nullable
public Bookshelf getFromLocation(@NotNull Location location) {
return locationLookup.get(location);
}
/**
* Registers the given bookshelf to this handler
*
* @param bookshelf <p>The bookshelf to register</p>
*/
public void registerBookshelf(@NotNull Bookshelf bookshelf) {
this.bookshelves.add(bookshelf);
this.locationLookup.put(bookshelf.getLocation(), bookshelf);
}
/**
* Unregisters the given bookshelf from this handler
*
* @param bookshelf <p>The bookshelf to unregister</p>
*/
public void unregisterBookshelf(@NotNull Bookshelf bookshelf) {
this.locationLookup.remove(bookshelf.getLocation());
this.bookshelves.remove(bookshelf);
}
/**
* Loads all stored bookshelves
*/
public void load() {
this.bookshelves = new HashSet<>();
this.locationLookup = new HashMap<>();
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(bookshelfFile);
ConfigurationSection bookshelfSection = configuration.getConfigurationSection("bookshelves");
if (bookshelfSection == null) {
BooksWithoutBorders.log(Level.INFO,
"BooksWithoutBorders found no bookshelves to load");
return;
}
for (String key : bookshelfSection.getKeys(false)) {
String[] locationInfo = key.split(",");
World world = Bukkit.getWorld(UUID.fromString(locationInfo[0]));
double x = Integer.parseInt(locationInfo[1]);
double y = Integer.parseInt(locationInfo[2]);
double z = Integer.parseInt(locationInfo[3]);
Location bookshelfLocation = new Location(world, x, y, z);
String titleKey = key + ".title";
String loreKey = key + ".lore";
String title = bookshelfSection.getString(titleKey, null);
List<String> lore = bookshelfSection.getStringList(loreKey);
if (title != null) {
registerBookshelf(new Bookshelf(bookshelfLocation, title, lore));
}
}
}
/**
* Saves all current bookshelves
*/
public void save() {
try {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection bookshelfSection = configuration.createSection("bookshelves");
for (Bookshelf bookshelf : bookshelves) {
saveBookshelf(bookshelfSection, bookshelf);
}
configuration.save(bookshelfFile);
} catch (IOException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to save bookshelves!");
}
}
/**
* Saves a bookshelf to the given configuration section
*
* @param section <p>The configuration section to save to</p>
* @param bookshelf <p>The bookshelf to save</p>
*/
private void saveBookshelf(@NotNull ConfigurationSection section, @NotNull Bookshelf bookshelf) {
Location location = bookshelf.getLocation();
if (location.getWorld() == null) {
return;
}
String key = location.getWorld().getUID() + "," + location.getBlockX() + "," + location.getBlockY() +
"," + location.getBlockZ();
String titleKey = key + ".title";
String loreKey = key + ".lore";
section.set(titleKey, bookshelf.getTitle());
section.set(loreKey, bookshelf.getLore());
}
}

View File

@@ -1,10 +1,12 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerEditBookEvent;
import org.jetbrains.annotations.NotNull;
/**
* A listener for listening to book events
@@ -14,8 +16,9 @@ import org.bukkit.event.player.PlayerEditBookEvent;
public class BookEventListener implements Listener {
@EventHandler
public void onBookSign(PlayerEditBookEvent event) {
if (event.isCancelled() || !event.isSigning() || !BooksWithoutBordersConfig.formatBooks()) {
public void onBookSign(@NotNull PlayerEditBookEvent event) {
if (event.isCancelled() || !event.isSigning() || !BooksWithoutBorders.getConfiguration().formatBooks() ||
!event.getPlayer().hasPermission(Permission.FORMAT.toString())) {
return;
}
event.setNewBookMeta(BookFormatter.formatPages(event.getNewBookMeta()));

View File

@@ -1,35 +1,58 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.container.Bookshelf;
import net.knarcraft.bookswithoutborders.handler.BookshelfHandler;
import net.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
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.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
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 org.jetbrains.annotations.NotNull;
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(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onBookshelfBreak(@NotNull BlockBreakEvent event) {
Block block = event.getBlock();
if (block.getType() != Material.CHISELED_BOOKSHELF) {
return;
}
BookshelfHandler bookshelfHandler = BooksWithoutBorders.getBookshelfHandler();
Bookshelf bookshelf = bookshelfHandler.getFromLocation(block.getLocation());
if (bookshelf != null) {
bookshelfHandler.unregisterBookshelf(bookshelf);
bookshelfHandler.save();
}
}
@EventHandler
public void onBookshelfClick(PlayerInteractEvent event) {
public void onBookshelfClick(@NotNull PlayerInteractEvent event) {
Player player = event.getPlayer();
// If left-clicking a chiseled bookshelf and sneaking, display contents
@@ -40,7 +63,7 @@ public class BookshelfListener implements Listener {
}
// Check if bookshelf peeking is enabled, and the player can peek
if (!BooksWithoutBordersConfig.getEnableBookshelfPeeking() ||
if (!BooksWithoutBorders.getConfiguration().getEnableBookshelfPeeking() ||
!event.getPlayer().hasPermission("bookswithoutborders.peekbookshelf")) {
return;
}
@@ -49,29 +72,53 @@ public class BookshelfListener implements Listener {
event.setUseItemInHand(Event.Result.DENY);
ChiseledBookshelfInventory bookshelfInventory = chiseledBookshelf.getInventory();
player.sendMessage(getBookshelfDescription(bookshelfInventory));
player.sendMessage(getBookshelfDescription(bookshelfInventory, event.getClickedBlock().getLocation()));
}
/**
* Gets the description for a bookshelf's contents
*
* @param bookshelfInventory <p>The inventory of the bookshelf to describe</p>
* @param location <p>The location of the clicked bookshelf</p>
* @return <p>A textual description of the bookshelf's contents</p>
*/
private String getBookshelfDescription(ChiseledBookshelfInventory bookshelfInventory) {
StringBuilder builder = new StringBuilder(getSuccessColor() + "Books in shelf:");
for (ItemStack itemStack : bookshelfInventory.getStorageContents()) {
@NotNull
private String getBookshelfDescription(@NotNull ChiseledBookshelfInventory bookshelfInventory, @NotNull Location location) {
StringBuilder builder = new StringBuilder();
Bookshelf bookshelf = BooksWithoutBorders.getBookshelfHandler().getFromLocation(location);
if (bookshelf != null) {
builder.append(ChatColor.of("#FF5700")).append("Books in ").append(bookshelf.getTitle())
.append(":").append(ChatColor.RESET);
for (String lore : bookshelf.getLore()) {
builder.append("\n ").append(ChatColor.LIGHT_PURPLE).append(lore);
}
} else {
builder.append(ChatColor.of("#FF5700")).append("Books in shelf:").append(ChatColor.RESET);
}
for (int i = 0; i < bookshelfInventory.getSize(); i++) {
int index = (i % 3) + 1;
if (i % 3 == 0) {
builder.append("\n ").append(ChatColor.of("#FF5700")).append(
i < 3 ? "Top Row:" : "Bottom Row:").append(ChatColor.RESET);
}
builder.append("\n ").append(ChatColor.of("#ffd700")).append(index).append(". ").append(ChatColor.RESET);
ItemStack itemStack = bookshelfInventory.getItem(i);
if (itemStack == null) {
builder.append(ChatColor.GRAY).append("<empty>");
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));
} else if (meta != null) {
builder.append(getPlainBookDescription(meta));
builder.append(ChatColor.of("#A5682A")).append("[P]").append(ChatColor.RESET).append(getPlainBookDescription(meta));
}
}
return builder.toString();
@@ -83,7 +130,8 @@ public class BookshelfListener implements Listener {
* @param itemMeta <p>The metadata for the book to describe</p>
* @return <p>The description of the book</p>
*/
private String getPlainBookDescription(ItemMeta itemMeta) {
@NotNull
private String getPlainBookDescription(@NotNull ItemMeta itemMeta) {
String name = itemMeta.getDisplayName();
if (name.isEmpty()) {
name = "Plain book";
@@ -97,7 +145,8 @@ public class BookshelfListener implements Listener {
* @param bookMeta <p>The metadata for the book to describe</p>
* @return <p>The book's description</p>
*/
private String getBookDescription(BookMeta bookMeta) {
@NotNull
private String getBookDescription(@NotNull BookMeta bookMeta) {
String title;
String author;
if (!bookMeta.hasTitle() || bookMeta.getTitle() == null) {
@@ -106,11 +155,11 @@ public class BookshelfListener implements Listener {
title = bookMeta.getTitle();
}
if (!bookMeta.hasAuthor() || bookMeta.getAuthor() == null) {
author = "Unknown";
author = BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_AUTHOR);
} else {
author = bookMeta.getAuthor();
}
return title + " by " + author;
return ChatColor.of("#686868") + "[Q]" + ChatColor.RESET + title + ChatColor.RESET + " by " + author;
}
/**
@@ -119,9 +168,10 @@ public class BookshelfListener implements Listener {
* @param enchantmentStorageMeta <p>The metadata for the enchanted book to describe</p>
* @return <p>The enchanted book's description</p>
*/
private String getEnchantedBookDescription(EnchantmentStorageMeta enchantmentStorageMeta) {
@NotNull
private String getEnchantedBookDescription(@NotNull EnchantmentStorageMeta enchantmentStorageMeta) {
StringBuilder builder = new StringBuilder();
builder.append("Enchanted book (");
builder.append(ChatColor.of("#A64CFF")).append("[E]").append(ChatColor.RESET);
Map<Enchantment, Integer> enchantmentMap = enchantmentStorageMeta.getStoredEnchants();
List<String> enchantments = new ArrayList<>(enchantmentMap.size());
for (Map.Entry<Enchantment, Integer> enchantmentEntry : enchantmentMap.entrySet()) {
@@ -129,7 +179,6 @@ public class BookshelfListener implements Listener {
IntegerToRomanConverter.getRomanNumber(enchantmentEntry.getValue()));
}
builder.append(String.join(", ", enchantments));
builder.append(")");
return builder.toString();
}
@@ -139,7 +188,8 @@ public class BookshelfListener implements Listener {
* @param enchantment <p>The enchantment to get the name of</p>
* @return <p>The prettified enchantment name</p>
*/
private String getEnchantmentName(Enchantment enchantment) {
@NotNull
private String getEnchantmentName(@NotNull Enchantment enchantment) {
// Note: While depreciated, changing this is incompatible with Paper
return uppercaseFirst(enchantment.getKey().getKey().replace("_", " "));
}
@@ -150,7 +200,8 @@ public class BookshelfListener implements Listener {
* @param input <p>The input to uppercase</p>
* @return <p>The input string with more uppercase</p>
*/
private String uppercaseFirst(String input) {
@NotNull
private String uppercaseFirst(@NotNull 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);

View File

@@ -2,21 +2,14 @@ package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.logging.Level;
@@ -29,40 +22,16 @@ public class PlayerEventListener implements Listener {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@EventHandler
public void onHold(PlayerItemHeldEvent event) {
if (event.isCancelled()) {
return;
}
Player player = event.getPlayer();
int selectedSlot = event.getNewSlot();
PlayerInventory playerInventory = player.getInventory();
ItemStack selectedItem = playerInventory.getItem(selectedSlot);
//Ignore irrelevant items
if (selectedItem == null || selectedItem.getType() != Material.WRITTEN_BOOK) {
return;
}
ItemMeta itemMetadata = selectedItem.getItemMeta();
if (itemMetadata == null) {
return;
}
//Update the book the user is viewing
updateBookInHand(player, itemMetadata, true);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
Player player = event.getPlayer();
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//If a book directory exists with this player's name, move it to this player's UUID
String bookFolder = BooksWithoutBordersConfig.getBookFolder();
String bookFolder = config.getBookFolder();
File file = new File(bookFolder + InputCleaningHelper.cleanString(player.getName()));
if (file.exists()) {
if (!file.renameTo(new File(bookFolder + player.getUniqueId()))) {
BooksWithoutBorders.getInstance().getLogger().log(Level.WARNING, "Unable to migrate player book " +
BooksWithoutBorders.log(Level.WARNING, "Unable to migrate player book " +
"directory for player " + player.getName());
}
}
@@ -72,22 +41,10 @@ public class PlayerEventListener implements Listener {
boolean sendMessage = true;
//Gives new players necessary books
for (String bookName : BooksWithoutBordersConfig.getFirstBooks()) {
for (String bookName : config.getFirstBooks()) {
sendMessage = giveBookToNewPlayer(bookName, player, sendMessage);
}
}
//Updates any books in either hand
ItemStack mainHandItem = InventoryHelper.getHeldItem(player, true);
ItemStack offHandItem = InventoryHelper.getHeldItem(player, false);
if (mainHandItem.getType() == Material.WRITTEN_BOOK) {
ItemMeta itemMetadata = mainHandItem.getItemMeta();
updateBookInHand(player, itemMetadata, true);
}
if (offHandItem.getType() == Material.WRITTEN_BOOK) {
ItemMeta itemMetadata = offHandItem.getItemMeta();
updateBookInHand(player, itemMetadata, false);
}
}
/**
@@ -98,7 +55,7 @@ public class PlayerEventListener implements Listener {
* @param sendMessage <p>Whether to send a message to the joining player</p>
* @return <p>True if a message has yet to be sent</p>
*/
private boolean giveBookToNewPlayer(String bookName, Player player, boolean sendMessage) {
private boolean giveBookToNewPlayer(@NotNull String bookName, @NotNull Player player, boolean sendMessage) {
if (!bookName.trim().isEmpty()) {
//Give the book to the player if it exists
@@ -108,7 +65,7 @@ public class PlayerEventListener implements Listener {
}
//Send the player a welcome message if it exists
String welcomeMessage = BooksWithoutBordersConfig.getWelcomeMessage();
String welcomeMessage = BooksWithoutBorders.getConfiguration().getWelcomeMessage();
if (!welcomeMessage.trim().isEmpty() && newBook != null && sendMessage) {
sendMessage = false;
booksWithoutBorders.getServer().getScheduler().scheduleSyncDelayedTask(booksWithoutBorders,
@@ -118,68 +75,4 @@ public class PlayerEventListener implements Listener {
return sendMessage;
}
/**
* Updates a book in one of the player's hands
*
* @param player <p>The player to update</p>
* @param itemMetadata <p>Information about the held book</p>
* @param mainHand <p>Whether to update the book in the player's main hand</p>
*/
private void updateBookInHand(Player player, ItemMeta itemMetadata, boolean mainHand) {
PlayerInventory playerInventory = player.getInventory();
ItemStack updatedBook = updateBook(player, (BookMeta) itemMetadata);
if (updatedBook != null) {
if (mainHand) {
playerInventory.setItemInMainHand(updatedBook);
} else {
playerInventory.setItemInOffHand(updatedBook);
}
}
}
/**
* Updates old books to a newer format
*
* @param player <p>The player holding the book</p>
* @param oldBook <p>Metadata about the held book</p>
* @return <p>An updated book</p>
*/
public ItemStack updateBook(Player player, BookMeta oldBook) {
//handles hacked title-less books
if (oldBook.getTitle() == null || oldBook.getTitle().length() < 3) {
return null;
}
if (oldBook.getTitle().substring(oldBook.getTitle().length() - 3).equalsIgnoreCase("[U]")) {
String fileName;
if (oldBook.getAuthor() != null && oldBook.getAuthor().equalsIgnoreCase("unknown")) {
//Unknown author is ignored
fileName = oldBook.getTitle();
} else {
fileName = oldBook.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + oldBook.getAuthor();
}
String playerFolderPath = BookHelper.getBookDirectoryPathString(BookDirectory.PLAYER, player);
String publicFolderPath = BookHelper.getBookDirectoryPathString(BookDirectory.PUBLIC, player);
String[] possiblePaths = new String[]{
publicFolderPath + fileName + ".yml",
publicFolderPath + fileName + ".txt",
playerFolderPath + fileName + ".yml",
playerFolderPath + fileName + ".txt"
};
for (String path : possiblePaths) {
File file = new File(path);
if (file.isFile()) {
return BookLoader.loadBook(player, fileName, "true", "player");
}
}
return null;
}
return null;
}
}

View File

@@ -2,8 +2,11 @@ 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.config.Permission;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
@@ -24,11 +27,11 @@ import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.BookFileHelper.isBookListIndex;
/**
@@ -36,10 +39,8 @@ import static net.knarcraft.bookswithoutborders.utility.BookFileHelper.isBookLis
*/
public class SignEventListener implements Listener {
private final String slash = getSlash();
@EventHandler
public void onSignChange(SignChangeEvent event) {
public void onSignChange(@NotNull SignChangeEvent event) {
if (event.isCancelled()) {
return;
}
@@ -56,10 +57,11 @@ public class SignEventListener implements Listener {
event.setLine(0, ChatColor.DARK_GREEN + "[BwB]");
//Check if the sign is of a valid type
if (!((lines[1].equalsIgnoreCase("[Encrypt]") || lines[1].equalsIgnoreCase("[Decrypt]") ||
lines[1].equalsIgnoreCase("[Give]")) && !lines[2].trim().isEmpty())) {
if ((!lines[1].equalsIgnoreCase("[Encrypt]") && !lines[1].equalsIgnoreCase("[Decrypt]") &&
!lines[1].equalsIgnoreCase("[Give]")) || lines[2].trim().isEmpty()) {
//Mark the second line as invalid
event.setLine(1, ChatColor.DARK_RED + lines[1]);
player.sendMessage("Invalid sign!");
return;
}
@@ -79,12 +81,7 @@ public class SignEventListener implements Listener {
}
@EventHandler
public void onClick(PlayerInteractEvent event) {
if (event.getClickedBlock() == null) {
return;
}
Material clickedBlockType = event.getClickedBlock().getType();
public void onClick(@NotNull PlayerInteractEvent event) {
Player player = event.getPlayer();
PlayerInventory playerInventory = player.getInventory();
EquipmentSlot hand = event.getHand();
@@ -97,15 +94,18 @@ public class SignEventListener implements Listener {
}
Material heldItemType = heldItem.getType();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && (Tag.SIGNS.isTagged(clickedBlockType) ||
Tag.WALL_SIGNS.isTagged(clickedBlockType))) {
event.setUseItemInHand(Event.Result.DENY);
if (event.getClickedBlock() != null && (event.getAction() == Action.RIGHT_CLICK_BLOCK &&
(Tag.SIGNS.isTagged(event.getClickedBlock().getType()) ||
Tag.WALL_SIGNS.isTagged(event.getClickedBlock().getType())))) {
//The player right-clicked a sign
Sign sign = (Sign) event.getClickedBlock().getState();
if (!signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) {
return;
}
event.setUseItemInHand(Event.Result.DENY);
event.setCancelled(true);
if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) {
encryptHeldBookUsingSign(sign, heldItemType, player, hand);
} else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) {
@@ -121,6 +121,7 @@ public class SignEventListener implements Listener {
}
} else if (heldItemType == Material.WRITTEN_BOOK && (event.getAction() == Action.LEFT_CLICK_AIR
|| event.getAction() == Action.LEFT_CLICK_BLOCK)) {
BookMeta oldBook = (BookMeta) heldItem.getItemMeta();
if (oldBook == null) {
return;
@@ -138,16 +139,21 @@ public class SignEventListener implements Listener {
* @param player <p>The player which clicked the sign</p>
* @param hand <p>The EquipmentSlot of the used hand</p>
*/
private void decryptHeldBookUsingSign(Sign sign, Material heldItemType, Player player, EquipmentSlot hand) {
private void decryptHeldBookUsingSign(@NotNull Sign sign, @NotNull Material heldItemType, @NotNull Player player,
@NotNull EquipmentSlot hand) {
//Decrypt the held book and replace it
if (heldItemType == Material.WRITTEN_BOOK) {
player.closeInventory();
//Converts user supplied key into integer form
String lineText = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2));
String lineText = BookFormatter.stripColor(sign.getSide(Side.FRONT).getLine(2));
String key = EncryptionHelper.getNumberKeyFromStringKey(lineText);
ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false);
ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false, false);
if (book == null) {
book = EncryptionHelper.loadEncryptedBookLegacy(player, key, false);
}
if (book != null) {
player.getInventory().setItem(hand, book);
player.sendMessage(ChatColor.GREEN + "Book decrypted!");
@@ -161,9 +167,10 @@ public class SignEventListener implements Listener {
* @param sign <p>The sign to check</p>
* @return <p>The color of the sign</p>
*/
private ChatColor getSignLine2Color(Sign sign) {
@Nullable
private ChatColor getSignLine2Color(@NotNull Sign sign) {
String line = sign.getSide(Side.FRONT).getLine(2);
if (!ChatColor.stripColor(line).equals(line)) {
if (!BookFormatter.stripColor(line).equals(line)) {
return ChatColor.getByChar(sign.getSide(Side.FRONT).getLine(2).substring(1, 2).charAt(0));
} else {
return null;
@@ -179,7 +186,8 @@ public class SignEventListener implements Listener {
* @param color <p>The color to match</p>
* @return <p>True if the given string is what's on the sign</p>
*/
private boolean signLineEquals(Sign sign, int lineNumber, String compareTo, ChatColor color) {
private boolean signLineEquals(@NotNull Sign sign, int lineNumber, @NotNull String compareTo,
@NotNull ChatColor color) {
String line = sign.getSide(Side.FRONT).getLine(lineNumber);
return line.equalsIgnoreCase(color + compareTo);
}
@@ -191,7 +199,8 @@ public class SignEventListener implements Listener {
* @param lines <p>The lines on the sign</p>
* @param player <p>The player which edited the sign</p>
*/
private void generateGiveSign(SignChangeEvent event, String[] lines, Player player) {
private void generateGiveSign(@NotNull SignChangeEvent event, @NotNull String[] lines,
@NotNull Player player) {
if (lines[2].length() > 13 || lines[3].length() > 13) {
BooksWithoutBorders.sendErrorMessage(player,
"[Give] signs' 3rd and 4th lines must be 13 characters or less!");
@@ -200,7 +209,7 @@ public class SignEventListener implements Listener {
}
//Tests if a full file name has been supplied and points to an actual file
String signFile = getBookFolder() + lines[2] + lines[3];
String signFile = BooksWithoutBorders.getConfiguration().getBookFolder() + lines[2] + lines[3];
if (BookFileHelper.bookFileExists(signFile)) {
markGiveSignValidity(event, true);
return;
@@ -220,7 +229,7 @@ public class SignEventListener implements Listener {
* @param event <p>The event causing the creation of the give sign</p>
* @param isValid <p>Whether the created sign is valid</p>
*/
private void markGiveSignValidity(SignChangeEvent event, boolean isValid) {
private void markGiveSignValidity(@NotNull SignChangeEvent event, boolean isValid) {
String[] lines = event.getLines();
if (isValid) {
event.setLine(2, ChatColor.DARK_GREEN + lines[2]);
@@ -239,7 +248,10 @@ public class SignEventListener implements Listener {
* @param heldItem <p>The type of the held book</p>
* @param hand <p>The hand the player is using to hold the book</p>
*/
private void decryptBook(BookMeta oldBook, Player player, ItemStack heldItem, EquipmentSlot hand) {
private void decryptBook(@NotNull BookMeta oldBook, @NotNull Player player, @NotNull ItemStack heldItem,
@NotNull EquipmentSlot hand) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
ItemStack newBook;
//Check if the book is encrypted by Books Without Borders
@@ -250,25 +262,32 @@ public class SignEventListener implements Listener {
String groupName = oldBook.getLore().get(0).substring(3).split(" encrypted")[0];
//Permission check
if (!player.hasPermission("bookswithoutborders.decrypt." + groupName) &&
!(BooksWithoutBordersConfig.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin"))) {
if (!player.hasPermission(Permission.DECRYPT + "." + groupName) &&
!(config.getAdminDecrypt() && player.hasPermission(Permission.ADMIN.toString()))) {
BooksWithoutBorders.sendErrorMessage(player, "You are not allowed to decrypt that book");
return;
}
String fileName = oldBook.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + oldBook.getAuthor();
String encryptedFolder = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
String fileName = oldBook.getTitle() + config.getTitleAuthorSeparator() + oldBook.getAuthor();
String encryptionFile = InputCleaningHelper.cleanString(groupName) + slash + fileName + ".yml";
File file = new File(getBookFolder() + "Encrypted" + slash + encryptionFile);
if (!file.isFile()) {
file = new File(getBookFolder() + fileName + ".txt");
if (!file.isFile()) {
return;
File file = BookFileHelper.findBookFile(encryptedFolder + InputCleaningHelper.cleanString(groupName) +
config.getSlash(), oldBook);
if (file == null) {
file = BookFileHelper.findBookFile(encryptedFolder, oldBook);
if (file == null) {
file = BookFileHelper.findBookFile(config.getBookFolder(), oldBook);
if (file == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to find encrypted book");
return;
}
}
}
newBook = BookLoader.loadBook(player, fileName, "true", groupName, heldItem.getAmount());
newBook = BookLoader.loadBook(player, fileName, "true", BookDirectory.ENCRYPTED, groupName, heldItem.getAmount());
if (newBook == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to load the unencrypted book!");
return;
}
@@ -285,14 +304,15 @@ public class SignEventListener implements Listener {
* @param player <p>The player which clicked the sign</p>
* @param hand <p>The EquipmentSlot of the used hand</p>
*/
private void encryptHeldBookUsingSign(Sign sign, Material heldItemType, Player player, EquipmentSlot hand) {
private void encryptHeldBookUsingSign(@NotNull Sign sign, @NotNull Material heldItemType, @NotNull Player player,
@NotNull EquipmentSlot hand) {
ItemStack eBook;
String[] lines = sign.getSide(Side.FRONT).getLines();
boolean mainHand = hand == EquipmentSlot.HAND;
if (heldItemType == Material.WRITTEN_BOOK) {
player.closeInventory();
eBook = EncryptionHelper.encryptBook(player, mainHand, ChatColor.stripColor(lines[2]),
EncryptionStyle.getFromString(ChatColor.stripColor(lines[3])));
eBook = EncryptionHelper.encryptBook(player, mainHand, BookFormatter.stripColor(lines[2]),
EncryptionStyle.getFromString(BookFormatter.stripColor(lines[3])));
if (eBook != null) {
player.getInventory().setItem(hand, eBook);
}
@@ -305,8 +325,8 @@ public class SignEventListener implements Listener {
* @param sign <p>The sign the user clicked</p>
* @param player <p>The player which clicked the sign</p>
*/
private void giveBook(Sign sign, Player player) {
String fileName = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2));
private void giveBook(@NotNull Sign sign, @NotNull Player player) {
String fileName = BookFormatter.stripColor(sign.getSide(Side.FRONT).getLine(2));
boolean isLoadListNumber = false;
try {
@@ -318,7 +338,7 @@ public class SignEventListener implements Listener {
//Add the third line to the second line for the full filename
String thirdLine = sign.getSide(Side.FRONT).getLine(3);
if (!isLoadListNumber && thirdLine.length() >= 2) {
fileName += ChatColor.stripColor(thirdLine);
fileName += BookFormatter.stripColor(thirdLine);
}
ItemStack newBook = BookLoader.loadBook(player, fileName, "true", "public");

View File

@@ -1,7 +1,8 @@
package net.knarcraft.bookswithoutborders.utility;
package net.knarcraft.bookswithoutborders.manager;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material;
import org.bukkit.Server;
@@ -12,19 +13,36 @@ import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Helper class for economy related functions
* A manager for managing Vault economy integration
*/
public final class EconomyHelper {
public class EconomyManager {
private static Economy economy;
private @Nullable Economy economy = null;
private EconomyHelper() {
/**
* Instantiates the economy manager
*/
public EconomyManager() {
Server server = BooksWithoutBorders.getInstance().getServer();
Plugin plugin = server.getPluginManager().getPlugin("Vault");
ServicesManager servicesManager = server.getServicesManager();
if (plugin == null) {
return;
}
RegisteredServiceProvider<Economy> economyProvider = servicesManager.getRegistration(Economy.class);
if (economyProvider != null) {
this.economy = economyProvider.getProvider();
}
}
/**
@@ -32,34 +50,11 @@ public final class EconomyHelper {
*
* @return <p>An economy instance, or null if it's not initialized</p>
*/
public static Economy getEconomy() {
@Nullable
public Economy getEconomy() {
return economy;
}
/**
* Tries to set up economy
*
* @return <p>True if economy is set up and enabled</p>
*/
public static boolean setupEconomy() {
if (economy != null) {
return true;
}
Server server = BooksWithoutBorders.getInstance().getServer();
Plugin plugin = server.getPluginManager().getPlugin("Vault");
ServicesManager servicesManager = server.getServicesManager();
if (plugin != null) {
RegisteredServiceProvider<Economy> economyProvider = servicesManager.getRegistration(Economy.class);
if (economyProvider != null) {
economy = economyProvider.getProvider();
}
}
return (economy != null);
}
/**
* Makes the player pay for printing a given number of books
*
@@ -67,15 +62,16 @@ public final class EconomyHelper {
* @param numCopies <p>The number of copies the player is trying to print</p>
* @return <p>True if the player cannot pay for the printing of the books</p>
*/
public static boolean cannotPayForBookPrinting(Player player, int numCopies) {
public boolean cannotPayForBookPrinting(@NotNull Player player, int numCopies) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//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 = BooksWithoutBordersConfig.getBookPriceType();
double cost = BooksWithoutBordersConfig.getBookPriceQuantity() * numCopies;
Material bookCurrency = config.getBookPriceType();
double cost = config.getBookPriceQuantity() * numCopies;
int itemCost = (int) cost;
if (bookCurrency == Material.AIR) {
return !EconomyHelper.payForBookPrintingEconomy(player, cost, numCopies);
return !payForBookPrintingEconomy(player, cost, numCopies);
} else {
if (bookCurrency == Material.WRITABLE_BOOK) {
//Writable books are treated as a special case to prevent WIP books from being used
@@ -100,7 +96,7 @@ public final class EconomyHelper {
* @param itemCost <p>The number of writable books to pay</p>
* @return <p>True if the payment was successful</p>
*/
private static boolean takeWritableBookPayment(Player player, int itemCost) {
private boolean takeWritableBookPayment(@NotNull Player player, int itemCost) {
List<ItemStack> books = getPlayersEmptyBooks(player);
if (countItems(books) < itemCost) {
BooksWithoutBorders.sendErrorMessage(player, itemCost + " empty " + Material.WRITABLE_BOOK +
@@ -131,7 +127,7 @@ public final class EconomyHelper {
* @param items <p>The items to count</p>
* @return <p>The total number of items</p>
*/
private static int countItems(List<ItemStack> items) {
private int countItems(@NotNull List<ItemStack> items) {
int totalItems = 0;
for (ItemStack itemStack : items) {
totalItems += itemStack.getAmount();
@@ -145,7 +141,8 @@ public final class EconomyHelper {
* @param player <p>The player to get books for</p>
* @return <p>The empty books in the player's inventory</p>
*/
private static List<ItemStack> getPlayersEmptyBooks(Player player) {
@NotNull
private List<ItemStack> getPlayersEmptyBooks(@NotNull Player player) {
List<ItemStack> validBooks = new ArrayList<>();
for (ItemStack itemStack : player.getInventory().getContents()) {
if (itemStack == null || itemStack.getType() != Material.WRITABLE_BOOK) {
@@ -168,7 +165,12 @@ public final class EconomyHelper {
* @param numCopies <p>The number of books the player is printing</p>
* @return <p>True if the player had the money and it has been withdrawn</p>
*/
private static boolean payForBookPrintingEconomy(Player player, double cost, int numCopies) {
private boolean payForBookPrintingEconomy(@NotNull Player player, double cost, int numCopies) {
if (economy == null) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_VAULT_COST_BUT_UNAVAILABLE);
return false;
}
if ((economy.getBalance(player) - cost) >= 0) {
economy.withdrawPlayer(player, cost);
BooksWithoutBorders.sendSuccessMessage(player, economy.format(cost) + " withdrawn to create " +
@@ -188,12 +190,12 @@ public final class EconomyHelper {
* @param player <p>The player which needs to pay</p>
* @param itemCost <p>The number of items to pay</p>
*/
private static void payForBookPrintingItem(Player player, int itemCost) {
private void payForBookPrintingItem(@NotNull Player player, int itemCost) {
PlayerInventory playerInventory = player.getInventory();
int clearedAmount = 0;
while (clearedAmount < itemCost) {
int firstItemIndex = playerInventory.first(BooksWithoutBordersConfig.getBookPriceType());
int firstItemIndex = playerInventory.first(BooksWithoutBorders.getConfiguration().getBookPriceType());
ItemStack firstItem = playerInventory.getItem(firstItemIndex);
if (Objects.requireNonNull(firstItem).getAmount() <= itemCost - clearedAmount) {

View File

@@ -1,5 +1,8 @@
package net.knarcraft.bookswithoutborders.state;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* This enum represents the different directories books can be saved in
*/
@@ -26,7 +29,8 @@ public enum BookDirectory {
* @param directory <p>The directory to transform</p>
* @return <p>A book directory, or null if the given directory is empty</p>
*/
public static BookDirectory getFromString(String directory) {
@Nullable
public static BookDirectory getFromString(@NotNull String directory) {
if (directory.equalsIgnoreCase("public")) {
return BookDirectory.PUBLIC;
} else if (directory.equalsIgnoreCase("player")) {

View File

@@ -1,32 +0,0 @@
package net.knarcraft.bookswithoutborders.state;
/**
* This enum represents the different available encryption styles
*/
public enum EncryptionStyle {
DNA("dna"),
SUBSTITUTION("substitution");
private final String name;
EncryptionStyle(String name) {
this.name = name;
}
/**
* Gets an encryption style given its name
*
* @param name <p>The name of the encryption style</p>
* @return <p>An encryption style or null if no match is found</p>
*/
public static EncryptionStyle getFromString(String name) {
for (EncryptionStyle style : EncryptionStyle.values()) {
if (style.name.equalsIgnoreCase(name)) {
return style;
}
}
return SUBSTITUTION;
}
}

View File

@@ -0,0 +1,150 @@
package net.knarcraft.bookswithoutborders.thread;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
/**
* A thread for doing book migrations without locking up the main thread
*/
public class MigrationQueueThread implements Runnable {
private Boolean success = null;
private int taskId;
@Override
public void run() {
long systemTime = System.nanoTime();
//Repeat for at most 0.025 seconds
while (System.nanoTime() - systemTime < 25000000) {
if (pollQueue()) {
break;
}
}
}
/**
* Sets the task id used for stopping this task
*
* @param taskId <p>The id of this task</p>
*/
public void setTaskId(int taskId) {
this.taskId = taskId;
}
/**
* Polls the migration queue for any waiting requests
*
* @return <p>True if the queue is empty and it's safe to quit</p>
*/
public boolean pollQueue() {
MigrationRequest migrationRequest = BooksWithoutBorders.getMigrationQueue().poll();
if (migrationRequest == null) {
return true;
}
if (success == null) {
success = true;
}
success = success & migrateFile(migrationRequest.file(), migrationRequest.player());
if (BooksWithoutBorders.getMigrationQueue().peek() == null) {
Player player = migrationRequest.player();
if (success) {
BooksWithoutBorders.sendSuccessMessage(player, "Successfully migrated all books");
} else {
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate all books");
}
BooksWithoutBorders.getInstance().getServer().getScheduler().cancelTask(this.taskId);
success = null;
return true;
}
return false;
}
/**
* Migrates a single book file
*
* @param file <p>The file to migrate</p>
* @param player <p>The player causing this code to be executed</p>
* @return <p>True if the migration completed successfully</p>
*/
private boolean migrateFile(@NotNull File file, @NotNull Player player) {
BookMeta bookMeta = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK);
if (bookMeta == null) {
return false;
}
BookMeta loadedBook;
String extension = BookFileHelper.getExtensionFromPath(file.getName());
if (extension.equalsIgnoreCase("yml")) {
loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true);
} else if (extension.equalsIgnoreCase("txt")) {
loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta);
} else {
BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!");
return true;
}
if (loadedBook == null) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getName());
return false;
}
// Attempt to retain UUID naming
boolean isPublic = true;
OfflinePlayer author = player;
try {
UUID authorId = UUID.fromString(file.getParentFile().getName());
author = Bukkit.getOfflinePlayer(authorId);
isPublic = false;
} catch (IllegalArgumentException ignored) {
}
try {
String newName = BookHelper.getBookFile(loadedBook, author, isPublic);
return saveBook(file.getParentFile(), newName, loadedBook, file);
} catch (IllegalArgumentException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate book: " + file.getName() + " Cause:");
BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return false;
}
}
/**
* Saves a migrated book
*
* @param parent <p>The parent folder the file belongs to</p>
* @param newName <p>The new name of the file</p>
* @param bookMeta <p>The metadata of the book to migrate</p>
* @param oldFile <p>The old file path, in case it should be deleted</p>
* @return <p>True if successfully saved</p>
*/
private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta,
@NotNull File oldFile) {
try {
BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta);
if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) {
return oldFile.delete();
}
return true;
} catch (IOException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Failed to save migrated book: " + newName);
return false;
}
}
}

View File

@@ -1,19 +1,24 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
/**
* Helper class for dealing with files
*/
@@ -28,8 +33,8 @@ public final class BookFileHelper {
* @param possibleIndex <p>The string which might be a book index</p>
* @return <p>True if the number is a book index</p>
*/
public static boolean isBookListIndex(String possibleIndex) {
File bookDirectory = new File(getBookFolder().replaceAll("[\\\\/]$", ""));
public static boolean isBookListIndex(@NotNull String possibleIndex) {
File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder().replaceAll("[\\\\/]$", ""));
try {
//Tests if a load list number has been supplied
@@ -49,7 +54,7 @@ public final class BookFileHelper {
* @param bookFile <p>The path to a book</p>
* @return <p>True if the file exists and points to a book file</p>
*/
public static boolean bookFileExists(String bookFile) {
public static boolean bookFileExists(@NotNull String bookFile) {
return ((new File(bookFile).isFile() && (bookFile.endsWith(".txt") ||
bookFile.endsWith(".yml"))) || new File(bookFile + ".txt").isFile() ||
new File(bookFile + ".yml").isFile()) && !bookFile.contains("../") && !bookFile.contains("..\\");
@@ -63,7 +68,8 @@ public final class BookFileHelper {
* @param bookPath <p>The path of the book to get</p>
* @return <p>The file the path points to, or null otherwise</p>
*/
public static File getBookFile(String bookPath) {
@Nullable
public static File getBookFile(@NotNull String bookPath) {
if (!bookFileExists(bookPath)) {
return null;
}
@@ -93,7 +99,8 @@ public final class BookFileHelper {
* @param listPublic <p>Whether to list public or personal files</p>
* @return <p>A list of available files</p>
*/
public static List<String> listFiles(CommandSender sender, Boolean listPublic) {
@Nullable
public static List<String> listFiles(@NotNull CommandSender sender, @NotNull Boolean listPublic) {
File file = BookHelper.getBookDirectoryPath(listPublic ? BookDirectory.PUBLIC : BookDirectory.PLAYER, sender);
if (file == null) {
return new ArrayList<>();
@@ -102,31 +109,23 @@ public final class BookFileHelper {
}
/**
* Prints the available books
* Gets a map between characters, and the first instance of a book's title starting with that character
*
* @param sender <p>The sender to display the books to</p>
* @param listPublic <p>Whether to display public books</p>
* @param books <p>The books to look through</p>
* @return <p>The map of the first index containing each character</p>
*/
public static void printBooks(CommandSender sender, boolean listPublic) {
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
BookFileHelper.printFiles(sender, availableBooks);
}
/**
* Prints a list of files
*
* @param sender <p>The command sender to show the list to</p>
* @param fileList <p>The files to list</p>
*/
public static void printFiles(CommandSender sender, List<String> fileList) {
BooksWithoutBorders.sendSuccessMessage(sender, "Available Books:");
if (fileList == null) {
return;
}
int listSize = fileList.size();
for (int fileIndex = 0; fileIndex < listSize; fileIndex++) {
sender.sendMessage(ChatColor.GRAY + "[" + (fileIndex + 1) + "] " + fileList.get(fileIndex));
@NotNull
public static Map<Character, Integer> populateLetterIndices(@NotNull List<String> books) {
Map<Character, Integer> firstEncounter = new HashMap<>();
Character current = null;
for (int i = 0; i < books.size(); i++) {
char first = BookFormatter.stripColor(books.get(i)).toLowerCase().charAt(0);
if (current == null || current != first) {
current = first;
firstEncounter.put(first, i);
}
}
return firstEncounter;
}
/**
@@ -136,7 +135,8 @@ public final class BookFileHelper {
* @param searchDirectory <p>The directory to search for files</p>
* @return <p>A list of available files</p>
*/
private static List<String> listFiles(CommandSender sender, File searchDirectory) {
@Nullable
private static List<String> listFiles(@NotNull CommandSender sender, @NotNull File searchDirectory) {
List<String> fileList = new ArrayList<>();
File[] existingFiles = searchDirectory.listFiles();
@@ -146,23 +146,26 @@ public final class BookFileHelper {
}
for (File foundFile : existingFiles) {
if (!foundFile.isFile()) {
// Filter out invalid files
if (!foundFile.isFile() || foundFile.getName().contains("§")) {
continue;
}
String fileName = foundFile.getName();
String separator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
if (fileName.contains(separator)) {
//Convert the UUID into a username if necessary
String[] data = fileName.split(separator);
String extension = data[1].substring(data[1].length() - 4);
String userName = data[1].substring(0, data[1].length() - 4);
data[1] = BookHelper.authorFromUUID(userName) + extension;
fileList.add(String.join(separator, data));
String userName = getBookAuthorFromPath(fileName);
String title = getBookTitleFromPath(fileName);
fileList.add(title + separator + BookHelper.authorFromUUID(userName));
} else {
fileList.add(fileName);
}
}
// Sort the book list
Comparator<String> bookComparator = Comparator.naturalOrder();
fileList.sort((a, b) -> bookComparator.compare(BookFormatter.stripColor(a).toLowerCase(),
BookFormatter.stripColor(b).toLowerCase()));
return fileList;
}
@@ -173,7 +176,7 @@ public final class BookFileHelper {
* @param fileName <p>The name of the file which might already exist</p>
* @return <p>The number of found duplicates</p>
*/
public static int findDuplicates(File[] foundFiles, String fileName) {
public static int findDuplicates(@NotNull File[] foundFiles, @NotNull String fileName) {
int foundDuplicates = 0;
for (File foundFile : foundFiles) {
if (foundFile.getName().matches("(\\([0-9]+\\))?" + Pattern.quote(fileName) + "(\\.yml|\\.txt)?")) {
@@ -183,4 +186,131 @@ public final class BookFileHelper {
return foundDuplicates;
}
/**
* Gets a book's title given the book's path
*
* @param path <p>The path of the book</p>
* @return <p>The book title</p>
*/
@NotNull
public static String getBookTitleFromPath(@NotNull String path) {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
String stripped = stripExtensionFromPath(path);
if (stripped.contains(separator)) {
return stripped.split(separator)[0];
} else {
return stripped;
}
}
/**
* Gets a book's author given the book's path
*
* @param path <p>The path of the book</p>
* @return <p>The author name</p>
*/
@NotNull
public static String getBookAuthorFromPath(@NotNull String path) {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
String stripped = stripExtensionFromPath(path);
if (stripped.contains(separator)) {
return stripped.split(separator)[1];
} else {
return BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_AUTHOR);
}
}
/**
* Strips the extension from the given path
*
* @param path <p>The path to strip the extension from</p>
* @return <p>The input with the extension stripped</p>
*/
@NotNull
public static String getExtensionFromPath(@NotNull String path) {
int dotIndex = path.lastIndexOf(".");
if (dotIndex > 0) {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
if (path.lastIndexOf(separator) < dotIndex && (path.length() - dotIndex == 4)) {
return path.substring(dotIndex + 1);
}
}
return path;
}
/**
* Strips the extension from the given path
*
* @param path <p>The path to strip the extension from</p>
* @return <p>The input with the extension stripped</p>
*/
@NotNull
public static String stripExtensionFromPath(@NotNull String path) {
int dotIndex = path.lastIndexOf(".");
if (dotIndex > 0) {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
if (path.lastIndexOf(separator) < dotIndex && (path.length() - dotIndex == 4)) {
return path.substring(0, dotIndex);
}
}
return path;
}
/**
* Attempts to find the correct book file
*
* @param folder <p>The folder the book is in</p>
* @param bookMeta <p>The book meta of the book to find</p>
* @return <p>The book's file, or null if not found</p>
*/
@Nullable
public static File findBookFile(@NotNull String folder, @NotNull BookMeta bookMeta) {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
String fileName = bookMeta.getTitle() + separator + bookMeta.getAuthor();
return findBookFile(folder, fileName);
}
/**
* Attempts to find the correct book file
*
* @param folder <p>The folder the book is in</p>
* @param fileName <p>The name of the book to find</p>
* @return <p>The book's file, or null if not found</p>
*/
@Nullable
public static File findBookFile(@NotNull String folder, @NotNull String fileName) {
fileName = InputCleaningHelper.cleanString(fileName);
File file = new File(folder, fileName + ".yml");
if (file.exists()) {
return getBookFile(file.getAbsolutePath());
}
file = new File(folder, fileName.replace(" ", "_") + ".yml");
if (file.exists()) {
return getBookFile(file.getAbsolutePath());
}
file = new File(folder, fileName.replace(" ", "_") + ".txt");
if (file.exists()) {
return getBookFile(file.getAbsolutePath());
} else {
return null;
}
}
/**
* Replaces an author name with a player UUID if matched
*
* @param fileName <p>The filename to replace the author of</p>
* @return <p>The filename, or the filename with the author replaced with UUID</p>
*/
public static String replaceAuthorWithUUID(@NotNull String fileName) {
String userName = BookFormatter.stripColor(getBookAuthorFromPath(fileName));
Player player = Bukkit.getPlayerExact(userName);
if (player != null) {
return userName.replace(userName, player.getUniqueId().toString());
} else {
return userName;
}
}
}

View File

@@ -3,6 +3,7 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@@ -24,7 +25,7 @@ public final class BookFormatter {
*
* @param rawPages <p>A list of pages</p>
*/
public static void formatLastPage(List<String> rawPages) {
public static void formatLastPage(@NotNull List<String> rawPages) {
int maxPageText = 256;
int fitsNewline = maxPageText - 2;
@@ -49,7 +50,7 @@ public final class BookFormatter {
* @param maxPageText <p>The max number of characters which fit on a page</p>
* @param fitsNewline <p>The max number of characters on a page which still fits a newline character</p>
*/
public static void formatLastPageSplitOverflow(List<String> rawPages, int maxPageText, int fitsNewline) {
public static void formatLastPageSplitOverflow(@NotNull List<String> rawPages, int maxPageText, int fitsNewline) {
while (rawPages.get(rawPages.size() - 1).length() > maxPageText) {
int splitPosition;
String fittingText = rawPages.get(rawPages.size() - 1).substring(0, maxPageText);
@@ -77,7 +78,7 @@ public final class BookFormatter {
* @param rawPages <p>The raw pages to format</p>
* @param maxPageText <p>The max number of characters which fit on a page</p>
*/
public static void formatLastPageCombinePages(List<String> rawPages, int maxPageText) {
public static void formatLastPageCombinePages(@NotNull List<String> rawPages, int maxPageText) {
int lastPageIndex = rawPages.size() - 1;
int nextToLastIndex = rawPages.size() - 2;
if (rawPages.get(nextToLastIndex).length() + rawPages.get(lastPageIndex).length() <= maxPageText) {
@@ -92,10 +93,13 @@ public final class BookFormatter {
* @param rawPages <p>The raw pages to format</p>
* @param fitsNewline <p>The max number of characters on a page which still fits a newline character</p>
*/
public static void formatLastPageAddNewline(List<String> rawPages, int fitsNewline) {
public static void formatLastPageAddNewline(@NotNull List<String> rawPages, int fitsNewline) {
int pageIndex = rawPages.size() - 1;
if (rawPages.get(pageIndex).length() <= fitsNewline && !rawPages.get(pageIndex).isEmpty()) {
rawPages.set(pageIndex, (rawPages.get(pageIndex)) + "\n");
String pageContents = rawPages.get(pageIndex);
if (pageContents == null) {
rawPages.set(pageIndex, "");
} else if (pageContents.length() <= fitsNewline && !pageContents.isEmpty()) {
rawPages.set(pageIndex, pageContents + "\n");
}
}
@@ -105,7 +109,8 @@ public final class BookFormatter {
* @param bookMeta <p>The book meta to change</p>
* @return <p>The changed book meta</p>
*/
public static BookMeta formatPages(BookMeta bookMeta) {
@NotNull
public static BookMeta formatPages(@NotNull BookMeta bookMeta) {
List<String> formattedPages = new ArrayList<>(Objects.requireNonNull(bookMeta).getPageCount());
for (String page : bookMeta.getPages()) {
formattedPages.add(ColorHelper.translateColorCodes(page, ColorConversion.RGB));
@@ -114,4 +119,15 @@ public final class BookFormatter {
return bookMeta;
}
/**
* Strips the color from the given input
*
* @param input <p>The input to strip</p>
* @return <p>The color stripped input</p>
*/
@NotNull
public static String stripColor(@NotNull String input) {
return ColorHelper.stripColorCodes(input, ColorConversion.RGB);
}
}

View File

@@ -2,19 +2,24 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.WritableBookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
import java.util.logging.Level;
/**
* Helper class for getting abstract book information
@@ -25,15 +30,48 @@ public final class BookHelper {
}
/**
* Un-signs a book
*
* @param oldMetadata <p>The signed book's metadata</p>
* @param amount <p>The amount of unsigned books to produce</p>
* @return <p>The unsigned books, or null if unable to generate metadata</p>
*/
@Nullable
public static ItemStack unsignBook(@NotNull BookMeta oldMetadata, int amount) {
WritableBookMeta newMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITABLE_BOOK);
if (newMetadata == null) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to create writable book metadata");
return null;
}
//Create a new unsigned book with the same data
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK, amount);
if (oldMetadata.hasLore()) {
List<String> oldLore = oldMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
newMetadata.setLore(newLore);
}
}
newMetadata.setPages(oldMetadata.getPages());
newBook.setItemMeta(newMetadata);
// Give the player the new book
return newBook;
}
/**
* Converts the author of a book from UUID if necessary
*
* @param author <p>The author string</p>
* @return <p>The author string, converted if it was a UUID</p>
*/
public static String authorFromUUID(String author) {
@NotNull
public static String authorFromUUID(@NotNull String author) {
try {
UUID authorID = UUID.fromString(author);
UUID authorID = UUID.fromString(BookFormatter.stripColor(author));
Player player = Bukkit.getPlayer(authorID);
if (player != null) {
author = player.getName();
@@ -50,7 +88,8 @@ public final class BookHelper {
* @param sender <p>The command sender trying to get the directory</p>
* @return <p>The path of the directory, or null if not possible to get</p>
*/
public static File getBookDirectoryPath(BookDirectory bookDirectory, CommandSender sender) {
@Nullable
public static File getBookDirectoryPath(@NotNull BookDirectory bookDirectory, @NotNull CommandSender sender) {
String bookFolderString = getBookDirectoryPathString(bookDirectory, sender);
if (bookFolderString == null) {
return null;
@@ -65,13 +104,15 @@ public final class BookHelper {
* @param sender <p>The command sender trying to get the directory</p>
* @return <p>The path of the directory, or null if not possible to get</p>
*/
public static String getBookDirectoryPathString(BookDirectory bookDirectory, CommandSender sender) {
@Nullable
public static String getBookDirectoryPathString(@NotNull BookDirectory bookDirectory, @NotNull CommandSender sender) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
String folder = null;
String bookFolder = BooksWithoutBordersConfig.getBookFolder();
String bookFolder = config.getBookFolder();
if (bookDirectory == BookDirectory.PUBLIC) {
folder = bookFolder;
} else if (bookDirectory == BookDirectory.PLAYER && sender instanceof Player player) {
folder = bookFolder + player.getUniqueId() + getSlash();
folder = bookFolder + player.getUniqueId() + config.getSlash();
}
return folder;
}
@@ -81,9 +122,9 @@ public final class BookHelper {
*
* @param bookItem <p>The book item to increase the generation of</p>
*/
public static void increaseGeneration(ItemStack bookItem) {
public static void increaseGeneration(@NotNull ItemStack bookItem) {
BookMeta bookMeta = (BookMeta) bookItem.getItemMeta();
if (BooksWithoutBordersConfig.changeGenerationOnCopy() && bookMeta != null) {
if (BooksWithoutBorders.getConfiguration().changeGenerationOnCopy() && bookMeta != null) {
bookMeta.setGeneration(BookHelper.getNextGeneration(bookMeta.getGeneration()));
bookItem.setItemMeta(bookMeta);
}
@@ -98,7 +139,8 @@ public final class BookHelper {
* @param currentGeneration <p>The current generation of the book</p>
* @return <p>The next generation of the book</p>
*/
public static BookMeta.Generation getNextGeneration(BookMeta.Generation currentGeneration) {
@NotNull
public static BookMeta.Generation getNextGeneration(@Nullable BookMeta.Generation currentGeneration) {
if (currentGeneration == null) {
return BookMeta.Generation.COPY_OF_ORIGINAL;
}
@@ -115,27 +157,43 @@ public final class BookHelper {
* @param book <p>The book to get the file of</p>
* @param player <p>The player trying to do something with the book</p>
* @return <p>The book file</p>
* @throws IllegalArgumentException <p>If the book title or author contains the title author separator</p>
*/
public static String getBookFile(BookMeta book, Player player, boolean isPublic) {
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
@NotNull
public static String getBookFile(@NotNull BookMeta book, @NotNull OfflinePlayer player, boolean isPublic) throws IllegalArgumentException {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
String bookName;
if (book.hasTitle()) {
bookName = book.getTitle();
if (bookName == null) {
bookName = "Untitled";
}
} else {
bookName = "Untitled";
}
String playerName = player.getName() == null ? player.getUniqueId().toString() : player.getName();
String authorName;
if ((!book.hasAuthor() || isAuthor(player.getName(), book.getAuthor())) && !isPublic) {
if ((!book.hasAuthor() || isAuthor(playerName, book.getAuthor())) && !isPublic) {
//Store as unique id to account for name changes
authorName = player.getUniqueId().toString();
} else if (!book.hasAuthor()) {
authorName = player.getName();
} else {
authorName = book.getAuthor();
if (authorName == null) {
authorName = BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_AUTHOR);
}
}
return fixName(cleanString(bookName + titleAuthorSeparator + authorName), false);
if (InputCleaningHelper.cleanString(bookName).contains(separator) ||
InputCleaningHelper.cleanString(authorName).contains(separator)) {
throw new IllegalArgumentException("The author; " + authorName + " or title; " + bookName +
" contains the title author separator (" + separator + "). Saving this " +
"book would lead to unexpected problems.");
}
return InputCleaningHelper.cleanString(bookName + separator + authorName);
}
/**
@@ -145,7 +203,7 @@ public final class BookHelper {
* @param book <p>The book to check</p>
* @return <p>True if the player is not the book's author</p>
*/
public static boolean isNotAuthor(Player player, BookMeta book) {
public static boolean isNotAuthor(@NotNull Player player, @NotNull BookMeta book) {
if (isAuthor(player.getName(), book.getAuthor())) {
return false;
} else {
@@ -162,7 +220,7 @@ public final class BookHelper {
* @param author <p>The author to check</p>
* @return <p>True if the player is the author</p>
*/
private static boolean isAuthor(String playerName, String author) {
private static boolean isAuthor(@NotNull String playerName, @Nullable String author) {
playerName = InputCleaningHelper.cleanString(playerName);
return author != null && playerName.equalsIgnoreCase(InputCleaningHelper.cleanString(author));
}

View File

@@ -3,16 +3,18 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
/**
* A helper class for loading books from files
@@ -31,7 +33,9 @@ public final class BookLoader {
* @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) {
@Nullable
public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned,
@NotNull String directory) {
return loadBook(sender, fileName, isSigned, directory, 1);
}
@@ -45,59 +49,75 @@ public final class BookLoader {
* @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) {
@Nullable
public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned,
@NotNull String directory, int numCopies) {
BookDirectory bookDirectory = BookDirectory.getFromString(directory);
if (bookDirectory == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unrecognized book directory!");
return null;
}
return loadBook(sender, fileName, isSigned, bookDirectory, directory, numCopies);
}
/**
* 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 bookDirectory <p>The type of directory to save in</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>
*/
@Nullable
public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned,
@NotNull BookDirectory bookDirectory, @NotNull String directory, int numCopies) {
//Find the filename if a book index is given
try {
int bookIndex = Integer.parseInt(fileName);
List<String> availableFiles = BooksWithoutBorders.getAvailableBooks(sender, bookDirectory == BookDirectory.PUBLIC);
List<String> availableFiles = BooksWithoutBorders.getAvailableBooks(sender,
bookDirectory == BookDirectory.PUBLIC);
if (bookIndex <= availableFiles.size()) {
fileName = availableFiles.get(Integer.parseInt(fileName) - 1);
}
} catch (NumberFormatException ignored) {
}
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) {
//Try converting the username to UUID
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
String[] data = fileName.split(titleAuthorSeparator);
String extension = data[1].substring(data[1].length() - 4);
String userName = data[1].substring(0, data[1].length() - 4);
Player player = Bukkit.getPlayer(userName);
if (player != null) {
data[1] = player.getUniqueId() + extension;
file = getFullPath(sender, String.join(titleAuthorSeparator, data), bookDirectory, directory);
if (file == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
return null;
}
} else {
String replaced = BookFileHelper.replaceAuthorWithUUID(fileName);
file = getFullPath(sender, replaced, bookDirectory, directory);
if (file == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
return null;
}
}
//Make sure the player can pay for the book
if (BooksWithoutBordersConfig.booksHavePrice() &&
if (config.booksHavePrice() &&
!sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
EconomyHelper.cannotPayForBookPrinting((Player) sender, numCopies)) {
config.getEconomyManager().cannotPayForBookPrinting((Player) sender, numCopies)) {
return null;
}
//Generate a new empty book
ItemStack book;
ItemStack book = new ItemStack(Material.WRITTEN_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);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unable to create blank book metadata!");
return null;
}
//Load the book from the given file
BookToFromTextHelper.bookFromFile(file, bookMetadata);
bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(sender, "File was blank!!");
return null;
@@ -119,6 +139,10 @@ public final class BookLoader {
BookHelper.increaseGeneration(book);
book.setAmount(numCopies);
if (!isSigned.equalsIgnoreCase("true") && book.getItemMeta() != null) {
return BookHelper.unsignBook((BookMeta) book.getItemMeta(), book.getAmount());
}
return book;
}
@@ -131,14 +155,21 @@ public final class BookLoader {
* @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;
String slash = BooksWithoutBordersConfig.getSlash();
String bookFolder = BooksWithoutBordersConfig.getBookFolder();
@Nullable
private static File getFullPath(@NotNull CommandSender sender, @NotNull String fileName,
@NotNull BookDirectory bookDirectory, @NotNull String directory) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
File file = null;
String slash = config.getSlash();
if (bookDirectory == BookDirectory.ENCRYPTED) {
file = BookFileHelper.getBookFile(bookFolder + "Encrypted" + slash + directory + slash + fileName);
file = BookFileHelper.findBookFile(config.getEncryptedBookPath() + directory + slash, fileName);
} else {
file = BookFileHelper.getBookFile(BookHelper.getBookDirectoryPathString(bookDirectory, sender) + fileName);
String folder = BookHelper.getBookDirectoryPathString(bookDirectory, sender);
if (folder != null) {
file = BookFileHelper.findBookFile(folder, fileName);
} else {
BooksWithoutBorders.log(Level.WARNING, "Unknown directory " + bookDirectory);
}
}
if (file == null || !file.isFile()) {
return null;

View File

@@ -1,25 +1,25 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.knarlib.util.FileHelper;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.utility.BookHelper.authorFromUUID;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
/**
* Helper class for converting books to and from text/yml files
@@ -37,8 +37,56 @@ public final class BookToFromTextHelper {
* @param bookMetadata <p>Metadata about the book to save</p>
* @throws IOException <p>If unable to save the book</p>
*/
public static void bookToYml(String path, String fileName, BookMeta bookMetadata) throws IOException {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(new File(path, "blank"));
public static void bookToYml(@NotNull String path, @NotNull String fileName, @NotNull BookMeta bookMetadata) throws IOException {
FileConfiguration bookYml = getBookConfiguration(bookMetadata);
bookYml.save(new File(path, fileName + ".yml"));
}
/**
* Saves an encrypted book's contents to a .yml file
*
* @param path <p>The path of the folder to save to. Must end with a slash</p>
* @param fileName <p>The name of the file to load to</p>
* @param bookMetadata <p>Metadata about the book to save</p>
* @throws IOException <p>If unable to save the book</p>
*/
public static void encryptedBookToYml(@NotNull String path, @NotNull String fileName, @NotNull BookMeta bookMetadata,
@NotNull EncryptionStyle encryptionStyle, @NotNull String encryptionKey,
@Nullable AESConfiguration aesConfiguration) throws IOException {
FileConfiguration bookYml = getBookConfiguration(bookMetadata);
bookYml.set("Encryption.Style", encryptionStyle.toString());
bookYml.set("Encryption.Key", encryptionKey);
if (encryptionStyle == EncryptionStyle.AES) {
if (aesConfiguration == null) {
throw new IOException("Attempted to save AES encrypted book without supplying a configuration!");
}
bookYml.set("Encryption.AES.IV", EncryptionHelper.bytesToHex(aesConfiguration.iv()));
bookYml.set("Encryption.AES.Salt", EncryptionHelper.bytesToHex(aesConfiguration.salt()));
}
List<String> encryptedPages = EncryptionHelper.encryptDecryptBookPages(bookMetadata, encryptionStyle,
aesConfiguration, encryptionKey, true);
if (encryptedPages == null || encryptedPages.isEmpty()) {
throw new IOException("Book encryption failed!");
}
bookYml.set("Encryption.Data", encryptedPages);
// Make sure the plaintext cannot simply be seen in the file
if (BooksWithoutBorders.getConfiguration().useRealEncryption()) {
bookYml.set("Pages", null);
}
bookYml.save(path + fileName + ".yml");
}
/**
* Gets a file configuration containing a book's information
*
* @param bookMetadata <p>Metadata about the book to save</p>
*/
private static FileConfiguration getBookConfiguration(@NotNull BookMeta bookMetadata) {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(new File("", "blank"));
if (bookMetadata.hasTitle()) {
bookYml.set("Title", bookMetadata.getTitle());
@@ -58,7 +106,7 @@ public final class BookToFromTextHelper {
bookYml.set("Lore", bookMetadata.getLore());
}
bookYml.save(path + fileName + ".yml");
return bookYml;
}
/**
@@ -68,7 +116,8 @@ public final class BookToFromTextHelper {
* @param bookMetadata <p>The book metadata to use for saving the book</p>
* @return <p>The book metadata of the loaded book</p>
*/
public static BookMeta bookFromFile(File file, BookMeta bookMetadata) {
@Nullable
public static BookMeta bookFromFile(@NotNull File file, @NotNull BookMeta bookMetadata) {
if (file.getName().endsWith(".txt")) {
return bookFromTXT(file.getName(), file, bookMetadata);
} else if (file.getName().endsWith(".yml")) {
@@ -79,30 +128,67 @@ public final class BookToFromTextHelper {
}
/**
* Saves a book's contents to a text file
* Loads a book from a .yml file
*
* @param folderPath <p>The folder path to save to. Must end with a slash</p>
* @param fileName <p>The name of the file to save to</p>
* @param bookMetadata <p>Metadata about the book to save</p>
* @throws IOException <p>If unable to save the book</p>
* @param file <p>The path of the file to load</p>
* @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @param userKey <p>The user-supplied decryption key</p>
* @param forceDecrypt <p>Whether to use the saved key for decryption, ignoring the supplied key</p>
* @return <p>Metadata for the loaded book</p>
*/
public static void bookToTXT(String folderPath, String fileName, BookMeta bookMetadata) throws IOException {
FileWriter fileWriter = new FileWriter(folderPath + fileName + ".txt", StandardCharsets.UTF_8);
PrintWriter printWriter = new PrintWriter(fileWriter);
List<String> pages = bookMetadata.getPages();
@Nullable
public static BookMeta encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata, @NotNull String userKey, boolean forceDecrypt) {
BookMeta meta;
BookMeta.Generation generation = bookMetadata.getGeneration();
if (generation == null) {
generation = BookMeta.Generation.ORIGINAL;
try {
meta = bookFromYml(file, bookMetadata);
if (meta == null) {
return null;
}
} catch (IllegalArgumentException exception) {
return null;
}
String generationString = ":" + generation.name();
//Save each page of the book as a text line
printWriter.println("[Book]" + generationString);
for (String page : pages) {
printWriter.println(page);
// If the plaintext is stored in the file, don't bother with real decryption
if (!meta.getPages().isEmpty()) {
return meta;
}
printWriter.close();
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file);
userKey = EncryptionHelper.sha256(userKey);
String realKey = bookYml.getString("Encryption.Key", "");
if (forceDecrypt) {
userKey = realKey;
}
if (!userKey.equals(realKey)) {
BooksWithoutBorders.log(Level.INFO, "Supplied key: " + userKey + " does not match real key: " + realKey);
return null;
}
List<String> data = bookYml.getStringList("Encryption.Data");
if (data.isEmpty()) {
return null;
}
EncryptionStyle encryptionStyle = EncryptionStyle.getFromString(bookYml.getString("Encryption.Style",
EncryptionStyle.SUBSTITUTION.toString()));
AESConfiguration aesConfiguration = null;
if (encryptionStyle == EncryptionStyle.AES) {
byte[] iv = EncryptionHelper.hexStringToByteArray(bookYml.getString("Encryption.AES.IV", ""));
byte[] salt = EncryptionHelper.hexStringToByteArray(bookYml.getString("Encryption.AES.Salt", ""));
aesConfiguration = new AESConfiguration(iv, salt, userKey);
}
meta.setPages(data);
List<String> decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle,
aesConfiguration, userKey, false);
if (decryptedPages != null && !decryptedPages.isEmpty()) {
meta.setPages(decryptedPages);
} else {
return null;
}
return meta;
}
/**
@@ -112,13 +198,15 @@ public final class BookToFromTextHelper {
* @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @return <p>Metadata for the loaded book</p>
*/
private static BookMeta bookFromYml(File file, BookMeta bookMetadata) {
@Nullable
private static BookMeta bookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata) {
try {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file);
bookMetadata.setGeneration(BookMeta.Generation.valueOf(bookYml.getString("Generation", "ORIGINAL")));
bookMetadata.setTitle(bookYml.getString("Title", "Untitled"));
bookMetadata.setAuthor(authorFromUUID(bookYml.getString("Author", "Unknown")));
bookMetadata.setAuthor(authorFromUUID(bookYml.getString("Author",
BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_AUTHOR))));
bookMetadata.setPages(bookYml.getStringList("Pages"));
bookMetadata.setLore(bookYml.getStringList("Lore"));
} catch (IllegalArgumentException e) {
@@ -135,37 +223,27 @@ public final class BookToFromTextHelper {
* @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @return <p>Metadata for the loaded book</p>
*/
private static BookMeta bookFromTXT(String fileName, File file, BookMeta bookMetadata) {
String author;
String title;
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
//Remove .txt extension
fileName = fileName.substring(0, fileName.length() - 4);
@Nullable
private static BookMeta bookFromTXT(@NotNull String fileName, @NotNull File file, @NotNull BookMeta bookMetadata) {
//Get title and author from the file name
if (fileName.contains(titleAuthorSeparator)) {
String[] titleAuthor = fileName.split(titleAuthorSeparator);
title = titleAuthor[0];
author = titleAuthor[1];
} else {
author = "Unknown";
title = fileName;
}
//Replace underscores with spaces
title = fixName(title, true);
String title = BookFileHelper.getBookTitleFromPath(fileName);
String author = BookFileHelper.getBookAuthorFromPath(fileName);
//Read the .txt file
List<String> rawPages;
try {
rawPages = readTextFile(file);
if (rawPages == null) {
BooksWithoutBorders.log(Level.SEVERE, "Text file's first line was null");
return null;
}
} catch (IOException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to read text file");
BooksWithoutBorders.log(Level.SEVERE, "Unable to read text file");
return null;
}
//Parse the generation from the book data
if (rawPages != null && !rawPages.isEmpty() && rawPages.get(0).startsWith("Generation:")) {
if (!rawPages.isEmpty() && rawPages.get(0).startsWith("Generation:")) {
bookMetadata.setGeneration(BookMeta.Generation.valueOf(rawPages.get(0).split(":")[1]));
rawPages.remove(0);
}
@@ -175,7 +253,7 @@ public final class BookToFromTextHelper {
//Update the metadata of the book with its new values
bookMetadata.setAuthor(authorFromUUID(author));
bookMetadata.setTitle(title);
bookMetadata.setTitle(title.substring(0, 32));
bookMetadata.setPages(pages);
return bookMetadata;
@@ -188,7 +266,8 @@ public final class BookToFromTextHelper {
* @return <p>A string list where each string is the text on one page</p>
* @throws IOException <p>If unable to read from the file</p>
*/
private static List<String> readTextFile(File file) throws IOException {
@Nullable
private static List<String> readTextFile(@NotNull File file) throws IOException {
List<String> rawPages = new ArrayList<>();
BufferedReader bufferedReader = FileHelper.getBufferedReaderFromInputStream(new FileInputStream(file));

View File

@@ -2,32 +2,42 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.encryption.AES;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.bookswithoutborders.encryption.Encryptor;
import net.knarcraft.bookswithoutborders.encryption.GenenCrypt;
import net.knarcraft.bookswithoutborders.encryption.Magic;
import net.knarcraft.bookswithoutborders.encryption.OneTimePad;
import net.knarcraft.bookswithoutborders.encryption.SubstitutionCipher;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
/**
* Helper class for book encryption
*/
public final class EncryptionHelper {
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private EncryptionHelper() {
}
@@ -35,46 +45,79 @@ public final class EncryptionHelper {
* Transforms a string key/password into its numerical values
*
* @param key <p>The key to transform</p>
* @return <p>The numbers representing the key's characters</p>
* @return <p>A comma-separated string of the numbers representing the key's characters</p>
*/
public static String getNumberKeyFromStringKey(String key) {
StringBuilder integerKey = new StringBuilder();
for (int x = 0; x < key.length(); x++) {
integerKey.append(Character.getNumericValue(Character.codePointAt(key, x)));
@NotNull
public static String getNumberKeyFromStringKey(@NotNull String key) {
StringBuilder integerKey = new StringBuilder(String.valueOf(Character.codePointAt(key, 0)));
for (int x = 1; x < key.length(); x++) {
integerKey.append(", ").append(Character.codePointAt(key, x));
}
return integerKey.toString();
}
/**
* Performs sha256 hashing on the input string
*
* @param input <p>The input to hash</p>
* @return <p>The hashed input</p>
*/
@NotNull
public static String sha256(@NotNull String input) {
String hashed;
try {
final MessageDigest digest = MessageDigest.getInstance("SHA3-256");
final byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
hashed = Base64.getEncoder().encodeToString(hashBytes);
} catch (NoSuchAlgorithmException exception) {
hashed = input;
}
return hashed;
}
/**
* Encrypts the pages of a book
*
* @param book <p>The book to encrypt</p>
* @param style <p>The encryption style to use</p>
* @param integerKey <p>The encryption key to use</p>
* @param player <p>The player trying to encrypt a book</p>
* @param book <p>The book to encrypt</p>
* @param style <p>The encryption style to use</p>
* @param aesConfiguration <p>The AES configuration to use, if encrypting using AES</p>
* @param key <p>The encryption key to use</p>
* @param encrypt <p>Whether to perform an encryption or a decryption</p>
* @return <p>The pages of the book in encrypted form</p>
*/
public static List<String> encryptBookPages(BookMeta book, EncryptionStyle style, String integerKey, Player player) {
@Nullable
public static List<String> encryptDecryptBookPages(@NotNull BookMeta book, @NotNull EncryptionStyle style,
@Nullable AESConfiguration aesConfiguration, @NotNull String key,
boolean encrypt) {
Encryptor encryptor = switch (style) {
case DNA -> new GenenCrypt(EncryptionHelper.getNumberKeyFromStringKey(key));
case SUBSTITUTION -> new SubstitutionCipher(EncryptionHelper.getNumberKeyFromStringKey(key));
case AES -> {
if (aesConfiguration == null) {
throw new IllegalArgumentException("Attempted to perform AES encryption without a valid AES configuration");
} else {
yield new AES(aesConfiguration);
}
}
case ONE_TIME_PAD -> new OneTimePad(key);
case MAGIC -> new Magic();
};
List<String> encryptedPages = new ArrayList<>();
//Scramble the book's contents
if (style == EncryptionStyle.DNA) {
//Encrypt the pages using gene-based encryption
GenenCrypt gc = new GenenCrypt(integerKey);
for (int x = 0; x < book.getPages().size(); x++) {
encryptedPages.add(gc.encrypt(book.getPage(x + 1)));
for (int x = 0; x < book.getPages().size(); x++) {
String text = book.getPage(x + 1);
String output;
if (encrypt) {
output = encryptor.encryptText(text);
} else {
output = encryptor.decryptText(text);
}
return encryptedPages;
} else if (style == EncryptionStyle.SUBSTITUTION) {
//Encrypt the pages using a substitution cipher
SubstitutionCipher sc = new SubstitutionCipher();
for (int x = 0; x < book.getPages().size(); x++) {
encryptedPages.add(sc.encrypt(book.getPage(x + 1), integerKey));
if (output == null || output.isEmpty()) {
return null;
}
return encryptedPages;
} else {
BooksWithoutBorders.sendErrorMessage(player, "Invalid encryption style encountered!");
return null;
encryptedPages.add(output);
}
return encryptedPages;
}
/**
@@ -86,7 +129,9 @@ public final class EncryptionHelper {
* @param style <p>The encryption style to use</p>
* @return <p>An encrypted version of the book</p>
*/
public static ItemStack encryptBook(Player player, boolean mainHand, String key, EncryptionStyle style) {
@Nullable
public static ItemStack encryptBook(@NotNull Player player, boolean mainHand, @NotNull String key,
@NotNull EncryptionStyle style) {
return encryptBook(player, mainHand, key, style, "");
}
@@ -100,25 +145,32 @@ public final class EncryptionHelper {
* @param groupName <p>The name of the group to encrypt for, or "" otherwise</p>
* @return <p>An encrypted version of the book</p>
*/
public static ItemStack encryptBook(Player player, boolean mainHand, String key, EncryptionStyle style, String groupName) {
//converts user supplied key into integer form
String integerKey = EncryptionHelper.getNumberKeyFromStringKey(key);
@Nullable
public static ItemStack encryptBook(Player player, boolean mainHand, @NotNull String key,
@NotNull EncryptionStyle style, @NotNull String groupName) {
BookMeta book = InventoryHelper.getHeldBookMetadata(player, mainHand);
if (book == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata from the held book!");
return null;
}
if (!book.hasPages()) {
BooksWithoutBorders.sendErrorMessage(player, "Book is empty!");
return null;
}
String hashedKey = sha256(key);
AESConfiguration configuration = AESConfiguration.getNewConfiguration(hashedKey);
//Save the book's un-encrypted contents to a file
BookMeta newMetadata = saveBookPlaintext(groupName, player, book, integerKey);
BookMeta newMetadata = saveBookPlaintext(groupName, player, book, style, hashedKey, configuration);
if (newMetadata == null) {
return null;
}
//Get the encrypted pages
List<String> encryptedPages = EncryptionHelper.encryptBookPages(book, style, integerKey, player);
List<String> encryptedPages = EncryptionHelper.encryptDecryptBookPages(book, style, configuration, hashedKey,
true);
if (encryptedPages == null) {
return null;
}
@@ -143,7 +195,9 @@ public final class EncryptionHelper {
* @param newMetadata <p>The new metadata of the book</p>
* @return <p>An encrypted version of the book</p>
*/
private static ItemStack createEncryptedBook(BookMeta book, List<String> newPages, Player player, BookMeta newMetadata) {
@NotNull
private static ItemStack createEncryptedBook(@NotNull BookMeta book, @NotNull List<String> newPages,
@NotNull Player player, @NotNull BookMeta newMetadata) {
//Create the encrypted book
ItemStack encryptedBook = new ItemStack(Material.WRITTEN_BOOK);
book.setPages(newPages);
@@ -160,17 +214,22 @@ public final class EncryptionHelper {
/**
* Saves a book's plain text to a file
*
* @param groupName <p>The group who's allowed to decrypt the book, or ""</p>
* @param player <p>The player trying to encrypt the book</p>
* @param book <p>The book to encrypt</p>
* @param integerKey <p>The key used to encrypt the book</p>
* @param groupName <p>The group who's allowed to decrypt the book, or ""</p>
* @param player <p>The player trying to encrypt the book</p>
* @param book <p>The book to encrypt</p>
* @param encryptionStyle <p>The encryption style used for the book</p>
* @param key <p>The key used to encrypt the book</p>
* @param aesConfiguration <p>The AES configuration to use, if encrypting using AES</p>
* @return <p>The new metadata for the book, or null if it could not be saved</p>
*/
private static BookMeta saveBookPlaintext(String groupName, Player player, BookMeta book, String integerKey) {
@Nullable
private static BookMeta saveBookPlaintext(@NotNull String groupName, @NotNull Player player,
@NotNull BookMeta book, @NotNull EncryptionStyle encryptionStyle,
@NotNull String key, @NotNull AESConfiguration aesConfiguration) {
BookMeta newMetadata = book;
boolean wasSaved;
if (groupName.trim().isEmpty()) {
wasSaved = saveEncryptedBook(player, book, integerKey);
wasSaved = saveEncryptedBook(player, book, encryptionStyle, key, aesConfiguration);
} else {
newMetadata = saveEncryptedBookForGroup(player, book, groupName);
wasSaved = newMetadata != null;
@@ -188,19 +247,88 @@ public final class EncryptionHelper {
* @param player <p>The player trying to load the book</p>
* @param key <p>The encryption key/password for decryption</p>
* @param deleteEncryptedFile <p>Whether to delete the plaintext file after decryption is finished</p>
* @param forceDecrypt <p>Whether to force decryption using the stored key</p>
* @return <p>The loaded book, or null if no book could be loaded</p>
*/
public static ItemStack loadEncryptedBook(Player player, String key, boolean deleteEncryptedFile) {
@Nullable
public static ItemStack loadEncryptedBook(@NotNull Player player, @NotNull String key, boolean deleteEncryptedFile,
boolean forceDecrypt) {
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta();
String path = getBookFolder() + "Encrypted" + getSlash();
String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
if (bookMetadata == null) {
return null;
}
String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetadata, player, true);
fileName = fixName(cleanString(fileName), false);
String fileName = BookHelper.getBookFile(bookMetadata, player, true);
fileName = cleanString(fileName);
File file = new File(path + fileName + ".yml");
if (!file.isFile()) {
file = new File(path + fileName + ".txt");
if (!file.isFile()) {
BooksWithoutBorders.sendErrorMessage(player, "Book not found!");
return null;
}
} else {
try {
bookMetadata = BookToFromTextHelper.encryptedBookFromYml(file, bookMetadata, key, forceDecrypt);
if (bookMetadata == null) {
throw new IllegalArgumentException();
}
} catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!");
return null;
}
}
if (deleteEncryptedFile) {
Logger logger = BooksWithoutBorders.getInstance().getLogger();
try {
if (!file.delete()) {
logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\n" +
"File location:" + file.getPath());
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\nFile location:" + file.getPath());
}
}
ItemStack newBook = new ItemStack(Material.WRITTEN_BOOK);
newBook.setItemMeta(bookMetadata);
newBook.setAmount(InventoryHelper.getHeldBook(player, true).getAmount());
return newBook;
}
/**
* Loads an encrypted book
*
* @param player <p>The player trying to load the book</p>
* @param key <p>The encryption key/password for decryption</p>
* @param deleteEncryptedFile <p>Whether to delete the plaintext file after decryption is finished</p>
* @return <p>The loaded book, or null if no book could be loaded</p>
*/
@Nullable
public static ItemStack loadEncryptedBookLegacy(@NotNull Player player, @NotNull String key, boolean deleteEncryptedFile) {
BooksWithoutBorders.sendErrorMessage(player, "Attempting legacy decryption");
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta();
String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
if (bookMetadata == null) {
return null;
}
StringBuilder integerKey = new StringBuilder();
for (int x = 0; x < key.length(); x++) {
integerKey.append(Character.getNumericValue(Character.codePointAt(key, x)));
}
String fileName = "[" + integerKey + "]" + BookHelper.getBookFile(bookMetadata, player, true);
fileName = cleanString(fileName).replace(" ", "_");
File file = new File(path + fileName + ".yml");
if (!file.isFile()) {
@@ -213,6 +341,10 @@ public final class EncryptionHelper {
} else {
try {
bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!");
return null;
}
} catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!");
return null;
@@ -220,24 +352,55 @@ public final class EncryptionHelper {
}
if (deleteEncryptedFile) {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getConsoleSender();
Logger logger = BooksWithoutBorders.getInstance().getLogger();
try {
if (!file.delete()) {
BooksWithoutBorders.sendErrorMessage(consoleSender, "Book encryption data failed to delete upon decryption!");
BooksWithoutBorders.sendErrorMessage(consoleSender, "File location:" + file.getPath());
logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\n" +
"File location:" + file.getPath());
}
} catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(consoleSender, "Book encryption data failed to delete upon decryption!");
BooksWithoutBorders.sendErrorMessage(consoleSender, "File location:" + file.getPath());
logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\nFile location:" + file.getPath());
}
}
ItemStack newBook = new ItemStack(Material.WRITTEN_BOOK);//Book(book.getAuthor(), book.getTitle(), pages, 1, 387);
ItemStack newBook = new ItemStack(Material.WRITTEN_BOOK);
newBook.setItemMeta(bookMetadata);
newBook.setAmount(InventoryHelper.getHeldBook(player, true).getAmount());
return newBook;
}
/**
* Converts a byte array to a hexadecimal string
*
* @param bytes <p>The bytes to convert</p>
* @return <p>The resulting hexadecimal string</p>
*/
public static String bytesToHex(byte[] bytes) {
byte[] hexChars = new byte[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars, StandardCharsets.UTF_8);
}
/**
* Converts a string of hexadecimals to a byte array
*
* @param input <p>The hexadecimal input to parse</p>
* @return <p>The resulting byte array</p>
*/
public static byte[] hexStringToByteArray(@NotNull String input) {
int len = input.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(input.charAt(i), 16) << 4) +
Character.digit(input.charAt(i + 1), 16));
}
return data;
}
/**
* Saves an encrypted book to be decryptable for the given group
*
@@ -246,18 +409,21 @@ public final class EncryptionHelper {
* @param groupName <p>The group which should be able to decrypt the book</p>
* @return <p>The new encrypted metadata for the book, or null if encryption failed</p>
*/
private static BookMeta saveEncryptedBookForGroup(Player player, BookMeta bookMetadata, String groupName) {
String path = getBookFolder() + "Encrypted" + getSlash() + cleanString(groupName) + getSlash();
@Nullable
private static BookMeta saveEncryptedBookForGroup(@NotNull Player player, @NotNull BookMeta bookMetadata,
@NotNull String groupName) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
String path = config.getEncryptedBookPath() + cleanString(groupName) + config.getSlash();
File dirTest = new File(path);
//Creates group dir
if (!dirTest.exists()) {
try {
if (!dirTest.mkdir()) {
BooksWithoutBorders.sendErrorMessage(BooksWithoutBorders.getConsoleSender(), "Unable to create encryption group folder!");
BooksWithoutBorders.log(Level.SEVERE, "Unable to create encryption group folder!");
return null;
}
} catch (Exception exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to save group encrypted book");
BooksWithoutBorders.log(Level.SEVERE, "Unable to save group encrypted book");
return null;
}
}
@@ -275,8 +441,7 @@ public final class EncryptionHelper {
bookMetadata.setLore(newLore);
//Save file
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") :
new File(path + fileName + ".txt");
File file = new File(path + fileName + ".yml");
if (!file.isFile()) {
try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetadata);
@@ -292,26 +457,30 @@ public final class EncryptionHelper {
/**
* Saves an encrypted book to be decryptable for the given user
*
* @param player <p>The player encrypting the book</p>
* @param bookMetaData <p>Metadata for the book to encrypt</p>
* @param key <p>The key to use for encryption</p>
* @param player <p>The player encrypting the book</p>
* @param bookMetaData <p>Metadata for the book to encrypt</p>
* @param encryptionStyle <p>The style of encryption used</p>
* @param key <p>The key to use for encryption</p>
* @param aesConfiguration <p>The AES configuration to use if encrypting with AES</p>
* @return <p>The new encrypted metadata for the book, or null if encryption failed</p>
*/
private static Boolean saveEncryptedBook(Player player, BookMeta bookMetaData, String key) {
String path = getBookFolder() + "Encrypted" + getSlash();
@NotNull
private static Boolean saveEncryptedBook(@NotNull Player player, @NotNull BookMeta bookMetaData,
@NotNull EncryptionStyle encryptionStyle, @NotNull String key,
@Nullable AESConfiguration aesConfiguration) {
String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetaData, player, true);
fileName = fixName(cleanString(fileName), false);
String fileName = BookHelper.getBookFile(bookMetaData, player, true);
fileName = cleanString(fileName);
//cancels saving if file is already encrypted
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") :
new File(path + fileName + ".txt");
File file = new File(path + fileName + ".yml");
if (file.isFile()) {
return true;
}
try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetaData);
BookToFromTextHelper.encryptedBookToYml(path, fileName, bookMetaData, encryptionStyle, key, aesConfiguration);
} catch (IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!");
return false;

View File

@@ -1,7 +1,12 @@
package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Helper class for cleaning input and names
@@ -17,7 +22,8 @@ public final class InputCleaningHelper {
* @param list <p>The list to clean</p>
* @return <p>A clean list containing all relevant values</p>
*/
public static List<String> cleanList(List<String> list) {
@NotNull
public static List<String> cleanList(@NotNull List<String> list) {
List<String> resultList = new ArrayList<>(list);
resultList.removeIf((item) -> item == null || item.trim().isEmpty());
return resultList;
@@ -29,7 +35,8 @@ public final class InputCleaningHelper {
* @param fileName <p>The file name to clean</p>
* @return <p>The cleaned file name</p>
*/
public static String cleanString(String fileName) {
@NotNull
public static String cleanString(@NotNull String fileName) {
fileName = fileName.replace("/", "");
fileName = fileName.replace("\\", "");
fileName = fileName.replace("*", "");
@@ -39,23 +46,81 @@ public final class InputCleaningHelper {
fileName = fileName.replace(">", "");
fileName = fileName.replace("?", "");
fileName = fileName.replace("\"", "");
fileName = fileName.replace("§", "&");
return fileName;
}
/**
* Changes spaces to underscores or underscores to spaces, depending on context
* Parses a page number for a string like "page1"
*
* @param fileName <p>The file name to fix</p>
* @param isLoading <p>Whether loading from a file as opposed to saving to a file</p>
* @return <p>The fixed name</p>
* @param input <p>The input to parse</p>
* @return <p>The page number, or 0 if not valid</p>
*/
public static String fixName(String fileName, Boolean isLoading) {
if (isLoading) {
fileName = fileName.replace("_", " ");
} else {
fileName = fileName.replace(" ", "_");
public static int parsePageNumber(@NotNull String input) {
try {
Pattern pattern = Pattern.compile("page([0-9]+)");
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
return Integer.parseInt(matcher.group(1));
} else {
return 0;
}
} catch (NumberFormatException exception) {
return 0;
}
return fileName;
}
/**
* Parses an author specifier given in a command
*
* @param input <p>The input to parse</p>
* @return <p>The author name, or null if not an author specifier</p>
*/
@Nullable
public static String parseAuthorSpecifier(@NotNull String input) {
Pattern pattern = Pattern.compile("author([\\p{L}0-9_.,!#%&'`^@$+]+)");
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
return matcher.group(1);
} else {
return null;
}
}
/**
* Checks whether the given input is a boolean
*
* @param input <p>The input to validate</p>
* @return <p>True if the given input is a boolean</p>
*/
public static boolean isBoolean(@NotNull String input) {
return input.matches("(true|false)");
}
/**
* Checks whether the given input is an integer
*
* @param input <p>The input to validate</p>
* @return <p>True if the given input is an integer</p>
*/
public static boolean isInt(@NotNull String input) {
return input.matches("[0-9]+");
}
/**
* Merges all arguments to a string with spaces
*
* @param arguments <p>The arguments to merge</p>
* @param stripLastN <p>How many of the last arguments to ignore</p>
* @return <p>The merged arguments</p>
*/
@NotNull
public static String mergeArguments(String[] arguments, int stripLastN) {
StringBuilder builder = new StringBuilder(arguments[0]);
for (int i = 1; i < arguments.length - stripLastN; i++) {
builder.append(" ").append(arguments[i]);
}
return builder.toString();
}
}

View File

@@ -1,5 +1,7 @@
package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@@ -40,6 +42,7 @@ public final class IntegerToRomanConverter {
* @param number <p>The number to convert</p>
* @return <p>The roman representation of the number</p>
*/
@NotNull
public static String getRomanNumber(int number) {
StringBuilder output = new StringBuilder();
int remainder = number;
@@ -83,6 +86,7 @@ public final class IntegerToRomanConverter {
* @param times <p>The number of times to repeat the character</p>
* @return <p>The repeated string</p>
*/
@NotNull
private static String repeat(char character, int times) {
return String.valueOf(character).repeat(Math.max(0, times));
}

View File

@@ -7,6 +7,8 @@ import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The inventory helper mainly helps with getting and setting books
@@ -16,6 +18,25 @@ public final class InventoryHelper {
private InventoryHelper() {
}
/**
* Gets the book from a player's main hand
*
* @param player <p>The player holding the book</p>
* @return <p>The held book, or null if not holding one book in the main hand</p>
*/
public static ItemStack getHeldBook(@NotNull Player player) {
@NotNull ItemSlot heldSigned = InventoryHelper.getHeldSlotBook(player, true, true,
true, true);
@NotNull ItemSlot heldUnSigned = InventoryHelper.getHeldSlotBook(player, true, true,
true, false);
if (heldSigned == ItemSlot.MAIN_HAND || heldUnSigned == ItemSlot.MAIN_HAND) {
boolean holdingSigned = heldSigned == ItemSlot.MAIN_HAND;
return InventoryHelper.getHeldBook(player, holdingSigned);
} else {
return null;
}
}
/**
* Gets the book the holder is playing
*
@@ -23,7 +44,8 @@ public final class InventoryHelper {
* @param signedBook <p>Whether to check for signed or unsigned books</p>
* @return <p>The book the player is holding</p>
*/
public static ItemStack getHeldBook(Player player, boolean signedBook) {
@NotNull
public static ItemStack getHeldBook(@NotNull Player player, boolean signedBook) {
ItemSlot heldSlot = getHeldSlotBook(player, false, false, true, signedBook);
if (heldSlot == ItemSlot.MAIN_HAND) {
return getHeldItem(player, true);
@@ -40,7 +62,7 @@ public final class InventoryHelper {
* @param player <p>The player holding the book</p>
* @param newBook <p>The new book the player should hold</p>
*/
public static void setHeldWrittenBook(Player player, ItemStack newBook) {
public static void setHeldWrittenBook(@NotNull Player player, @NotNull ItemStack newBook) {
ItemSlot itemSlot = getHeldSlotBook(player, false, false, true, true);
if (itemSlot == ItemSlot.MAIN_HAND) {
replaceHeldItem(player, newBook, true);
@@ -59,7 +81,8 @@ public final class InventoryHelper {
* @param twoBooksMessage <p>The message to display if the player is holding one book in each hand</p>
* @return <p>False if the player is holding exactly one book</p>
*/
public static boolean notHoldingOneWritableBookCheck(Player player, String noBookMessage, String twoBooksMessage) {
public static boolean notHoldingOneWritableBookCheck(@NotNull Player player, @NotNull String noBookMessage,
@NotNull String twoBooksMessage) {
BookHoldingState holdingState = getBookHoldingState(player);
if (holdingState == BookHoldingState.NONE || holdingState == BookHoldingState.SIGNED_BOTH_HANDS ||
@@ -84,7 +107,8 @@ public final class InventoryHelper {
* @param twoBooksMessage <p>The message to display if the player is holding one book in each hand</p>
* @return <p>False if the player is holding exactly one book</p>
*/
public static boolean notHoldingOneWrittenBookCheck(Player player, String noBookMessage, String twoBooksMessage) {
public static boolean notHoldingOneWrittenBookCheck(@NotNull Player player, @NotNull String noBookMessage,
@NotNull String twoBooksMessage) {
BookHoldingState holdingState = getBookHoldingState(player);
if (holdingState == BookHoldingState.NONE || holdingState == BookHoldingState.UNSIGNED_BOTH_HANDS ||
@@ -111,7 +135,8 @@ public final class InventoryHelper {
* @param writtenBook <p>Whether to search for written or unwritten books, if it's relevant</p>
* @return <p>The slot of the player's held book</p>
*/
public static ItemSlot getHeldSlotBook(Player player, boolean handMatters, boolean mainHand,
@NotNull
public static ItemSlot getHeldSlotBook(@NotNull Player player, boolean handMatters, boolean mainHand,
boolean typeMatters, boolean writtenBook) {
BookHoldingState state = getBookHoldingState(player);
ItemStack mainHandItem = getHeldItem(player, true);
@@ -168,7 +193,8 @@ public final class InventoryHelper {
* @param player <p>The player to check</p>
* @return <p>The state of the player's book holding</p>
*/
private static BookHoldingState getBookHoldingState(Player player) {
@NotNull
private static BookHoldingState getBookHoldingState(@NotNull Player player) {
ItemStack mainHandItem = getHeldItem(player, true);
ItemStack offHandItem = getHeldItem(player, false);
@@ -205,7 +231,8 @@ public final class InventoryHelper {
* @param mainHand <p>Whether to get information about a book in the player's main hand or off hand</p>
* @return <p>Information about the held book</p>
*/
public static BookMeta getHeldBookMetadata(Player player, boolean mainHand) {
@Nullable
public static BookMeta getHeldBookMetadata(@NotNull Player player, boolean mainHand) {
return (BookMeta) getHeldItem(player, mainHand).getItemMeta();
}
@@ -216,7 +243,8 @@ public final class InventoryHelper {
* @param mainHand <p>Whether to get the item in the player's main hand or off hand</p>
* @return <p>The item the player is holding in the given hand</p>
*/
public static ItemStack getHeldItem(Player player, boolean mainHand) {
@NotNull
public static ItemStack getHeldItem(@NotNull Player player, boolean mainHand) {
if (mainHand) {
return player.getInventory().getItemInMainHand();
} else {
@@ -231,7 +259,7 @@ public final class InventoryHelper {
* @param newBook <p>The new book the player should hold</p>
* @param mainHand <p>Whether to replace the item in the player's main hand or off hand</p>
*/
public static void replaceHeldItem(Player player, ItemStack newBook, boolean mainHand) {
public static void replaceHeldItem(@NotNull Player player, @NotNull ItemStack newBook, boolean mainHand) {
if (mainHand) {
player.getInventory().setItemInMainHand(newBook);
} else {

View File

@@ -1,5 +1,7 @@
package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
@@ -16,6 +18,7 @@ public final class TabCompletionTypeHelper {
*
* @return <p>A list of booleans</p>
*/
@NotNull
public static List<String> getBooleans() {
List<String> booleans = new ArrayList<>();
booleans.add("true");
@@ -30,6 +33,7 @@ public final class TabCompletionTypeHelper {
* @param end <p>The end number</p>
* @return <p>A list of numbers</p>
*/
@NotNull
public static List<String> getNumbers(int start, int end) {
List<String> numbers = new ArrayList<>();
for (int i = start; i <= end; i++) {
@@ -45,6 +49,7 @@ public final class TabCompletionTypeHelper {
* @param end <p>The end number</p>
* @return <p>A list of booleans and numbers</p>
*/
@NotNull
public static List<String> getBooleansAndNumbers(int start, int end) {
List<String> booleansAndNumbers = new ArrayList<>();
List<String> booleans = getBooleans();
@@ -54,4 +59,27 @@ public final class TabCompletionTypeHelper {
return booleansAndNumbers;
}
/**
* Gets tab-completions with only remaining text, from a list of full strings
*
* @param arguments <p>The arguments given by the user</p>
* @param filtered <p>Tab-completions filtered by user input</p>
* @return <p>The cleaned tab-completions</p>
*/
public static @NotNull List<String> getCleanedTabCompletions(@NotNull String[] arguments,
@NotNull List<String> filtered) {
List<String> cleaned = new ArrayList<>();
for (String name : filtered) {
String[] parts = name.split(" ");
if (parts[arguments.length - 2].equalsIgnoreCase(arguments[arguments.length - 2])) {
StringBuilder builder = new StringBuilder(parts[arguments.length - 1]);
for (int i = arguments.length; i < parts.length; i++) {
builder.append(" ").append(parts[i]);
}
cleaned.add(builder.toString());
}
}
return cleaned;
}
}

View File

@@ -1,9 +1,10 @@
Options:
# Whether to use YAML for saved books instead of just storing them as text
Save_Books_in_Yaml_Format: true
# The language to use. Only "en" is built-in, but custom languages can be added
Language: "en"
# 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
# The separator used to separate the book title and the book author. While this is a ',' by default for backwards
# compatibility, a rarely used character like '¤' is better to prevent restrictions on book titles.
Title-Author_Separator: ","
# The separator used to denote a new line in the book/item lore
Lore_line_separator: "~"
@@ -31,4 +32,8 @@ Options:
# vanilla behavior where a copy of a copy cannot be copied further.
Change_Generation_On_Copy: false
# Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents.
Enable_Book_Peeking: false
Enable_Book_Peeking: true
# Whether to use true AES encryption when encrypting and decrypting books. While the hashed password used for
# encryption is still stored in the book file, the real contents of the book are not. Admin decrypt can be used to
# peek at books, if an admin gets a hold of one, but only the encrypted AES cypher text is stored in the book.
Use_Real_Encryption: false

View File

@@ -97,6 +97,22 @@ commands:
description: Reloads BwB's configuration file
usage: /<command>
permission: bookswithoutborders.reload
setBookshelfData:
description: Sets custom data for a chiseled bookshelf used when peeking at the bookshelf
usage: /<command> <delete/name/lore> <text> [more text]
permission: bookswithoutborders.editbookshelf
addBookTitlePage:
description: Adds a blank page, title page or chapter page depending on input and whether the book is signed
usage: /<command> [page index] [title~description]
permission: bookswithoutborders.addtitlepage
deleteBookPage:
description: Deletes one page from a book
usage: /<command> <page>
permission: bookswithoutborders.deletepage
migrateBooks:
description: Migrates all txt books to yml, and fixes any incorrect filenames
usage: /<command>
permission: bookswithoutborders.admin
permissions:
bookswithoutborders.*:
description: Grants all permissions
@@ -125,6 +141,7 @@ permissions:
bookswithoutborders.setbookprice: true
bookswithoutborders.reload: true
bookswithoutborders.setgeneration: true
bookswithoutborders.editbookshelf: true
bookswithoutborders.use:
description: Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled
children:
@@ -142,6 +159,8 @@ permissions:
bookswithoutborders.setlore: true
bookswithoutborders.format: true
bookswithoutborders.setgeneration: true
bookswithoutborders.addtitlepage: true
bookswithoutborders.deletepage: true
bookswithoutborders.format:
description: Allows a player to format a book
bookswithoutborders.save:
@@ -193,4 +212,10 @@ permissions:
bookswithoutborders.setgeneration:
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
description: Allows player to left-click a bookshelf to see the contents of the shelf
bookswithoutborders.editbookshelf:
description: Allows player to set name/lore for bookshelves, used for peeking
bookswithoutborders.addtitlepage:
description: Allows player to add a blank title page to a book
bookswithoutborders.deletepage:
description: Allows player to delete a page from a book

View File

@@ -0,0 +1,58 @@
en:
PREFIX: "[BwB]"
SUCCESS_COPY: "Book copied!"
SUCCESS_CLEARED: "Book cleared!"
SUCCESS_DECRYPTED: "Book decrypted!"
SUCCESS_AUTO_DECRYPTED: "Book auto-decrypted!"
SUCCESS_DELETED: "\"{file}\" deleted successfully"
SUCCESS_FORMATTED: "Book formatted!"
SUCCESS_GIVE_SENT: "Book sent!"
SUCCESS_GIVE_RECEIVED: "Book received!"
ERROR_PLAYER_ONLY: "This command can only be used by a player!"
ERROR_NOT_HOLDING_WRITTEN_BOOK: "You must be holding a written book to {action} it!"
ERROR_NOT_HOLDING_WRITABLE_BOOK: "You must be holding a writable book to {action} it!"
ERROR_NOT_HOLDING_ANY_BOOK: "You must be holding a book to perform this command"
ERROR_ONLY_ONE_BOOK: "You cannot {action} two books at once!"
ACTION_COPY: "copy"
ACTION_CLEAR: "clear"
ACTION_DECRYPT: "decrypt"
ACTION_ENCRYPT: "encrypt"
ACTION_FORMAT: "format"
ERROR_COPY_COUNT_NOT_SPECIFIED: "You must specify the number of copies to be made!"
ERROR_COPY_NEGATIVE_AMOUNT: "Number of copies must be larger than 0!"
ERROR_COPY_INVALID_AMOUNT: |
Book not copied!
Number specified was invalid!
ERROR_BOOK_COPIED_TOO_FAR: "You cannot copy this book any further. You must have the original or a direct copy."
ERROR_INVENTORY_FULL: "You need an available slot in your inventory."
ERROR_METADATA_MISSING: "Unable to get metadata for the held book!"
ERROR_VAULT_COST_BUT_UNAVAILABLE: "&4The cost was set to economy, but Vault is unavailable!"
ERROR_DECRYPT_MISSING_KEY: "No decryption password given!"
ERROR_DECRYPT_FAILED: "Failed to decrypt book!"
ERROR_ENCRYPTED_DIRECTORY_EMPTY_OR_MISSING: "Could not find any encrypted files!"
ERROR_ENCRYPTED_BOOK_UNKNOWN: "No matching encrypted book found!"
ERROR_DELETE_EMPTY: "No files available to delete!"
ERROR_INCORRECT_FILE_NAME: "Incorrect file name!"
ERROR_DELETE_FAILED_SILENT: "Deletion failed without an exception!"
ERROR_DELETE_FAILED_EXCEPTION: "Deletion failed!"
ERROR_ENCRYPT_NO_KEY: "You must specify a key to encrypt a book!"
ERROR_ENCRYPT_EMPTY: "Book must have contents to encrypt!"
ERROR_TOO_MANY_ARGUMENTS_COMMAND: "You have given too many arguments for this command!"
ERROR_GIVE_NO_RECIPIENT: "You have not specified the recipient of the book!"
ERROR_GIVE_RECIPIENT_UNKNOWN: "Player not found!"
ERROR_GIVE_RECIPIENT_FULL: "Receiving player must have space in their inventory to receive books!"
ERROR_GIVE_INVALID_COPIES_AMOUNT: "Invalid number of book copies specified!"
ERROR_GIVE_LOAD_FAILED: "Book failed to load!"
NEUTRAL_COMMANDS_HEADER: |
&e[] denote optional parameters
<> denote required parameters
{} denote required permission
In some cases, commands with required parameters can be called with no parameters
{bookPrice}&eCommands:
{commands}
NEUTRAL_COMMANDS_BOOK_PRICE_ECO: "\n&c[{price} is required to create a book]"
NEUTRAL_COMMANDS_BOOK_PRICE_ITEM: "&c[{quantity} {type} (s) are required to create a book]\n"
NEUTRAL_COMMANDS_COMMAND: "\n \n&e{usage}: &a{description}"
NEUTRAL_COMMANDS_COMMAND_NO_PERMISSION_REQUIRED: "None"
NEUTRAL_COMMANDS_COMMAND_PERMISSION: " &7{{permission}}"
NEUTRAL_UNKNOWN_AUTHOR: "Unknown"

View File

@@ -4,6 +4,7 @@ import net.knarcraft.bookswithoutborders.encryption.GenenCrypt;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class GenenCryptTest {
@@ -11,8 +12,11 @@ public class GenenCryptTest {
public void encryptDecryptTest() {
GenenCrypt gc = new GenenCrypt("Another Key");
gc.printCodonTable();
String encrypted = gc.encrypt("Hello World!");
assertEquals("HELLO WORLD!", gc.decrypt(encrypted));
String encrypted = gc.encryptText("Hello World!");
assertNotNull(encrypted);
assertEquals("HELLO WORLD!", gc.decryptText(encrypted));
}
}

View File

@@ -3,20 +3,31 @@ package net.knarcraft.bookswithoutborders.encryption;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
public class AESTest {
@Test
public void encryptDecryptTest() {
String plainText = "A lot of text";
String password = "abc123";
String plainText = "Flåklypa";
String password = "TqOZdpY9RjjjVE9JjCWVecUYObv5MYidByrpI3cxjoY=";
AES aes = new AES(AES.generateIV(), AES.generateIV());
System.out.println("Plaintext: " + plainText);
System.out.println("Encryption password: " + password);
String encrypted = aes.encryptDecryptText(plainText, password, true);
assertNotSame(encrypted, plainText);
String decrypted = aes.encryptDecryptText(encrypted, password, false);
AESConfiguration configuration = new AESConfiguration(new byte[]{-85, 103, -82, 71, 119, 28, 73, -75, -81, 102, -127, -125, -8, -75, 81, -111},
new byte[]{(byte) 104, -42, 63, 31, -120, -2, 14, -119, 35, 122, 109, -64, 122, 117, 33, -85}, password);
AES aes = new AES(configuration);
String cypherText = aes.encryptText(plainText);
System.out.println("Cypher text: " + cypherText);
assertNotNull(cypherText);
assertNotEquals(plainText, cypherText);
String decrypted = aes.decryptText(cypherText);
System.out.println("Decrypted: " + decrypted);
assertEquals(plainText, decrypted);
}

View File

@@ -0,0 +1,28 @@
package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
public class GenenCryptTest {
@Test
public void encryptDecryptTest() {
String encryptionKey = EncryptionHelper.getNumberKeyFromStringKey("My secret password!");
String plaintext = "Very secret &4colored&r message.";
GenenCrypt genenCrypt = new GenenCrypt(encryptionKey);
String cypherText = genenCrypt.encryptText(plaintext);
assertNotNull(cypherText);
assertNotSame(cypherText, plaintext);
String decrypted = genenCrypt.decryptText(cypherText);
assertEquals(plaintext.toUpperCase(), decrypted);
}
}

View File

@@ -0,0 +1,26 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
public class OneTimePadTest {
@Test
public void oneTimePadTest() {
String plaintext = "Very secret text that should be kept secret";
String key = "Very secret key!";
OneTimePad oneTimePad = new OneTimePad(key);
String cypherText = oneTimePad.encryptText(plaintext);
assertNotNull(cypherText);
assertNotSame(plaintext, cypherText);
String decrypted = oneTimePad.decryptText(cypherText);
assertEquals(plaintext, decrypted);
}
}

View File

@@ -0,0 +1,26 @@
package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
public class SubstitutionCipherTest {
@Test
public void encryptDecryptTest() {
String plaintext = "Very secret text that should be kept secret";
String integerKey = EncryptionHelper.getNumberKeyFromStringKey("Very secret key!");
SubstitutionCipher substitutionCipher = new SubstitutionCipher(integerKey);
String cypherText = substitutionCipher.encryptText(plaintext);
assertNotNull(cypherText);
assertNotSame(plaintext, cypherText);
String decrypted = substitutionCipher.decryptText(cypherText);
assertEquals(plaintext, decrypted);
}
}

View File

@@ -10,7 +10,7 @@ public class EncryptionHelperTest {
@Test
public void getNumberKeyFromStringKey() {
String numberKey = EncryptionHelper.getNumberKeyFromStringKey("hello");
assertEquals("1714212124", numberKey);
assertEquals("104, 101, 108, 108, 111", numberKey);
}
}