68 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
74d59bf71b Bump version for release
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-28 04:18:54 +02:00
a1ed6b9566 Re-implements depreciated enchantment key getting
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-28 04:15:53 +02:00
3095586d2b Bumps version for development
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-27 21:03:15 +02:00
1da61dc820 Bumps version for release
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-27 20:57:16 +02:00
48f69000b0 Adds color support to book authors
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-27 20:45:00 +02:00
4ddfafe6ec Fixes books being loaded twice
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-07-27 18:42:43 +02:00
3f3566089e Adds gitignore to repo
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2024-11-27 12:18:23 +01:00
0b701ddef1 Bumps version for development
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2024-09-07 01:44:40 +02:00
77 changed files with 4568 additions and 1350 deletions

113
.gitignore vendored Normal file
View File

@@ -0,0 +1,113 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/

117
README.md
View File

@@ -12,7 +12,7 @@ Books without Borders has got your back!
### Features ### 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 - 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 - 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 - 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 - 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 - Add lore to any item with a simple command
- Supports adding and saving color to title, lore, and book contents - Supports adding and saving color to title, lore, and book contents
- Color and formatting codes can be manually turned into formatting using /formatbook - Color and formatting codes can be manually turned into formatting using `/formatbook`
- Clear a book, and start anew with /clearbook - 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 - Formatting and color codes can be turned into formatting once any book is signed. This is enabled through a config
value value
- Change generation of books. Create tattered books for your RPG server! - Change generation of books. Create tattered books for your RPG server!
- Optionally, make it impossible to duplicate the original version of a book - 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
- Group encryption allows every player with the bookswithoutborders.decrypt.\<group> permission to decrypt the encrypted - Group encryption allows every player with the bookswithoutborders.decrypt.\<group> permission to decrypt the encrypted
book without using a password. 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: ### Commands:
An in-game description of available commands is available through the /bwb command. An in-game description of available commands is available through the /bwb command.
| Command | Arguments | Description | | Command | Arguments | Permission | Description |
|----------------------|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------|----------------------------------------------------------------------------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| /bookswithoutborders | None | Displays information about commands (and permissions if the user has bookswithoutborders.admin) | | /bookswithoutborders | None | | Displays information about commands (and permissions if the user has bookswithoutborders.admin) |
| /copybook | \<# of copies> | Copies the book the player is holding | | /copybook | \<# of copies> | bookswithoutborders.copy | 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 | | /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> | Deletes the specified file in the player's directory | | /deletebook | \<file name or number> | bookswithoutborders.delete | Deletes the specified file in the player's directory |
| /deletepublicbook | \<file name or number> | Same as deletebook, but deletes files in the public directory | | /deletepublicbook | \<file name or number> | bookswithoutborders.admin | 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 "" | | /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 | Formats the held written book (converts color and formatting codes to the corresponding formatted text) | | /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)] | Gives the selected player a book from your personal directory | | /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)] | Same as givebook, but uses books from the public 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)] | 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 | | /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)] | Same as loadbook, but views files in the public directory | | /loadpublicbook | \<file name or number> \[# of copies] \[signed (true/false)] | bookswithoutborders.loadpublic | Same as loadbook, but views files in the public directory |
| /reload | None | Reloads BwB's configuration file | | /reload | None | bookswithoutborders.reload | 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 | | /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)] | Same as savebook, but saves files in the public directory | | /savepublicbook | \[overwrite (true/false)] | bookswithoutborders.savepublic | Same as savebook, but saves files in the public directory |
| /setbookauthor | \<author> | Sets the author of the book the player is holding | | /setbookauthor | \<author> | bookswithoutborders.setauthor | 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) | | /setbookgeneration | \<generation> | bookswithoutborders.setgeneration | 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. | | /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> | Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default) | | /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> | Sets the title of the book/item the player is holding | | /settitle | \<title> | bookswithoutborders.settitle | Sets the title of the book/item the player is holding |
| /unsignbook | None | Un-signs the book the player is holding | | /unsignbook | None | bookswithoutborders.unsign | Un-signs the book the player is holding |
| /clearbook | None | Removes all text from the held un-signed book | | /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: ### Permissions:
@@ -72,7 +96,7 @@ An in-game description of available commands is available through the /bwb comma
| bookswithoutborders.* | Grants all permissions | | bookswithoutborders.* | Grants all permissions |
| bookswithoutborders.admin | 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.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.reload | Allows player to reload this plugin |
| bookswithoutborders.format | Allows a player to format a book | | bookswithoutborders.format | Allows a player to format a book |
| bookswithoutborders.save | Allows a player to save books to their personal directory | | 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.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.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.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 ### Signs
@@ -123,20 +150,20 @@ The **_decrypt_** sign must have **\[Decrypt]** on its second line. The third li
### Configuration options: ### Configuration options:
| Option | Description | | 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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.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 |
| 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 |
| 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_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_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 |
| 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 |
| 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. |
| 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. |
| 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> <groupId>net.knarcraft</groupId>
<artifactId>BooksWithoutBorders</artifactId> <artifactId>BooksWithoutBorders</artifactId>
<version>1.3.5</version> <version>1.3.10-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<licenses> <licenses>
@@ -20,7 +20,7 @@
<description>A continuation of the original Books Without Borders</description> <description>A continuation of the original Books Without Borders</description>
<properties> <properties>
<java.version>16</java.version> <java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -113,7 +113,7 @@
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version> <version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -1,10 +1,12 @@
package net.knarcraft.bookswithoutborders; package net.knarcraft.bookswithoutborders;
import net.knarcraft.bookswithoutborders.command.CommandAddTitlePage;
import net.knarcraft.bookswithoutborders.command.CommandBooksWithoutBorders; import net.knarcraft.bookswithoutborders.command.CommandBooksWithoutBorders;
import net.knarcraft.bookswithoutborders.command.CommandClear; import net.knarcraft.bookswithoutborders.command.CommandClear;
import net.knarcraft.bookswithoutborders.command.CommandCopy; import net.knarcraft.bookswithoutborders.command.CommandCopy;
import net.knarcraft.bookswithoutborders.command.CommandDecrypt; import net.knarcraft.bookswithoutborders.command.CommandDecrypt;
import net.knarcraft.bookswithoutborders.command.CommandDelete; import net.knarcraft.bookswithoutborders.command.CommandDelete;
import net.knarcraft.bookswithoutborders.command.CommandDeletePage;
import net.knarcraft.bookswithoutborders.command.CommandDeletePublic; import net.knarcraft.bookswithoutborders.command.CommandDeletePublic;
import net.knarcraft.bookswithoutborders.command.CommandEncrypt; import net.knarcraft.bookswithoutborders.command.CommandEncrypt;
import net.knarcraft.bookswithoutborders.command.CommandFormat; 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.CommandGroupEncrypt;
import net.knarcraft.bookswithoutborders.command.CommandLoad; import net.knarcraft.bookswithoutborders.command.CommandLoad;
import net.knarcraft.bookswithoutborders.command.CommandLoadPublic; import net.knarcraft.bookswithoutborders.command.CommandLoadPublic;
import net.knarcraft.bookswithoutborders.command.CommandMigrate;
import net.knarcraft.bookswithoutborders.command.CommandReload; import net.knarcraft.bookswithoutborders.command.CommandReload;
import net.knarcraft.bookswithoutborders.command.CommandSave; import net.knarcraft.bookswithoutborders.command.CommandSave;
import net.knarcraft.bookswithoutborders.command.CommandSavePublic; import net.knarcraft.bookswithoutborders.command.CommandSavePublic;
import net.knarcraft.bookswithoutborders.command.CommandSetAuthor; import net.knarcraft.bookswithoutborders.command.CommandSetAuthor;
import net.knarcraft.bookswithoutborders.command.CommandSetBookPrice; import net.knarcraft.bookswithoutborders.command.CommandSetBookPrice;
import net.knarcraft.bookswithoutborders.command.CommandSetBookshelfData;
import net.knarcraft.bookswithoutborders.command.CommandSetGeneration; import net.knarcraft.bookswithoutborders.command.CommandSetGeneration;
import net.knarcraft.bookswithoutborders.command.CommandSetLore; import net.knarcraft.bookswithoutborders.command.CommandSetLore;
import net.knarcraft.bookswithoutborders.command.CommandSetTitle; import net.knarcraft.bookswithoutborders.command.CommandSetTitle;
import net.knarcraft.bookswithoutborders.command.CommandUnSign; import net.knarcraft.bookswithoutborders.command.CommandUnSign;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; 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.BookEventListener;
import net.knarcraft.bookswithoutborders.listener.BookshelfListener; import net.knarcraft.bookswithoutborders.listener.BookshelfListener;
import net.knarcraft.bookswithoutborders.listener.PlayerEventListener; import net.knarcraft.bookswithoutborders.listener.PlayerEventListener;
import net.knarcraft.bookswithoutborders.listener.SignEventListener; import net.knarcraft.bookswithoutborders.listener.SignEventListener;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper; 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.knarcraft.knarlib.util.UpdateChecker;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -39,38 +51,72 @@ import org.bukkit.inventory.ItemFactory;
import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; 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 * The main Books Without Borders class
*/ */
public class BooksWithoutBorders extends JavaPlugin { 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 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() { public static void log(@NotNull Level level, @NotNull String message) {
return consoleSender; 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> * @param getPublic <p>Whether to get available public books</p>
* @return <p>A list of available 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) { if (getPublic) {
return new ArrayList<>(publicBooksList); return new ArrayList<>(getInstance().publicBooksList);
} else if (sender instanceof Player player) { } else if (sender instanceof Player player) {
UUID playerUUID = player.getUniqueId(); UUID playerUUID = player.getUniqueId();
if (!playerBooksList.containsKey(playerUUID)) { if (!getInstance().playerBooksList.containsKey(playerUUID)) {
List<String> newFiles = BookFileHelper.listFiles(sender, false); 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 { } else {
return new ArrayList<>(); 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 * Updates available books
* *
* @param sender <p>The sender to update books for</p> * @param sender <p>The sender to update books for</p>
* @param updatePublic <p>Whether to update public books</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); List<String> newFiles = BookFileHelper.listFiles(sender, updatePublic);
if (newFiles == null) {
return;
}
if (updatePublic) { if (updatePublic) {
publicBooksList = newFiles; getInstance().publicBooksList = newFiles;
getInstance().publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles);
} else if (sender instanceof Player player) { } 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 @Override
public void onEnable() { public void onEnable() {
FileConfiguration config = this.getConfig(); FileConfiguration config = this.getConfig();
@@ -120,15 +205,28 @@ public class BooksWithoutBorders extends JavaPlugin {
PluginDescriptionFile pluginDescriptionFile = this.getDescription(); PluginDescriptionFile pluginDescriptionFile = this.getDescription();
String pluginVersion = pluginDescriptionFile.getVersion(); 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; booksWithoutBorders = this;
consoleSender = this.getServer().getConsoleSender();
playerBooksList = new HashMap<>(); playerBooksList = new HashMap<>();
BooksWithoutBordersConfig.initialize(this); playerLetterIndex = new HashMap<>();
publicBooksList = BookFileHelper.listFiles(consoleSender, true); 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(); PluginManager pluginManager = this.getServer().getPluginManager();
if (getSlash() != null && initialize()) { if (getConfiguration().getSlash() != null && initialize()) {
pluginManager.registerEvents(new PlayerEventListener(), this); pluginManager.registerEvents(new PlayerEventListener(), this);
pluginManager.registerEvents(new SignEventListener(), this); pluginManager.registerEvents(new SignEventListener(), this);
pluginManager.registerEvents(new BookEventListener(), this); pluginManager.registerEvents(new BookEventListener(), this);
@@ -148,6 +246,7 @@ public class BooksWithoutBorders extends JavaPlugin {
* *
* @return <p>An instance of this plugin</p> * @return <p>An instance of this plugin</p>
*/ */
@NotNull
public static BooksWithoutBorders getInstance() { public static BooksWithoutBorders getInstance() {
return booksWithoutBorders; return booksWithoutBorders;
} }
@@ -156,28 +255,32 @@ public class BooksWithoutBorders extends JavaPlugin {
* Registers all commands used by this plugin * Registers all commands used by this plugin
*/ */
private void registerCommands() { private void registerCommands() {
registerCommand("giveBook", new CommandGive()); registerCommand(BwBCommand.GIVE_BOOK.toString(), new CommandGive());
registerCommand("givePublicBook", new CommandGivePublic()); registerCommand(BwBCommand.GIVE_PUBLIC_BOOK.toString(), new CommandGivePublic());
registerCommand("decryptBook", new CommandDecrypt()); registerCommand(BwBCommand.DECRYPT_BOOK.toString(), new CommandDecrypt());
registerCommand("groupEncryptBook", new CommandGroupEncrypt()); registerCommand(BwBCommand.GROUP_ENCRYPT_BOOK.toString(), new CommandGroupEncrypt());
registerCommand("deleteBook", new CommandDelete()); registerCommand(BwBCommand.DELETE_BOOK.toString(), new CommandDelete());
registerCommand("deletePublicBook", new CommandDeletePublic()); registerCommand(BwBCommand.DELETE_PUBLIC_BOOK.toString(), new CommandDeletePublic());
registerCommand("copyBook", new CommandCopy()); registerCommand(BwBCommand.COPY_BOOK.toString(), new CommandCopy());
registerCommand("unSignBook", new CommandUnSign()); registerCommand(BwBCommand.UNSIGN_BOOK.toString(), new CommandUnSign());
registerCommand("encryptBook", new CommandEncrypt()); registerCommand(BwBCommand.ENCRYPT_BOOK.toString(), new CommandEncrypt());
registerCommand("setBookPrice", new CommandSetBookPrice()); registerCommand(BwBCommand.SET_BOOK_PRICE.toString(), new CommandSetBookPrice());
registerCommand("setLore", new CommandSetLore()); registerCommand(BwBCommand.SET_LORE.toString(), new CommandSetLore());
registerCommand("savePublicBook", new CommandSavePublic()); registerCommand(BwBCommand.SAVE_PUBLIC_BOOK.toString(), new CommandSavePublic());
registerCommand("saveBook", new CommandSave()); registerCommand(BwBCommand.SAVE_BOOK.toString(), new CommandSave());
registerCommand("setBookAuthor", new CommandSetAuthor()); registerCommand(BwBCommand.SET_BOOK_AUTHOR.toString(), new CommandSetAuthor());
registerCommand("setTitle", new CommandSetTitle()); registerCommand(BwBCommand.SET_TITLE.toString(), new CommandSetTitle());
registerCommand("loadBook", new CommandLoad()); registerCommand(BwBCommand.LOAD_BOOK.toString(), new CommandLoad());
registerCommand("loadPublicBook", new CommandLoadPublic()); registerCommand(BwBCommand.LOAD_PUBLIC_BOOK.toString(), new CommandLoadPublic());
registerCommand("booksWithoutBorders", new CommandBooksWithoutBorders()); registerCommand(BwBCommand.BOOKS_WITHOUT_BORDERS.toString(), new CommandBooksWithoutBorders());
registerCommand("reload", new CommandReload()); registerCommand(BwBCommand.RELOAD.toString(), new CommandReload());
registerCommand("formatBook", new CommandFormat()); registerCommand(BwBCommand.FORMAT_BOOK.toString(), new CommandFormat());
registerCommand("setBookGeneration", new CommandSetGeneration()); registerCommand(BwBCommand.SET_BOOK_GENERATION.toString(), new CommandSetGeneration());
registerCommand("clearBook", new CommandClear()); 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 commandName <p>The name of the command to register</p>
* @param executor <p>The executor to register for the command</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); PluginCommand pluginCommand = this.getCommand(commandName);
if (pluginCommand != null) { if (pluginCommand != null) {
pluginCommand.setExecutor(executor); pluginCommand.setExecutor(executor);
} else { } 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 //Initialize Item Factory
try { try {
itemFactory = this.getServer().getItemFactory(); itemFactory = this.getServer().getItemFactory();
} catch (java.lang.NoSuchMethodError nsmE) { } catch (java.lang.NoSuchMethodError noSuchMethodError) {
sendErrorMessage(consoleSender, "Warning! [BooksWithoutBorders] failed to initialize!"); getLogger().log(Level.SEVERE, """
sendErrorMessage(consoleSender, "Please confirm the correct version of [BooksWithoutBorders] is"); Warning! [BooksWithoutBorders] failed to initialize!
sendErrorMessage(consoleSender, "being run for this version of bukkit!"); Please confirm the correct version of [BooksWithoutBorders] is
return false; being run for this version of spigot!""");
}
//Load config
if (!BooksWithoutBordersConfig.loadConfig()) {
return false; return false;
} }
//Save config with loaded values to fix invalid config values //Save config with loaded values to fix invalid config values
BooksWithoutBordersConfig.saveConfigValues(); getConfiguration().saveConfigValues();
return testFileSaving(); 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 * Gets the server's item factory
* *
* @return <p>The server's item factory</p> * @return <p>The server's item factory</p>
*/ */
@NotNull
public static ItemFactory getItemFactory() { 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> * @return <p>True if necessary folders exist</p>
*/ */
private boolean testFileSaving() { private boolean testFileSaving() {
File fileTest = new File(getBookFolder()); File fileTest = new File(getConfiguration().getBookFolder());
File encryptedFileTest = new File(getBookFolder() + "Encrypted" + getSlash()); File encryptedFileTest = new File(getConfiguration().getEncryptedBookPath());
if (!fileTest.exists()) { if (!fileTest.exists()) {
try { try {
if (!fileTest.mkdir()) { if (!fileTest.mkdir()) {
sendErrorMessage(consoleSender, "Saving failed! Aborting..."); getLogger().log(Level.SEVERE, StaticMessage.BOOK_SAVING_FAILED.toString());
return false; return false;
} }
} catch (Exception exception) { } 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; return false;
} }
} }
if (!encryptedFileTest.exists()) { if (!encryptedFileTest.exists()) {
try { try {
if (!encryptedFileTest.mkdir()) { if (!encryptedFileTest.mkdir()) {
sendErrorMessage(consoleSender, "Saving failed! Aborting..."); getLogger().log(Level.SEVERE, StaticMessage.BOOK_SAVING_FAILED.toString());
return false; return false;
} }
} catch (Exception exception) { } 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; return false;
} }
} }
@@ -270,8 +379,8 @@ public class BooksWithoutBorders extends JavaPlugin {
* @param sender <p>The sender to send the message to</p> * @param sender <p>The sender to send the message to</p>
* @param message <p>The message to send</p> * @param message <p>The message to send</p>
*/ */
public static void sendSuccessMessage(CommandSender sender, String message) { public static void sendSuccessMessage(@NotNull CommandSender sender, @NotNull String message) {
sender.sendMessage(getSuccessColor() + 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 sender <p>The sender to send the message to</p>
* @param message <p>The message to send</p> * @param message <p>The message to send</p>
*/ */
public static void sendErrorMessage(CommandSender sender, String message) { public static void sendErrorMessage(@NotNull CommandSender sender, @NotNull String message) {
sender.sendMessage(getErrorColor() + 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; 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.Material;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@@ -13,10 +18,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.BooksWithoutBorders.sendErrorMessage;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getCommandColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSuccessColor;
/** /**
* Command executor for the books without borders (bwb) command * 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 { public class CommandBooksWithoutBorders implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
sender.sendMessage(getCommandColor() + "[] denote optional parameters"); @NotNull String[] arguments) {
sender.sendMessage(getCommandColor() + "<> denote required parameters"); StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
sender.sendMessage(getCommandColor() + "{} denote required permission"); String header = stringFormatter.replacePlaceholders(Translatable.NEUTRAL_COMMANDS_HEADER,
sender.sendMessage(getCommandColor() + "In some cases, commands with required parameters can be called with no parameters"); List.of("{bookPrice}", "{commands}"), List.of(getBookPrice(), getCommands(sender)));
if (sender instanceof Player) { sender.sendMessage(header);
showPlayerCommands(sender);
} else {
showConsoleCommands(sender);
}
return true; 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 * Shows all commands available to the console
* *
* @param sender <p>The console which sent the command</p> * @param sender <p>The console which sent the command</p>
*/ */
private void showConsoleCommands(CommandSender sender) { @NotNull
sender.sendMessage(getCommandColor() + "Commands:"); private String showConsoleCommands(@NotNull CommandSender sender) {
showCommandInfo("deletePublicBook", sender); StringBuilder builder = new StringBuilder();
showCommandInfo("givePublicBook", sender); for (BwBCommand command : BwBCommand.values()) {
showCommandInfo("reload", sender); 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> * @param sender <p>The player which sent the command</p>
*/ */
private void showPlayerCommands(CommandSender sender) { @NotNull
//Lists all commands private String showPlayerCommands(@NotNull CommandSender sender) {
Material bookPriceType = BooksWithoutBordersConfig.getBookPriceType(); StringBuilder builder = new StringBuilder();
double bookPriceQuantity = BooksWithoutBordersConfig.getBookPriceQuantity(); for (BwBCommand command : BwBCommand.values()) {
if (BooksWithoutBordersConfig.booksHavePrice()) { builder.append(showCommandInfo(command.toString(), sender));
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]");
}
} }
sender.sendMessage(getCommandColor() + "Commands:"); return builder.toString();
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);
} }
/** /**
@@ -97,26 +117,37 @@ public class CommandBooksWithoutBorders implements TabExecutor {
* @param command <p>The command to get information about</p> * @param command <p>The command to get information about</p>
* @param sender <p>The sender asking to see command info</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); PluginCommand pluginCommand = BooksWithoutBorders.getInstance().getCommand(command);
if (pluginCommand != null) { if (pluginCommand == null) {
String permission = pluginCommand.getPermission(); BooksWithoutBorders.log(Level.SEVERE, StringFormatter.replacePlaceholder(
if (permission == null || sender.hasPermission(permission)) { StaticMessage.COMMAND_NOT_REGISTERED.toString(), "{command}", command));
String commandInfo = "\n" + getCommandColor() + pluginCommand.getUsage().replace("<command>", return "";
pluginCommand.getName()) + ": " + getSuccessColor() + pluginCommand.getDescription();
if (sender.hasPermission("bookswithoutborders.admin")) {
if (permission == null) {
permission = "None";
}
commandInfo += getCommandColor() + " {" + permission + "}";
}
sender.sendMessage(commandInfo);
}
} }
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 @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<>(); return new ArrayList<>();
} }

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
package net.knarcraft.bookswithoutborders.command; package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; 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.BookHelper;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper; import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
@@ -17,94 +19,111 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 * Command executor for the decrypt command
*/ */
public class CommandDecrypt implements TabExecutor { public class CommandDecrypt implements TabExecutor {
@Override @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)) { 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; return false;
} }
if (InventoryHelper.notHoldingOneWrittenBookCheck(player, if (InventoryHelper.notHoldingOneWrittenBookCheck(player,
"You must be holding a written book to decrypt it!", stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITTEN_BOOK, "{action}",
"You cannot decrypt two books at once!")) { stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_DECRYPT)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_DECRYPT)))) {
return false; return false;
} }
ItemStack heldItem = InventoryHelper.getHeldBook(player, true); ItemStack heldItem = InventoryHelper.getHeldBook(player, true);
BookMeta bookMetadata = (BookMeta) heldItem.getItemMeta(); BookMeta bookMetadata = (BookMeta) heldItem.getItemMeta();
if (bookMetadata == null) { if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(player, "Your book seems to be corrupt!"); stringFormatter.displayErrorMessage(sender, Translatable.ERROR_METADATA_MISSING);
return false; return false;
} }
//Warning: admin decrypt only allows decrypting files created by the same player. Not sure if intended //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")) { if (arguments.length == 0 && BooksWithoutBorders.getConfiguration().getAdminDecrypt() &&
String path = getBookFolder() + "Encrypted" + getSlash(); player.hasPermission(Permission.ADMIN.toString())) {
return adminDecrypt(player, bookMetadata);
File encryptedDirectory = new File(path); } else if (arguments.length == 0) {
String[] encryptedFiles = encryptedDirectory.list(); stringFormatter.displayErrorMessage(player, Translatable.ERROR_DECRYPT_MISSING_KEY);
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!");
return false; 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) { if (book != null) {
InventoryHelper.setHeldWrittenBook(player, book); InventoryHelper.setHeldWrittenBook(player, book);
BooksWithoutBorders.sendSuccessMessage(player, "Book decrypted!"); stringFormatter.displaySuccessMessage(player, Translatable.SUCCESS_DECRYPTED);
return true; return true;
} else { } 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; return false;
} }
} }
@Override @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,
int argumentCount = args.length; @NotNull String[] arguments) {
int argumentCount = arguments.length;
if (argumentCount == 1) { if (argumentCount == 1) {
List<String> info = new ArrayList<>(); return List.of("<password>");
info.add("<password>");
return info;
} }
return new ArrayList<>(); return new ArrayList<>();
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
package net.knarcraft.bookswithoutborders.command; package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; 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.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper; import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
@@ -22,20 +22,22 @@ import java.util.List;
public class CommandLoad implements TabExecutor { public class CommandLoad implements TabExecutor {
@Override @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,
return loadBook(sender, args, "player", false); @NotNull String[] arguments) {
return loadBook(sender, arguments, "player", false);
} }
/** /**
* Loads a stored book * Loads a stored book
* *
* @param sender <p>The sender asking to load the book</p> * @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 directory <p>The directory to load from (public/player)</p>
* @param loadPublic <p>Whether to list public files as loadable</p> * @param loadPublic <p>Whether to list public files as loadable</p>
* @return <p>True if the book was loaded successfully</p> * @return <p>True if the book was loaded successfully</p>
*/ */
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)) { if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!"); BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false; return false;
@@ -46,26 +48,32 @@ public class CommandLoad implements TabExecutor {
return false; return false;
} }
int argumentCount = args.length; int argumentCount = arguments.length;
//Show books available to the player String command = loadPublic ? "loadpublicbook" : "loadbook";
if (argumentCount == 0) { if (PagedBookIndex.displayPage(arguments, sender, loadPublic, command)) {
BookFileHelper.printBooks(sender, loadPublic);
return true; return true;
} }
//Organize and parse input //Organize and parse input
String bookIdentifier = args[0]; String bookIdentifier = arguments[0];
String copies = "1"; String copies = "1";
String isSigned = "true"; String isSigned = "true";
if (args.length == 3) {
copies = args[1]; if (argumentCount > 1) {
isSigned = args[2]; if (argumentCount > 2 && InputCleaningHelper.isInt(arguments[argumentCount - 2]) &&
} else if (args.length == 2) { InputCleaningHelper.isBoolean(arguments[argumentCount - 1])) {
if (args[1].equalsIgnoreCase("true") || args[1].equalsIgnoreCase("false")) { isSigned = arguments[argumentCount - 1];
isSigned = args[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 { } else {
copies = args[1]; bookIdentifier = InputCleaningHelper.mergeArguments(arguments, 0);
} }
} }
@@ -95,37 +103,48 @@ public class CommandLoad implements TabExecutor {
} }
@Override @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,
return doTabCompletion(sender, args, false); @NotNull String[] arguments) {
return doTabCompletion(sender, arguments, false);
} }
/** /**
* Performs the actual tab completion * Performs the actual tab completion
* *
* @param sender <p>The sender of the command</p> * @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> * @param loadPublic <p>Whether to list public files or player files</p>
* @return <p>A list of available choices</p> * @return <p>A list of available choices</p>
*/ */
protected List<String> doTabCompletion(CommandSender sender, String[] args, boolean loadPublic) { @NotNull
int argumentCount = args.length; protected List<String> doTabCompletion(@NotNull CommandSender sender, @NotNull String[] arguments, boolean loadPublic) {
int argumentCount = arguments.length;
if (argumentCount == 1) { if (argumentCount == 1) {
//Return list of books return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, loadPublic), arguments[0]);
return TabCompletionHelper.filterMatchingContains(BooksWithoutBorders.getAvailableBooks(sender, loadPublic), }
args[0]);
} else if (argumentCount == 2) { List<String> output = new ArrayList<>();
//Number of copies List<String> books = BooksWithoutBorders.getAvailableBooks(sender, loadPublic);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), args[1]); List<String> filtered = TabCompletionHelper.filterMatchingContains(books,
} else if (argumentCount == 3) { InputCleaningHelper.mergeArguments(arguments, 0));
//Signed
try { if (!filtered.isEmpty()) {
Integer.parseInt(args[1]); List<String> cleaned = TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), args[2]); if (!books.contains(InputCleaningHelper.mergeArguments(arguments, 1))) {
} catch (NumberFormatException e) { return cleaned;
return new ArrayList<>(); } 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 { public class CommandLoadPublic extends CommandLoad implements CommandExecutor {
@Override @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,
return loadBook(sender, args, "public", true); @NotNull String[] arguments) {
return loadBook(sender, arguments, "public", true);
} }
@Override @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,
return doTabCompletion(sender, args, true); @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; package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
@@ -16,18 +15,24 @@ import java.util.List;
public class CommandReload implements TabExecutor { public class CommandReload implements TabExecutor {
@Override @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,
if (BooksWithoutBordersConfig.loadConfig()) { @NotNull String[] arguments) {
if (BooksWithoutBorders.getConfiguration().loadConfig()) {
BooksWithoutBorders.sendSuccessMessage(sender, "BooksWithoutBorders configuration reloaded!"); BooksWithoutBorders.sendSuccessMessage(sender, "BooksWithoutBorders configuration reloaded!");
} else { } else {
BooksWithoutBorders.sendErrorMessage(sender, "Reload Failed!"); BooksWithoutBorders.sendErrorMessage(sender, "Reload Failed!");
BooksWithoutBorders.sendErrorMessage(sender, "See console for details"); BooksWithoutBorders.sendErrorMessage(sender, "See console for details");
} }
// Reload books
BooksWithoutBorders.updateBooks(sender, true);
BooksWithoutBorders.clearBookData();
return true; return true;
} }
@Override @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<>(); 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.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper; import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
@@ -22,38 +23,34 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level; 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 * Command executor for the save command
*/ */
public class CommandSave implements TabExecutor { public class CommandSave implements TabExecutor {
@Override @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, String[] arguments) {
return saveHeldBook(sender, args, false); return saveHeldBook(sender, arguments, false);
} }
/** /**
* Saves the player's held book if it exists * Saves the player's held book if it exists
* *
* @param sender <p>The sender of the command</p> * @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> * @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> * @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)) { if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!"); BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false; return false;
} }
ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, 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); 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); saveBook(player, holdingItem, duplicate, savePublic);
return true; return true;
} else { } else {
@@ -70,25 +67,38 @@ public class CommandSave implements TabExecutor {
* @param overwrite <p>Whether to overwrite any existing books</p> * @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> * @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(); BookMeta book = (BookMeta) heldBook.getItemMeta();
if (book == null) { if (book == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata for your held book!"); BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata for your held book!");
return; return;
} }
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//Only allow saving of own books if enabled //Only allow saving of own books if enabled
if (BooksWithoutBordersConfig.getAuthorOnlySave() && !saveToPublicFolder && if (config.getAuthorOnlySave() && !saveToPublicFolder &&
(!player.hasPermission("bookswithoutborders.bypassAuthorOnlySave") && (!player.hasPermission("bookswithoutborders.bypassAuthorOnlySave") &&
BookHelper.isNotAuthor(player, book))) { BookHelper.isNotAuthor(player, book))) {
return; return;
} }
String savePath = BookHelper.getBookDirectoryPathString( String savePath = BookHelper.getBookDirectoryPathString(saveToPublicFolder ?
saveToPublicFolder ? BookDirectory.PUBLIC : BookDirectory.PLAYER, player); BookDirectory.PUBLIC : BookDirectory.PLAYER, player);
if (savePath == null) {
BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! Unable to find the save path!");
return;
}
//Generate book filename //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 //Make sure the used folders exist
File file = new File(savePath); File file = new File(savePath);
@@ -111,47 +121,45 @@ public class CommandSave implements TabExecutor {
if (foundDuplicates > 0) { if (foundDuplicates > 0) {
//TODO: Decide if this makes sense or needs to be changed //TODO: Decide if this makes sense or needs to be changed
//Skip duplicate book //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, "Book is already saved!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " + BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ?
getErrorColor() + "to overwrite!"); "/savepublicbook" : "/savebook") + " true " + config.getErrorColor() + "to overwrite!");
return; return;
} }
//Skip if duplicate limit is reached //Skip if duplicate limit is reached
if (foundDuplicates > BooksWithoutBordersConfig.getBookDuplicateLimit()) { if (foundDuplicates > config.getBookDuplicateLimit()) {
BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName + BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName +
" duplicates reached!"); " duplicates reached!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " + BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ?
getErrorColor() + "to overwrite!"); "/savepublicbook" : "/savebook") + " true " +
config.getErrorColor() + "to overwrite!");
return; return;
} }
//Alter duplicated filename //Alter duplicated filename
if (fileName.contains("Untitled" + getTitleAuthorSeparator()) && !overwrite) { if (fileName.contains("Untitled" + config.getTitleAuthorSeparator()) && !overwrite) {
fileName = "(" + foundDuplicates + ")" + fileName; fileName = "(" + foundDuplicates + ")" + fileName;
} }
} }
try { try {
if (BooksWithoutBordersConfig.getUseYml()) { BookToFromTextHelper.bookToYml(savePath, fileName, book);
BookToFromTextHelper.bookToYml(savePath, fileName, book);
} else {
BookToFromTextHelper.bookToTXT(savePath, fileName, book);
}
//Update the relevant book list //Update the relevant book list
BooksWithoutBorders.updateBooks(player, saveToPublicFolder); BooksWithoutBorders.updateBooks(player, saveToPublicFolder);
BooksWithoutBorders.sendSuccessMessage(player, "Book Saved as \"" + fileName + "\""); BooksWithoutBorders.sendSuccessMessage(player, "Book Saved as \"" + fileName + ChatColor.RESET + "\"");
} catch (IOException exception) { } catch (IOException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to save book"); BooksWithoutBorders.log(Level.SEVERE, "Unable to save book");
} }
} }
@Override @Override
@NotNull
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) { @NotNull String[] arguments) {
return new ArrayList<>(); return new ArrayList<>();
} }

View File

@@ -11,8 +11,9 @@ import org.jetbrains.annotations.NotNull;
public class CommandSavePublic extends CommandSave implements TabExecutor { public class CommandSavePublic extends CommandSave implements TabExecutor {
@Override @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,
return saveHeldBook(sender, args, true); @NotNull String[] arguments) {
return saveHeldBook(sender, arguments, true);
} }
} }

View File

@@ -3,6 +3,8 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.state.ItemSlot; import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
@@ -10,6 +12,7 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -20,13 +23,14 @@ import java.util.List;
public class CommandSetAuthor implements TabExecutor { public class CommandSetAuthor implements TabExecutor {
@Override @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)) { if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!"); BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false; return false;
} }
if (args.length < 1) { if (arguments.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "Too few command arguments!"); BooksWithoutBorders.sendErrorMessage(player, "Too few command arguments!");
return false; return false;
} }
@@ -41,8 +45,12 @@ public class CommandSetAuthor implements TabExecutor {
boolean mainHand = heldBookSlot == ItemSlot.MAIN_HAND; boolean mainHand = heldBookSlot == ItemSlot.MAIN_HAND;
ItemStack heldBook = InventoryHelper.getHeldItem(player, mainHand); ItemStack heldBook = InventoryHelper.getHeldItem(player, mainHand);
BookMeta bookMetaData = InventoryHelper.getHeldBookMetadata(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 = String.join(" ", args); String author = ColorHelper.translateColorCodes(String.join(" ", arguments), ColorConversion.RGB);
bookMetaData.setAuthor(author); bookMetaData.setAuthor(author);
heldBook.setItemMeta(bookMetaData); heldBook.setItemMeta(bookMetaData);
BooksWithoutBorders.sendSuccessMessage(player, "Book author set to " + author + "!"); BooksWithoutBorders.sendSuccessMessage(player, "Book author set to " + author + "!");
@@ -50,8 +58,10 @@ public class CommandSetAuthor implements TabExecutor {
} }
@Override @Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) { @Nullable
if (args.length == 1) { public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return null; return null;
} else { } else {
return new ArrayList<>(); return new ArrayList<>();

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,20 @@
package net.knarcraft.bookswithoutborders.command; package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.ItemSlot; import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.BookHelper; import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.Material;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.WritableBookMeta;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level;
/** /**
* Command executor for the unsign command * Command executor for the unsign command
@@ -26,7 +22,8 @@ import java.util.logging.Level;
public class CommandUnSign implements TabExecutor { public class CommandUnSign implements TabExecutor {
@Override @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)) { if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!"); BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false; return false;
@@ -49,44 +46,59 @@ public class CommandUnSign implements TabExecutor {
* @param player <p>The player holding the book</p> * @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> * @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 //Get the old book
BookMeta oldMetadata = InventoryHelper.getHeldBookMetadata(player, mainHand); 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); ItemStack heldBook = InventoryHelper.getHeldBook(player, mainHand);
//Only allow the owner to un-sign the book //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))) { if (BookHelper.isNotAuthor(player, Objects.requireNonNull(oldMetadata))) {
return; return;
} }
} }
WritableBookMeta newMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITABLE_BOOK); // Give the player the new book
if (newMetadata == null) { ItemStack book = BookHelper.unsignBook(oldMetadata, heldBook.getAmount());
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create writable book metadata"); if (book == null) {
return; return;
} }
//Create a new unsigned book with the same data reverseColorCodes(book);
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK, heldBook.getAmount()); InventoryHelper.replaceHeldItem(player, book, mainHand);
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);
} }
@Override @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<>(); 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; package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; 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 net.md_5.bungee.api.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.Configuration; import org.bukkit.configuration.Configuration;
import org.jetbrains.annotations.NotNull;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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; import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
/** /**
@@ -20,50 +21,64 @@ import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.clea
*/ */
public class BooksWithoutBordersConfig { public class BooksWithoutBordersConfig {
private static final ChatColor errorColor = ChatColor.RED; private final ChatColor errorColor = ChatColor.RED;
private static final ChatColor successColor = ChatColor.GREEN; private final ChatColor successColor = ChatColor.GREEN;
private static final ChatColor commandColor = ChatColor.YELLOW; private final ChatColor commandColor = ChatColor.YELLOW;
private static final String SLASH = FileSystems.getDefault().getSeparator(); private final String SLASH = FileSystems.getDefault().getSeparator();
private static boolean isInitialized; private boolean isInitialized;
public static String bookFolder; private final String bookFolder;
private static int bookDuplicateLimit; private int bookDuplicateLimit;
private static String titleAuthorSeparator; private String titleAuthorSeparator;
private static String loreSeparator; private String loreSeparator;
private static List<String> firstBooks = new ArrayList<>(); private List<String> firstBooks = new ArrayList<>();
private static String welcomeMessage; private String welcomeMessage;
private static Material bookPriceType = null; private Material bookPriceType = null;
private static double bookPriceQuantity; private double bookPriceQuantity;
private static boolean authorOnlyCopy; private boolean authorOnlyCopy;
private static boolean authorOnlyUnsign; private boolean authorOnlyUnsign;
private static boolean authorOnlySave; private boolean authorOnlySave;
private static boolean useYml; private boolean adminDecrypt;
private static boolean adminDecrypt; private boolean formatBooks;
private static boolean formatBooks; private boolean changeGenerationOnCopy;
private static boolean changeGenerationOnCopy; private boolean enableBookshelfPeeking;
private static boolean enableBookshelfPeeking; private boolean useRealEncryption;
private final Translator translator;
private EconomyManager economyManager;
/** /**
* Initializes the books without borders settings class * Initializes the books without borders settings class
* *
* @param booksWithoutBorders <p>The books without borders object used for getting required data</p> * @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) { if (isInitialized) {
throw new IllegalArgumentException("Settings class initialized twice. This should not happen!"); throw new IllegalArgumentException("Settings class initialized twice. This should not happen!");
} }
this.translator = translator;
isInitialized = true; isInitialized = true;
bookFolder = booksWithoutBorders.getDataFolder().getAbsolutePath() + getSlash() + "Books" + getSlash(); bookFolder = booksWithoutBorders.getDataFolder().getAbsolutePath() + getSlash() + "Books" + getSlash();
loadConfig(); 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 * Gets the folder used for storing books
* *
* @return <p>The folder used for storing books</p> * @return <p>The folder used for storing books</p>
*/ */
public static String getBookFolder() { public String getBookFolder() {
return bookFolder; return this.bookFolder;
} }
/** /**
@@ -71,8 +86,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The color to use for error messages</p> * @return <p>The color to use for error messages</p>
*/ */
public static ChatColor getErrorColor() { public ChatColor getErrorColor() {
return errorColor; return this.errorColor;
} }
/** /**
@@ -80,8 +95,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The color to use for success messages</p> * @return <p>The color to use for success messages</p>
*/ */
public static ChatColor getSuccessColor() { public ChatColor getSuccessColor() {
return successColor; return this.successColor;
} }
/** /**
@@ -89,8 +104,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The color used to color commands</p> * @return <p>The color used to color commands</p>
*/ */
public static ChatColor getCommandColor() { public ChatColor getCommandColor() {
return commandColor; return this.commandColor;
} }
/** /**
@@ -98,8 +113,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The slash to use for file separators</p> * @return <p>The slash to use for file separators</p>
*/ */
public static String getSlash() { public String getSlash() {
return SLASH; return this.SLASH;
} }
/** /**
@@ -107,8 +122,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>Whether only the book author can copy it</p> * @return <p>Whether only the book author can copy it</p>
*/ */
public static boolean getAuthorOnlyCopy() { public boolean getAuthorOnlyCopy() {
return authorOnlyCopy; return this.authorOnlyCopy;
} }
/** /**
@@ -116,8 +131,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>Whether only the book author can unsign it</p> * @return <p>Whether only the book author can unsign it</p>
*/ */
public static boolean getAuthorOnlyUnsign() { public boolean getAuthorOnlyUnsign() {
return authorOnlyUnsign; return this.authorOnlyUnsign;
} }
/** /**
@@ -125,8 +140,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>Whether a player can only save their own books</p> * @return <p>Whether a player can only save their own books</p>
*/ */
public static boolean getAuthorOnlySave() { public boolean getAuthorOnlySave() {
return authorOnlySave; return this.authorOnlySave;
} }
/** /**
@@ -134,17 +149,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>True if players can peek at the contained books</p> * @return <p>True if players can peek at the contained books</p>
*/ */
public static boolean getEnableBookshelfPeeking() { public boolean getEnableBookshelfPeeking() {
return enableBookshelfPeeking; return this.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;
} }
/** /**
@@ -152,8 +158,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>Whether admins can bypass the encryption password</p> * @return <p>Whether admins can bypass the encryption password</p>
*/ */
public static boolean getAdminDecrypt() { public boolean getAdminDecrypt() {
return adminDecrypt; return this.adminDecrypt;
} }
/** /**
@@ -161,8 +167,8 @@ public class BooksWithoutBordersConfig {
* *
* @param newQuantity <p>The new quantity necessary for payment</p> * @param newQuantity <p>The new quantity necessary for payment</p>
*/ */
public static void setBookPriceQuantity(double newQuantity) { public void setBookPriceQuantity(double newQuantity) {
bookPriceQuantity = newQuantity; this.bookPriceQuantity = newQuantity;
} }
/** /**
@@ -170,8 +176,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The quantity necessary for payment</p> * @return <p>The quantity necessary for payment</p>
*/ */
public static double getBookPriceQuantity() { public double getBookPriceQuantity() {
return bookPriceQuantity; return this.bookPriceQuantity;
} }
/** /**
@@ -182,8 +188,8 @@ public class BooksWithoutBordersConfig {
* *
* @param newType <p>The new item type to use for book pricing</p> * @param newType <p>The new item type to use for book pricing</p>
*/ */
public static void setBookPriceType(Material newType) { public void setBookPriceType(Material newType) {
bookPriceType = newType; this.bookPriceType = newType;
} }
/** /**
@@ -194,8 +200,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The item type used for book pricing</p> * @return <p>The item type used for book pricing</p>
*/ */
public static Material getBookPriceType() { public Material getBookPriceType() {
return bookPriceType; return this.bookPriceType;
} }
/** /**
@@ -203,8 +209,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The welcome message to show new players</p> * @return <p>The welcome message to show new players</p>
*/ */
public static String getWelcomeMessage() { public String getWelcomeMessage() {
return welcomeMessage; return this.welcomeMessage;
} }
/** /**
@@ -212,8 +218,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The book duplicate limit</p> * @return <p>The book duplicate limit</p>
*/ */
public static int getBookDuplicateLimit() { public int getBookDuplicateLimit() {
return bookDuplicateLimit; return this.bookDuplicateLimit;
} }
/** /**
@@ -221,8 +227,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>True if books should change their generation</p> * @return <p>True if books should change their generation</p>
*/ */
public static boolean changeGenerationOnCopy() { public boolean changeGenerationOnCopy() {
return changeGenerationOnCopy; return this.changeGenerationOnCopy;
} }
/** /**
@@ -230,8 +236,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The separator between title and author</p> * @return <p>The separator between title and author</p>
*/ */
public static String getTitleAuthorSeparator() { public String getTitleAuthorSeparator() {
return titleAuthorSeparator; return this.titleAuthorSeparator;
} }
/** /**
@@ -239,8 +245,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The separator used to denote lore newline</p> * @return <p>The separator used to denote lore newline</p>
*/ */
public static String getLoreSeparator() { public String getLoreSeparator() {
return loreSeparator; return this.loreSeparator;
} }
/** /**
@@ -248,8 +254,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>Whether all books should be formatted</p> * @return <p>Whether all books should be formatted</p>
*/ */
public static boolean formatBooks() { public boolean formatBooks() {
return formatBooks; return this.formatBooks;
} }
/** /**
@@ -257,8 +263,8 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>The books to give new players</p> * @return <p>The books to give new players</p>
*/ */
public static List<String> getFirstBooks() { public List<String> getFirstBooks() {
return new ArrayList<>(firstBooks); return new ArrayList<>(this.firstBooks);
} }
/** /**
@@ -266,29 +272,48 @@ public class BooksWithoutBordersConfig {
* *
* @return <p>True if players need to pay for printing books</p> * @return <p>True if players need to pay for printing books</p>
*/ */
public static boolean booksHavePrice() { public boolean booksHavePrice() {
return (bookPriceType != null && bookPriceQuantity > 0); 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 * Saves the config
*/ */
public static void saveConfigValues() { public void saveConfigValues() {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getInstance().getServer().getConsoleSender(); Logger logger = BooksWithoutBorders.getInstance().getLogger();
Configuration config = BooksWithoutBorders.getInstance().getConfig(); Configuration config = BooksWithoutBorders.getInstance().getConfig();
config.set(ConfigOption.USE_YAML.getConfigNode(), useYml); config.set(ConfigOption.MAX_DUPLICATES.getConfigNode(), this.bookDuplicateLimit);
config.set(ConfigOption.MAX_DUPLICATES.getConfigNode(), bookDuplicateLimit); config.set(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(), this.titleAuthorSeparator);
config.set(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(), titleAuthorSeparator); config.set(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(), this.loreSeparator);
config.set(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(), loreSeparator); config.set(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode(), this.firstBooks);
config.set(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode(), firstBooks); config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), this.welcomeMessage);
config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), welcomeMessage); config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), this.formatBooks);
config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), formatBooks); config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), this.enableBookshelfPeeking);
config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), enableBookshelfPeeking); config.set(ConfigOption.USE_REAL_ENCRYPTION.getConfigNode(), this.useRealEncryption);
String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode(); String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode();
if (bookPriceType != null) { if (this.bookPriceType != null) {
if (bookPriceType != Material.AIR) { if (this.bookPriceType != Material.AIR) {
config.set(itemTypeNode, bookPriceType.toString()); config.set(itemTypeNode, this.bookPriceType.toString());
} else { } else {
config.set(itemTypeNode, "Economy"); config.set(itemTypeNode, "Economy");
} }
@@ -296,24 +321,23 @@ public class BooksWithoutBordersConfig {
config.set(itemTypeNode, "Item type name"); config.set(itemTypeNode, "Item type name");
} }
config.set(ConfigOption.PRICE_QUANTITY.getConfigNode(), bookPriceQuantity); config.set(ConfigOption.PRICE_QUANTITY.getConfigNode(), this.bookPriceQuantity);
config.set(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(), adminDecrypt); config.set(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(), this.adminDecrypt);
config.set(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(), authorOnlyCopy); config.set(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(), this.authorOnlyCopy);
config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), authorOnlyUnsign); config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), this.authorOnlyUnsign);
config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), authorOnlySave); config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), this.authorOnlySave);
config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), changeGenerationOnCopy); config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), this.changeGenerationOnCopy);
//Handles old book and quill settings //Handles old book and quill settings
if (config.contains("Options.Require_book_and_quill_to_create_book")) { if (config.contains("Options.Require_book_and_quill_to_create_book")) {
sendSuccessMessage(consoleSender, "[BooksWithoutBorders] Found old config setting " + logger.log(Level.INFO, "[BooksWithoutBorders] Found old config setting " +
"\"Require_book_and_quill_to_create_book\""); "\"Require_book_and_quill_to_create_book\"\nUpdating to \"Price_to_create_book\" settings");
sendSuccessMessage(consoleSender, "Updating to \"Price_to_create_book\" settings");
if (config.getBoolean("Options.Require_book_and_quill_to_create_book")) { if (config.getBoolean("Options.Require_book_and_quill_to_create_book")) {
bookPriceType = Material.WRITABLE_BOOK; this.bookPriceType = Material.WRITABLE_BOOK;
bookPriceQuantity = 1; this.bookPriceQuantity = 1;
config.set("Options.Price_to_create_book.Item_type", bookPriceType.toString()); config.set("Options.Price_to_create_book.Item_type", this.bookPriceType.toString());
config.set("Options.Price_to_create_book.Required_quantity", bookPriceQuantity); config.set("Options.Price_to_create_book.Required_quantity", this.bookPriceQuantity);
} }
config.set("Options.Require_book_and_quill_to_create_book", null); 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> * @return <p>True if the config was loaded successfully</p>
*/ */
public static boolean loadConfig() { public boolean loadConfig() {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getInstance().getServer().getConsoleSender(); Logger logger = BooksWithoutBorders.getInstance().getLogger();
BooksWithoutBorders.getInstance().reloadConfig(); BooksWithoutBorders.getInstance().reloadConfig();
Configuration config = BooksWithoutBorders.getInstance().getConfig(); Configuration config = BooksWithoutBorders.getInstance().getConfig();
try { try {
useYml = getBoolean(config, ConfigOption.USE_YAML); this.bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(),
bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(),
(Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue()); (Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue());
titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR); this.titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR);
loreSeparator = getString(config, ConfigOption.LORE_LINE_SEPARATOR); this.loreSeparator = getString(config, ConfigOption.LORE_LINE_SEPARATOR);
adminDecrypt = getBoolean(config, ConfigOption.ADMIN_AUTO_DECRYPT); this.adminDecrypt = getBoolean(config, ConfigOption.ADMIN_AUTO_DECRYPT);
authorOnlyCopy = getBoolean(config, ConfigOption.AUTHOR_ONLY_COPY); this.authorOnlyCopy = getBoolean(config, ConfigOption.AUTHOR_ONLY_COPY);
authorOnlyUnsign = getBoolean(config, ConfigOption.AUTHOR_ONLY_UNSIGN); this.authorOnlyUnsign = getBoolean(config, ConfigOption.AUTHOR_ONLY_UNSIGN);
authorOnlySave = getBoolean(config, ConfigOption.AUTHOR_ONLY_SAVE); this.authorOnlySave = getBoolean(config, ConfigOption.AUTHOR_ONLY_SAVE);
firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode()); this.firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode());
welcomeMessage = getString(config, ConfigOption.MESSAGE_FOR_NEW_PLAYERS); this.welcomeMessage = getString(config, ConfigOption.MESSAGE_FOR_NEW_PLAYERS);
formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING); this.formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING);
changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY); this.changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY);
enableBookshelfPeeking = getBoolean(config, ConfigOption.ENABLE_BOOKSHELF_PEEKING); 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 //Convert string into material
this.economyManager = new EconomyManager();
String paymentMaterial = getString(config, ConfigOption.PRICE_ITEM_TYPE); String paymentMaterial = getString(config, ConfigOption.PRICE_ITEM_TYPE);
if (paymentMaterial.equalsIgnoreCase("Economy")) { if (paymentMaterial.equalsIgnoreCase("Economy")) {
if (EconomyHelper.setupEconomy()) { if (this.economyManager.getEconomy() == null) {
bookPriceType = Material.AIR; logger.log(Level.SEVERE, StaticMessage.EXCEPTION_VAULT_NOT_AVAILABLE.toString());
this.bookPriceType = null;
} else { } else {
sendErrorMessage(consoleSender, this.bookPriceType = Material.AIR;
"BooksWithoutBorders failed to hook into Vault! Book price not set!");
bookPriceType = null;
} }
} else if (!paymentMaterial.trim().isEmpty()) { } else if (!paymentMaterial.trim().isEmpty()) {
Material material = Material.matchMaterial(paymentMaterial); Material material = Material.matchMaterial(paymentMaterial);
if (material != null) { 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()); (Double) ConfigOption.PRICE_QUANTITY.getDefaultValue());
//Make sure titleAuthorSeparator is a valid value //Make sure titleAuthorSeparator is a valid value
titleAuthorSeparator = cleanString(titleAuthorSeparator); this.titleAuthorSeparator = cleanString(this.titleAuthorSeparator);
if (titleAuthorSeparator.length() != 1) { if (this.titleAuthorSeparator.length() != 1) {
sendErrorMessage(consoleSender, "Title-Author_Separator is set to an invalid value!"); logger.log(Level.SEVERE, "Title-Author_Separator is set to an invalid value!\n" +
sendErrorMessage(consoleSender, "Reverting to default value of \",\""); "Reverting to default value of \",\"");
titleAuthorSeparator = ","; this.titleAuthorSeparator = ",";
config.set("Options.Title-Author_Separator", titleAuthorSeparator); config.set("Options.Title-Author_Separator", this.titleAuthorSeparator);
} }
} catch (Exception e) { } catch (Exception exception) {
sendErrorMessage(consoleSender, "Warning! Config.yml failed to load!"); logger.log(Level.SEVERE, exception.getMessage());
sendErrorMessage(consoleSender, "Try Looking for settings that are missing values!"); logger.log(Level.SEVERE, "Warning! Config.yml failed to load!\n" +
"Try Looking for settings that are missing values!");
return false; return false;
} }
@@ -388,7 +415,7 @@ public class BooksWithoutBordersConfig {
* @param configOption <p>The configuration option to get the value for</p> * @param configOption <p>The configuration option to get the value for</p>
* @return <p>The value of the option</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()); 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> * @param configOption <p>The configuration option to get the value for</p>
* @return <p>The value of the option</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()); 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; package net.knarcraft.bookswithoutborders.config;
import org.jetbrains.annotations.NotNull;
/** /**
* A representation of the different available config options * A representation of the different available config options
*/ */
public enum ConfigOption { 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 * 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 * 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; 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 configNode <p>The config node in the config file this option represents</p>
* @param defaultValue <p>The default value for this config option</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.configNode = configNode;
this.defaultValue = defaultValue; 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; package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@@ -27,32 +29,58 @@ import java.util.logging.Level;
* *
* @author EpicKnarvik97 * @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 @NotNull IvParameterSpec ivParameterSpec;
private final IvParameterSpec ivParameterSpec;
private final byte[] passwordSalt; private final byte[] passwordSalt;
private final @NotNull String password;
/** /**
* Instantiates a new AES encryptor * Instantiates a new AES encryptor
* *
* @param initializationVector <p>The initialization vector to use for CBC</p> * @param aesConfiguration <p>The AES configuration to use</p>
* @param passwordSalt <p>The password salt to use</p>
*/ */
public AES(byte[] initializationVector, byte[] passwordSalt) { public AES(@NotNull AESConfiguration aesConfiguration) {
this.ivParameterSpec = new IvParameterSpec(initializationVector); this.ivParameterSpec = new IvParameterSpec(aesConfiguration.iv());
this.passwordSalt = passwordSalt; 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 * Encrypts or decrypts the given text
* *
* @param input <p>The input to encrypt or decrypt</p> * <p>Note: The same IV and salt must be used during instantiation in order to decrypt an encrypted message.</p>
* @param password <p>The password to use for key generation</p> *
* @param encrypt <p>Whether to encrypt or decrypt the input</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> * @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 //Make a key from the password
SecretKeySpec secretKeySpec = getKeyFromPassword(password); SecretKeySpec secretKeySpec = getKeyFromPassword(password);
//Get cipher instance //Get cipher instance
@@ -70,31 +98,22 @@ public class AES {
try { try {
aes.init(mode, secretKeySpec, ivParameterSpec); aes.init(mode, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException exception) { } catch (InvalidKeyException | InvalidAlgorithmParameterException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES input given!"); BooksWithoutBorders.log(Level.SEVERE, "Invalid AES input given!");
return null; return null;
} }
//Perform encryption/decryption and output result //Perform encryption/decryption and output result
try { try {
byte[] output = aes.doFinal(getInputBytes(input, encrypt)); byte[] output = aes.doFinal(getInputBytes(input, encrypt));
return createResult(output, encrypt); return createResult(output, encrypt);
} catch (IllegalBlockSizeException | BadPaddingException exception) { } catch (IllegalBlockSizeException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES block size or padding"); 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; 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 * 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> * @param encryption <p>Whether the input should be encrypted or decrypted</p>
* @return <p>The input in byte format</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) { if (encryption) {
return input.getBytes(); return input.getBytes();
} else { } else {
@@ -117,6 +136,7 @@ public class AES {
* @param encryption <p>Whether the output came from encryption or decryption</p> * @param encryption <p>Whether the output came from encryption or decryption</p>
* @return <p>The output as a string</p> * @return <p>The output as a string</p>
*/ */
@NotNull
private String createResult(byte[] output, boolean encryption) { private String createResult(byte[] output, boolean encryption) {
if (encryption) { if (encryption) {
return Base64.getEncoder().encodeToString(output); 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> * @return <p>An AES cipher instance, or null if something went wrong</p>
*/ */
@Nullable
private Cipher getAESCipher() { private Cipher getAESCipher() {
Cipher aes; Cipher aes;
try { try {
aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException exception) { } catch (NoSuchAlgorithmException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES algorithm or padding"); 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 null;
} }
return aes; return aes;
@@ -147,20 +171,21 @@ public class AES {
* @param password <p>A user supplied password</p> * @param password <p>A user supplied password</p>
* @return <p>A secret key spec or null if something went wrong</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); PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), this.passwordSalt, 1000, 128);
SecretKeyFactory keyFactory; SecretKeyFactory keyFactory;
try { try {
keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
} catch (NoSuchAlgorithmException exception) { } catch (NoSuchAlgorithmException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES algorithm"); BooksWithoutBorders.log(Level.SEVERE, "Invalid AES algorithm");
return null; return null;
} }
SecretKey tmp; SecretKey tmp;
try { try {
tmp = keyFactory.generateSecret(spec); tmp = keyFactory.generateSecret(spec);
} catch (InvalidKeySpecException exception) { } catch (InvalidKeySpecException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES key specification"); BooksWithoutBorders.log(Level.SEVERE, "Invalid AES key specification");
return null; return null;
} }
return new SecretKeySpec(tmp.getEncoded(), "AES"); 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; package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Random; 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 * <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> * <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 Random ranGen;
private final String[] bases; private final String[] bases;
private final String[] charList; private final String[] availableCharacters;
private final HashMap<String, String[]> codonTable; private final HashMap<String, String[]> codonTable;
private final HashMap<String, String> decryptTable; private final HashMap<String, String> decryptTable;
private final String key; private final String key;
@@ -24,7 +27,7 @@ public class GenenCrypt {
* *
* @param key <p>The key used to generate the codon table</p> * @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 // define the initial, unshuffled codon list of 4 base codons
ArrayList<String> originalCodonList = new ArrayList<>(); ArrayList<String> originalCodonList = new ArrayList<>();
@@ -60,32 +63,40 @@ public class GenenCrypt {
// 10 digits // 10 digits
// space, newline, and tab // space, newline, and tab
// the symbols . , ? " ! @ # $ % ^ & * ( ) - + = / _ \ : ; < > // 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", " ", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", " ",
"\t", "\n", ".", ",", "?", "\"", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "+", "=", "/", "\t", "\n", ".", ",", "?", "\"", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "+", "=", "/",
"_", "\\", ":", ";", "<", ">", "|"}; "_", "\\", ":", ";", "<", ">", "|"};
// define the codon table to encode text // define the codon table to encode text
codonTable = new HashMap<>(); 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), String[] tempArray = new String[]{shuffledCodonList.get(4 * i), shuffledCodonList.get(4 * i + 1),
shuffledCodonList.get(4 * i + 2), shuffledCodonList.get(4 * i + 3)}; shuffledCodonList.get(4 * i + 2), shuffledCodonList.get(4 * i + 3)};
//System.out.println(i); //System.out.println(i);
codonTable.put(charList[i], tempArray); codonTable.put(availableCharacters[i], tempArray);
} }
// define the decryption table // define the decryption table
decryptTable = new HashMap<>(); decryptTable = new HashMap<>();
for (int i = 0; i < codonTable.size(); i++) { for (int i = 0; i < codonTable.size(); i++) {
String s = charList[i]; String s = availableCharacters[i];
String[] sa = codonTable.get(s); String[] sa = codonTable.get(s);
decryptTable.put(sa[0], s); decryptTable.put(sa[0], s);
decryptTable.put(sa[1], s); decryptTable.put(sa[1], s);
decryptTable.put(sa[2], s); decryptTable.put(sa[2], s);
decryptTable.put(sa[3], 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() { public void printCodonTable() {
// print the codon table // print the codon table
for (int i = 0; i < codonTable.size(); i++) { for (int i = 0; i < codonTable.size(); i++) {
String s = charList[i]; String s = availableCharacters[i];
String[] sa = codonTable.get(s); String[] sa = codonTable.get(s);
switch (s) { switch (s) {
case "\t" -> case "\t" ->
@@ -114,7 +125,8 @@ public class GenenCrypt {
* @param input <p>The input to encrypt</p> * @param input <p>The input to encrypt</p>
* @return <p>The encrypted input</p> * @return <p>The encrypted input</p>
*/ */
public String encrypt(String input) { @NotNull
private String encrypt(@NotNull String input) {
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
for (int i = 0; i < input.length(); i++) { for (int i = 0; i < input.length(); i++) {
// insert junk bases // insert junk bases
@@ -147,7 +159,8 @@ public class GenenCrypt {
* @param input <p>The input to decrypt</p> * @param input <p>The input to decrypt</p>
* @return <p>The decrypted input</p> * @return <p>The decrypted input</p>
*/ */
public String decrypt(String input) { @NotNull
private String decrypt(@NotNull String input) {
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
int keyCount = 0; int keyCount = 0;
int junk = key.charAt(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; package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.StringTokenizer; import java.util.StringTokenizer;
/** /**
* A simple substitution cipher * 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. @Override
// the substitution is made harder to crack by public @Nullable String encryptText(@NotNull String input) {
// using a string for the key, it is converted return encryptDecrypt(input, true);
// 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();
try { @Override
offsetArray[i] = Integer.parseInt(nt); public @Nullable String decryptText(@NotNull String input) {
} catch (NumberFormatException e) { return encryptDecrypt(input, false);
BigInteger big = new BigInteger(nt); }
offsetArray[i] = Math.abs(big.intValue());
} /**
* 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; // uses the next offset in the key, goes back to first offset if at end of list
for (int i = 0; i < in.length(); i++) { if (offsetPosition < offsetArray.length - 1) {
output.append((char) (in.charAt(i) + offsetArray[offsetPosition])); //encrypts the letter and adds to the output string offsetPosition++;
// uses the next offset in the key, goes back to first offset if at end of list } else {
if (offsetPosition < offsetArray.length - 1) { offsetPosition = 0;
offsetPosition++;
} else {
offsetPosition = 0;
}
} }
} }
return output.toString(); return output.toString();
} }
// decrypts a string using the same substitution method, /**
// but in reverse. Could probably be combined into one * Tokenizes a key and generates an offset array for substitution
// method with a flag for encryption / decryption, but *
// I'm lazy. * @param key <p>The key to make an offset array for</p>
@SuppressWarnings("unused") * @return <p>The offset array</p>
public String decrypt(String in, String key) { */
StringBuilder output = new StringBuilder(); private int[] getOffsetArray(@NotNull String key) {
if (key != null && !key.isEmpty()) { StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key
StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key // converts each number in the key to an integer and adds to an array
// converts each number in the key to an integer and adds to an array int[] offsetArray = new int[tokenizer.countTokens()];
int[] offsetArray = new int[tokenizer.countTokens()]; for (int i = 0; i < offsetArray.length; i++) {
for (int i = 0; i < offsetArray.length; i++) { String nextToken = tokenizer.nextToken();
offsetArray[i] = Integer.parseInt(tokenizer.nextToken());
} try {
int offsetPosition = 0; offsetArray[i] = Integer.parseInt(nextToken);
for (int i = 0; i < in.length(); i++) { } catch (NumberFormatException e) {
output.append((char) (in.charAt(i) - offsetArray[offsetPosition])); //encrypts the letter and adds to the output string BigInteger big = new BigInteger(nextToken);
// uses the next offset in the key, goes back to first offset if at end of list offsetArray[i] = Math.abs(big.intValue());
if (offsetPosition < offsetArray.length - 1) {
offsetPosition++;
} else {
offsetPosition = 0;
}
} }
} }
return output.toString(); return offsetArray;
}
// 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();
} }
} }

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

View File

@@ -1,35 +1,58 @@
package net.knarcraft.bookswithoutborders.listener; 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.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter;
import net.md_5.bungee.api.ChatColor; 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.block.ChiseledBookshelf;
import org.bukkit.enchantments.Enchantment; import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Event; import org.bukkit.event.Event;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.Action; import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ChiseledBookshelfInventory; import org.bukkit.inventory.ChiseledBookshelfInventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSuccessColor;
/** /**
* A listener for bookshelf clicking * A listener for bookshelf clicking
*/ */
public class BookshelfListener implements Listener { 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 @EventHandler
public void onBookshelfClick(PlayerInteractEvent event) { public void onBookshelfClick(@NotNull PlayerInteractEvent event) {
Player player = event.getPlayer(); Player player = event.getPlayer();
// If left-clicking a chiseled bookshelf and sneaking, display contents // 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 // Check if bookshelf peeking is enabled, and the player can peek
if (!BooksWithoutBordersConfig.getEnableBookshelfPeeking() || if (!BooksWithoutBorders.getConfiguration().getEnableBookshelfPeeking() ||
!event.getPlayer().hasPermission("bookswithoutborders.peekbookshelf")) { !event.getPlayer().hasPermission("bookswithoutborders.peekbookshelf")) {
return; return;
} }
@@ -49,29 +72,53 @@ public class BookshelfListener implements Listener {
event.setUseItemInHand(Event.Result.DENY); event.setUseItemInHand(Event.Result.DENY);
ChiseledBookshelfInventory bookshelfInventory = chiseledBookshelf.getInventory(); ChiseledBookshelfInventory bookshelfInventory = chiseledBookshelf.getInventory();
player.sendMessage(getBookshelfDescription(bookshelfInventory)); player.sendMessage(getBookshelfDescription(bookshelfInventory, event.getClickedBlock().getLocation()));
} }
/** /**
* Gets the description for a bookshelf's contents * Gets the description for a bookshelf's contents
* *
* @param bookshelfInventory <p>The inventory of the bookshelf to describe</p> * @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> * @return <p>A textual description of the bookshelf's contents</p>
*/ */
private String getBookshelfDescription(ChiseledBookshelfInventory bookshelfInventory) { @NotNull
StringBuilder builder = new StringBuilder(getSuccessColor() + "Books in shelf:"); private String getBookshelfDescription(@NotNull ChiseledBookshelfInventory bookshelfInventory, @NotNull Location location) {
for (ItemStack itemStack : bookshelfInventory.getStorageContents()) { 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) { if (itemStack == null) {
builder.append(ChatColor.GRAY).append("<empty>");
continue; continue;
} }
ItemMeta meta = itemStack.getItemMeta(); ItemMeta meta = itemStack.getItemMeta();
builder.append("\n ").append(ChatColor.GRAY).append(" - ");
if (meta instanceof BookMeta bookMeta) { if (meta instanceof BookMeta bookMeta) {
builder.append(getBookDescription(bookMeta)); builder.append(getBookDescription(bookMeta));
} else if (meta instanceof EnchantmentStorageMeta enchantmentStorageMeta) { } else if (meta instanceof EnchantmentStorageMeta enchantmentStorageMeta) {
builder.append(getEnchantedBookDescription(enchantmentStorageMeta)); builder.append(getEnchantedBookDescription(enchantmentStorageMeta));
} else if (meta != null) { } else if (meta != null) {
builder.append(getPlainBookDescription(meta)); builder.append(ChatColor.of("#A5682A")).append("[P]").append(ChatColor.RESET).append(getPlainBookDescription(meta));
} }
} }
return builder.toString(); return builder.toString();
@@ -83,7 +130,8 @@ public class BookshelfListener implements Listener {
* @param itemMeta <p>The metadata for the book to describe</p> * @param itemMeta <p>The metadata for the book to describe</p>
* @return <p>The description of the book</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(); String name = itemMeta.getDisplayName();
if (name.isEmpty()) { if (name.isEmpty()) {
name = "Plain book"; name = "Plain book";
@@ -97,7 +145,8 @@ public class BookshelfListener implements Listener {
* @param bookMeta <p>The metadata for the book to describe</p> * @param bookMeta <p>The metadata for the book to describe</p>
* @return <p>The book's description</p> * @return <p>The book's description</p>
*/ */
private String getBookDescription(BookMeta bookMeta) { @NotNull
private String getBookDescription(@NotNull BookMeta bookMeta) {
String title; String title;
String author; String author;
if (!bookMeta.hasTitle() || bookMeta.getTitle() == null) { if (!bookMeta.hasTitle() || bookMeta.getTitle() == null) {
@@ -106,11 +155,11 @@ public class BookshelfListener implements Listener {
title = bookMeta.getTitle(); title = bookMeta.getTitle();
} }
if (!bookMeta.hasAuthor() || bookMeta.getAuthor() == null) { if (!bookMeta.hasAuthor() || bookMeta.getAuthor() == null) {
author = "Unknown"; author = BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_AUTHOR);
} else { } else {
author = bookMeta.getAuthor(); 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> * @param enchantmentStorageMeta <p>The metadata for the enchanted book to describe</p>
* @return <p>The enchanted book's description</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(); StringBuilder builder = new StringBuilder();
builder.append("Enchanted book ("); builder.append(ChatColor.of("#A64CFF")).append("[E]").append(ChatColor.RESET);
Map<Enchantment, Integer> enchantmentMap = enchantmentStorageMeta.getStoredEnchants(); Map<Enchantment, Integer> enchantmentMap = enchantmentStorageMeta.getStoredEnchants();
List<String> enchantments = new ArrayList<>(enchantmentMap.size()); List<String> enchantments = new ArrayList<>(enchantmentMap.size());
for (Map.Entry<Enchantment, Integer> enchantmentEntry : enchantmentMap.entrySet()) { for (Map.Entry<Enchantment, Integer> enchantmentEntry : enchantmentMap.entrySet()) {
@@ -129,7 +179,6 @@ public class BookshelfListener implements Listener {
IntegerToRomanConverter.getRomanNumber(enchantmentEntry.getValue())); IntegerToRomanConverter.getRomanNumber(enchantmentEntry.getValue()));
} }
builder.append(String.join(", ", enchantments)); builder.append(String.join(", ", enchantments));
builder.append(")");
return builder.toString(); return builder.toString();
} }
@@ -139,7 +188,9 @@ public class BookshelfListener implements Listener {
* @param enchantment <p>The enchantment to get the name of</p> * @param enchantment <p>The enchantment to get the name of</p>
* @return <p>The prettified enchantment name</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("_", " ")); return uppercaseFirst(enchantment.getKey().getKey().replace("_", " "));
} }
@@ -149,7 +200,8 @@ public class BookshelfListener implements Listener {
* @param input <p>The input to uppercase</p> * @param input <p>The input to uppercase</p>
* @return <p>The input string with more 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(" "); String[] parts = input.split(" ");
for (int i = 0; i < parts.length; i++) { for (int i = 0; i < parts.length; i++) {
parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1); 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; 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.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory; import org.jetbrains.annotations.NotNull;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.File; import java.io.File;
import java.util.logging.Level; import java.util.logging.Level;
@@ -29,40 +22,16 @@ public class PlayerEventListener implements Listener {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance(); private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@EventHandler @EventHandler
public void onHold(PlayerItemHeldEvent event) { public void onPlayerJoin(@NotNull PlayerJoinEvent 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) {
Player player = event.getPlayer(); 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 //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())); File file = new File(bookFolder + InputCleaningHelper.cleanString(player.getName()));
if (file.exists()) { if (file.exists()) {
if (!file.renameTo(new File(bookFolder + player.getUniqueId()))) { 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()); "directory for player " + player.getName());
} }
} }
@@ -72,22 +41,10 @@ public class PlayerEventListener implements Listener {
boolean sendMessage = true; boolean sendMessage = true;
//Gives new players necessary books //Gives new players necessary books
for (String bookName : BooksWithoutBordersConfig.getFirstBooks()) { for (String bookName : config.getFirstBooks()) {
sendMessage = giveBookToNewPlayer(bookName, player, sendMessage); 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> * @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> * @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()) { if (!bookName.trim().isEmpty()) {
//Give the book to the player if it exists //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 //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) { if (!welcomeMessage.trim().isEmpty() && newBook != null && sendMessage) {
sendMessage = false; sendMessage = false;
booksWithoutBorders.getServer().getScheduler().scheduleSyncDelayedTask(booksWithoutBorders, booksWithoutBorders.getServer().getScheduler().scheduleSyncDelayedTask(booksWithoutBorders,
@@ -118,68 +75,4 @@ public class PlayerEventListener implements Listener {
return sendMessage; 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; 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.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.BookLoader; import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper; import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
@@ -24,11 +27,11 @@ import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File; 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; 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 { public class SignEventListener implements Listener {
private final String slash = getSlash();
@EventHandler @EventHandler
public void onSignChange(SignChangeEvent event) { public void onSignChange(@NotNull SignChangeEvent event) {
if (event.isCancelled()) { if (event.isCancelled()) {
return; return;
} }
@@ -56,10 +57,11 @@ public class SignEventListener implements Listener {
event.setLine(0, ChatColor.DARK_GREEN + "[BwB]"); event.setLine(0, ChatColor.DARK_GREEN + "[BwB]");
//Check if the sign is of a valid type //Check if the sign is of a valid type
if (!((lines[1].equalsIgnoreCase("[Encrypt]") || lines[1].equalsIgnoreCase("[Decrypt]") || if ((!lines[1].equalsIgnoreCase("[Encrypt]") && !lines[1].equalsIgnoreCase("[Decrypt]") &&
lines[1].equalsIgnoreCase("[Give]")) && !lines[2].trim().isEmpty())) { !lines[1].equalsIgnoreCase("[Give]")) || lines[2].trim().isEmpty()) {
//Mark the second line as invalid //Mark the second line as invalid
event.setLine(1, ChatColor.DARK_RED + lines[1]); event.setLine(1, ChatColor.DARK_RED + lines[1]);
player.sendMessage("Invalid sign!");
return; return;
} }
@@ -79,12 +81,7 @@ public class SignEventListener implements Listener {
} }
@EventHandler @EventHandler
public void onClick(PlayerInteractEvent event) { public void onClick(@NotNull PlayerInteractEvent event) {
if (event.getClickedBlock() == null) {
return;
}
Material clickedBlockType = event.getClickedBlock().getType();
Player player = event.getPlayer(); Player player = event.getPlayer();
PlayerInventory playerInventory = player.getInventory(); PlayerInventory playerInventory = player.getInventory();
EquipmentSlot hand = event.getHand(); EquipmentSlot hand = event.getHand();
@@ -97,15 +94,18 @@ public class SignEventListener implements Listener {
} }
Material heldItemType = heldItem.getType(); Material heldItemType = heldItem.getType();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && (Tag.SIGNS.isTagged(clickedBlockType) || if (event.getClickedBlock() != null && (event.getAction() == Action.RIGHT_CLICK_BLOCK &&
Tag.WALL_SIGNS.isTagged(clickedBlockType))) { (Tag.SIGNS.isTagged(event.getClickedBlock().getType()) ||
event.setUseItemInHand(Event.Result.DENY); Tag.WALL_SIGNS.isTagged(event.getClickedBlock().getType())))) {
//The player right-clicked a sign //The player right-clicked a sign
Sign sign = (Sign) event.getClickedBlock().getState(); Sign sign = (Sign) event.getClickedBlock().getState();
if (!signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) { if (!signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) {
return; return;
} }
event.setUseItemInHand(Event.Result.DENY);
event.setCancelled(true);
if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) { if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) {
encryptHeldBookUsingSign(sign, heldItemType, player, hand); encryptHeldBookUsingSign(sign, heldItemType, player, hand);
} else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) { } 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 } else if (heldItemType == Material.WRITTEN_BOOK && (event.getAction() == Action.LEFT_CLICK_AIR
|| event.getAction() == Action.LEFT_CLICK_BLOCK)) { || event.getAction() == Action.LEFT_CLICK_BLOCK)) {
BookMeta oldBook = (BookMeta) heldItem.getItemMeta(); BookMeta oldBook = (BookMeta) heldItem.getItemMeta();
if (oldBook == null) { if (oldBook == null) {
return; return;
@@ -138,16 +139,21 @@ public class SignEventListener implements Listener {
* @param player <p>The player which clicked the sign</p> * @param player <p>The player which clicked the sign</p>
* @param hand <p>The EquipmentSlot of the used hand</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 //Decrypt the held book and replace it
if (heldItemType == Material.WRITTEN_BOOK) { if (heldItemType == Material.WRITTEN_BOOK) {
player.closeInventory(); player.closeInventory();
//Converts user supplied key into integer form //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); 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) { if (book != null) {
player.getInventory().setItem(hand, book); player.getInventory().setItem(hand, book);
player.sendMessage(ChatColor.GREEN + "Book decrypted!"); player.sendMessage(ChatColor.GREEN + "Book decrypted!");
@@ -161,9 +167,10 @@ public class SignEventListener implements Listener {
* @param sign <p>The sign to check</p> * @param sign <p>The sign to check</p>
* @return <p>The color of the sign</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); 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)); return ChatColor.getByChar(sign.getSide(Side.FRONT).getLine(2).substring(1, 2).charAt(0));
} else { } else {
return null; return null;
@@ -179,7 +186,8 @@ public class SignEventListener implements Listener {
* @param color <p>The color to match</p> * @param color <p>The color to match</p>
* @return <p>True if the given string is what's on the sign</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); String line = sign.getSide(Side.FRONT).getLine(lineNumber);
return line.equalsIgnoreCase(color + compareTo); return line.equalsIgnoreCase(color + compareTo);
} }
@@ -191,7 +199,8 @@ public class SignEventListener implements Listener {
* @param lines <p>The lines on the sign</p> * @param lines <p>The lines on the sign</p>
* @param player <p>The player which edited 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) { if (lines[2].length() > 13 || lines[3].length() > 13) {
BooksWithoutBorders.sendErrorMessage(player, BooksWithoutBorders.sendErrorMessage(player,
"[Give] signs' 3rd and 4th lines must be 13 characters or less!"); "[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 //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)) { if (BookFileHelper.bookFileExists(signFile)) {
markGiveSignValidity(event, true); markGiveSignValidity(event, true);
return; return;
@@ -220,7 +229,7 @@ public class SignEventListener implements Listener {
* @param event <p>The event causing the creation of the give sign</p> * @param event <p>The event causing the creation of the give sign</p>
* @param isValid <p>Whether the created sign is valid</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(); String[] lines = event.getLines();
if (isValid) { if (isValid) {
event.setLine(2, ChatColor.DARK_GREEN + lines[2]); 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 heldItem <p>The type of the held book</p>
* @param hand <p>The hand the player is using to hold the 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; ItemStack newBook;
//Check if the book is encrypted by Books Without Borders //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]; String groupName = oldBook.getLore().get(0).substring(3).split(" encrypted")[0];
//Permission check //Permission check
if (!player.hasPermission("bookswithoutborders.decrypt." + groupName) && if (!player.hasPermission(Permission.DECRYPT + "." + groupName) &&
!(BooksWithoutBordersConfig.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin"))) { !(config.getAdminDecrypt() && player.hasPermission(Permission.ADMIN.toString()))) {
BooksWithoutBorders.sendErrorMessage(player, "You are not allowed to decrypt that book");
return; 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 = BookFileHelper.findBookFile(encryptedFolder + InputCleaningHelper.cleanString(groupName) +
config.getSlash(), oldBook);
File file = new File(getBookFolder() + "Encrypted" + slash + encryptionFile); if (file == null) {
if (!file.isFile()) { file = BookFileHelper.findBookFile(encryptedFolder, oldBook);
file = new File(getBookFolder() + fileName + ".txt"); if (file == null) {
if (!file.isFile()) { file = BookFileHelper.findBookFile(config.getBookFolder(), oldBook);
return; 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) { if (newBook == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to load the unencrypted book!");
return; return;
} }
@@ -285,14 +304,15 @@ public class SignEventListener implements Listener {
* @param player <p>The player which clicked the sign</p> * @param player <p>The player which clicked the sign</p>
* @param hand <p>The EquipmentSlot of the used hand</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; ItemStack eBook;
String[] lines = sign.getSide(Side.FRONT).getLines(); String[] lines = sign.getSide(Side.FRONT).getLines();
boolean mainHand = hand == EquipmentSlot.HAND; boolean mainHand = hand == EquipmentSlot.HAND;
if (heldItemType == Material.WRITTEN_BOOK) { if (heldItemType == Material.WRITTEN_BOOK) {
player.closeInventory(); player.closeInventory();
eBook = EncryptionHelper.encryptBook(player, mainHand, ChatColor.stripColor(lines[2]), eBook = EncryptionHelper.encryptBook(player, mainHand, BookFormatter.stripColor(lines[2]),
EncryptionStyle.getFromString(ChatColor.stripColor(lines[3]))); EncryptionStyle.getFromString(BookFormatter.stripColor(lines[3])));
if (eBook != null) { if (eBook != null) {
player.getInventory().setItem(hand, eBook); player.getInventory().setItem(hand, eBook);
} }
@@ -305,8 +325,8 @@ public class SignEventListener implements Listener {
* @param sign <p>The sign the user clicked</p> * @param sign <p>The sign the user clicked</p>
* @param player <p>The player which clicked the sign</p> * @param player <p>The player which clicked the sign</p>
*/ */
private void giveBook(Sign sign, Player player) { private void giveBook(@NotNull Sign sign, @NotNull Player player) {
String fileName = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2)); String fileName = BookFormatter.stripColor(sign.getSide(Side.FRONT).getLine(2));
boolean isLoadListNumber = false; boolean isLoadListNumber = false;
try { try {
@@ -318,7 +338,7 @@ public class SignEventListener implements Listener {
//Add the third line to the second line for the full filename //Add the third line to the second line for the full filename
String thirdLine = sign.getSide(Side.FRONT).getLine(3); String thirdLine = sign.getSide(Side.FRONT).getLine(3);
if (!isLoadListNumber && thirdLine.length() >= 2) { if (!isLoadListNumber && thirdLine.length() >= 2) {
fileName += ChatColor.stripColor(thirdLine); fileName += BookFormatter.stripColor(thirdLine);
} }
ItemStack newBook = BookLoader.loadBook(player, fileName, "true", "public"); 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.Server; import org.bukkit.Server;
@@ -12,19 +13,36 @@ import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager; import org.bukkit.plugin.ServicesManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; 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> * @return <p>An economy instance, or null if it's not initialized</p>
*/ */
public static Economy getEconomy() { @Nullable
public Economy getEconomy() {
return economy; 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 * 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> * @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> * @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 //BookPriceQuantity: How many items are required to pay for each book
//BookPriceType: Which item is used to pay for the books. AIR = use economy //BookPriceType: Which item is used to pay for the books. AIR = use economy
Material bookCurrency = BooksWithoutBordersConfig.getBookPriceType(); Material bookCurrency = config.getBookPriceType();
double cost = BooksWithoutBordersConfig.getBookPriceQuantity() * numCopies; double cost = config.getBookPriceQuantity() * numCopies;
int itemCost = (int) cost; int itemCost = (int) cost;
if (bookCurrency == Material.AIR) { if (bookCurrency == Material.AIR) {
return !EconomyHelper.payForBookPrintingEconomy(player, cost, numCopies); return !payForBookPrintingEconomy(player, cost, numCopies);
} else { } else {
if (bookCurrency == Material.WRITABLE_BOOK) { if (bookCurrency == Material.WRITABLE_BOOK) {
//Writable books are treated as a special case to prevent WIP books from being used //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> * @param itemCost <p>The number of writable books to pay</p>
* @return <p>True if the payment was successful</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); List<ItemStack> books = getPlayersEmptyBooks(player);
if (countItems(books) < itemCost) { if (countItems(books) < itemCost) {
BooksWithoutBorders.sendErrorMessage(player, itemCost + " empty " + Material.WRITABLE_BOOK + BooksWithoutBorders.sendErrorMessage(player, itemCost + " empty " + Material.WRITABLE_BOOK +
@@ -131,7 +127,7 @@ public final class EconomyHelper {
* @param items <p>The items to count</p> * @param items <p>The items to count</p>
* @return <p>The total number of items</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; int totalItems = 0;
for (ItemStack itemStack : items) { for (ItemStack itemStack : items) {
totalItems += itemStack.getAmount(); totalItems += itemStack.getAmount();
@@ -145,7 +141,8 @@ public final class EconomyHelper {
* @param player <p>The player to get books for</p> * @param player <p>The player to get books for</p>
* @return <p>The empty books in the player's inventory</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<>(); List<ItemStack> validBooks = new ArrayList<>();
for (ItemStack itemStack : player.getInventory().getContents()) { for (ItemStack itemStack : player.getInventory().getContents()) {
if (itemStack == null || itemStack.getType() != Material.WRITABLE_BOOK) { 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> * @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> * @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) { if ((economy.getBalance(player) - cost) >= 0) {
economy.withdrawPlayer(player, cost); economy.withdrawPlayer(player, cost);
BooksWithoutBorders.sendSuccessMessage(player, economy.format(cost) + " withdrawn to create " + 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 player <p>The player which needs to pay</p>
* @param itemCost <p>The number of items 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(); PlayerInventory playerInventory = player.getInventory();
int clearedAmount = 0; int clearedAmount = 0;
while (clearedAmount < itemCost) { while (clearedAmount < itemCost) {
int firstItemIndex = playerInventory.first(BooksWithoutBordersConfig.getBookPriceType()); int firstItemIndex = playerInventory.first(BooksWithoutBorders.getConfiguration().getBookPriceType());
ItemStack firstItem = playerInventory.getItem(firstItemIndex); ItemStack firstItem = playerInventory.getItem(firstItemIndex);
if (Objects.requireNonNull(firstItem).getAmount() <= itemCost - clearedAmount) { if (Objects.requireNonNull(firstItem).getAmount() <= itemCost - clearedAmount) {

View File

@@ -1,5 +1,8 @@
package net.knarcraft.bookswithoutborders.state; 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 * 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> * @param directory <p>The directory to transform</p>
* @return <p>A book directory, or null if the given directory is empty</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")) { if (directory.equalsIgnoreCase("public")) {
return BookDirectory.PUBLIC; return BookDirectory.PUBLIC;
} else if (directory.equalsIgnoreCase("player")) { } 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; package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; 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.knarcraft.bookswithoutborders.state.BookDirectory;
import net.md_5.bungee.api.ChatColor; import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender; 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.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
/** /**
* Helper class for dealing with files * 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> * @param possibleIndex <p>The string which might be a book index</p>
* @return <p>True if the number is a book index</p> * @return <p>True if the number is a book index</p>
*/ */
public static boolean isBookListIndex(String possibleIndex) { public static boolean isBookListIndex(@NotNull String possibleIndex) {
File bookDirectory = new File(getBookFolder().replaceAll("[\\\\/]$", "")); File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder().replaceAll("[\\\\/]$", ""));
try { try {
//Tests if a load list number has been supplied //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> * @param bookFile <p>The path to a book</p>
* @return <p>True if the file exists and points to a book file</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") || return ((new File(bookFile).isFile() && (bookFile.endsWith(".txt") ||
bookFile.endsWith(".yml"))) || new File(bookFile + ".txt").isFile() || bookFile.endsWith(".yml"))) || new File(bookFile + ".txt").isFile() ||
new File(bookFile + ".yml").isFile()) && !bookFile.contains("../") && !bookFile.contains("..\\"); 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> * @param bookPath <p>The path of the book to get</p>
* @return <p>The file the path points to, or null otherwise</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)) { if (!bookFileExists(bookPath)) {
return null; return null;
} }
@@ -93,7 +99,8 @@ public final class BookFileHelper {
* @param listPublic <p>Whether to list public or personal files</p> * @param listPublic <p>Whether to list public or personal files</p>
* @return <p>A list of available 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); File file = BookHelper.getBookDirectoryPath(listPublic ? BookDirectory.PUBLIC : BookDirectory.PLAYER, sender);
if (file == null) { if (file == null) {
return new ArrayList<>(); 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 books <p>The books to look through</p>
* @param listPublic <p>Whether to display public books</p> * @return <p>The map of the first index containing each character</p>
*/ */
public static void printBooks(CommandSender sender, boolean listPublic) { @NotNull
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic); public static Map<Character, Integer> populateLetterIndices(@NotNull List<String> books) {
BookFileHelper.printFiles(sender, availableBooks); 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);
* Prints a list of files if (current == null || current != first) {
* current = first;
* @param sender <p>The command sender to show the list to</p> firstEncounter.put(first, i);
* @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));
} }
return firstEncounter;
} }
/** /**
@@ -136,7 +135,8 @@ public final class BookFileHelper {
* @param searchDirectory <p>The directory to search for files</p> * @param searchDirectory <p>The directory to search for files</p>
* @return <p>A list of available 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<>(); List<String> fileList = new ArrayList<>();
File[] existingFiles = searchDirectory.listFiles(); File[] existingFiles = searchDirectory.listFiles();
@@ -146,22 +146,26 @@ public final class BookFileHelper {
} }
for (File foundFile : existingFiles) { for (File foundFile : existingFiles) {
if (!foundFile.isFile()) { // Filter out invalid files
if (!foundFile.isFile() || foundFile.getName().contains("§")) {
continue; continue;
} }
String fileName = foundFile.getName(); String fileName = foundFile.getName();
String separator = BooksWithoutBordersConfig.getTitleAuthorSeparator(); String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
if (fileName.contains(separator)) { if (fileName.contains(separator)) {
//Convert the UUID into a username if necessary //Convert the UUID into a username if necessary
String[] data = fileName.split(separator); String userName = getBookAuthorFromPath(fileName);
String extension = data[1].substring(data[1].length() - 4); String title = getBookTitleFromPath(fileName);
String userName = data[1].substring(0, data[1].length() - 4); fileList.add(title + separator + BookHelper.authorFromUUID(userName));
data[1] = BookHelper.authorFromUUID(userName) + extension; } else {
fileList.add(String.join(separator, data)); fileList.add(fileName);
} }
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; return fileList;
} }
@@ -172,7 +176,7 @@ public final class BookFileHelper {
* @param fileName <p>The name of the file which might already exist</p> * @param fileName <p>The name of the file which might already exist</p>
* @return <p>The number of found duplicates</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; int foundDuplicates = 0;
for (File foundFile : foundFiles) { for (File foundFile : foundFiles) {
if (foundFile.getName().matches("(\\([0-9]+\\))?" + Pattern.quote(fileName) + "(\\.yml|\\.txt)?")) { if (foundFile.getName().matches("(\\([0-9]+\\))?" + Pattern.quote(fileName) + "(\\.yml|\\.txt)?")) {
@@ -182,4 +186,131 @@ public final class BookFileHelper {
return foundDuplicates; 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.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper; import net.knarcraft.knarlib.util.ColorHelper;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -24,7 +25,7 @@ public final class BookFormatter {
* *
* @param rawPages <p>A list of pages</p> * @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 maxPageText = 256;
int fitsNewline = maxPageText - 2; 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 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> * @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) { while (rawPages.get(rawPages.size() - 1).length() > maxPageText) {
int splitPosition; int splitPosition;
String fittingText = rawPages.get(rawPages.size() - 1).substring(0, maxPageText); 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 rawPages <p>The raw pages to format</p>
* @param maxPageText <p>The max number of characters which fit on a page</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 lastPageIndex = rawPages.size() - 1;
int nextToLastIndex = rawPages.size() - 2; int nextToLastIndex = rawPages.size() - 2;
if (rawPages.get(nextToLastIndex).length() + rawPages.get(lastPageIndex).length() <= maxPageText) { 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 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> * @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; int pageIndex = rawPages.size() - 1;
if (rawPages.get(pageIndex).length() <= fitsNewline && !rawPages.get(pageIndex).isEmpty()) { String pageContents = rawPages.get(pageIndex);
rawPages.set(pageIndex, (rawPages.get(pageIndex)) + "\n"); 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> * @param bookMeta <p>The book meta to change</p>
* @return <p>The changed book meta</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()); List<String> formattedPages = new ArrayList<>(Objects.requireNonNull(bookMeta).getPageCount());
for (String page : bookMeta.getPages()) { for (String page : bookMeta.getPages()) {
formattedPages.add(ColorHelper.translateColorCodes(page, ColorConversion.RGB)); formattedPages.add(ColorHelper.translateColorCodes(page, ColorConversion.RGB));
@@ -114,4 +119,15 @@ public final class BookFormatter {
return bookMeta; 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.state.BookDirectory; import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; 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.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level;
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 getting abstract book information * 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 * Converts the author of a book from UUID if necessary
* *
* @param author <p>The author string</p> * @param author <p>The author string</p>
* @return <p>The author string, converted if it was a UUID</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 { try {
UUID authorID = UUID.fromString(author); UUID authorID = UUID.fromString(BookFormatter.stripColor(author));
Player player = Bukkit.getPlayer(authorID); Player player = Bukkit.getPlayer(authorID);
if (player != null) { if (player != null) {
author = player.getName(); author = player.getName();
@@ -50,7 +88,8 @@ public final class BookHelper {
* @param sender <p>The command sender trying to get the directory</p> * @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> * @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); String bookFolderString = getBookDirectoryPathString(bookDirectory, sender);
if (bookFolderString == null) { if (bookFolderString == null) {
return null; return null;
@@ -65,13 +104,15 @@ public final class BookHelper {
* @param sender <p>The command sender trying to get the directory</p> * @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> * @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 folder = null;
String bookFolder = BooksWithoutBordersConfig.getBookFolder(); String bookFolder = config.getBookFolder();
if (bookDirectory == BookDirectory.PUBLIC) { if (bookDirectory == BookDirectory.PUBLIC) {
folder = bookFolder; folder = bookFolder;
} else if (bookDirectory == BookDirectory.PLAYER && sender instanceof Player player) { } else if (bookDirectory == BookDirectory.PLAYER && sender instanceof Player player) {
folder = bookFolder + player.getUniqueId() + getSlash(); folder = bookFolder + player.getUniqueId() + config.getSlash();
} }
return folder; return folder;
} }
@@ -81,9 +122,9 @@ public final class BookHelper {
* *
* @param bookItem <p>The book item to increase the generation of</p> * @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(); BookMeta bookMeta = (BookMeta) bookItem.getItemMeta();
if (BooksWithoutBordersConfig.changeGenerationOnCopy() && bookMeta != null) { if (BooksWithoutBorders.getConfiguration().changeGenerationOnCopy() && bookMeta != null) {
bookMeta.setGeneration(BookHelper.getNextGeneration(bookMeta.getGeneration())); bookMeta.setGeneration(BookHelper.getNextGeneration(bookMeta.getGeneration()));
bookItem.setItemMeta(bookMeta); bookItem.setItemMeta(bookMeta);
} }
@@ -98,7 +139,8 @@ public final class BookHelper {
* @param currentGeneration <p>The current generation of the book</p> * @param currentGeneration <p>The current generation of the book</p>
* @return <p>The next 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) { if (currentGeneration == null) {
return BookMeta.Generation.COPY_OF_ORIGINAL; 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 book <p>The book to get the file of</p>
* @param player <p>The player trying to do something with the book</p> * @param player <p>The player trying to do something with the book</p>
* @return <p>The book file</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) { @NotNull
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator(); public static String getBookFile(@NotNull BookMeta book, @NotNull OfflinePlayer player, boolean isPublic) throws IllegalArgumentException {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
String bookName; String bookName;
if (book.hasTitle()) { if (book.hasTitle()) {
bookName = book.getTitle(); bookName = book.getTitle();
if (bookName == null) {
bookName = "Untitled";
}
} else { } else {
bookName = "Untitled"; bookName = "Untitled";
} }
String playerName = player.getName() == null ? player.getUniqueId().toString() : player.getName();
String authorName; 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 //Store as unique id to account for name changes
authorName = player.getUniqueId().toString(); authorName = player.getUniqueId().toString();
} else if (!book.hasAuthor()) { } else if (!book.hasAuthor()) {
authorName = player.getName(); authorName = player.getName();
} else { } else {
authorName = book.getAuthor(); 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> * @param book <p>The book to check</p>
* @return <p>True if the player is not the book's author</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())) { if (isAuthor(player.getName(), book.getAuthor())) {
return false; return false;
} else { } else {
@@ -162,7 +220,7 @@ public final class BookHelper {
* @param author <p>The author to check</p> * @param author <p>The author to check</p>
* @return <p>True if the player is the author</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); playerName = InputCleaningHelper.cleanString(playerName);
return author != null && playerName.equalsIgnoreCase(InputCleaningHelper.cleanString(author)); 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory; import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level;
/** /**
* A helper class for loading books from files * 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> * @param directory <p>The directory to save the book in</p>
* @return <p>The loaded book</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); 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> * @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</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); 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 //Find the filename if a book index is given
try { try {
int bookIndex = Integer.parseInt(fileName); 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()) { if (bookIndex <= availableFiles.size()) {
fileName = availableFiles.get(Integer.parseInt(fileName) - 1); fileName = availableFiles.get(Integer.parseInt(fileName) - 1);
} }
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
} }
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//Get the full path of the book to load //Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory); File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) { if (file == null) {
//Try converting the username to UUID //Try converting the username to UUID
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator(); String replaced = BookFileHelper.replaceAuthorWithUUID(fileName);
String[] data = fileName.split(titleAuthorSeparator); file = getFullPath(sender, replaced, bookDirectory, directory);
String extension = data[1].substring(data[1].length() - 4); if (file == null) {
String userName = data[1].substring(0, data[1].length() - 4); BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
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 {
return null; return null;
} }
} }
//Make sure the player can pay for the book //Make sure the player can pay for the book
if (BooksWithoutBordersConfig.booksHavePrice() && if (config.booksHavePrice() &&
!sender.hasPermission("bookswithoutborders.bypassBookPrice") && !sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) && (bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
EconomyHelper.cannotPayForBookPrinting((Player) sender, numCopies)) { config.getEconomyManager().cannotPayForBookPrinting((Player) sender, numCopies)) {
return null; return null;
} }
//Generate a new empty book //Generate a new empty book
ItemStack book; ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta bookMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK); BookMeta bookMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK);
if (isSigned.equalsIgnoreCase("true")) {
book = new ItemStack(Material.WRITTEN_BOOK); if (bookMetadata == null) {
} else { BooksWithoutBorders.sendErrorMessage(sender, "Unable to create blank book metadata!");
book = new ItemStack(Material.WRITABLE_BOOK); return null;
} }
//Load the book from the given file //Load the book from the given file
BookToFromTextHelper.bookFromFile(file, bookMetadata); bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) { if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(sender, "File was blank!!"); BooksWithoutBorders.sendErrorMessage(sender, "File was blank!!");
return null; return null;
@@ -119,6 +139,10 @@ public final class BookLoader {
BookHelper.increaseGeneration(book); BookHelper.increaseGeneration(book);
book.setAmount(numCopies); book.setAmount(numCopies);
if (!isSigned.equalsIgnoreCase("true") && book.getItemMeta() != null) {
return BookHelper.unsignBook((BookMeta) book.getItemMeta(), book.getAmount());
}
return book; return book;
} }
@@ -131,14 +155,21 @@ public final class BookLoader {
* @param directory <p>The relative directory given</p> * @param directory <p>The relative directory given</p>
* @return <p>A file or null if it does not exist</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) { @Nullable
File file; private static File getFullPath(@NotNull CommandSender sender, @NotNull String fileName,
String slash = BooksWithoutBordersConfig.getSlash(); @NotNull BookDirectory bookDirectory, @NotNull String directory) {
String bookFolder = BooksWithoutBordersConfig.getBookFolder(); BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
File file = null;
String slash = config.getSlash();
if (bookDirectory == BookDirectory.ENCRYPTED) { if (bookDirectory == BookDirectory.ENCRYPTED) {
file = BookFileHelper.getBookFile(bookFolder + "Encrypted" + slash + directory + slash + fileName); file = BookFileHelper.findBookFile(config.getEncryptedBookPath() + directory + slash, fileName);
} else { } 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()) { if (file == null || !file.isFile()) {
return null; return null;

View File

@@ -1,25 +1,25 @@
package net.knarcraft.bookswithoutborders.utility; package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; 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 net.knarcraft.knarlib.util.FileHelper;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.utility.BookHelper.authorFromUUID; 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 * 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> * @param bookMetadata <p>Metadata about the book to save</p>
* @throws IOException <p>If unable to save the book</p> * @throws IOException <p>If unable to save the book</p>
*/ */
public static void bookToYml(String path, String fileName, BookMeta bookMetadata) throws IOException { public static void bookToYml(@NotNull String path, @NotNull String fileName, @NotNull BookMeta bookMetadata) throws IOException {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(new File(path, "blank")); 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()) { if (bookMetadata.hasTitle()) {
bookYml.set("Title", bookMetadata.getTitle()); bookYml.set("Title", bookMetadata.getTitle());
@@ -58,7 +106,7 @@ public final class BookToFromTextHelper {
bookYml.set("Lore", bookMetadata.getLore()); 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> * @param bookMetadata <p>The book metadata to use for saving the book</p>
* @return <p>The book metadata of the loaded 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")) { if (file.getName().endsWith(".txt")) {
return bookFromTXT(file.getName(), file, bookMetadata); return bookFromTXT(file.getName(), file, bookMetadata);
} else if (file.getName().endsWith(".yml")) { } 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 file <p>The path of the file to load</p>
* @param fileName <p>The name of the file to save to</p> * @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @param bookMetadata <p>Metadata about the book to save</p> * @param userKey <p>The user-supplied decryption key</p>
* @throws IOException <p>If unable to save the book</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 { @Nullable
FileWriter fileWriter = new FileWriter(folderPath + fileName + ".txt", StandardCharsets.UTF_8); public static BookMeta encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata, @NotNull String userKey, boolean forceDecrypt) {
PrintWriter printWriter = new PrintWriter(fileWriter); BookMeta meta;
List<String> pages = bookMetadata.getPages();
BookMeta.Generation generation = bookMetadata.getGeneration(); try {
if (generation == null) { meta = bookFromYml(file, bookMetadata);
generation = BookMeta.Generation.ORIGINAL; if (meta == null) {
return null;
}
} catch (IllegalArgumentException exception) {
return null;
} }
String generationString = ":" + generation.name();
//Save each page of the book as a text line // If the plaintext is stored in the file, don't bother with real decryption
printWriter.println("[Book]" + generationString); if (!meta.getPages().isEmpty()) {
for (String page : pages) { return meta;
printWriter.println(page);
} }
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> * @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @return <p>Metadata for the loaded book</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 { try {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file); FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file);
bookMetadata.setGeneration(BookMeta.Generation.valueOf(bookYml.getString("Generation", "ORIGINAL"))); bookMetadata.setGeneration(BookMeta.Generation.valueOf(bookYml.getString("Generation", "ORIGINAL")));
bookMetadata.setTitle(bookYml.getString("Title", "Untitled")); 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.setPages(bookYml.getStringList("Pages"));
bookMetadata.setLore(bookYml.getStringList("Lore")); bookMetadata.setLore(bookYml.getStringList("Lore"));
} catch (IllegalArgumentException e) { } 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> * @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @return <p>Metadata for the loaded book</p> * @return <p>Metadata for the loaded book</p>
*/ */
private static BookMeta bookFromTXT(String fileName, File file, BookMeta bookMetadata) { @Nullable
String author; private static BookMeta bookFromTXT(@NotNull String fileName, @NotNull File file, @NotNull BookMeta bookMetadata) {
String title;
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
//Remove .txt extension
fileName = fileName.substring(0, fileName.length() - 4);
//Get title and author from the file name //Get title and author from the file name
if (fileName.contains(titleAuthorSeparator)) { String title = BookFileHelper.getBookTitleFromPath(fileName);
String[] titleAuthor = fileName.split(titleAuthorSeparator); String author = BookFileHelper.getBookAuthorFromPath(fileName);
title = titleAuthor[0];
author = titleAuthor[1];
} else {
author = "Unknown";
title = fileName;
}
//Replace underscores with spaces
title = fixName(title, true);
//Read the .txt file //Read the .txt file
List<String> rawPages; List<String> rawPages;
try { try {
rawPages = readTextFile(file); rawPages = readTextFile(file);
if (rawPages == null) {
BooksWithoutBorders.log(Level.SEVERE, "Text file's first line was null");
return null;
}
} catch (IOException exception) { } 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; return null;
} }
//Parse the generation from the book data //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])); bookMetadata.setGeneration(BookMeta.Generation.valueOf(rawPages.get(0).split(":")[1]));
rawPages.remove(0); rawPages.remove(0);
} }
@@ -175,7 +253,7 @@ public final class BookToFromTextHelper {
//Update the metadata of the book with its new values //Update the metadata of the book with its new values
bookMetadata.setAuthor(authorFromUUID(author)); bookMetadata.setAuthor(authorFromUUID(author));
bookMetadata.setTitle(title); bookMetadata.setTitle(title.substring(0, 32));
bookMetadata.setPages(pages); bookMetadata.setPages(pages);
return bookMetadata; 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> * @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> * @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<>(); List<String> rawPages = new ArrayList<>();
BufferedReader bufferedReader = FileHelper.getBufferedReaderFromInputStream(new FileInputStream(file)); 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; 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.GenenCrypt;
import net.knarcraft.bookswithoutborders.encryption.Magic;
import net.knarcraft.bookswithoutborders.encryption.OneTimePad;
import net.knarcraft.bookswithoutborders.encryption.SubstitutionCipher; import net.knarcraft.bookswithoutborders.encryption.SubstitutionCipher;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.logging.Level; 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.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
/** /**
* Helper class for book encryption * Helper class for book encryption
*/ */
public final class EncryptionHelper { public final class EncryptionHelper {
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private EncryptionHelper() { private EncryptionHelper() {
} }
@@ -35,46 +45,79 @@ public final class EncryptionHelper {
* Transforms a string key/password into its numerical values * Transforms a string key/password into its numerical values
* *
* @param key <p>The key to transform</p> * @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) { @NotNull
StringBuilder integerKey = new StringBuilder(); public static String getNumberKeyFromStringKey(@NotNull String key) {
for (int x = 0; x < key.length(); x++) { StringBuilder integerKey = new StringBuilder(String.valueOf(Character.codePointAt(key, 0)));
integerKey.append(Character.getNumericValue(Character.codePointAt(key, x))); for (int x = 1; x < key.length(); x++) {
integerKey.append(", ").append(Character.codePointAt(key, x));
} }
return integerKey.toString(); 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 * Encrypts the pages of a book
* *
* @param book <p>The book to encrypt</p> * @param book <p>The book to encrypt</p>
* @param style <p>The encryption style to use</p> * @param style <p>The encryption style to use</p>
* @param integerKey <p>The encryption key to use</p> * @param aesConfiguration <p>The AES configuration to use, if encrypting using AES</p>
* @param player <p>The player trying to encrypt a book</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> * @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<>(); List<String> encryptedPages = new ArrayList<>();
//Scramble the book's contents for (int x = 0; x < book.getPages().size(); x++) {
if (style == EncryptionStyle.DNA) { String text = book.getPage(x + 1);
//Encrypt the pages using gene-based encryption String output;
GenenCrypt gc = new GenenCrypt(integerKey); if (encrypt) {
for (int x = 0; x < book.getPages().size(); x++) { output = encryptor.encryptText(text);
encryptedPages.add(gc.encrypt(book.getPage(x + 1))); } else {
output = encryptor.decryptText(text);
} }
return encryptedPages; if (output == null || output.isEmpty()) {
} else if (style == EncryptionStyle.SUBSTITUTION) { return null;
//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));
} }
return encryptedPages; encryptedPages.add(output);
} else {
BooksWithoutBorders.sendErrorMessage(player, "Invalid encryption style encountered!");
return null;
} }
return encryptedPages;
} }
/** /**
@@ -86,7 +129,9 @@ public final class EncryptionHelper {
* @param style <p>The encryption style to use</p> * @param style <p>The encryption style to use</p>
* @return <p>An encrypted version of the book</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, ""); 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> * @param groupName <p>The name of the group to encrypt for, or "" otherwise</p>
* @return <p>An encrypted version of the book</p> * @return <p>An encrypted version of the book</p>
*/ */
public static ItemStack encryptBook(Player player, boolean mainHand, String key, EncryptionStyle style, String groupName) { @Nullable
//converts user supplied key into integer form public static ItemStack encryptBook(Player player, boolean mainHand, @NotNull String key,
String integerKey = EncryptionHelper.getNumberKeyFromStringKey(key); @NotNull EncryptionStyle style, @NotNull String groupName) {
BookMeta book = InventoryHelper.getHeldBookMetadata(player, mainHand); 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()) { if (!book.hasPages()) {
BooksWithoutBorders.sendErrorMessage(player, "Book is empty!"); BooksWithoutBorders.sendErrorMessage(player, "Book is empty!");
return null; return null;
} }
String hashedKey = sha256(key);
AESConfiguration configuration = AESConfiguration.getNewConfiguration(hashedKey);
//Save the book's un-encrypted contents to a file //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) { if (newMetadata == null) {
return null; return null;
} }
//Get the encrypted pages //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) { if (encryptedPages == null) {
return null; return null;
} }
@@ -143,7 +195,9 @@ public final class EncryptionHelper {
* @param newMetadata <p>The new metadata of the book</p> * @param newMetadata <p>The new metadata of the book</p>
* @return <p>An encrypted version 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 //Create the encrypted book
ItemStack encryptedBook = new ItemStack(Material.WRITTEN_BOOK); ItemStack encryptedBook = new ItemStack(Material.WRITTEN_BOOK);
book.setPages(newPages); book.setPages(newPages);
@@ -160,17 +214,22 @@ public final class EncryptionHelper {
/** /**
* Saves a book's plain text to a file * Saves a book's plain text to a file
* *
* @param groupName <p>The group who's allowed to decrypt the book, or ""</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 player <p>The player trying to encrypt the book</p>
* @param book <p>The book to encrypt</p> * @param book <p>The book to encrypt</p>
* @param integerKey <p>The key used to encrypt the book</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> * @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; BookMeta newMetadata = book;
boolean wasSaved; boolean wasSaved;
if (groupName.trim().isEmpty()) { if (groupName.trim().isEmpty()) {
wasSaved = saveEncryptedBook(player, book, integerKey); wasSaved = saveEncryptedBook(player, book, encryptionStyle, key, aesConfiguration);
} else { } else {
newMetadata = saveEncryptedBookForGroup(player, book, groupName); newMetadata = saveEncryptedBookForGroup(player, book, groupName);
wasSaved = newMetadata != null; wasSaved = newMetadata != null;
@@ -188,19 +247,88 @@ public final class EncryptionHelper {
* @param player <p>The player trying to load the book</p> * @param player <p>The player trying to load the book</p>
* @param key <p>The encryption key/password for decryption</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 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> * @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); ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta(); BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta();
String path = getBookFolder() + "Encrypted" + getSlash(); String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
if (bookMetadata == null) { if (bookMetadata == null) {
return null; return null;
} }
String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetadata, player, true); String fileName = BookHelper.getBookFile(bookMetadata, player, true);
fileName = fixName(cleanString(fileName), false); 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"); File file = new File(path + fileName + ".yml");
if (!file.isFile()) { if (!file.isFile()) {
@@ -213,6 +341,10 @@ public final class EncryptionHelper {
} else { } else {
try { try {
bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata); bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!");
return null;
}
} catch (Exception e) { } catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!"); BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!");
return null; return null;
@@ -220,24 +352,55 @@ public final class EncryptionHelper {
} }
if (deleteEncryptedFile) { if (deleteEncryptedFile) {
ConsoleCommandSender consoleSender = BooksWithoutBorders.getConsoleSender(); Logger logger = BooksWithoutBorders.getInstance().getLogger();
try { try {
if (!file.delete()) { if (!file.delete()) {
BooksWithoutBorders.sendErrorMessage(consoleSender, "Book encryption data failed to delete upon decryption!"); logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\n" +
BooksWithoutBorders.sendErrorMessage(consoleSender, "File location:" + file.getPath()); "File location:" + file.getPath());
} }
} catch (Exception e) { } catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(consoleSender, "Book encryption data failed to delete upon decryption!"); logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\nFile location:" + file.getPath());
BooksWithoutBorders.sendErrorMessage(consoleSender, "File 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.setItemMeta(bookMetadata);
newBook.setAmount(InventoryHelper.getHeldBook(player, true).getAmount()); newBook.setAmount(InventoryHelper.getHeldBook(player, true).getAmount());
return newBook; 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 * 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> * @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> * @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) { @Nullable
String path = getBookFolder() + "Encrypted" + getSlash() + cleanString(groupName) + getSlash(); 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); File dirTest = new File(path);
//Creates group dir //Creates group dir
if (!dirTest.exists()) { if (!dirTest.exists()) {
try { try {
if (!dirTest.mkdir()) { 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; return null;
} }
} catch (Exception exception) { } 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; return null;
} }
} }
@@ -275,8 +441,7 @@ public final class EncryptionHelper {
bookMetadata.setLore(newLore); bookMetadata.setLore(newLore);
//Save file //Save file
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") : File file = new File(path + fileName + ".yml");
new File(path + fileName + ".txt");
if (!file.isFile()) { if (!file.isFile()) {
try { try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetadata); BookToFromTextHelper.bookToYml(path, fileName, bookMetadata);
@@ -292,26 +457,30 @@ public final class EncryptionHelper {
/** /**
* Saves an encrypted book to be decryptable for the given user * Saves an encrypted book to be decryptable for the given user
* *
* @param player <p>The player encrypting the book</p> * @param player <p>The player encrypting the book</p>
* @param bookMetaData <p>Metadata for the book to encrypt</p> * @param bookMetaData <p>Metadata for the book to encrypt</p>
* @param key <p>The key to use for encryption</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> * @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) { @NotNull
String path = getBookFolder() + "Encrypted" + getSlash(); 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); String fileName = BookHelper.getBookFile(bookMetaData, player, true);
fileName = fixName(cleanString(fileName), false); fileName = cleanString(fileName);
//cancels saving if file is already encrypted //cancels saving if file is already encrypted
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") : File file = new File(path + fileName + ".yml");
new File(path + fileName + ".txt");
if (file.isFile()) { if (file.isFile()) {
return true; return true;
} }
try { try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetaData); BookToFromTextHelper.encryptedBookToYml(path, fileName, bookMetaData, encryptionStyle, key, aesConfiguration);
} catch (IOException exception) { } catch (IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!"); BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!");
return false; return false;

View File

@@ -1,7 +1,12 @@
package net.knarcraft.bookswithoutborders.utility; package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Helper class for cleaning input and names * Helper class for cleaning input and names
@@ -17,7 +22,8 @@ public final class InputCleaningHelper {
* @param list <p>The list to clean</p> * @param list <p>The list to clean</p>
* @return <p>A clean list containing all relevant values</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); List<String> resultList = new ArrayList<>(list);
resultList.removeIf((item) -> item == null || item.trim().isEmpty()); resultList.removeIf((item) -> item == null || item.trim().isEmpty());
return resultList; return resultList;
@@ -29,7 +35,8 @@ public final class InputCleaningHelper {
* @param fileName <p>The file name to clean</p> * @param fileName <p>The file name to clean</p>
* @return <p>The cleaned file name</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("\\", ""); 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("?", "");
fileName = fileName.replace("\"", ""); fileName = fileName.replace("\"", "");
fileName = fileName.replace("§", "&");
return fileName; 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 input <p>The input to parse</p>
* @param isLoading <p>Whether loading from a file as opposed to saving to a file</p> * @return <p>The page number, or 0 if not valid</p>
* @return <p>The fixed name</p>
*/ */
public static String fixName(String fileName, Boolean isLoading) { public static int parsePageNumber(@NotNull String input) {
if (isLoading) { try {
fileName = fileName.replace("_", " "); Pattern pattern = Pattern.compile("page([0-9]+)");
} else { Matcher matcher = pattern.matcher(input);
fileName = fileName.replace(" ", "_"); 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; package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -40,6 +42,7 @@ public final class IntegerToRomanConverter {
* @param number <p>The number to convert</p> * @param number <p>The number to convert</p>
* @return <p>The roman representation of the number</p> * @return <p>The roman representation of the number</p>
*/ */
@NotNull
public static String getRomanNumber(int number) { public static String getRomanNumber(int number) {
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
int remainder = number; int remainder = number;
@@ -83,6 +86,7 @@ public final class IntegerToRomanConverter {
* @param times <p>The number of times to repeat the character</p> * @param times <p>The number of times to repeat the character</p>
* @return <p>The repeated string</p> * @return <p>The repeated string</p>
*/ */
@NotNull
private static String repeat(char character, int times) { private static String repeat(char character, int times) {
return String.valueOf(character).repeat(Math.max(0, 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.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta; 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 * The inventory helper mainly helps with getting and setting books
@@ -16,6 +18,25 @@ public final class InventoryHelper {
private 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 * 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> * @param signedBook <p>Whether to check for signed or unsigned books</p>
* @return <p>The book the player is holding</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); ItemSlot heldSlot = getHeldSlotBook(player, false, false, true, signedBook);
if (heldSlot == ItemSlot.MAIN_HAND) { if (heldSlot == ItemSlot.MAIN_HAND) {
return getHeldItem(player, true); return getHeldItem(player, true);
@@ -40,7 +62,7 @@ public final class InventoryHelper {
* @param player <p>The player holding the book</p> * @param player <p>The player holding the book</p>
* @param newBook <p>The new book the player should hold</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); ItemSlot itemSlot = getHeldSlotBook(player, false, false, true, true);
if (itemSlot == ItemSlot.MAIN_HAND) { if (itemSlot == ItemSlot.MAIN_HAND) {
replaceHeldItem(player, newBook, true); 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> * @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> * @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); BookHoldingState holdingState = getBookHoldingState(player);
if (holdingState == BookHoldingState.NONE || holdingState == BookHoldingState.SIGNED_BOTH_HANDS || 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> * @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> * @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); BookHoldingState holdingState = getBookHoldingState(player);
if (holdingState == BookHoldingState.NONE || holdingState == BookHoldingState.UNSIGNED_BOTH_HANDS || 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> * @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> * @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) { boolean typeMatters, boolean writtenBook) {
BookHoldingState state = getBookHoldingState(player); BookHoldingState state = getBookHoldingState(player);
ItemStack mainHandItem = getHeldItem(player, true); ItemStack mainHandItem = getHeldItem(player, true);
@@ -168,7 +193,8 @@ public final class InventoryHelper {
* @param player <p>The player to check</p> * @param player <p>The player to check</p>
* @return <p>The state of the player's book holding</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 mainHandItem = getHeldItem(player, true);
ItemStack offHandItem = getHeldItem(player, false); 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> * @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> * @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(); 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> * @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> * @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) { if (mainHand) {
return player.getInventory().getItemInMainHand(); return player.getInventory().getItemInMainHand();
} else { } else {
@@ -231,7 +259,7 @@ public final class InventoryHelper {
* @param newBook <p>The new book the player should hold</p> * @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> * @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) { if (mainHand) {
player.getInventory().setItemInMainHand(newBook); player.getInventory().setItemInMainHand(newBook);
} else { } else {

View File

@@ -1,5 +1,7 @@
package net.knarcraft.bookswithoutborders.utility; package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -16,6 +18,7 @@ public final class TabCompletionTypeHelper {
* *
* @return <p>A list of booleans</p> * @return <p>A list of booleans</p>
*/ */
@NotNull
public static List<String> getBooleans() { public static List<String> getBooleans() {
List<String> booleans = new ArrayList<>(); List<String> booleans = new ArrayList<>();
booleans.add("true"); booleans.add("true");
@@ -30,6 +33,7 @@ public final class TabCompletionTypeHelper {
* @param end <p>The end number</p> * @param end <p>The end number</p>
* @return <p>A list of numbers</p> * @return <p>A list of numbers</p>
*/ */
@NotNull
public static List<String> getNumbers(int start, int end) { public static List<String> getNumbers(int start, int end) {
List<String> numbers = new ArrayList<>(); List<String> numbers = new ArrayList<>();
for (int i = start; i <= end; i++) { for (int i = start; i <= end; i++) {
@@ -45,6 +49,7 @@ public final class TabCompletionTypeHelper {
* @param end <p>The end number</p> * @param end <p>The end number</p>
* @return <p>A list of booleans and numbers</p> * @return <p>A list of booleans and numbers</p>
*/ */
@NotNull
public static List<String> getBooleansAndNumbers(int start, int end) { public static List<String> getBooleansAndNumbers(int start, int end) {
List<String> booleansAndNumbers = new ArrayList<>(); List<String> booleansAndNumbers = new ArrayList<>();
List<String> booleans = getBooleans(); List<String> booleans = getBooleans();
@@ -54,4 +59,27 @@ public final class TabCompletionTypeHelper {
return booleansAndNumbers; 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: Options:
# Whether to use YAML for saved books instead of just storing them as text # The language to use. Only "en" is built-in, but custom languages can be added
Save_Books_in_Yaml_Format: true Language: "en"
# The maximum number of duplicates of a saved book allowed # The maximum number of duplicates of a saved book allowed
Max_Number_of_Duplicates: 5 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: "," Title-Author_Separator: ","
# The separator used to denote a new line in the book/item lore # The separator used to denote a new line in the book/item lore
Lore_line_separator: "~" Lore_line_separator: "~"
@@ -31,4 +32,8 @@ Options:
# vanilla behavior where a copy of a copy cannot be copied further. # vanilla behavior where a copy of a copy cannot be copied further.
Change_Generation_On_Copy: false Change_Generation_On_Copy: false
# Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents. # 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 description: Reloads BwB's configuration file
usage: /<command> usage: /<command>
permission: bookswithoutborders.reload 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: permissions:
bookswithoutborders.*: bookswithoutborders.*:
description: Grants all permissions description: Grants all permissions
@@ -125,6 +141,7 @@ permissions:
bookswithoutborders.setbookprice: true bookswithoutborders.setbookprice: true
bookswithoutborders.reload: true bookswithoutborders.reload: true
bookswithoutborders.setgeneration: true bookswithoutborders.setgeneration: true
bookswithoutborders.editbookshelf: true
bookswithoutborders.use: bookswithoutborders.use:
description: Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled description: Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled
children: children:
@@ -142,6 +159,8 @@ permissions:
bookswithoutborders.setlore: true bookswithoutborders.setlore: true
bookswithoutborders.format: true bookswithoutborders.format: true
bookswithoutborders.setgeneration: true bookswithoutborders.setgeneration: true
bookswithoutborders.addtitlepage: true
bookswithoutborders.deletepage: true
bookswithoutborders.format: bookswithoutborders.format:
description: Allows a player to format a book description: Allows a player to format a book
bookswithoutborders.save: bookswithoutborders.save:
@@ -193,4 +212,10 @@ permissions:
bookswithoutborders.setgeneration: bookswithoutborders.setgeneration:
description: Allows player to change the generation of a book (Original, Copy, Copy of Copy) description: Allows player to change the generation of a book (Original, Copy, Copy of Copy)
bookswithoutborders.peekbookshelf: 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 org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class GenenCryptTest { public class GenenCryptTest {
@@ -11,8 +12,11 @@ public class GenenCryptTest {
public void encryptDecryptTest() { public void encryptDecryptTest() {
GenenCrypt gc = new GenenCrypt("Another Key"); GenenCrypt gc = new GenenCrypt("Another Key");
gc.printCodonTable(); gc.printCodonTable();
String encrypted = gc.encrypt("Hello World!"); String encrypted = gc.encryptText("Hello World!");
assertEquals("HELLO WORLD!", gc.decrypt(encrypted));
assertNotNull(encrypted);
assertEquals("HELLO WORLD!", gc.decryptText(encrypted));
} }
} }

View File

@@ -3,20 +3,31 @@ package net.knarcraft.bookswithoutborders.encryption;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; 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 { public class AESTest {
@Test @Test
public void encryptDecryptTest() { public void encryptDecryptTest() {
String plainText = "A lot of text"; String plainText = "Flåklypa";
String password = "abc123"; 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); AESConfiguration configuration = new AESConfiguration(new byte[]{-85, 103, -82, 71, 119, 28, 73, -75, -81, 102, -127, -125, -8, -75, 81, -111},
assertNotSame(encrypted, plainText); new byte[]{(byte) 104, -42, 63, 31, -120, -2, 14, -119, 35, 122, 109, -64, 122, 117, 33, -85}, password);
String decrypted = aes.encryptDecryptText(encrypted, password, false); 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); 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 @Test
public void getNumberKeyFromStringKey() { public void getNumberKeyFromStringKey() {
String numberKey = EncryptionHelper.getNumberKeyFromStringKey("hello"); String numberKey = EncryptionHelper.getNumberKeyFromStringKey("hello");
assertEquals("1714212124", numberKey); assertEquals("104, 101, 108, 108, 111", numberKey);
} }
} }